From d5c95e2858d94571ddae764263abfce6cb8e7c66 Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Thu, 26 Aug 2021 14:42:59 +1000 Subject: [PATCH 1/6] Refactor UI test to prepare for multiple tests --- Tests/TestAppUITests/TestAppUITests.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Tests/TestAppUITests/TestAppUITests.swift b/Tests/TestAppUITests/TestAppUITests.swift index 43605a1..c8a357b 100644 --- a/Tests/TestAppUITests/TestAppUITests.swift +++ b/Tests/TestAppUITests/TestAppUITests.swift @@ -3,14 +3,14 @@ import XCTest class TestAppUITests: XCTestCase { + let app = XCUIApplication() + override func setUpWithError() throws { + app.launch() continueAfterFailure = false } - func testExample() throws { - let app = XCUIApplication() - app.launch() - + func testIsLoadedReturnsTrueWhenScreenIsLoaded() throws { let screen = try HelloWorldScreen() XCTAssertTrue(screen.isLoaded) } From ece1e7ef21ff5b2e3a7a279c78e4f91ad543fdc1 Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Thu, 26 Aug 2021 14:45:57 +1000 Subject: [PATCH 2/6] Add test for current `init` behavior --- Tests/TestAppUITests/TestAppUITests.swift | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Tests/TestAppUITests/TestAppUITests.swift b/Tests/TestAppUITests/TestAppUITests.swift index c8a357b..162d270 100644 --- a/Tests/TestAppUITests/TestAppUITests.swift +++ b/Tests/TestAppUITests/TestAppUITests.swift @@ -14,6 +14,11 @@ class TestAppUITests: XCTestCase { let screen = try HelloWorldScreen() XCTAssertTrue(screen.isLoaded) } + + func testScreenInitFailsWhenScreenIsNotLoaded() throws { + XCTExpectFailure("Attempting to init a screen that is not loaded currently fails the tests") + _ = try MissingScreen(app: app) + } } final class HelloWorldScreen: ScreenObject { @@ -22,3 +27,15 @@ final class HelloWorldScreen: ScreenObject { try super.init(expectedElementGetter: { $0.staticTexts["Hello, world!"] }) } } + +/// A screen that doesn't exist. Use it to test the init failure behavior. +class MissingScreen: ScreenObject { + + init(app: XCUIApplication) throws { + try super.init( + expectedElementGetter: { $0.staticTexts["this screen does not exist"] }, + app: app, + waitTimeout: 1 + ) + } +} From 4547368decf2d055ff4699bd3876073adb9ee680 Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Thu, 26 Aug 2021 14:47:59 +1000 Subject: [PATCH 3/6] Actually use stored timeout in `ScreenObject` `waitForScreen` --- Sources/ScreenObject/ScreenObject.swift | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Sources/ScreenObject/ScreenObject.swift b/Sources/ScreenObject/ScreenObject.swift index eaec62d..c6b9481 100644 --- a/Sources/ScreenObject/ScreenObject.swift +++ b/Sources/ScreenObject/ScreenObject.swift @@ -35,15 +35,19 @@ open class ScreenObject { @discardableResult func waitForScreen() throws -> Self { XCTContext.runActivity(named: "Confirm screen \(self) is loaded") { (activity) in - let result = waitFor(element: expectedElement, predicate: "isEnabled == true", timeout: 20) + let result = waitFor( + element: expectedElement, + predicate: "isEnabled == true", + timeout: self.waitTimeout + ) XCTAssert(result, "Screen \(self) is not loaded.") } return self } - private func waitFor(element: XCUIElement, predicate: String, timeout: Int = 5) -> Bool { + private func waitFor(element: XCUIElement, predicate: String, timeout: TimeInterval = 5) -> Bool { let elementPredicate = XCTNSPredicateExpectation(predicate: NSPredicate(format: predicate), object: element) - let result = XCTWaiter.wait(for: [elementPredicate], timeout: TimeInterval(timeout)) + let result = XCTWaiter.wait(for: [elementPredicate], timeout: timeout) return result == .completed } From 58b48052788759be4f055026192601cdeac96b62 Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Thu, 26 Aug 2021 15:12:35 +1000 Subject: [PATCH 4/6] Make `init` simply `throw` instead of failing the tests when not loaded --- Sources/ScreenObject/ScreenObject.swift | 9 +++++++-- Tests/TestAppUITests/TestAppUITests.swift | 10 +++++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/Sources/ScreenObject/ScreenObject.swift b/Sources/ScreenObject/ScreenObject.swift index c6b9481..607833d 100644 --- a/Sources/ScreenObject/ScreenObject.swift +++ b/Sources/ScreenObject/ScreenObject.swift @@ -6,6 +6,10 @@ import XCTest // and those are obviously specific to each screen, hence should be added by each subclass. open class ScreenObject { + public enum WaitForScreenError: Equatable, Error { + case timedOut + } + /// The `XCUIApplication` instance this screen is part of. This is the value passed at /// initialization time. public let app: XCUIApplication @@ -34,13 +38,14 @@ open class ScreenObject { @discardableResult func waitForScreen() throws -> Self { - XCTContext.runActivity(named: "Confirm screen \(self) is loaded") { (activity) in + try XCTContext.runActivity(named: "Confirm screen \(self) is loaded") { (activity) in let result = waitFor( element: expectedElement, predicate: "isEnabled == true", timeout: self.waitTimeout ) - XCTAssert(result, "Screen \(self) is not loaded.") + + guard result else { throw WaitForScreenError.timedOut } } return self } diff --git a/Tests/TestAppUITests/TestAppUITests.swift b/Tests/TestAppUITests/TestAppUITests.swift index 162d270..3fe859a 100644 --- a/Tests/TestAppUITests/TestAppUITests.swift +++ b/Tests/TestAppUITests/TestAppUITests.swift @@ -15,9 +15,13 @@ class TestAppUITests: XCTestCase { XCTAssertTrue(screen.isLoaded) } - func testScreenInitFailsWhenScreenIsNotLoaded() throws { - XCTExpectFailure("Attempting to init a screen that is not loaded currently fails the tests") - _ = try MissingScreen(app: app) + func testScreenInitThrowsWhenScreenIsNotLoaded() throws { + do { + _ = try MissingScreen(app: app) + XCTFail("Expected `ScreenObject` `init` to throw, but it didn't") + } catch { + XCTAssertEqual(error as? ScreenObject.WaitForScreenError, .timedOut) + } } } From 93fc87c5f74990a9aabcc25d22e8b2c97745bcac Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Thu, 26 Aug 2021 15:13:44 +1000 Subject: [PATCH 5/6] Remove unnecessary default parameter in private method --- Sources/ScreenObject/ScreenObject.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ScreenObject/ScreenObject.swift b/Sources/ScreenObject/ScreenObject.swift index 607833d..72ef3f0 100644 --- a/Sources/ScreenObject/ScreenObject.swift +++ b/Sources/ScreenObject/ScreenObject.swift @@ -50,7 +50,7 @@ open class ScreenObject { return self } - private func waitFor(element: XCUIElement, predicate: String, timeout: TimeInterval = 5) -> Bool { + private func waitFor(element: XCUIElement, predicate: String, timeout: TimeInterval) -> Bool { let elementPredicate = XCTNSPredicateExpectation(predicate: NSPredicate(format: predicate), object: element) let result = XCTWaiter.wait(for: [elementPredicate], timeout: timeout) From 650e4a79f3bcd09c50335ef6d43ca1fafc2673bf Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Thu, 26 Aug 2021 15:16:17 +1000 Subject: [PATCH 6/6] Use `XCTWaiter.Result` directly to evaluate wait for screen outcome This removes a layer of abstraction. I haven't inlined the entire function yet just because I can see us using that flexible wait method in the future. --- Sources/ScreenObject/ScreenObject.swift | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/Sources/ScreenObject/ScreenObject.swift b/Sources/ScreenObject/ScreenObject.swift index 72ef3f0..222b3d8 100644 --- a/Sources/ScreenObject/ScreenObject.swift +++ b/Sources/ScreenObject/ScreenObject.swift @@ -45,15 +45,24 @@ open class ScreenObject { timeout: self.waitTimeout ) - guard result else { throw WaitForScreenError.timedOut } + guard result == .completed else { throw WaitForScreenError.timedOut } } return self } - private func waitFor(element: XCUIElement, predicate: String, timeout: TimeInterval) -> Bool { - let elementPredicate = XCTNSPredicateExpectation(predicate: NSPredicate(format: predicate), object: element) - let result = XCTWaiter.wait(for: [elementPredicate], timeout: timeout) - - return result == .completed + private func waitFor( + element: XCUIElement, + predicate: String, + timeout: TimeInterval + ) -> XCTWaiter.Result { + XCTWaiter.wait( + for: [ + XCTNSPredicateExpectation( + predicate: NSPredicate(format: predicate), + object: element + ) + ], + timeout: timeout + ) } }