From 405d0572c5d14506585de182ce6e1c81b8a04049 Mon Sep 17 00:00:00 2001 From: Doug <6060466+pixlwave@users.noreply.github.com> Date: Thu, 1 May 2025 12:07:04 +0100 Subject: [PATCH] Fix the integration tests. (#4084) * Fix logging/alerts during OIDC cancellation. - Cancelling from within the web view wasn't being handled since moving the UserIndicatorController into the presenter. - The WAS canceledLogin error code is also used when the system cancels the login. When the system cancels there's a failure reason included in the error. * Allow UI tests to tap on any point within a view. * Make the homeserver optional in integration tests. * Dismiss the keyboard after entering a username to reveal the password text field. Do the same after entering the password field too, just in case. * Add a loop while waiting for the WAS prompt to be shown. --- .../Other/Extensions/XCUIElement.swift | 7 +- .../OIDCAuthenticationPresenter.swift | 11 ++- IntegrationTests/Sources/Application.swift | 4 +- IntegrationTests/Sources/Common.swift | 86 ++++++++++++------- IntegrationTests/Sources/UserFlowTests.swift | 28 +++--- 5 files changed, 84 insertions(+), 52 deletions(-) diff --git a/ElementX/Sources/Other/Extensions/XCUIElement.swift b/ElementX/Sources/Other/Extensions/XCUIElement.swift index d466a9aa8..778cdafd7 100644 --- a/ElementX/Sources/Other/Extensions/XCUIElement.swift +++ b/ElementX/Sources/Other/Extensions/XCUIElement.swift @@ -5,11 +5,12 @@ // Please see LICENSE files in the repository root for full details. // +import SwiftUI import XCTest extension XCUIElement { func clearAndTypeText(_ text: String, app: XCUIApplication) { - tapCenter() + tap(.center) app.showKeyboardIfNeeded() @@ -26,8 +27,8 @@ extension XCUIElement { } } - func tapCenter() { - let coordinate: XCUICoordinate = coordinate(withNormalizedOffset: .init(dx: 0.5, dy: 0.5)) + func tap(_ point: UnitPoint) { + let coordinate = coordinate(withNormalizedOffset: .init(dx: point.x, dy: point.y)) coordinate.tap() } } diff --git a/ElementX/Sources/Screens/Authentication/OIDCAuthenticationPresenter.swift b/ElementX/Sources/Screens/Authentication/OIDCAuthenticationPresenter.swift index 9c352ebc8..374f47d95 100644 --- a/ElementX/Sources/Screens/Authentication/OIDCAuthenticationPresenter.swift +++ b/ElementX/Sources/Screens/Authentication/OIDCAuthenticationPresenter.swift @@ -43,13 +43,17 @@ class OIDCAuthenticationPresenter: NSObject { // Check for user cancellation to avoid showing an alert in that instance. if let nsError = error as? NSError, nsError.domain == ASWebAuthenticationSessionErrorDomain, - nsError.code == ASWebAuthenticationSessionError.canceledLogin.rawValue { + nsError.code == ASWebAuthenticationSessionError.canceledLogin.rawValue, + // If there's a failure reason then the cancellation wasn't made by the user. + nsError.localizedFailureReason == nil { // No need to show an error here, just abort and return a failure. await authenticationService.abortOIDCLogin(data: oidcData) return .failure(.oidcError(.userCancellation)) } - MXLog.error("Missing callback URL from the web authentication session.") + let errorDescription = error.map(String.init(describing:)) ?? "Unknown error" + MXLog.error("Missing callback URL from the web authentication session: \(errorDescription)") + userIndicatorController.alertInfo = AlertInfo(id: UUID()) await authenticationService.abortOIDCLogin(data: oidcData) return .failure(.oidcError(.unknown)) @@ -62,6 +66,9 @@ class OIDCAuthenticationPresenter: NSObject { switch await authenticationService.loginWithOIDCCallback(url) { case .success(let userSession): return .success(userSession) + case .failure(.oidcError(.userCancellation)): + // No need to show an error here, just return the failure. + return .failure(.oidcError(.userCancellation)) case .failure(let error): MXLog.error("Error occurred: \(error)") userIndicatorController.alertInfo = AlertInfo(id: UUID()) diff --git a/IntegrationTests/Sources/Application.swift b/IntegrationTests/Sources/Application.swift index e5450086d..675cc9ab5 100644 --- a/IntegrationTests/Sources/Application.swift +++ b/IntegrationTests/Sources/Application.swift @@ -23,10 +23,10 @@ enum Application { } extension XCUIApplication { - var homeserver: String { + var homeserver: String? { guard let homeserver = ProcessInfo.processInfo.environment["INTEGRATION_TESTS_HOST"], homeserver.count > 0 else { - return "default" + return nil } return homeserver diff --git a/IntegrationTests/Sources/Common.swift b/IntegrationTests/Sources/Common.swift index 9bad7adbf..981c9ce9c 100644 --- a/IntegrationTests/Sources/Common.swift +++ b/IntegrationTests/Sources/Common.swift @@ -8,54 +8,78 @@ import XCTest extension XCUIApplication { + private var doesNotExistPredicate: NSPredicate { NSPredicate(format: "exists == 0") } + func login(currentTestCase: XCTestCase) { let getStartedButton = buttons[A11yIdentifiers.authenticationStartScreen.signIn] XCTAssertTrue(getStartedButton.waitForExistence(timeout: 10.0)) - getStartedButton.tapCenter() + getStartedButton.tap(.center) - let changeHomeserverButton = buttons[A11yIdentifiers.serverConfirmationScreen.changeServer] - XCTAssertTrue(changeHomeserverButton.waitForExistence(timeout: 10.0)) - changeHomeserverButton.tapCenter() - - let homeserverTextField = textFields[A11yIdentifiers.changeServerScreen.server] - XCTAssertTrue(homeserverTextField.waitForExistence(timeout: 10.0)) - - homeserverTextField.clearAndTypeText(homeserver, app: self) - - let confirmButton = buttons[A11yIdentifiers.changeServerScreen.continue] - XCTAssertTrue(confirmButton.waitForExistence(timeout: 10.0)) - confirmButton.tapCenter() - - // Wait for server confirmation to finish - let doesNotExistPredicate = NSPredicate(format: "exists == 0") - currentTestCase.expectation(for: doesNotExistPredicate, evaluatedWith: confirmButton) - currentTestCase.waitForExpectations(timeout: 300.0) - - let continueButton = buttons[A11yIdentifiers.serverConfirmationScreen.continue] - XCTAssertTrue(continueButton.waitForExistence(timeout: 30.0)) - continueButton.tapCenter() + if let homeserver { + let changeHomeserverButton = buttons[A11yIdentifiers.serverConfirmationScreen.changeServer] + XCTAssertTrue(changeHomeserverButton.waitForExistence(timeout: 10.0)) + changeHomeserverButton.tap(.center) + + let homeserverTextField = textFields[A11yIdentifiers.changeServerScreen.server] + XCTAssertTrue(homeserverTextField.waitForExistence(timeout: 10.0)) + + homeserverTextField.clearAndTypeText(homeserver, app: self) + + let confirmButton = buttons[A11yIdentifiers.changeServerScreen.continue] + XCTAssertTrue(confirmButton.waitForExistence(timeout: 10.0)) + confirmButton.tap(.center) + + // Wait for server confirmation to finish + currentTestCase.expectation(for: doesNotExistPredicate, evaluatedWith: confirmButton) + currentTestCase.waitForExpectations(timeout: 300.0) + } let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard") let webAuthenticationSessionAlertContinueButton = springboard.buttons["Continue"].firstMatch - XCTAssertTrue(webAuthenticationSessionAlertContinueButton.waitForExistence(timeout: 30.0)) - webAuthenticationSessionAlertContinueButton.tapCenter() + + // On a fresh simulator the webcredentials association is sometimes slow to be resolved. + // This results in an error alert being shown instead of the Web Authentication Session alert. + // Keep looping on the Continue button for ~5 minutes until the Authentication Session is happy. + var remainingAttempts = 10 + while !webAuthenticationSessionAlertContinueButton.exists { + let continueButton = buttons[A11yIdentifiers.serverConfirmationScreen.continue] + XCTAssertTrue(continueButton.waitForExistence(timeout: 30.0)) + continueButton.tap(.center) + + if webAuthenticationSessionAlertContinueButton.waitForExistence(timeout: 30.0) { + break + } + + remainingAttempts -= 1 + if remainingAttempts <= 0 { + XCTFail("Failed to present the web authentication session.") + } + + if alerts.count > 0 { + alerts.firstMatch.buttons["OK"].tap() + } + } + + webAuthenticationSessionAlertContinueButton.tap(.center) let webAuthenticationView = webViews.firstMatch XCTAssertTrue(webAuthenticationView.waitForExistence(timeout: 10.0)) - webAuthenticationView.tap() // Tap the web view to properly focus the app again. + webAuthenticationView.tap(.top) // Tap the web view to properly focus the app again. let webUsernameTextField = textFields["Username or Email"] XCTAssertTrue(webUsernameTextField.waitForExistence(timeout: 10.0)) webUsernameTextField.clearAndTypeText(username, app: self) + buttons["Done"].tap() // Dismiss the keyboard so that the password text field is fully hittable. let webPasswordTextField = secureTextFields["Password"] XCTAssertTrue(webPasswordTextField.waitForExistence(timeout: 10.0)) webPasswordTextField.clearAndTypeText(password, app: self) + buttons["Done"].tap() // Dismiss the keyboard so that the continue button is fully hittable. let webLoginButton = webAuthenticationView.buttons["Continue"] XCTAssertTrue(webLoginButton.waitForExistence(timeout: 10.0)) - webLoginButton.tapCenter() + webLoginButton.tap(.center) // Handle the password saving dialog let savePasswordButton = buttons["Save Password"] @@ -63,12 +87,12 @@ extension XCUIApplication { // Tapping the sheet button while animating upwards fails. Wait for it to settle sleep(1) - savePasswordButton.tapCenter() + buttons["Not Now"].tap(.center) } let webConsentButton = webAuthenticationView.buttons["Continue"] XCTAssertTrue(webConsentButton.waitForExistence(timeout: 10.0)) - webConsentButton.tapCenter() + webConsentButton.tap(.center) // Wait for login to finish currentTestCase.expectation(for: doesNotExistPredicate, evaluatedWith: webUsernameTextField) @@ -88,7 +112,7 @@ extension XCUIApplication { let profileButton = buttons[A11yIdentifiers.homeScreen.userAvatar] // `Failed to scroll to visible (by AX action) Button` https://stackoverflow.com/a/33534187/730924 - profileButton.tapCenter() + profileButton.tap(.center) // Make the logout button visible swipeUp() @@ -96,12 +120,12 @@ extension XCUIApplication { // Logout let logoutButton = buttons[A11yIdentifiers.settingsScreen.logout] XCTAssertTrue(logoutButton.waitForExistence(timeout: 10.0)) - logoutButton.tapCenter() + logoutButton.tap(.center) // Confirm logout let alertLogoutButton = alerts.firstMatch.buttons["Sign out"] XCTAssertTrue(alertLogoutButton.waitForExistence(timeout: 10.0)) - alertLogoutButton.tapCenter() + alertLogoutButton.tap(.center) // Check that we're back on the login screen let getStartedButton = buttons[A11yIdentifiers.authenticationStartScreen.signIn] diff --git a/IntegrationTests/Sources/UserFlowTests.swift b/IntegrationTests/Sources/UserFlowTests.swift index 0b432d3b3..f747f6395 100644 --- a/IntegrationTests/Sources/UserFlowTests.swift +++ b/IntegrationTests/Sources/UserFlowTests.swift @@ -42,7 +42,7 @@ class UserFlowTests: XCTestCase { // And open it let firstRoom = app.buttons.matching(NSPredicate(format: "identifier CONTAINS %@", Self.integrationTestsRoomName)).firstMatch XCTAssertTrue(firstRoom.waitForExistence(timeout: 10.0)) - firstRoom.tapCenter() + firstRoom.tap(.center) sendMessages() @@ -62,7 +62,7 @@ class UserFlowTests: XCTestCase { // Cancel initial the room search let searchCancelButton = app.buttons["Cancel"].firstMatch XCTAssertTrue(searchCancelButton.waitForExistence(timeout: 10.0)) - searchCancelButton.tapCenter() + searchCancelButton.tap(.center) } private func sendMessages() { @@ -72,7 +72,7 @@ class UserFlowTests: XCTestCase { var sendButton = app.buttons[A11yIdentifiers.roomScreen.sendButton].firstMatch XCTAssertTrue(sendButton.waitForExistence(timeout: 10.0)) - sendButton.tapCenter() + sendButton.tap(.center) sleep(10) // Wait for the message to be sent @@ -86,12 +86,12 @@ class UserFlowTests: XCTestCase { sendButton = app.buttons[A11yIdentifiers.roomScreen.sendButton].firstMatch XCTAssertTrue(sendButton.waitForExistence(timeout: 10.0)) - sendButton.tapCenter() + sendButton.tap(.center) sleep(5) // Wait for the message to be sent // Close the formatting options - app.buttons[A11yIdentifiers.roomScreen.composerToolbar.closeFormattingOptions].tapCenter() + app.buttons[A11yIdentifiers.roomScreen.composerToolbar.closeFormattingOptions].tap(.center) } private func checkPhotoSharing() { @@ -103,7 +103,7 @@ class UserFlowTests: XCTestCase { // Tap on the second image. First one is always broken on simulators. let secondImage = app.scrollViews.images.element(boundBy: 1) XCTAssertTrue(secondImage.waitForExistence(timeout: 20.0)) // Photo library takes a bit to load - secondImage.tapCenter() + secondImage.tap(.center) // Wait for the image to be processed and the new screen to appear sleep(10) @@ -134,7 +134,7 @@ class UserFlowTests: XCTestCase { // Handle map loading errors (missing credentials) let alertOkButton = app.alerts.firstMatch.buttons["OK"].firstMatch if alertOkButton.waitForExistence(timeout: 10.0) { - alertOkButton.tapCenter() + alertOkButton.tap(.center) } allowLocationPermissionOnce() @@ -146,7 +146,7 @@ class UserFlowTests: XCTestCase { let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard") let notificationAlertAllowButton = springboard.buttons["Allow Once"].firstMatch if notificationAlertAllowButton.waitForExistence(timeout: 10.0) { - notificationAlertAllowButton.tapCenter() + notificationAlertAllowButton.tap(.center) } } @@ -183,7 +183,7 @@ class UserFlowTests: XCTestCase { // Open the room details let roomHeader = app.staticTexts[A11yIdentifiers.roomScreen.name] XCTAssertTrue(roomHeader.waitForExistence(timeout: 10.0)) - roomHeader.tapCenter() + roomHeader.tap(.center) // Open the room member details tapOnButton(A11yIdentifiers.roomDetailsScreen.people) @@ -191,7 +191,7 @@ class UserFlowTests: XCTestCase { // Open the first member's details. Loading members for big rooms can take a while. let firstRoomMember = app.scrollViews.buttons.firstMatch XCTAssertTrue(firstRoomMember.waitForExistence(timeout: 1000.0)) - firstRoomMember.tapCenter() + firstRoomMember.tap(.center) // Go back to the room member details tapOnBackButton("People") @@ -211,7 +211,7 @@ class UserFlowTests: XCTestCase { let profileButton = app.buttons[A11yIdentifiers.homeScreen.userAvatar] // `Failed to scroll to visible (by AX action) Button` https://stackoverflow.com/a/33534187/730924 - profileButton.tapCenter() + profileButton.tap(.center) // Open analytics tapOnButton(A11yIdentifiers.settingsScreen.analytics) @@ -238,7 +238,7 @@ class UserFlowTests: XCTestCase { private func tapOnButton(_ identifier: String, waitForDisappearance: Bool = false) { let button = app.buttons[identifier] XCTAssertTrue(button.waitForExistence(timeout: 10.0)) - button.tapCenter() + button.tap(.center) if waitForDisappearance { let doesNotExistPredicate = NSPredicate(format: "exists == 0") @@ -250,7 +250,7 @@ class UserFlowTests: XCTestCase { private func tapOnMenu(_ identifier: String) { let button = app.buttons[identifier] XCTAssertTrue(button.waitForExistence(timeout: 10.0)) - button.tapCenter() + button.tap(.center) } /// Taps on a back button that the system configured with a label but no identifier. @@ -260,6 +260,6 @@ class UserFlowTests: XCTestCase { private func tapOnBackButton(_ label: String = "Back") { let button = app.buttons.matching(NSPredicate(format: "label == %@ && identifier == ''", label)).firstMatch XCTAssertTrue(button.waitForExistence(timeout: 10.0)) - button.tapCenter() + button.tap(.center) } }