Skip to content

Instantly share code, notes, and snippets.

@ryanpato
Last active January 4, 2024 04:05
Show Gist options
  • Star 16 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save ryanpato/5f060c5877bce6138aab88759cadc0c8 to your computer and use it in GitHub Desktop.
Save ryanpato/5f060c5877bce6138aab88759cadc0c8 to your computer and use it in GitHub Desktop.
XCUIElement+Wait
//
// XCUIElement+Wait.swift
//
// Created by Ryan Paterson on 12/12/2020.
//
import XCTest
extension XCUIElement {
/// The period of time in seconds to wait explicitly for expectations.
static let waitTimeout: TimeInterval = 15
/// Explicitly wait until either `expression` evaluates to `true` or the `timeout` expires.
///
/// If the condition fails to evaluate before the timeout expires, a failure will be recorded.
///
/// **Example Usage:**
///
/// ```
/// element.wait(until: { !$0.exists })
/// element.wait(until: { _ in otherElement.isEnabled }, timeout: 5)
/// ```
///
/// - Parameters:
/// - expression: The expression that should be evaluated before the timeout expires.
/// - timeout: The specificied amount of time to check for the given condition to match the expected value.
/// - message: An optional description of a failure.
/// - Returns: The XCUIElement.
@discardableResult
func wait(
until expression: @escaping (XCUIElement) -> Bool,
timeout: TimeInterval = waitTimeout,
message: @autoclosure () -> String = "",
file: StaticString = #file,
line: UInt = #line
) -> Self {
if expression(self) {
return self
}
let predicate = NSPredicate { _, _ in
expression(self)
}
let expectation = XCTNSPredicateExpectation(predicate: predicate, object: nil)
let result = XCTWaiter().wait(for: [expectation], timeout: timeout)
if result != .completed {
XCTFail(
message().isEmpty ? "expectation not matched after waiting" : message(),
file: file,
line: line
)
}
return self
}
/// Explicitly wait until the value of the given `keyPath` equates to `match`.
///
/// If the `keyPath` fails to match before the timeout expires, a failure will be recorded.
///
/// **Example Usage:**
///
/// ```
/// element.wait(until: \.isEnabled, matches: false)
/// element.wait(until: \.label, matches: "Downloading...", timeout: 5)
/// ```
///
/// - Parameters:
/// - keyPath: A key path to the property of the receiver that should be evaluated.
/// - match: The value that the receivers key path should equal.
/// - timeout: The specificied amount of time to check for the given condition to match the expected value.
/// - message: An optional description of a failure.
/// - Returns: The XCUIElement.
@discardableResult
func wait<Value: Equatable>(
until keyPath: KeyPath<XCUIElement, Value>,
matches match: Value,
timeout: TimeInterval = waitTimeout,
message: @autoclosure () -> String = "",
file: StaticString = #file,
line: UInt = #line
) -> Self {
wait(
until: { $0[keyPath: keyPath] == match },
timeout: timeout,
message: message,
file: file,
line: line
)
}
/// Explicitly wait until the value of the value of the given `keyPath` equals `true`.
///
/// If the `keyPath` fails to match before the timeout expires, a failure will be recorded.
///
/// **Example Usage:**
///
/// ```
/// element.wait(until: \.exists)
/// element.wait(until: \.exists, timeout: 5)
/// ```
///
/// - Parameters:
/// - keyPath: The KeyPath that represents which property of the receiver should be evaluated.
/// - timeout: The specificied amount of time to check for the given condition to match the expected value.
/// - message: An optional description of a failure.
/// - Returns: The XCUIElement.
func wait(
until keyPath: KeyPath<XCUIElement, Bool>,
timeout: TimeInterval = waitTimeout,
message: @autoclosure () -> String = "",
file: StaticString = #file,
line: UInt = #line
) -> Self {
wait(
until: keyPath,
matches: true,
timeout: timeout,
message: message(),
file: file,
line: line
)
}
/// Explicitly wait until the value of the given `keyPath` does not equate to `match`.
///
/// If the `keyPath` fails to not match before the timeout expires, a failure will be recorded.
///
/// **Example Usage:**
///
/// ```
/// element.wait(until: \.isEnabled, doesNotMatch: false)
/// element.wait(until: \.label, doesNotMatch: "Downloading...", timeout: 5)
/// ```
///
/// - Parameters:
/// - keyPath: A key path to the property of the receiver that should be evaluated.
/// - match: The value that the receivers key path should not equal.
/// - timeout: The specificied amount of time to check for the given condition to match the expected value.
/// - message: An optional description of a failure.
/// - Returns: The XCUIElement.
@discardableResult
func wait<Value: Equatable>(
until keyPath: KeyPath<XCUIElement, Value>,
doesNotMatch match: Value,
timeout: TimeInterval = waitTimeout,
message: @autoclosure () -> String = "",
file: StaticString = #file,
line: UInt = #line
) -> Self {
wait(
until: { $0[keyPath: keyPath] != match },
timeout: timeout,
message: message(),
file: file,
line: line
)
}
}
@v-i-s-h-a-l
Copy link

v-i-s-h-a-l commented Apr 8, 2021

Hey Ryan

Firstly thanks for sharing this awesome gist. I found it very useful. 👍

I fell into the need of a different kind of usage. It is just opposite to the current match function. I wanted to wait for an element's value to change from an existing value within the timeout.

For example, if a label has text "ABC" and an action is performed, then it would wait till the text is not "ABC" but anything else.

