diff --git a/Features.xcodeproj/project.pbxproj b/Features.xcodeproj/project.pbxproj index 93c7b00..51d73e7 100644 --- a/Features.xcodeproj/project.pbxproj +++ b/Features.xcodeproj/project.pbxproj @@ -32,6 +32,14 @@ 64A6131A2CD56A5F00FC2BF2 /* APICallsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A613192CD56A5F00FC2BF2 /* APICallsView.swift */; }; 64DA2B6B2C741B820025D34B /* TextFieldView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64DA2B6A2C741B820025D34B /* TextFieldView.swift */; }; 64F102DB2C98913D00EE2E47 /* ApplePayButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F102DA2C98913D00EE2E47 /* ApplePayButtonView.swift */; }; + A1B2C3012F70000100AA0001 /* BaseUITestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3112F70000100AA0001 /* BaseUITestCase.swift */; }; + A1B2C3012F70000100AA0002 /* HomeScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3112F70000100AA0002 /* HomeScreen.swift */; }; + A1B2C3012F70000100AA0003 /* AlertScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3112F70000100AA0003 /* AlertScreen.swift */; }; + A1B2C3012F70000100AA0004 /* TextFieldScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3112F70000100AA0004 /* TextFieldScreen.swift */; }; + A1B2C3012F70000100AA0005 /* APICallsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3112F70000100AA0005 /* APICallsScreen.swift */; }; + A1B2C3012F70000100AA0006 /* AlertScreenTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3112F70000100AA0006 /* AlertScreenTests.swift */; }; + A1B2C3012F70000100AA0007 /* TextFieldScreenTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3112F70000100AA0007 /* TextFieldScreenTests.swift */; }; + A1B2C3012F70000100AA0008 /* APICallsScreenTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3112F70000100AA0008 /* APICallsScreenTests.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -80,6 +88,14 @@ 64A613192CD56A5F00FC2BF2 /* APICallsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APICallsView.swift; sourceTree = ""; }; 64DA2B6A2C741B820025D34B /* TextFieldView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldView.swift; sourceTree = ""; }; 64F102DA2C98913D00EE2E47 /* ApplePayButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplePayButtonView.swift; sourceTree = ""; }; + A1B2C3112F70000100AA0001 /* BaseUITestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseUITestCase.swift; sourceTree = ""; }; + A1B2C3112F70000100AA0002 /* HomeScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreen.swift; sourceTree = ""; }; + A1B2C3112F70000100AA0003 /* AlertScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertScreen.swift; sourceTree = ""; }; + A1B2C3112F70000100AA0004 /* TextFieldScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldScreen.swift; sourceTree = ""; }; + A1B2C3112F70000100AA0005 /* APICallsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APICallsScreen.swift; sourceTree = ""; }; + A1B2C3112F70000100AA0006 /* AlertScreenTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertScreenTests.swift; sourceTree = ""; }; + A1B2C3112F70000100AA0007 /* TextFieldScreenTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldScreenTests.swift; sourceTree = ""; }; + A1B2C3112F70000100AA0008 /* APICallsScreenTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APICallsScreenTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -176,12 +192,36 @@ children = ( 532E1C392F2B0F800074BEC8 /* Info.plist */, 647416CB2D0A1F7300D0E1A8 /* TestHelpers.swift */, + A1B2C3212F70000100AA0001 /* Screens */, + A1B2C3212F70000100AA0002 /* Tests */, 17E30DC92C03922500E60561 /* FeaturesUITests.swift */, 17E30DCB2C03922500E60561 /* FeaturesUITestsLaunchTests.swift */, ); path = FeaturesUITests; sourceTree = ""; }; + A1B2C3212F70000100AA0001 /* Screens */ = { + isa = PBXGroup; + children = ( + A1B2C3112F70000100AA0002 /* HomeScreen.swift */, + A1B2C3112F70000100AA0003 /* AlertScreen.swift */, + A1B2C3112F70000100AA0004 /* TextFieldScreen.swift */, + A1B2C3112F70000100AA0005 /* APICallsScreen.swift */, + ); + path = Screens; + sourceTree = ""; + }; + A1B2C3212F70000100AA0002 /* Tests */ = { + isa = PBXGroup; + children = ( + A1B2C3112F70000100AA0001 /* BaseUITestCase.swift */, + A1B2C3112F70000100AA0006 /* AlertScreenTests.swift */, + A1B2C3112F70000100AA0007 /* TextFieldScreenTests.swift */, + A1B2C3112F70000100AA0008 /* APICallsScreenTests.swift */, + ); + path = Tests; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -348,6 +388,14 @@ buildActionMask = 2147483647; files = ( 175423962C03E52B00310374 /* WebView.swift in Sources */, + A1B2C3012F70000100AA0001 /* BaseUITestCase.swift in Sources */, + A1B2C3012F70000100AA0002 /* HomeScreen.swift in Sources */, + A1B2C3012F70000100AA0003 /* AlertScreen.swift in Sources */, + A1B2C3012F70000100AA0004 /* TextFieldScreen.swift in Sources */, + A1B2C3012F70000100AA0005 /* APICallsScreen.swift in Sources */, + A1B2C3012F70000100AA0006 /* AlertScreenTests.swift in Sources */, + A1B2C3012F70000100AA0007 /* TextFieldScreenTests.swift in Sources */, + A1B2C3012F70000100AA0008 /* APICallsScreenTests.swift in Sources */, 17E30DCC2C03922500E60561 /* FeaturesUITestsLaunchTests.swift in Sources */, 17E30DCA2C03922500E60561 /* FeaturesUITests.swift in Sources */, 647416CC2D0A1F7900D0E1A8 /* TestHelpers.swift in Sources */, diff --git a/Features.xcodeproj/project.xcworkspace/xcuserdata/stuart.minchington@saucelabs.com.xcuserdatad/UserInterfaceState.xcuserstate b/Features.xcodeproj/project.xcworkspace/xcuserdata/stuart.minchington@saucelabs.com.xcuserdatad/UserInterfaceState.xcuserstate index 1657031..d02c2ed 100644 Binary files a/Features.xcodeproj/project.xcworkspace/xcuserdata/stuart.minchington@saucelabs.com.xcuserdatad/UserInterfaceState.xcuserstate and b/Features.xcodeproj/project.xcworkspace/xcuserdata/stuart.minchington@saucelabs.com.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/Features/APICallsView.swift b/Features/APICallsView.swift index 5e3c8d2..020629d 100644 --- a/Features/APICallsView.swift +++ b/Features/APICallsView.swift @@ -18,9 +18,12 @@ struct APICallsView: View { VStack(spacing: 20) { Text("Request Status") .font(.headline) + .accessibilityIdentifier("api.statusHeader") Text("Success: \(successCount) / 25") + .accessibilityIdentifier("api.successLabel") Text("Failures: \(failureCount) / 25") + .accessibilityIdentifier("api.failureLabel") Button(action: { sendRequests() @@ -31,6 +34,7 @@ struct APICallsView: View { .foregroundColor(.white) .cornerRadius(10) } + .accessibilityIdentifier("api.sendRequestsButton") .disabled(isRequesting) } .padding() diff --git a/Features/AlertView.swift b/Features/AlertView.swift index 137089f..199f207 100644 --- a/Features/AlertView.swift +++ b/Features/AlertView.swift @@ -15,6 +15,7 @@ struct AlertView: View { Button("Generate Alert") { showingAlert = true } + .accessibilityIdentifier("alerts.generateButton") .padding() .background(Color(red: 0, green: 0, blue: 0.5)) .foregroundStyle(.white) diff --git a/Features/TextFieldView.swift b/Features/TextFieldView.swift index eb195af..f013315 100644 --- a/Features/TextFieldView.swift +++ b/Features/TextFieldView.swift @@ -2,6 +2,7 @@ import SwiftUI struct TextFieldView: View { @State private var inputText = "" + @State private var submittedText = "" @State private var showBanner = false @Environment(\.presentationMode) var presentationMode @@ -11,19 +12,27 @@ struct TextFieldView: View { TextField("Enter your text here", text: $inputText) .textFieldStyle(.roundedBorder) .padding() + .accessibilityIdentifier("text.inputField") Button("Submit") { - // Handle the submitted text here + submittedText = inputText print("Submitted text: \(inputText)") } + .accessibilityIdentifier("text.submitButton") .padding() .background(Color(red: 0, green: 0, blue: 0.5)) .foregroundStyle(.white) .clipShape(Capsule()) + if !submittedText.isEmpty { + Text("Submitted: \(submittedText)") + .accessibilityIdentifier("text.submittedValueLabel") + } + Button("Back") { presentationMode.wrappedValue.dismiss() } + .accessibilityIdentifier("text.backButton") .padding() .background(Color(red: 0.5, green: 0, blue: 0)) .foregroundStyle(.white) diff --git a/FeaturesUITests/FeaturesUITests.swift b/FeaturesUITests/FeaturesUITests.swift index 2db3d44..036a8c2 100644 --- a/FeaturesUITests/FeaturesUITests.swift +++ b/FeaturesUITests/FeaturesUITests.swift @@ -8,59 +8,8 @@ import XCTest final class FeaturesUITests: XCTestCase { - var app: XCUIApplication! - - override func setUpWithError() throws { - try super.setUpWithError() - continueAfterFailure = false - - app = XCUIApplication() - app.launch() - } - - override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - - func testGenerateAlert() throws { - let app = XCUIApplication() - app.collectionViews.staticTexts["Alerts"].tap() - app.buttons["Generate Alert"].tap() - app.buttons["OK"].tap() - } - - func testTypeText() throws { - let app = XCUIApplication() - app.collectionViews/*@START_MENU_TOKEN@*/.staticTexts["Text"]/*[[".cells",".buttons[\"Text\"].staticTexts[\"Text\"]",".staticTexts[\"Text\"]"],[[[-1,2],[-1,1],[-1,0,1]],[[-1,2],[-1,1]]],[0]]@END_MENU_TOKEN@*/.tap() - app.textFields["Enter your text here"].tap() - app.typeText("Using standard typeText") - app.buttons["Submit"].tap() - } - - - func testSetText() throws { - let app = XCUIApplication() - app.collectionViews/*@START_MENU_TOKEN@*/.staticTexts["Text"]/*[[".cells",".buttons[\"Text\"].staticTexts[\"Text\"]",".staticTexts[\"Text\"]"],[[[-1,2],[-1,1],[-1,0,1]],[[-1,2],[-1,1]]],[0]]@END_MENU_TOKEN@*/.tap() - //sleep(1) - app.textFields["Enter your text here"].setText("Using setText", doubleTap: false) - app.buttons["Submit"].tap() - } - - - - func testAPICalls() throws { - let app = XCUIApplication() - app.collectionViews.staticTexts["Network Capture"].tap() - app.buttons["Send 25 GET Requests"].tap() - - let successLabel = app.staticTexts["Success: 25 / 25"] - - // Wait for up to 30 seconds until the success label appears - let exists = NSPredicate(format: "exists == true") - expectation(for: exists, evaluatedWith: successLabel, handler: nil) - waitForExpectations(timeout: 30, handler: nil) - - // Verify the success label or other elements if necessary - XCTAssertTrue(successLabel.exists, "The success label did not appear within the timeout period.") + func testLegacySuiteMigrated() { + // Tests are now organized in FeaturesUITests/Screens and FeaturesUITests/Tests. + XCTAssertTrue(true) } } diff --git a/FeaturesUITests/Screens/APICallsScreen.swift b/FeaturesUITests/Screens/APICallsScreen.swift new file mode 100644 index 0000000..c2ac04b --- /dev/null +++ b/FeaturesUITests/Screens/APICallsScreen.swift @@ -0,0 +1,46 @@ +import XCTest + +final class APICallsScreen { + static let shared = APICallsScreen() + + private let app = XCUIApplication() + + private init() {} + + var sendRequestsButton: XCUIElement { app.buttons["api.sendRequestsButton"] } + var successLabel: XCUIElement { app.staticTexts["api.successLabel"] } + var failureLabel: XCUIElement { app.staticTexts["api.failureLabel"] } + + func startRequests() async { + sendRequestsButton.tap() + } + + func waitUntilRequestsComplete(timeout: TimeInterval) -> Bool { + let predicate = NSPredicate(format: "isEnabled == true") + let expectation = XCTNSPredicateExpectation(predicate: predicate, object: sendRequestsButton) + return XCTWaiter.wait(for: [expectation], timeout: timeout) == .completed + } + + func parsedRequestCounts() -> (success: Int, failure: Int)? { + guard + let successText = successLabel.label.split(separator: ":").dropFirst().first, + let failureText = failureLabel.label.split(separator: ":").dropFirst().first + else { + return nil + } + + let successValue = successText.trimmingCharacters(in: .whitespaces).split(separator: " ").first + let failureValue = failureText.trimmingCharacters(in: .whitespaces).split(separator: " ").first + + guard + let successString = successValue, + let failureString = failureValue, + let success = Int(successString), + let failure = Int(failureString) + else { + return nil + } + + return (success, failure) + } +} diff --git a/FeaturesUITests/Screens/AlertScreen.swift b/FeaturesUITests/Screens/AlertScreen.swift new file mode 100644 index 0000000..f163f4f --- /dev/null +++ b/FeaturesUITests/Screens/AlertScreen.swift @@ -0,0 +1,21 @@ +import XCTest + +final class AlertScreen { + static let shared = AlertScreen() + + private let app = XCUIApplication() + + private init() {} + + var generateAlertButton: XCUIElement { app.buttons["alerts.generateButton"] } + var alert: XCUIElement { app.alerts["Alert Title"] } + var alertOKButton: XCUIElement { app.alerts["Alert Title"].buttons["OK"] } + + func generateAlert() async { + generateAlertButton.tap() + } + + func dismissAlertWithOK() async { + alertOKButton.tap() + } +} diff --git a/FeaturesUITests/Screens/HomeScreen.swift b/FeaturesUITests/Screens/HomeScreen.swift new file mode 100644 index 0000000..733c6bf --- /dev/null +++ b/FeaturesUITests/Screens/HomeScreen.swift @@ -0,0 +1,29 @@ +import XCTest + +final class HomeScreen { + static let shared = HomeScreen() + + private let app = XCUIApplication() + + private init() {} + + func featureItem(_ identifier: String) -> XCUIElement { + app.descendants(matching: .any).matching(identifier: identifier).firstMatch + } + + var alertsItem: XCUIElement { featureItem("Alerts") } + var textItem: XCUIElement { featureItem("Text") } + var networkCaptureItem: XCUIElement { featureItem("Network Capture") } + + func openAlerts() async { + alertsItem.tap() + } + + func openText() async { + textItem.tap() + } + + func openNetworkCapture() async { + networkCaptureItem.tap() + } +} diff --git a/FeaturesUITests/Screens/TextFieldScreen.swift b/FeaturesUITests/Screens/TextFieldScreen.swift new file mode 100644 index 0000000..9dd70df --- /dev/null +++ b/FeaturesUITests/Screens/TextFieldScreen.swift @@ -0,0 +1,21 @@ +import XCTest + +final class TextFieldScreen { + static let shared = TextFieldScreen() + + private let app = XCUIApplication() + + private init() {} + + var inputField: XCUIElement { app.textFields["text.inputField"] } + var submitButton: XCUIElement { app.buttons["text.submitButton"] } + var submittedValueLabel: XCUIElement { app.staticTexts["text.submittedValueLabel"] } + + func enterText(_ text: String) async { + inputField.setText(text, doubleTap: false) + } + + func submit() async { + submitButton.tap() + } +} diff --git a/FeaturesUITests/Tests/APICallsScreenTests.swift b/FeaturesUITests/Tests/APICallsScreenTests.swift new file mode 100644 index 0000000..b70a150 --- /dev/null +++ b/FeaturesUITests/Tests/APICallsScreenTests.swift @@ -0,0 +1,27 @@ +import XCTest + +final class APICallsScreenTests: BaseUITestCase { + private let homeScreen = HomeScreen.shared + private let apiCallsScreen = APICallsScreen.shared + + func testRequestsCompleteWithExpectedTotal() async throws { + XCTAssertTrue(waitForExists(homeScreen.networkCaptureItem, message: "Network Capture feature is not visible on home screen.")) + await homeScreen.openNetworkCapture() + + XCTAssertTrue(waitForExists(apiCallsScreen.sendRequestsButton, message: "Send requests button is missing.")) + await apiCallsScreen.startRequests() + + let completed = apiCallsScreen.waitUntilRequestsComplete(timeout: 40) + XCTAssertTrue(completed, "Requests did not complete within timeout.") + + XCTAssertTrue(waitForExists(apiCallsScreen.successLabel, message: "Success label is missing.")) + XCTAssertTrue(waitForExists(apiCallsScreen.failureLabel, message: "Failure label is missing.")) + + guard let counts = apiCallsScreen.parsedRequestCounts() else { + XCTFail("Unable to parse success/failure counts from labels.") + return + } + + XCTAssertEqual(counts.success + counts.failure, 25, "Total completed requests should always be 25.") + } +} diff --git a/FeaturesUITests/Tests/AlertScreenTests.swift b/FeaturesUITests/Tests/AlertScreenTests.swift new file mode 100644 index 0000000..4745e7a --- /dev/null +++ b/FeaturesUITests/Tests/AlertScreenTests.swift @@ -0,0 +1,20 @@ +import XCTest + +final class AlertScreenTests: BaseUITestCase { + private let homeScreen = HomeScreen.shared + private let alertScreen = AlertScreen.shared + + func testGenerateAndDismissAlert() async throws { + XCTAssertTrue(waitForExists(homeScreen.alertsItem, message: "Alerts feature is not visible on home screen.")) + await homeScreen.openAlerts() + + XCTAssertTrue(waitForExists(alertScreen.generateAlertButton, message: "Generate Alert button was not found.")) + await alertScreen.generateAlert() + + XCTAssertTrue(waitForExists(alertScreen.alert, timeout: 3, message: "Alert did not appear.")) + XCTAssertTrue(waitForExists(alertScreen.alertOKButton, message: "OK button is missing from alert.")) + + await alertScreen.dismissAlertWithOK() + XCTAssertFalse(alertScreen.alert.exists, "Alert should be dismissed after tapping OK.") + } +} diff --git a/FeaturesUITests/Tests/BaseUITestCase.swift b/FeaturesUITests/Tests/BaseUITestCase.swift new file mode 100644 index 0000000..8267920 --- /dev/null +++ b/FeaturesUITests/Tests/BaseUITestCase.swift @@ -0,0 +1,18 @@ +import XCTest + +class BaseUITestCase: XCTestCase { + let app = XCUIApplication() + + override func setUpWithError() throws { + try super.setUpWithError() + continueAfterFailure = false + app.launch() + } + + @discardableResult + func waitForExists(_ element: XCUIElement, timeout: TimeInterval = 5, message: String) -> Bool { + let exists = element.waitForExistence(timeout: timeout) + XCTAssertTrue(exists, message) + return exists + } +} diff --git a/FeaturesUITests/Tests/TextFieldScreenTests.swift b/FeaturesUITests/Tests/TextFieldScreenTests.swift new file mode 100644 index 0000000..bc330ff --- /dev/null +++ b/FeaturesUITests/Tests/TextFieldScreenTests.swift @@ -0,0 +1,20 @@ +import XCTest + +final class TextFieldScreenTests: BaseUITestCase { + private let homeScreen = HomeScreen.shared + private let textFieldScreen = TextFieldScreen.shared + + func testSubmitTextShowsConfirmation() async throws { + let valueToSubmit = "Using page objects" + + XCTAssertTrue(waitForExists(homeScreen.textItem, message: "Text feature is not visible on home screen.")) + await homeScreen.openText() + + XCTAssertTrue(waitForExists(textFieldScreen.inputField, message: "Text input field was not found.")) + await textFieldScreen.enterText(valueToSubmit) + await textFieldScreen.submit() + + XCTAssertTrue(waitForExists(textFieldScreen.submittedValueLabel, timeout: 3, message: "Submitted value label did not appear.")) + XCTAssertEqual(textFieldScreen.submittedValueLabel.label, "Submitted: \(valueToSubmit)") + } +} diff --git a/Secrets.xcconfig b/Secrets.xcconfig index 8b80259..9c348d4 100644 --- a/Secrets.xcconfig +++ b/Secrets.xcconfig @@ -9,4 +9,4 @@ // https://developer.apple.com/documentation/xcode/adding-a-build-configuration-file-to-your-project // Make a copy named Secrets.xcconfig, add your Backtrace specific info, then add to your .gitignore file -BACKTRACE_SUBMISSION_URL = https://submit.backtrace.io/sl-7021d636ac504cc38fa89cdcdab5551e/b02797d62c48648433e8ccfcb1f1d06d24a592f2d1d9f227fc3015b38157d982/plcrash +BACKTRACE_SUBMISSION_URL = https://submit.backtrace.io/sl-f8eccdf81c62461c852e6c906f777124/7f9251d576fedc28db9bab682ae5b593b394e1526dcb8149ee16e7fcded67773/plcrash