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

4

9

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

Bliss answered 19/12, 2017 at 5:15 Comment(0)
P
12

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
}
Pollie answered 22/12, 2017 at 20:58 Comment(5)
It works. Just a logical change is needed in while condition. Use while( !a.exists && !b.exists) because we need to run the loop while none of the both elements exist, i.e. neither a exists nor b exists. With ||, it loops forever even if one of the two elements come into existence. Besides this little tweak, GREAT MANY THANKS :)Bliss
Oh, totally agreed. Approved edit :) Glad that it helps you.Pollie
Nice answer; I updated it to make it more generic and copy/paste-ableAnorthosite
Rather than waiting for two items, I'd make this even more generic by having it accept an array of elements. For loop through each element for existence, return it if true.Burlie
@MikeCollins, this answers the OP's question, but the general idea is here. Making it more generic is as easy as it should be when you know how to do it with the two items.Pollie
H
4

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
}
Hardboard answered 11/12, 2020 at 17:42 Comment(0)
N
3

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.

Negrito answered 30/6, 2021 at 14:39 Comment(1)
This is an excellent answer. Flexible in type and quantity of elements or other conditions, adds no unnecessary wait time to the process, and teaches a useful technique of waiting for conditions of all kinds to resolve in an XCTestCase. Thank you!Reverie
U
-1

Hey other alternative that works for us. I hope help others too.

 XCTAssert(
            app.staticTexts["Hello Stack"]
                .waitForExistence(timeout: 10) || app.staticTexts["Hi Stack"]
                .waitForExistence(timeout: 10)
        )
Usia answered 2/7, 2021 at 16:31 Comment(1)
In the worst case will this code wait 20 seconds? It evaluates the left side first and only when it gives false will it try the right side of the ||?Rebuttal

© 2022 - 2024 — McMap. All rights reserved.