Do you think this can be added to the gist? Or is there a way to achieve it with the existing implementation.
Here is my proposed addition:

    /// Explicitly wait until the value of the given `keyPath` does not equate to `match`.
    ///
    /// If the `keyPath` fails to not match before the timeout expires, a failure will be recorded.
    ///
    /// **Example Usage:**
    ///
    /// ```
    /// element.wait(until: \.isEnabled, doesNotMatch: false)
    /// element.wait(until: \.label, doesNotMatch: "Downloading...", timeout: 5)
    /// ```
    ///
    /// - Parameters:
    ///   - keyPath: A key path to the property of the receiver that should be evaluated.
    ///   - match: The value that the receivers key path should not equal.
    ///   - timeout: The specificied amount of time to check for the given condition to match the expected value.
    ///   - message: An optional description of a failure.
    /// - Returns: The XCUIElement.
    @discardableResult
    func wait<Value: Equatable>(
        until keyPath: KeyPath<XCUIElement, Value>,
        doesNotMatch match: Value,
        timeout: TimeInterval = waitTimeout,
        message: @autoclosure () -> String = "",
        file: StaticString = #file,
        line: UInt = #line
    ) -> Self {
        wait(
            until: { $0[keyPath: keyPath] != match },
            timeout: timeout,
            message: message(),
            file: file,
            line: line
        )
    }

@ryanpato
Copy link
Author

ryanpato commented Apr 8, 2021

Hey v-i-s-h-a-l

Glad to hear it's proving useful!! Really appreciate the message!

I believe what you're looking for can be done with the current extension methods by using a closure.

Example:

app.staticTexts["identifier"].wait(until: {
    $0.label != "expected-text"
})

However, it's not the prettiest code to look at 😅 so if you've got many use-cases, like you've demonstrated we can just add a new custom method for this kind of thing too by flipping the expression operator. I've updated the extension to add the doesNotMatch variation of the method! Thank you for using and sharing, hope this helps!!

Example:

app.staticTexts["progress-status-label"]
    .wait(until: \.label, matches: "downloading...")
    .wait(until: \.label, doesNotMatch: "downloading...")

@v-i-s-h-a-l
Copy link

Thank you 👍

@ZhanatM
Copy link

ZhanatM commented Jun 3, 2021

Hey Ryan,

thank you very much for sharing this approach. I found it very helpful in my project:)

I was just wondering how you handle assertions. Do you use this waitings for assertions as well? Because mostly in assertions we verify if it some element exists or hittable.

@ryanpato
Copy link
Author

ryanpato commented Jun 7, 2021

Hey ZhanatM,

Really glad to hear its being useful! I prefer to use standard assertions to keep things clean when I can, but i'll admit I often rely on waitings for assertions too. However, I mention here that you could perhaps return a bool for success/failure instead of self.

In which case, maybe your assertions could look like:

let button = app.buttons["my_btn_id"]
XCTAssertTrue(button.wait(until: \.isHittable))

Which is very similar to:

// 
let timeout = 5
let button = app.buttons["my_btn_id"]
let result = button.waitForExistance(timeout: timeout)
XCTAssertTrue(result)

// 
let timeout = 5
let button = app.buttons["my_btn_id"]
XCTAssertTrue(button.waitForExistance(timeout: timeout))

I hope that helps anyway. I would be interested to know how you decide to use it!

@ZhanatM
Copy link

ZhanatM commented Jun 18, 2021

Hey Rayn,

thank you very much for answer, I also thought about the same way:)

@mohamedwasiq
Copy link

Thank you !

@ZhanatM
Copy link

ZhanatM commented May 5, 2022

Hi Ryan,

thank you again for your work:)

I faced an issue in my project. For example, this usage of a wait if the label matches a specific string:

app.staticTexts["progress-status-label"]
    .wait(until: \.label, matches: "downloading...")

I noticed, that it checks if the label is matching the given string, but it doesn't check first if static text exists, and then it should compare. So to avoid flakiness, it should look like this:

app.staticTexts["progress-status-label"]
    .wait(until: \.exists)
    .wait(until: \.label, matches: "downloading...")

Is it possible to add check if XCUIElement exists in equatable wait, where we compare two values? I couldn't come up with changes there by myself:(
Here:

@discardableResult
    func wait<Value: Equatable>(until keyPath: KeyPath<XCUIElement, Value>,
                                matches match: Value,
                                timeout: TimeInterval = waitTimeout,
                                message: @autoclosure () -> String = "",
                                file: StaticString = #file,
                                line: UInt = #line) -> Self {
        wait(until: { $0[keyPath: keyPath] == match },
             timeout: timeout,
             message: message(),
             file: file,
             line: line)
    }

@ryanpato
Copy link
Author

Hi @ZhanatM, sorry for the late reply.

Interesting point! Yeah lines #37 and #42 both check the expression (i.e label == match) but the element it's querying might not exist yet (your circumstance), so XCUITest attempts to find it first for you and fails after a few attempts.

t =     3.16s Find the StaticText
t =     4.19s     Find the StaticText (retry 1)
t =     5.20s     Find the StaticText (retry 2)

For the time being you could simply add a check in both instances that the element exists first.

// You can maybe even remove this initial check. Added for 'performance' but don't really know how much time it saves if any.
if exists && expression(self){
    return self
}

// This is needed, but if you always want it to exist first you can just append it here :shrug: 
let predicate = NSPredicate { _, _ in
    self.exists && expression(self)
}

I won't update the gist just yet as i'm not sure of any side-effects this may have for people's usage. But really I should probably just create a proper SPM package for handy XCUITest extensions like this and test them properly (performance too) 😅 maybe i'll do that soon

@ZhanatM
Copy link

ZhanatM commented Jun 1, 2022

@ryanpato thank you Ryan for reply:)
I will try out👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment