Inputting textfields is flaky when running all tests at once using parallel testing

I am trying to do UI Tests using XCUITest. Currently I have a bit of a problem in inputting a text into a UITextField. Here is what I've got in my Robot class to type the text using the keyboard:

func typeTextToTextField(_ element: XCUIElement, text: String, timeout: TimeInterval = timeInterval,
                     file: StaticString, line: UInt) {
    guard assertExists(element, timeout: timeout, file: file, line: line),
        element.isHittable else {
            return
    }

    element.tap()
    sleep(2)
    app.activate()
    element.typeText("\(text)\n")
    sleep(2)
}

I've tried these steps before it become like this:

  1. Only the element.tap() and element.typeText().
  2. Added the guard to wait for existence (the assertExists() block)
  3. Added the sleep() to wait for a few secs for the keyboard to show up.
  4. Added the app.activate() because apparently sometimes it took too long to do query. And an answer about the error here said that I should call app.activate() first.

If I run all the tests at once with parallel testing, some of the tests that use this will work, but some will fail. Some will correctly show the keyboard and typed the text correctly while others will fail to show the keyboard and displayed this error Failed to synthesize event: Neither element nor any descendant has keyboard focus.. But when I run the test one at a time, they will all be green and work perfectly.

So, next, I tried changing the way to input by pasting the text instead of typing it. That also doesn't work. Here's the code:

func pasteToTextField(_ element: XCUIElement, text: String, timeout: TimeInterval = timeInterval,
                      file: StaticString, line: UInt) {
    app.activate()
    guard assertExists(element, timeout: timeout, file: file, line: line),
        element.isHittable else {
            return
    }

    UIPasteboard.general.string = text
    app.activate()
    element.tap()
    element.doubleTap()
    element.press(forDuration: 1.2)
    tap(pasteMenuItem, timeout: timeout, file: file, line: line)
    sleep(5)
}

So I did:

  1. Only element.tap() and tap(pasteMenuItem, timeout: timeout, file: file, line: line) (which was the same with app.menuItems["Paste"].tap())
  2. Added the element.doubleTap()
  3. Added the element.press()

Using the paste function sometimes the Paste menu item doesn't show up with this error: failed - Element: "Paste" MenuItem does not exist!. But sometimes it worked just fine.

So, I'm at wits end now. How do I input a text into a textfield that will surely work at all conditions whether running all tests or running a singular test case?

Thanks in advance.

PS: It works just fine on my CI machine. That's weird.

Upvotes: 3

Views: 1011

Answers (1)

Ashraf Ali
Ashraf Ali

Reputation: 26

You can try something like this

extension XCUIElement {
    func clearAndEnterText(_ text: String, app: XCUIApplication) {
        let currentClipboard = UIPasteboard.general.string ?? ""

        waitForElementToBecomeHittable(timeout: .small)

        guard let stringValue = value as? String else {
            return XCTFail("Tried to clear and enter text into a non string value")
        }

        if stringValue == text {
            return
        }

        if stringValue.isNotEmpty {
            if app.isKeyboardKeysAvaliable(key: XCUIKeyboardKey.delete.rawValue) {
                let deleteString = stringValue.map { _ in XCUIKeyboardKey.delete.rawValue }.joined()
                typeText(deleteString)
            } else {
                let selectAllButton = app.menuItems.element(predicate: .label(Comparison.containsAny, "Select All")).firstMatch
                if !selectAllButton.waitForExistence(timeout: .small) {
                    press(forDuration: 1.1)
                }

                if selectAllButton.waitForExistence(timeout: .small) {
                    selectAllButton.tapElement()
                }
            }
        }

        if value(forKey: "hasKeyboardFocus") as? Bool ?? false {
            typeText(text)
        } else {
            UIPasteboard.general.string = text
            let pasteButton = app.menuItems.element(predicate: .label(Comparison.containsAny, "Paste"))

            if !pasteButton.waitForExistence(timeout: .small) {
                tap()
                press(forDuration: 2.1)
            }

            if pasteButton.waitForExistence(timeout: .small) {
                 pasteButton.tapElement()
            }
        }

        // Dismiss tooltip if it is still displayed
        if app.menuItems.element.waitForExistence(timeout: .small) {
            app.tap()
        }

        waitForElementToBecomeHittable(timeout: .small)

        UIPasteboard.general.string = currentClipboard
    }
}

Note: you'll need to import this file https://github.com/ZhipingYang/Einstein/blob/600854f9b6f93bb3694deddb3fb6edbae0f67f74/Class/UITest/Model/EasyPredicate.swift, also I have changed the name from EasyPredicate to Predicate

extension XCUIApplication {
    func isKeyboardKeysAvaliable(key: String) -> Bool {
        let keyboard = keyboards.element(boundBy: 0)
        if key.contains(all: "next") {
            return keyboard.buttons[key].exists
        } else {
            return keyboard.keys[key].exists
        }
    }
}

extension XCUIElement {
    @discardableResult
    func waitForElementToBecomeHittable(timeout: Timeout) -> Bool {
        return waitForExistence(timeout: timeout) && isHittable
    }

Upvotes: 1

Related Questions