Wait for a text field to have focus during an XCUITest

I was recently working on some XCUITests and attempting to get them working inside a CI/CD pipeline (Bitrise). It turns out that the virtual machines on Bitrise are a bit underpowered, so many things like building a project & running the simulator can be fairly slow. In one part of my UI test, I tap a text field & begin entering text. The test kept failing on Bitrise but running just fine on my MacBook - that seemed odd. 

After downloading the xcresult.zip file from Bitrise & opening up several screenshots (did you know that XCUITests automatically save screenshots on failed tests?!) I noticed that the text field never seemed to receive focus after a tap when the test failed. I also read in the documentation for XCUIElement.typeText() that “The element or a descendant must have keyboard focus; otherwise an error is raised”, and I began to realize that the test was attempting to type into the text field when it wasn’t active, causing the error. But why did the field not activate like it did on my laptop? Remember that the Bitrise machines are slow 🐱

After all this, I decided to make some helpers that wait for a text field to be focused before continuing. Here’s the code:


extension XCUIElement {
    var hasFocus: Bool { value(forKey: "hasKeyboardFocus") as? Bool ?? false }
}

extension XCTestCase {
    func waitUntilElementHasFocus(element: XCUIElement, timeout: TimeInterval = 600, file: StaticString = #file, line: UInt = #line) -> XCUIElement {
        let expectation = expectation(description: "waiting for element \(element) to have focus")
        
        let timer = Timer(timeInterval: 1, repeats: true) { timer in
            guard element.hasFocus else { return }
            
            expectation.fulfill()
            timer.invalidate()
        }
        
        RunLoop.current.add(timer, forMode: .common)
        
        wait(for: [expectation], timeout: timeout)
        
        return element
    }
}

With those extensions in place, I was able to do something like this in the XCUITest:


func test_typeText() {
    let app = XCUIApplication()
    app.launch()
    
    let textField = app.textFields["Text Field"]
    textField.waitUntilExists().tap()
    waitUntilElementHasFocus(element: textField).typeText("ios")
}

The tests began to work perfectly in Bitrise as well as on my MacBook - success at last. 

Hopefully this was helpful if you’ve run into similar UI test issues on slower processors (a typical issue when using cloud virtual machines). Also, if you’re curious about that waitUntilExists() method, I’ll include its code here. The method is extremely helpful when you have elements that take a few seconds to display after network calls, etc, and the chaining syntax is nice too:


extension XCUIElement {
    func waitUntilExists(timeout: TimeInterval = 600, file: StaticString = #file, line: UInt = #line) -> XCUIElement {
        let elementExists = waitForExistence(timeout: timeout)
        if elementExists {
            return self
        } else {
            XCTFail("Could not find \(self) before timeout", file: file, line: line)
        }
        
        return self
    }
}

See you next time!