How we're doing it at my current company is we create an XCUIElement expression expectation (to create a versatile wait method). We do it the following way to make sure it's maintainable (lots of expectation variety, and don't want to create lots of methods/specific predicates to do so.
Swift 5
Base method
The expression is used to form a dynamic predicate value. We can create XCTNSPredicateExpectation's from predicates, which we then pass into XCTWaiter to explicitly wait. If the result was anything other than completed, then we fail with an optional message.
@discardableResult
func wait(
until expression: @escaping (XCUIElement) -> Bool,
timeout: TimeInterval = 15,
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
}
Usage
app.buttons["my_button"].wait(until: { $0.exists })
app.buttons["my_button"].wait(until: { $0.isHittable })
Keypaths
Then we wrap that in a method where a keyPath and match value form the expression.
@discardableResult
func wait<Value: Equatable>(
until keyPath: KeyPath<XCUIElement, Value>,
matches match: Value,
timeout: TimeInterval = 15,
message: @autoclosure () -> String = "",
file: StaticString = #file,
line: UInt = #line
) -> Self {
wait(
until: { $0[keyPath: keyPath] == match },
timeout: timeout,
message: message,
file: file,
line: line
)
}
Usage
app.buttons["my_button"].wait(until: \.exists, matches: true)
app.buttons["my_button"].wait(until: \.isHittable, matches: false)
Then you can wrap that method, where the match value is always true for a use case I found most common.
Usage
app.buttons["my_button"].wait(until: \.exists)
app.buttons["my_button"].wait(until: \.isHittable)
I wrote a post about it, and get the full extension file there too: https://sourcediving.com/clean-waiting-in-xcuitest-43bab495230f