Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

In XCUITests, how to wait for existence of either of two ui elements

Looking at XCTWaiter().wait(...) I believe we can wait for multiple expectations to become true using this code

let notHittablePredicate = NSPredicate(format: "hittable == false")
let myExpectation = XCTNSPredicateExpectation(predicate: notHittablePredicate, object: element)
let result = XCTWaiter().wait(for: [myExpectation], timeout: timeout)
//for takes array of expectations

But this uses like AND among the supplied expectations. Is there a way to do OR among the supplied expectations.

Like i have a use case at login that after tapping submit, i want to wait for one of two elements. First element is "You are already logged in on another device. If you continue any unsaved data on your other device will be lost?". And second element is the main screen after login. So any one can appear. Currently I'm first waiting for first element until timeout occurs and then for the second element. But I want to optimize time here and move on as soon as any of two elements exist==true. Then i'll check if element1 exists then tap YES and then wait for main screen otherwise just assert existence of element2.

Please comment if something isn't clear in the question. Thanks

like image 717
Hasaan Ali Avatar asked Dec 19 '17 05:12

Hasaan Ali


3 Answers

Inspired by http://masilotti.com/ui-testing-tdd/, you don't have to rely on XCTWaiter. You can simply run a loop and test whether one of them exists.

/// Waits for either of the two elements to exist (i.e. for scenarios where you might have
/// conditional UI logic and aren't sure which will show)
///
/// - Parameters:
///   - elementA: The first element to check for
///   - elementB: The second, or fallback, element to check for
/// - Returns: the element that existed
@discardableResult
func waitForEitherElementToExist(_ elementA: XCUIElement, _ elementB: XCUIElement) -> XCUIElement? {
    let startTime = NSDate.timeIntervalSinceReferenceDate
    while (!elementA.exists && !elementB.exists) { // while neither element exists
        if (NSDate.timeIntervalSinceReferenceDate - startTime > 5.0) {
            XCTFail("Timed out waiting for either element to exist.")
            break
        }
        sleep(1)
    }

    if elementA.exists { return elementA }
    if elementB.exists { return elementB }
    return nil
}

then you could just do:

let foundElement = waitForEitherElementToExist(elementA, elementB)
if foundElement == elementA {
    // e.g. if it's a button, tap it
} else {
    // element B was found
}
like image 179
Mladen Avatar answered Nov 04 '22 23:11

Mladen


lagoman's answer is absolutely correct and great. I needed wait on more than 2 possible elements though, so I tweaked his code to support an Array of XCUIElement instead of just two.

@discardableResult
func waitForAnyElement(_ elements: [XCUIElement], timeout: TimeInterval) -> XCUIElement? {
    var returnValue: XCUIElement?
    let startTime = Date()
    
    while Date().timeIntervalSince(startTime) < timeout {
        if let elementFound = elements.first(where: { $0.exists }) {
            returnValue = elementFound
            break
        }
        sleep(1)
    }
    return returnValue
}

which can be used like

let element1 = app.tabBars.buttons["Home"]
let element2 = app.buttons["Submit"]
let element3 = app.staticTexts["Greetings"]
foundElement = waitForAnyElement([element1, element2, element3], timeout: 5)

// do whatever checks you may want
if foundElement == element1 {
     // code
}
like image 2
DBD Avatar answered Nov 04 '22 23:11

DBD


NSPredicate supports OR predicates too.

For example I wrote something like this to ensure my application is fully finished launching before I start trying to interact with it in UI tests. This is checking for the existence of various landmarks in the app that I know are uniquely present on each of the possible starting states after launch.

extension XCTestCase {
  func waitForLaunchToFinish(app: XCUIApplication) {
    let loginScreenPredicate = NSPredicate { _, _ in
      app.logInButton.exists
    }

    let tabBarPredicate = NSPredicate { _, _ in
      app.tabBar.exists
    }

    let helpButtonPredicate = NSPredicate { _, _ in
      app.helpButton.exists
    }

    let predicate = NSCompoundPredicate(
      orPredicateWithSubpredicates: [
        loginScreenPredicate,
        tabBarPredicate,
        helpButtonPredicate,
      ]
    )

    let finishedLaunchingExpectation = expectation(for: predicate, evaluatedWith: nil, handler: nil)
    wait(for: [finishedLaunchingExpectation], timeout: 30)
  }
}

In the console while the test is running there's a series of repeated checks for the existence of the various buttons I want to check for, with a variable amount of time between each check.

t = 13.76s Wait for com.myapp.name to idle

t = 18.15s Checking existence of "My Tab Bar" Button

t = 18.88s Checking existence of "Help" Button

t = 20.98s Checking existence of "Log In" Button

t = 22.99s Checking existence of "My Tab Bar" Button

t = 23.39s Checking existence of "Help" Button

t = 26.05s Checking existence of "Log In" Button

t = 32.51s Checking existence of "My Tab Bar" Button

t = 16.49s Checking existence of "Log In" Button

And voila, now instead of waiting for each element individually I can do it concurrently.

This is very flexible of course, since you can add as many elements as you want, with whatever conditions you want. And if you want a combination of OR and AND predicates you can do that too with NSCompoundPredicate. This can easily be adapted into a more generic function that accepts an array of elements like so:

func wait(for elements: XCUIElement...) { … }

Could even pass a parameter that controls whether it uses OR or AND.

like image 2
shim Avatar answered Nov 04 '22 21:11

shim