Skip to content

Instantly share code, notes, and snippets.

@ileitch
Created April 28, 2020 18:06
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save ileitch/cb9b00935a43615b45192e653ad9c0c1 to your computer and use it in GitHub Desktop.
Save ileitch/cb9b00935a43615b45192e653ad9c0c1 to your computer and use it in GitHub Desktop.
import XCTest
public enum TestWaitResult {
case wait, success
}
public enum WaitTimeoutCondition {
case fail, skip
}
public func waitFor(interval: TimeInterval) {
CFRunLoopRunInMode(CFRunLoopMode.defaultMode, interval, false)
}
public func waitForAnimations() {
waitFor(interval: 0.3)
}
public func waitUntil(_ closure: () -> Bool, timeoutCondition: WaitTimeoutCondition = .fail, file: StaticString = #file, line: UInt = #line) {
waitOrSkip(timeoutCondition: timeoutCondition, file: file, line: line, block: { closure() ? .success : .wait })
}
public func waitFor(timeout: TimeInterval = 10, message: String = "Timeout waiting for condition", file: StaticString = #file, line: UInt = #line, block: () -> TestWaitResult) {
waitOrSkip(timeoutCondition: .fail, timeout: timeout, message: message, file: file, line: line, block: block)
}
public func waitOrSkip(timeoutCondition: WaitTimeoutCondition,
timeout: TimeInterval = 10,
message: String = "Timeout waiting for condition",
file: StaticString = #file,
line: UInt = #line,
block: () -> TestWaitResult,
successBlock: (() -> Void)? = nil) {
let timeoutMs = UInt(timeout * 1000)
let startedAt = getAbsoluteTimeMs()
var result: TestWaitResult = .wait
while (getAbsoluteTimeMs() - startedAt) < timeoutMs {
result = block()
if result == .success {
successBlock?()
break
}
CFRunLoopRunInMode(CFRunLoopMode.defaultMode, 0.1, false)
result = block()
if result == .success {
successBlock?()
break
}
}
if timeoutCondition == .fail, result == .wait {
XCTFail("\(message)", file: file, line: line)
}
}
private func getAbsoluteTimeMs() -> UInt {
var info = mach_timebase_info(numer: 0, denom: 0)
mach_timebase_info(&info)
let numer = UInt64(info.numer)
let denom = UInt64(info.denom)
let nanoseconds = (mach_absolute_time() * numer) / denom
return UInt(nanoseconds / NSEC_PER_MSEC)
}
public extension XCUIApplication {
func scrollDownTo(_ element: XCUIElement, maxScrolls: Int = 5) {
guard !element.isHittable else { return }
for _ in 0 ..< maxScrolls {
scrollDown()
if element.isHittable {
break
}
}
}
func scrollUpTo(_ element: XCUIElement, maxScrolls: Int = 5) {
guard !element.isHittable else { return }
for _ in 0 ..< maxScrolls {
scrollUp()
if element.isHittable {
break
}
}
}
func scrollDown() {
// Drag from about half way down the screen to almost the top to make sure that the drag does not interact with the keyboard.
let fromCoordinate = windows.firstMatch.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.4))
let toCoordinate = windows.firstMatch.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.1))
fromCoordinate.press(forDuration: 0, thenDragTo: toCoordinate)
}
func scrollUp() {
// Drag from about half way down the screen to near the bottom
let fromCoordinate = windows.firstMatch.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.4))
let toCoordinate = windows.firstMatch.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.9))
fromCoordinate.press(forDuration: 0, thenDragTo: toCoordinate)
}
func pullToRefresh() {
// Drag from the top quarter of the screen to the bottom quarter.
let fromCoordinate = windows.firstMatch.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.25))
let toCoordinate = windows.firstMatch.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.75))
fromCoordinate.press(forDuration: 0, thenDragTo: toCoordinate)
}
func navigateBack() {
// The back button tends to be the first button of the navigation bar
navigationBars.buttons.element(boundBy: 0).waitAndTap()
}
func tapKeyboardDoneButton() {
buttons["Done"].firstMatch.waitAndTap()
keyboards.element.waitUntilDoesntExist()
}
func tapPickerDoneButton() {
buttons["Done"].firstMatch.waitAndTap()
pickers.element.waitUntilDoesntExist()
}
}
public extension XCUIElement {
func waitUntilExists(timeout: TimeInterval = 10, file: StaticString = #file, line: UInt = #line) {
waitFor(timeout: timeout, file: file, line: line) { exists ? .success : .wait }
}
func waitUntilDoesntExist(file: StaticString = #file, line: UInt = #line) {
waitFor(file: file, line: line) { !exists ? .success : .wait }
}
func waitUntilEnabled(file: StaticString = #file, line: UInt = #line) {
waitUntilExists(file: file, line: line)
waitFor(file: file, line: line) { isEnabled ? .success : .wait }
}
func waitUntilDisabled(file: StaticString = #file, line: UInt = #line) {
waitUntilExists(file: file, line: line)
waitFor(file: file, line: line) { !isEnabled ? .success : .wait }
}
func waitAndTap(file: StaticString = #file, line: UInt = #line) {
waitUntilExists(file: file, line: line)
waitFor(file: file, line: line) { isEnabled && isHittable ? .success : .wait }
tap()
}
func waitForFocus(file: StaticString = #file, line: UInt = #line) {
waitFor(file: file, line: line) { accessibilityElementIsFocused() ? .wait : .success }
}
func waitForFocusAndType(_ text: String, file: StaticString = #file, line: UInt = #line) {
waitForFocus(file: file, line: line)
waitFor(file: file, line: line) { accessibilityElementIsFocused() ? .wait : .success }
waitFor(file: file, line: line) { XCUIApplication().keyboards.isEmpty ? .wait : .success }
typeText(text)
}
func tapAndType(_ text: String, file: StaticString = #file, line: UInt = #line) {
if !isHittable {
XCUIApplication().scrollDownTo(self)
if !isHittable {
XCUIApplication().scrollUpTo(self)
if !isHittable {
XCTFail("\(self) is not hittable and it wasn't possible to drag it into view.", file: file, line: line)
}
}
}
tap()
waitFor(file: file, line: line) { accessibilityElementIsFocused() ? .wait : .success }
waitFor(file: file, line: line) { XCUIApplication().keyboards.isEmpty ? .wait : .success }
typeText(text)
}
func waitAndPerformIfExists(timeout: TimeInterval = 10, file: StaticString = #file, line: UInt = #line, block: @escaping () -> Void) {
waitOrSkip(timeoutCondition: .skip, timeout: timeout, file: file, line: line, block: { exists ? .success : .wait }, successBlock: block)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment