Are Elements Ready for Use?

When writing any UI Test automation it is important to interact with elements on the screen only when they are ready to be interacted with. When navigating to a new screen, you are not guaranteed that all of the screen elements exist and are hittable the very second that test code is executed.

This post explores techniques for checking if the minimum set of elements are ready for interaction as well as a way to determine that things like activity indicators (aka spinners) are not active.

Uses

  • Checking that activity indicators or other initial alerts, tool tips, and modals are dismissed is important in all cases. My opinion is that this is vital for any screen where I expect that those behaviors will occur.
  • Checking that all elements exist on a screen is an optional step to run all of the time either from a screenReady() function or within an assertion at start of a test.
  • Use with set of tests that only validate that navigation works and screens get into initial expected state.
  • Even if you would use this type of function infrequently, it is a forcing function to implement element queries. Best practices then can be applied so that accessibility identifiers exist for each element.

After working through a process where I always check that every element exists before interacting with a screen, I have come to the conclusion that sometimes it is okay to skip this comprehensive step and only check for existence right before an element is used. Always running an elementsReady() function takes added time and means that if an element’s query requires a change then all tests which call elementsReady()will fail. Finally, depending on my schedule constraints, I may want to defer the definition of screen elements which are not needed for test(s) being implemented. Sometimes, the process of defining a screen element will grow into making modifications to the app source code to add accessibility identifiers.

Below, I describe a few examples of code that check the state of screen elements. Each example has pros and cons making them suitable for some projects and not for others depending on your requirements.

Screen Ready

Wait for Blocking Activity to Complete

Below is an example for code I would use to check that an activity indicator is no longer present for a screen. Not shown are ways to deal with modals, alerts and tool tips.

func screenReady(_ timeout: TimeInterval = 5) -> Bool {
    let myExpectation = XCTNSPredicateExpectation(predicate: 
    NSPredicate(format: "exists == false"), object: activityIndicator)

    let result = XCTWaiter().wait(for: [myExpectation], timeout: 
    timeout)
    return result == .completed
}

Below, I describe a few examples of code that check the state of screen elements. Each example has pros and cons making them suitable for some projects and not for others depending on your requirements. For the first two methods, asserts are included for brevity in the examples.

Method 1 – XCUIElement’s exist property

func elementsReadyUsingExists() -> Bool {
        var result = false
        XCTContext.runActivity(named: "STEP - Elements Ready by `exists` property") { _ in
            result = labelStaticText.exists && openSafariButton.exists && showLabelButton.exists &&  && missingButton.exists && openSettingsButton.exists
        }
        XCTAssertTrue(outcome, "All elements do not exist using `exists` property")
        return result
    }

Method 2 – XCUIElement’s waitForExistencefunction

func elementsReadyUsingWaitForExpectations(timeout: TimeInterval = 5) -> Bool {
    XCTContext.runActivity(named: "STEP - Elements Ready by `waitForExistence` looping through element array") { _ in
    let elements: [XCUIElement] = [labelStaticText, openSafariButton, showLabelButton, missingButton, openSettingsButton]
    for element in elements {
        XCTAssertTrue(element.waitForExistence(timeout: timeout), "element named \(element.description) does not exist with in \(timeout) seconds")
        }
    }
    return true
}

Method 3 – XCTWaiter to wait for all elements at one time

func elementsReadyUsingXCTWaiter(timeout: TimeInterval = 5) -> Bool {
    var result:XCTWaiter.Result = .timedOut
    XCTContext.runActivity(named: "STEP - Elements Ready by `XCTWaiter.wait()") { _ in
    let elements: [XCUIElement] = [labelStaticText, openSafariButton, showLabelButton, missingButton, openSettingsButton]

    let expectations = elements.map { XCTNSPredicateExpectation(predicate: NSPredicate(format: "exists == true"), object: $0) }

    result = XCTWaiter().wait(for: expectations, timeout: timeout)
    }
    return result == .completed
}

// Assertion used when calling elementsReadyUsingXCTWaiter() from a test case
// XCTAssertTrue(demoScreen.elementsReadyUsingXCTWaiter(timeout: 5), "All elements do not exist (XCTWaiter method)")

Performance and Trade Offs

Performance data is also shown for a skeletal app I use for demos. It only has one screen containing 4 elements.

The data below is based on checking for existence of 4 elements which do exist and 1 element which has a query, but does not exist. Queries are only evaluated when used, so defining a dummy element is not a problem.

The logging data shown below the performance summary is for each method and first, in text, what the assertion portion of the log file looks like and then in the image, what the Test Report Navigator looks like, fully expanded.

ApproachTimeout = 0Timeout = 5Timeout = 30
exists property0.06n/an/a
check 1 at time0.068.133.11
check all at once0.075.0930
 t =     5.14s STEP - Elements Ready by `exists` property
 t =     5.21s Assertion Failure: DemoScreen.swift:49: XCTAssertTrue failed - All elements do not exist
Method 1 - Check using exist property with no timeout
t =    38.17s         Checking `Expect predicate `exists == 1` for object "missing-element" Button`
t =    38.27s     Assertion Failure: DemoScreen.swift:65: XCTAssertTrue failed - element named "missing-element" Button does not exist with in 30.0 seconds
Method 2
Method 2 - Check each element, one at a time
t =     5.04s STEP - Elements Ready by `XCTWaiter.wait()
...
t =    34.91s     Checking `Expect predicate `exists == 1` for object "missing-element" Button`
t =    35.04s Assertion Failure: DemoAutoUITests.swift:61: XCTAssertTrue failed - All elements do not exist
Method 3
Method 3 - Check all elements at once

Method 1 is as fast as possible to check that all elements expected to exist on loading a screen exist. Its disadvantage is that neither by looking at the log or the Test Navigator’s report is it possible to see which particular element does not exist.

Method 2 is slightly slower than other approaches. Its disadvantage is that you will always have extra checking greater than the timeout. Its advantage is that the assertion message shows exactly which element does not exist. There is no need to look in more detail at earlier steps in the test run.

Method 3 is as fast as Method 1. Its advantage over Method 2 is speed, though it is necessary to either expand an earlier step in the Test Navigator report or look at line before the assertion in the test log.

My preference is Method 2 since it shows exactly what the issue is only by looking at the assertion message. This makes it possible to show only the assertions emitted by the failed test cases without a large time penalty.

The case can be made that Method 3 is best since speed of test runs, especially as a test suite grows to tens or hundreds of cases is crucial for continuous integration pipelines. You could also argue that a test should fail infrequently, so test run time is more important than best clarity in assertion message.

Thank you for reading. Please leave comments and questions for this post. I check for them regularly.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.