diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ee505d31..7c5ac5f6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,7 @@ on: jobs: test_xcode10_ios12: name: Run tests on Xcode 10 and iOS 12 - runs-on: macOS-latest + runs-on: macos-10.15 steps: - name: Checkout @@ -54,17 +54,17 @@ jobs: steps: - name: Checkout uses: actions/checkout@v1 - - name: Set Xcode version to 12.1 - run: sudo xcode-select -switch /Applications/Xcode_12.1.app + - name: Set Xcode version to 12.5 + run: sudo xcode-select -switch /Applications/Xcode_12.5.app - name: Build for testing - run: xcodebuild build-for-testing -workspace Example/TABTestKit.xcworkspace -scheme TABTestKit-Example -destination 'platform=iOS Simulator,name=iPhone 11,OS=14.1' - - name: Test on iPhone 11 - run: xcodebuild test-without-building -workspace Example/TABTestKit.xcworkspace -scheme TABTestKit-Example -destination 'platform=iOS Simulator,name=iPhone 11,OS=14.1' + run: xcodebuild build-for-testing -workspace Example/TABTestKit.xcworkspace -scheme TABTestKit-Example -destination 'platform=iOS Simulator,name=iPhone 12,OS=14.5' + - name: Test on iPhone 12 + run: xcodebuild test-without-building -workspace Example/TABTestKit.xcworkspace -scheme TABTestKit-Example -destination 'platform=iOS Simulator,name=iPhone 12,OS=14.5' - name: Archive tests results if: ${{ failure() }} uses: actions/upload-artifact@v2 with: - name: Test-TABTestKit-Xcode12.1-iOS14.xcresult + name: Test-TABTestKit-Xcode12-iOS14.xcresult path: /Users/runner/Library/Developer/Xcode/DerivedData/*/Logs/Test/*.xcresult build_spm: @@ -78,14 +78,14 @@ jobs: run: sudo xcode-select -switch /Applications/Xcode_11.7.app - name: Build Swift Package Manager run: xcodebuild -workspace package.xcworkspace -scheme TABTestKit -destination 'platform=iOS Simulator,name=iPhone 11,OS=13.7' - - name: Set Xcode version to 12.1 - run: sudo xcode-select -switch /Applications/Xcode_12.1.app + - name: Set Xcode version to 12.5 + run: sudo xcode-select -switch /Applications/Xcode_12.5.app - name: Build Swift Package Manager - run: xcodebuild -workspace package.xcworkspace -scheme TABTestKit -destination 'platform=iOS Simulator,name=iPhone 11,OS=14.1' + run: xcodebuild -workspace package.xcworkspace -scheme TABTestKit -destination 'platform=iOS Simulator,name=iPhone 12,OS=14.5' build_carthage: name: Ensure Carthage builds - runs-on: macOS-latest + runs-on: macos-10.15 steps: - name: Checkout @@ -105,7 +105,7 @@ jobs: build_cocoapods: name: Ensure Cocoapods builds - runs-on: macOS-latest + runs-on: macos-10.15 steps: - name: Checkout diff --git a/CHANGELOG.md b/CHANGELOG.md index ed6ea246..0560c625 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,8 @@ # CHANGELOG -## Pending +## 1.8.0 +- Obsoleted the `await` function in Swift 5.5 and added a `waitFor` function because using `await` in Swift 5.5 will lead to ambiguity errors with the `await` keyword. No code changes are required for clients, unless they're on Swift 5.5 and are calling the `await` function in their code. In that case, they will need to update it to `waitFor`. - Upgraded GitHub actions: - Added Xcode 12.1 as part of the job for validating SwiftPM, Carthage and Cocoapods - Temporary allow Cocoapods to valid a library with warning for Xcode 12 diff --git a/README.md b/README.md index d12aab29..2c9ad336 100644 --- a/README.md +++ b/README.md @@ -383,7 +383,7 @@ struct ProfileScreen: Screen { A `Screen` has one required property for you to implement, which is its `trait`. A `trait` can be any [Element](#elements) that consistently, and uniquely, -identifies the screen, and is used to `await` for it to appear on-screen during tests when using [contexts](#contexts). +identifies the screen, and is used to `await`/`waitFor` for it to appear on-screen during tests when using [contexts](#contexts). #### Elements @@ -411,11 +411,19 @@ struct ProfileScreen: Screen { Once you've created your screen, you can use it in tests to interact with the elements: ```swift +// If using Swift 5.4 or below let profileScreen = ProfileScreen() profileScreen.await() // Makes sure the screen is visible before going any further by waiting for its trait profileScreen.logOutButton.tap() // You can't call tap on an element that isn't `Tappable`, but `Button` is! ``` +```swift +// If using Swift 5.5 +let profileScreen = ProfileScreen() +profileScreen.waitFor() // Makes sure the screen is visible before going any further by waiting for its trait +profileScreen.logOutButton.tap() // You can't call tap on an element that isn't `Tappable`, but `Button` is! +``` + All elements conform to the [`Element`](#element) protocol, which ensures that every element has a parent element (defaults to the `App`), underlying `type`, `index` (defaults to `0`), and has an optional ID. @@ -822,9 +830,15 @@ you can use anywhere in your tests called `keyboard`. This is useful for a number of things, like checking if the keyboard is visible: ```swift +// If using Swift 5.4 or below keyboard.await(.visible) ``` +```swift +// If using Swift 5.5 +keyboard.waitFor(.visible) +``` + Checking if the current softare keyboard is the expected type: ```swift @@ -1047,10 +1061,16 @@ You can, however, assert the states of the buttons, like checking if the buttons enabled: ```swift +// If using Swift 5.4 or below stepper.decrementButton.await(not: .enabled, timeout: 1) // Waits a max of 1 second for the button to be disabled ``` -You can learn more about `await(not:)` and other `Element` methods in the +```swift +// If using Swift 5.5 +stepper.decrementButton.waitFor(not: .enabled, timeout: 1) // Waits a max of 1 second for the button to be disabled +``` + +You can learn more about `await(not:)`/`waitFor(not:)` and other `Element` methods in the documentation for [`Element`](#element). #### SegmentedControl @@ -1724,10 +1744,17 @@ that anything conforming to `Element` will have access to. You can wait for the element to be (or not be) in a particular state: ```swift +// If using Swift 5.4 or below button.await(.visible, .enabled, timeout: 10) // You can provide more than one state to wait for :) button.await(not: .enabled, timeout: 10) ``` +```swift +// If using Swift 5.5 +button.waitFor(.visible, .enabled, timeout: 10) // You can provide more than one state to wait for :) +button.waitFor(not: .enabled, timeout: 10) +``` + If the element doesn't become the expected state within the timeout, the test will fail. If you're not using **TABTestKit** [contexts](#contexts), it is extremely advisable to diff --git a/TABTestKit.podspec b/TABTestKit.podspec index 05c87dad..2d9fa09d 100644 --- a/TABTestKit.podspec +++ b/TABTestKit.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TABTestKit' - s.version = '1.7.1' + s.version = '1.8.0' s.summary = 'Strongly typed Swift wrapper around XCTest / XCUI, enabling you to write BDD-style automation tests, without writing much code at all.' s.homepage = 'https://github.com/theappbusiness/TABTestKit' s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TABTestKit/Classes/Contexts/InteractionContext.swift b/TABTestKit/Classes/Contexts/InteractionContext.swift index 5c24414c..b0d14d2e 100644 --- a/TABTestKit/Classes/Contexts/InteractionContext.swift +++ b/TABTestKit/Classes/Contexts/InteractionContext.swift @@ -35,11 +35,19 @@ public extension InteractionContext { } func state(of element: Element, is states: ElementAttributes.State...) { - states.forEach { element.await($0) } + #if swift(>=5.5) + states.forEach { element.waitFor($0) } + #else + states.forEach { element.await($0) } + #endif } func state(of element: Element, isNot states: ElementAttributes.State...) { - states.forEach { element.await(not: $0) } + #if swift(>=5.5) + states.forEach { element.waitFor(not: $0) } + #else + states.forEach { element.await(not: $0) } + #endif } func scroll(_ element: Scrollable, _ direction: ElementAttributes.Direction, until otherElement: Element, is states: ElementAttributes.State..., maxTries: Int = 10) { diff --git a/TABTestKit/Classes/Contexts/NavigationContext.swift b/TABTestKit/Classes/Contexts/NavigationContext.swift index 71c4d5f3..ea2655d5 100644 --- a/TABTestKit/Classes/Contexts/NavigationContext.swift +++ b/TABTestKit/Classes/Contexts/NavigationContext.swift @@ -28,14 +28,22 @@ public extension NavigationContext { /// /// - Parameter element: The element to await. func see(_ element: Element) { - element.await(.exists, .visible) + #if swift(>=5.5) + element.waitFor(.exists, .visible) + #else + element.await(.exists, .visible) + #endif } /// Asserts that an element does not exist, by waiting for it to not exist. /// /// - Parameter element: The element to await. func doNotSee(_ element: Element) { - element.await(not: .exists) + #if swift(>=5.5) + element.waitFor(not: .exists) + #else + element.await(not: .exists) + #endif } /// Completes one or more things that knows how to complete itself. diff --git a/TABTestKit/Classes/Elements/Keyboard.swift b/TABTestKit/Classes/Elements/Keyboard.swift index 5b923e06..5c1ec02d 100644 --- a/TABTestKit/Classes/Elements/Keyboard.swift +++ b/TABTestKit/Classes/Elements/Keyboard.swift @@ -34,7 +34,11 @@ public struct Keyboard: Element { /// The current keyboard type. /// Attempting to access this before the keyboard is visible will fail the test. public var keyboardType: KeyboardType { - await(.exists, .visible) + #if swift(>=5.5) + waitFor(.exists, .visible) + #else + await(.exists, .visible) + #endif guard let type = KeyboardType.allCases.first(where: expectedKeysExist) else { XCTFatalFail("Unable to determine keyboard type") } return type } diff --git a/TABTestKit/Classes/Elements/TabBar.swift b/TABTestKit/Classes/Elements/TabBar.swift index 26139d40..3065e64f 100644 --- a/TABTestKit/Classes/Elements/TabBar.swift +++ b/TABTestKit/Classes/Elements/TabBar.swift @@ -45,7 +45,11 @@ public struct TabBar: Element { extension TabBar { public var numberOfTabs: Int { - await(.exists, .hittable) + #if swift(>=5.5) + waitFor(.exists, .hittable) + #else + await(.exists, .hittable) + #endif return underlyingXCUIElement.buttons.count } diff --git a/TABTestKit/Classes/Protocols/Completable.swift b/TABTestKit/Classes/Protocols/Completable.swift index 8897d4e8..98c3f3c2 100644 --- a/TABTestKit/Classes/Protocols/Completable.swift +++ b/TABTestKit/Classes/Protocols/Completable.swift @@ -13,8 +13,8 @@ import Foundation /// /// This works particularly well with NavigationContext. public protocol Completable { - + func await() func complete() - + } diff --git a/TABTestKit/Classes/Protocols/Dismissable.swift b/TABTestKit/Classes/Protocols/Dismissable.swift index 3e4d2cea..d4d9ca92 100644 --- a/TABTestKit/Classes/Protocols/Dismissable.swift +++ b/TABTestKit/Classes/Protocols/Dismissable.swift @@ -22,7 +22,11 @@ public protocol Dismissable { public extension Element where Self: Dismissable { func await() { - await(.exists, .hittable) + #if swift(>=5.5) + waitFor(.exists, .hittable) + #else + await(.exists, .hittable) + #endif } } diff --git a/TABTestKit/Classes/Protocols/Editable.swift b/TABTestKit/Classes/Protocols/Editable.swift index f2d13696..a9c9f22a 100644 --- a/TABTestKit/Classes/Protocols/Editable.swift +++ b/TABTestKit/Classes/Protocols/Editable.swift @@ -21,12 +21,20 @@ public protocol Editable { public extension Element where Self: Editable { func type(_ text: String) { - await(.exists, .hittable) + #if swift(>=5.5) + waitFor(.exists, .hittable) + #else + await(.exists, .hittable) + #endif underlyingXCUIElement.typeText(text) } func delete(numberOfCharacters: Int) { - await(.exists, .hittable) + #if swift(>=5.5) + waitFor(.exists, .hittable) + #else + await(.exists, .hittable) + #endif let deletionCharacters = String(repeating: XCUIKeyboardKey.delete.rawValue, count: numberOfCharacters) underlyingXCUIElement.typeText(deletionCharacters) } diff --git a/TABTestKit/Classes/Protocols/Element.swift b/TABTestKit/Classes/Protocols/Element.swift index 6db3342a..4f47e4bb 100644 --- a/TABTestKit/Classes/Protocols/Element.swift +++ b/TABTestKit/Classes/Protocols/Element.swift @@ -68,27 +68,60 @@ public extension Element { /// /// - Parameter states: The states to wait for. /// - Parameter timeout: The timeout. Defaults to 30 seconds. + @available(swift, introduced: 5.0, obsoleted: 5.5, renamed: "waitFor", message: "This method has been obsoleted to avoid conflicts with the new await concurrency keyword") func await(_ states: ElementAttributes.State..., timeout: TimeInterval = 30) { - guard !states.isEmpty else { XCTFatalFail("You must provide at least one state!") } - states.forEach { state in - XCTAssertTrue(determine(state, timeout: timeout), "Failed awaiting element to be \(state) with timeout \(timeout)") - } - } - - /// Converse to the `await(_ states...` function, this waits for the element to _not_ be in the states - /// provided. - /// For example, you could use this to wait for an element that you're expecting to become not hittable: - /// `await(not: .hittable)` - /// - /// - Parameters: - /// - states: The states to wait for the element to _not_ be in. - /// - timeout: The timout. Defaults to 30 seconds. - func await(not states: ElementAttributes.State..., timeout: TimeInterval = 30) { - guard !states.isEmpty else { XCTFatalFail("You must provide at least one state!") } - states.forEach { state in - XCTAssertTrue(determine(not: state, timeout: timeout), "Failed awaiting element to not be \(state) with timeout \(timeout)") - } + guard !states.isEmpty else { XCTFatalFail("You must provide at least one state!") } + states.forEach { state in + XCTAssertTrue(determine(state, timeout: timeout), "Failed awaiting element to be \(state) with timeout \(timeout)") + } } + + /// Converse to the `await(_ states...` function, this waits for the element to _not_ be in the states + /// provided. + /// For example, you could use this to wait for an element that you're expecting to become not hittable: + /// `await(not: .hittable)` + /// + /// - Parameters: + /// - states: The states to wait for the element to _not_ be in. + /// - timeout: The timout. Defaults to 30 seconds. + @available(swift, introduced: 5.0, obsoleted: 5.5, renamed: "waitFor", message: "This method has been obsoleted to avoid conflicts with the new await concurrency keyword") + func await(not states: ElementAttributes.State..., timeout: TimeInterval = 30) { + guard !states.isEmpty else { XCTFatalFail("You must provide at least one state!") } + states.forEach { state in + XCTAssertTrue(determine(not: state, timeout: timeout), "Failed awaiting element to be \(state) with timeout \(timeout)") + } + } + + /// Waits for the provided states to be true with a max timeout. + /// Unlike the standard `determine` function which returns the state after a max duration, this function will fail the test if any of the states do not become true before the timeout. + /// + /// You can provide multiple states, like `waitFor(.exists, .hittable)` + /// + /// - Parameter states: The states to wait for. + /// - Parameter timeout: The timeout. Defaults to 30 seconds. + @available(swift, introduced: 5.5) + func waitFor(_ states: ElementAttributes.State..., timeout: TimeInterval = 30) { + guard !states.isEmpty else { XCTFatalFail("You must provide at least one state!") } + states.forEach { state in + XCTAssertTrue(determine(state, timeout: timeout), "Failed awaiting element to be \(state) with timeout \(timeout)") + } + } + + /// Converse to the `waitFor(_ states...` function, this waits for the element to _not_ be in the states + /// provided. + /// For example, you could use this to wait for an element that you're expecting to become not hittable: + /// `waitFor(not: .hittable)` + /// + /// - Parameters: + /// - states: The states to wait for the element to _not_ be in. + /// - timeout: The timout. Defaults to 30 seconds. + @available(swift, introduced: 5.5) + func waitFor(not states: ElementAttributes.State..., timeout: TimeInterval = 30) { + guard !states.isEmpty else { XCTFatalFail("You must provide at least one state!") } + states.forEach { state in + XCTAssertTrue(determine(not: state, timeout: timeout), "Failed awaiting element to not be \(state) with timeout \(timeout)") + } + } /// Determines the sates for an element, within a a maximum duration. /// If the element becomes (or already is) in the correct state this function will exit early, diff --git a/TABTestKit/Classes/Protocols/Screen.swift b/TABTestKit/Classes/Protocols/Screen.swift index da3f5c59..88294d02 100644 --- a/TABTestKit/Classes/Protocols/Screen.swift +++ b/TABTestKit/Classes/Protocols/Screen.swift @@ -26,7 +26,11 @@ public protocol Screen { public extension Screen { func await() { - trait.await(.exists, .hittable, .visible) + #if swift(>=5.5) + trait.waitFor(.exists, .hittable, .visible) + #else + trait.await(.exists, .hittable, .visible) + #endif } } diff --git a/TABTestKit/Classes/Protocols/Scrollable.swift b/TABTestKit/Classes/Protocols/Scrollable.swift index 92de7fb9..bacb67df 100644 --- a/TABTestKit/Classes/Protocols/Scrollable.swift +++ b/TABTestKit/Classes/Protocols/Scrollable.swift @@ -21,7 +21,11 @@ public protocol Scrollable { public extension Element where Self: Scrollable { func scroll(_ direction: ElementAttributes.Direction) { - await(.exists, .hittable) + #if swift(>=5.5) + waitFor(.exists, .hittable) + #else + await(.exists, .hittable) + #endif switch direction { case .upwards: scroll(from: .topThird, to: .middle) diff --git a/TABTestKit/Classes/Protocols/Tappable.swift b/TABTestKit/Classes/Protocols/Tappable.swift index 9e58db80..f2a0bf2b 100644 --- a/TABTestKit/Classes/Protocols/Tappable.swift +++ b/TABTestKit/Classes/Protocols/Tappable.swift @@ -21,22 +21,38 @@ public protocol Tappable { public extension Element where Self: Tappable { func tap() { - await(.exists) + #if swift(>=5.5) + waitFor(.exists) + #else + await(.exists) + #endif underlyingXCUIElement.tap() } func doubleTap() { - await(.exists) + #if swift(>=5.5) + waitFor(.exists) + #else + await(.exists) + #endif underlyingXCUIElement.doubleTap() } func twoFingerTap() { - await(.exists) + #if swift(>=5.5) + waitFor(.exists) + #else + await(.exists) + #endif underlyingXCUIElement.twoFingerTap() } func longPress(duration: TimeInterval) { - await(.exists) + #if swift(>=5.5) + waitFor(.exists) + #else + await(.exists) + #endif underlyingXCUIElement.press(forDuration: duration) }