XCUITest - TextFields

Prerequisites


To follow along with the tutorial please download the repository here.

Before we begin, let's first open the project and run the application. Please run the application against the iPhone SE (2nd generation) simulator.

In the example application, there are four text fields to supply basic user information. In this tutorial, we are going to create a test that simply enters text into these fields in an attempt to understand how to, and also to learn a little bit more about some of the issues we can face when working with TextFields in XCUITest.

1. Typing Text


In Xcode, navigate to the ‘Test Navigator’ panel (shortcut CMD+6) and click the example test. As you can see the test is empty. If we run it now, the application will pass without doing anything. So let’s write the code that will enter some text into each TextField.

app.textFields["firstNameTextField"].typeText("John")
app.textFields["lastNameTextField"].typeText("Doe")
app.textFields["emailAddressTextField"].typeText("johndoe@uitest.com")
app.textFields["passwordTextField"].typeText("xcuitest")

As a brief overview to what this code is doing - we are querying the TextFields within the application for one that exists with a given accessibility identifier, then from the element returned, we are typing some data into the TextField.

However, if we run the test now, we can see it fails with the following error.

UI Test Activity: 
Assertion Failure: <unknown>:0: Failed to synthesize event: Neither element nor any descendant has keyboard focus. Event dispatch snapshot: TextField, identifier: 'usernameTextField'. 

The problem is that the element did not have keyboard focus. In XCUITest (unless you’re pasting data) the keyboard needs to be present in order to type text. This error is detailed in the documentation for typeText: https://developer.apple.com/documentation/xctest/xcuielement/1500968-typetext

To get keyboard focus, we can simply tap the TextField before typing our text. So let’s replace the previous code with the following and run the test again.

app.textFields["firstNameTextField"].tap()
app.textFields["firstNameTextField"].typeText("John")

app.textFields["lastNameTextField"].tap()
app.textFields["lastNameTextField"].typeText("Doe")

app.textFields["emailAddressTextField"].tap()
app.textFields["emailAddressTextField"].typeText("johndoe@uitest.com")

app.textFields["passwordTextField"].tap()
app.textFields["passwordTextField"].typeText("xcuitest")

The test failed again. This time, we saw that the keyboard did appear, and text was entered, but the test failed attempting to tap the password textfield.

Failed to synthesize event: Failed to scroll to visible (by AX action) TextField, identifier: 'passwordTextField', placeholderValue: 'Enter password', value: Enter password, error: Error kAXErrorCannotComplete performing AXAction 2003 on element AX element pid: 30910, elementOrHash.elementID: 140340061403136.23
                        

The error explains, our element does exist and is visible, but is is hidden behind the keyboard from the previous textfield, as such it is not hittable and the test fails. The solution is to dismiss the keyboard after typing our text.

2. Dismissing the keyboard


Create the following extension method:

extension XCTestCase {
    func dismissKeyboard() {
        XCUIApplication().keyboards.buttons["Return"].tap()
    }
}

This method is an extension of XCTestCase, so all of our test classes from now on will be able to access the method. Within the method, we are accessing the current XCUIApplication, querying its keyboards, and requesting a button (which is a child of any of the keyboards) with the specific accessibility identifier “Return” (which represents our return key), and then it taps the element.

In simple terms - this method taps the return button on the keyboard.

Note:
It is important to note that the “Return” string (with a capital R), represents the accessibility identifier for Apple’s default keyboard. “return” with a lowercase ‘r’, represents the accessibility label. The label can change depending on what language keyboard is being used, for example Chinese, Japanese, French etc.

English keyboard (XCUITest hieararchy output)

Button, 0x6000001b7060, {{256.0, 461.0}, {64.0, 107.0}}, identifier: 'Return', label: 'return'

Japanese keyboard (XCUITest hieararchy output)

Button, 0x6000001b7060, {{256.0, 461.0}, {64.0, 107.0}}, identifier: 'Return', label: '改行'

If you use the accessibility label instead of the identifier, you will see that the test passes. But if this test was run in on a device with a different language, the test would fail. I will explain more about this in my tutorials on accessibility in xcuitest.

Now we have our convenient dismissKeyboard method, let’s add the following to our test case, and run the test again.

app.textFields["passwordTextfield"].tap()
app.textFields["passwordTextfield"].typeText("myemail@email.com")
app.textFields["passwordTextfield"].dismissKeyboard()

Success! The test passed successfully, and we now have a better understanding of some of the issues we can face when using TextFields in XCUITest, such as keyboard focus, accessibility, and hittability.

3. Troubleshooting

In this section, I will demonstrate some of the issues that may have occured in the previous sections and how to fix them.

1. "I’m still seeing the ‘Neither element nor any descendant has keyboard focus’ error"

UI Test Activity: 
Assertion Failure: <unknown>:0: Failed to synthesize event: Neither element nor any descendant has keyboard focus. Event dispatch snapshot: TextField, identifier: 'usernameTextField'

If you are still seeing this error when running your tests. You likely have your simulator’s keyboard setting ‘Connect hardware keyboard’ enabled.

This setting allows you to use your macs keyboard to type into the simulators. Which can be useful when manually testing, but if you keep this setting on, the keyboard will not appear, and your test will fail as the element does not have keyboard focus.

Solution: On your simulator application, navigate to ‘I/O’ > ‘Keyboard’ and disable ‘Connect hardware keyboard’. If this was the issue you faced, you should see your tests running better.

2. "My keyboard still isn’t being dismissed when I press return" (WIP)

If you’re not using the sample application provided, you may encounter an issue where despite following this tutorial, your keyboard still isn’t being dismissed. The return button does not dismiss the keyboard.

The problem here is actually within in the developers code. The textfield has not been written to be dismissed when the return button is pressed. The code to do so may look something like this:

func textFieldShouldReturn(textField: UITextField!) -> Bool 
{
    textField.resignFirstResponder()
    return true;
}

I won’t go into too much detail about this, but it is important to know that accessibility support, and testing support are all closely tied to the developers code. In this case, essentially textfields have optional functions they can subscribe to, which the developer can implement. In this case, we say ‘when return is tapped, the textfield should lose its keyboard focus’. Then, we subscribe our textfield to that delegate.

So let’s add the responder and run the test again.
You can uncomment the line in the ViewController.swift file. Success!

Great! We’ve learned that to dismiss a TextField with the keyboard functions, the element in code needs to resign the first responder.

Alternatively, instead of tapping the ‘return’ key, you can type in the newline escape sequence into the textfield which will dismiss it in the same way.

extension XCTestCase {
    func dismissKeyboard() {
        XCUIApplication().typeText("\n")
    }
}

However the textfield still needs to be resignable

Please note, other keyboards within your application may have no return button, but instead implements a ‘done’ button, or tap elsewhere to resign the responder. However, if you imagine the concept of a textfield subscribing to an event which will close the keyboard on a condition. Then in the test, you simply need to meet that condition. Whether it’s a specific button tap, tapping away from the element. The design of your application will determine the implementation.

Copyrights © 2020 All Rights Reserved by Ryan Paterson