Skip to content

Instantly share code, notes, and snippets.

@ryanpato
Last active January 4, 2024 04:05
Show Gist options
  • 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
)
}
}
@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