In XCUITests, how to wait for existence of either of two ui elements
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@discardableResultfunc 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}
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.
@discardableResultfunc 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 wantif foundElement == element1 { // code}
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.