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.
This commit is contained in:
Doug
2025-05-01 12:07:04 +01:00
committed by GitHub
parent d45b232072
commit 3262fb4cab
5 changed files with 84 additions and 52 deletions
@@ -5,11 +5,12 @@
// Please see LICENSE files in the repository root for full details. // Please see LICENSE files in the repository root for full details.
// //
import SwiftUI
import XCTest import XCTest
extension XCUIElement { extension XCUIElement {
func clearAndTypeText(_ text: String, app: XCUIApplication) { func clearAndTypeText(_ text: String, app: XCUIApplication) {
tapCenter() tap(.center)
app.showKeyboardIfNeeded() app.showKeyboardIfNeeded()
@@ -26,8 +27,8 @@ extension XCUIElement {
} }
} }
func tapCenter() { func tap(_ point: UnitPoint) {
let coordinate: XCUICoordinate = coordinate(withNormalizedOffset: .init(dx: 0.5, dy: 0.5)) let coordinate = coordinate(withNormalizedOffset: .init(dx: point.x, dy: point.y))
coordinate.tap() coordinate.tap()
} }
} }
@@ -43,13 +43,17 @@ class OIDCAuthenticationPresenter: NSObject {
// Check for user cancellation to avoid showing an alert in that instance. // Check for user cancellation to avoid showing an alert in that instance.
if let nsError = error as? NSError, if let nsError = error as? NSError,
nsError.domain == ASWebAuthenticationSessionErrorDomain, 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. // No need to show an error here, just abort and return a failure.
await authenticationService.abortOIDCLogin(data: oidcData) await authenticationService.abortOIDCLogin(data: oidcData)
return .failure(.oidcError(.userCancellation)) 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()) userIndicatorController.alertInfo = AlertInfo(id: UUID())
await authenticationService.abortOIDCLogin(data: oidcData) await authenticationService.abortOIDCLogin(data: oidcData)
return .failure(.oidcError(.unknown)) return .failure(.oidcError(.unknown))
@@ -62,6 +66,9 @@ class OIDCAuthenticationPresenter: NSObject {
switch await authenticationService.loginWithOIDCCallback(url) { switch await authenticationService.loginWithOIDCCallback(url) {
case .success(let userSession): case .success(let userSession):
return .success(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): case .failure(let error):
MXLog.error("Error occurred: \(error)") MXLog.error("Error occurred: \(error)")
userIndicatorController.alertInfo = AlertInfo(id: UUID()) userIndicatorController.alertInfo = AlertInfo(id: UUID())
+2 -2
View File
@@ -23,10 +23,10 @@ enum Application {
} }
extension XCUIApplication { extension XCUIApplication {
var homeserver: String { var homeserver: String? {
guard let homeserver = ProcessInfo.processInfo.environment["INTEGRATION_TESTS_HOST"], guard let homeserver = ProcessInfo.processInfo.environment["INTEGRATION_TESTS_HOST"],
homeserver.count > 0 else { homeserver.count > 0 else {
return "default" return nil
} }
return homeserver return homeserver
+55 -31
View File
@@ -8,54 +8,78 @@
import XCTest import XCTest
extension XCUIApplication { extension XCUIApplication {
private var doesNotExistPredicate: NSPredicate { NSPredicate(format: "exists == 0") }
func login(currentTestCase: XCTestCase) { func login(currentTestCase: XCTestCase) {
let getStartedButton = buttons[A11yIdentifiers.authenticationStartScreen.signIn] let getStartedButton = buttons[A11yIdentifiers.authenticationStartScreen.signIn]
XCTAssertTrue(getStartedButton.waitForExistence(timeout: 10.0)) XCTAssertTrue(getStartedButton.waitForExistence(timeout: 10.0))
getStartedButton.tapCenter() getStartedButton.tap(.center)
let changeHomeserverButton = buttons[A11yIdentifiers.serverConfirmationScreen.changeServer] if let homeserver {
XCTAssertTrue(changeHomeserverButton.waitForExistence(timeout: 10.0)) let changeHomeserverButton = buttons[A11yIdentifiers.serverConfirmationScreen.changeServer]
changeHomeserverButton.tapCenter() XCTAssertTrue(changeHomeserverButton.waitForExistence(timeout: 10.0))
changeHomeserverButton.tap(.center)
let homeserverTextField = textFields[A11yIdentifiers.changeServerScreen.server]
XCTAssertTrue(homeserverTextField.waitForExistence(timeout: 10.0)) let homeserverTextField = textFields[A11yIdentifiers.changeServerScreen.server]
XCTAssertTrue(homeserverTextField.waitForExistence(timeout: 10.0))
homeserverTextField.clearAndTypeText(homeserver, app: self)
homeserverTextField.clearAndTypeText(homeserver, app: self)
let confirmButton = buttons[A11yIdentifiers.changeServerScreen.continue]
XCTAssertTrue(confirmButton.waitForExistence(timeout: 10.0)) let confirmButton = buttons[A11yIdentifiers.changeServerScreen.continue]
confirmButton.tapCenter() XCTAssertTrue(confirmButton.waitForExistence(timeout: 10.0))
confirmButton.tap(.center)
// Wait for server confirmation to finish
let doesNotExistPredicate = NSPredicate(format: "exists == 0") // Wait for server confirmation to finish
currentTestCase.expectation(for: doesNotExistPredicate, evaluatedWith: confirmButton) currentTestCase.expectation(for: doesNotExistPredicate, evaluatedWith: confirmButton)
currentTestCase.waitForExpectations(timeout: 300.0) currentTestCase.waitForExpectations(timeout: 300.0)
}
let continueButton = buttons[A11yIdentifiers.serverConfirmationScreen.continue]
XCTAssertTrue(continueButton.waitForExistence(timeout: 30.0))
continueButton.tapCenter()
let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard") let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
let webAuthenticationSessionAlertContinueButton = springboard.buttons["Continue"].firstMatch 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 let webAuthenticationView = webViews.firstMatch
XCTAssertTrue(webAuthenticationView.waitForExistence(timeout: 10.0)) 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"] let webUsernameTextField = textFields["Username or Email"]
XCTAssertTrue(webUsernameTextField.waitForExistence(timeout: 10.0)) XCTAssertTrue(webUsernameTextField.waitForExistence(timeout: 10.0))
webUsernameTextField.clearAndTypeText(username, app: self) webUsernameTextField.clearAndTypeText(username, app: self)
buttons["Done"].tap() // Dismiss the keyboard so that the password text field is fully hittable.
let webPasswordTextField = secureTextFields["Password"] let webPasswordTextField = secureTextFields["Password"]
XCTAssertTrue(webPasswordTextField.waitForExistence(timeout: 10.0)) XCTAssertTrue(webPasswordTextField.waitForExistence(timeout: 10.0))
webPasswordTextField.clearAndTypeText(password, app: self) webPasswordTextField.clearAndTypeText(password, app: self)
buttons["Done"].tap() // Dismiss the keyboard so that the continue button is fully hittable.
let webLoginButton = webAuthenticationView.buttons["Continue"] let webLoginButton = webAuthenticationView.buttons["Continue"]
XCTAssertTrue(webLoginButton.waitForExistence(timeout: 10.0)) XCTAssertTrue(webLoginButton.waitForExistence(timeout: 10.0))
webLoginButton.tapCenter() webLoginButton.tap(.center)
// Handle the password saving dialog // Handle the password saving dialog
let savePasswordButton = buttons["Save Password"] let savePasswordButton = buttons["Save Password"]
@@ -63,12 +87,12 @@ extension XCUIApplication {
// Tapping the sheet button while animating upwards fails. Wait for it to settle // Tapping the sheet button while animating upwards fails. Wait for it to settle
sleep(1) sleep(1)
savePasswordButton.tapCenter() buttons["Not Now"].tap(.center)
} }
let webConsentButton = webAuthenticationView.buttons["Continue"] let webConsentButton = webAuthenticationView.buttons["Continue"]
XCTAssertTrue(webConsentButton.waitForExistence(timeout: 10.0)) XCTAssertTrue(webConsentButton.waitForExistence(timeout: 10.0))
webConsentButton.tapCenter() webConsentButton.tap(.center)
// Wait for login to finish // Wait for login to finish
currentTestCase.expectation(for: doesNotExistPredicate, evaluatedWith: webUsernameTextField) currentTestCase.expectation(for: doesNotExistPredicate, evaluatedWith: webUsernameTextField)
@@ -88,7 +112,7 @@ extension XCUIApplication {
let profileButton = buttons[A11yIdentifiers.homeScreen.userAvatar] let profileButton = buttons[A11yIdentifiers.homeScreen.userAvatar]
// `Failed to scroll to visible (by AX action) Button` https://stackoverflow.com/a/33534187/730924 // `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 // Make the logout button visible
swipeUp() swipeUp()
@@ -96,12 +120,12 @@ extension XCUIApplication {
// Logout // Logout
let logoutButton = buttons[A11yIdentifiers.settingsScreen.logout] let logoutButton = buttons[A11yIdentifiers.settingsScreen.logout]
XCTAssertTrue(logoutButton.waitForExistence(timeout: 10.0)) XCTAssertTrue(logoutButton.waitForExistence(timeout: 10.0))
logoutButton.tapCenter() logoutButton.tap(.center)
// Confirm logout // Confirm logout
let alertLogoutButton = alerts.firstMatch.buttons["Sign out"] let alertLogoutButton = alerts.firstMatch.buttons["Sign out"]
XCTAssertTrue(alertLogoutButton.waitForExistence(timeout: 10.0)) XCTAssertTrue(alertLogoutButton.waitForExistence(timeout: 10.0))
alertLogoutButton.tapCenter() alertLogoutButton.tap(.center)
// Check that we're back on the login screen // Check that we're back on the login screen
let getStartedButton = buttons[A11yIdentifiers.authenticationStartScreen.signIn] let getStartedButton = buttons[A11yIdentifiers.authenticationStartScreen.signIn]
+14 -14
View File
@@ -42,7 +42,7 @@ class UserFlowTests: XCTestCase {
// And open it // And open it
let firstRoom = app.buttons.matching(NSPredicate(format: "identifier CONTAINS %@", Self.integrationTestsRoomName)).firstMatch let firstRoom = app.buttons.matching(NSPredicate(format: "identifier CONTAINS %@", Self.integrationTestsRoomName)).firstMatch
XCTAssertTrue(firstRoom.waitForExistence(timeout: 10.0)) XCTAssertTrue(firstRoom.waitForExistence(timeout: 10.0))
firstRoom.tapCenter() firstRoom.tap(.center)
sendMessages() sendMessages()
@@ -62,7 +62,7 @@ class UserFlowTests: XCTestCase {
// Cancel initial the room search // Cancel initial the room search
let searchCancelButton = app.buttons["Cancel"].firstMatch let searchCancelButton = app.buttons["Cancel"].firstMatch
XCTAssertTrue(searchCancelButton.waitForExistence(timeout: 10.0)) XCTAssertTrue(searchCancelButton.waitForExistence(timeout: 10.0))
searchCancelButton.tapCenter() searchCancelButton.tap(.center)
} }
private func sendMessages() { private func sendMessages() {
@@ -72,7 +72,7 @@ class UserFlowTests: XCTestCase {
var sendButton = app.buttons[A11yIdentifiers.roomScreen.sendButton].firstMatch var sendButton = app.buttons[A11yIdentifiers.roomScreen.sendButton].firstMatch
XCTAssertTrue(sendButton.waitForExistence(timeout: 10.0)) XCTAssertTrue(sendButton.waitForExistence(timeout: 10.0))
sendButton.tapCenter() sendButton.tap(.center)
sleep(10) // Wait for the message to be sent sleep(10) // Wait for the message to be sent
@@ -86,12 +86,12 @@ class UserFlowTests: XCTestCase {
sendButton = app.buttons[A11yIdentifiers.roomScreen.sendButton].firstMatch sendButton = app.buttons[A11yIdentifiers.roomScreen.sendButton].firstMatch
XCTAssertTrue(sendButton.waitForExistence(timeout: 10.0)) XCTAssertTrue(sendButton.waitForExistence(timeout: 10.0))
sendButton.tapCenter() sendButton.tap(.center)
sleep(5) // Wait for the message to be sent sleep(5) // Wait for the message to be sent
// Close the formatting options // Close the formatting options
app.buttons[A11yIdentifiers.roomScreen.composerToolbar.closeFormattingOptions].tapCenter() app.buttons[A11yIdentifiers.roomScreen.composerToolbar.closeFormattingOptions].tap(.center)
} }
private func checkPhotoSharing() { private func checkPhotoSharing() {
@@ -103,7 +103,7 @@ class UserFlowTests: XCTestCase {
// Tap on the second image. First one is always broken on simulators. // Tap on the second image. First one is always broken on simulators.
let secondImage = app.scrollViews.images.element(boundBy: 1) let secondImage = app.scrollViews.images.element(boundBy: 1)
XCTAssertTrue(secondImage.waitForExistence(timeout: 20.0)) // Photo library takes a bit to load 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 // Wait for the image to be processed and the new screen to appear
sleep(10) sleep(10)
@@ -134,7 +134,7 @@ class UserFlowTests: XCTestCase {
// Handle map loading errors (missing credentials) // Handle map loading errors (missing credentials)
let alertOkButton = app.alerts.firstMatch.buttons["OK"].firstMatch let alertOkButton = app.alerts.firstMatch.buttons["OK"].firstMatch
if alertOkButton.waitForExistence(timeout: 10.0) { if alertOkButton.waitForExistence(timeout: 10.0) {
alertOkButton.tapCenter() alertOkButton.tap(.center)
} }
allowLocationPermissionOnce() allowLocationPermissionOnce()
@@ -146,7 +146,7 @@ class UserFlowTests: XCTestCase {
let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard") let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
let notificationAlertAllowButton = springboard.buttons["Allow Once"].firstMatch let notificationAlertAllowButton = springboard.buttons["Allow Once"].firstMatch
if notificationAlertAllowButton.waitForExistence(timeout: 10.0) { if notificationAlertAllowButton.waitForExistence(timeout: 10.0) {
notificationAlertAllowButton.tapCenter() notificationAlertAllowButton.tap(.center)
} }
} }
@@ -183,7 +183,7 @@ class UserFlowTests: XCTestCase {
// Open the room details // Open the room details
let roomHeader = app.staticTexts[A11yIdentifiers.roomScreen.name] let roomHeader = app.staticTexts[A11yIdentifiers.roomScreen.name]
XCTAssertTrue(roomHeader.waitForExistence(timeout: 10.0)) XCTAssertTrue(roomHeader.waitForExistence(timeout: 10.0))
roomHeader.tapCenter() roomHeader.tap(.center)
// Open the room member details // Open the room member details
tapOnButton(A11yIdentifiers.roomDetailsScreen.people) 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. // Open the first member's details. Loading members for big rooms can take a while.
let firstRoomMember = app.scrollViews.buttons.firstMatch let firstRoomMember = app.scrollViews.buttons.firstMatch
XCTAssertTrue(firstRoomMember.waitForExistence(timeout: 1000.0)) XCTAssertTrue(firstRoomMember.waitForExistence(timeout: 1000.0))
firstRoomMember.tapCenter() firstRoomMember.tap(.center)
// Go back to the room member details // Go back to the room member details
tapOnBackButton("People") tapOnBackButton("People")
@@ -211,7 +211,7 @@ class UserFlowTests: XCTestCase {
let profileButton = app.buttons[A11yIdentifiers.homeScreen.userAvatar] let profileButton = app.buttons[A11yIdentifiers.homeScreen.userAvatar]
// `Failed to scroll to visible (by AX action) Button` https://stackoverflow.com/a/33534187/730924 // `Failed to scroll to visible (by AX action) Button` https://stackoverflow.com/a/33534187/730924
profileButton.tapCenter() profileButton.tap(.center)
// Open analytics // Open analytics
tapOnButton(A11yIdentifiers.settingsScreen.analytics) tapOnButton(A11yIdentifiers.settingsScreen.analytics)
@@ -238,7 +238,7 @@ class UserFlowTests: XCTestCase {
private func tapOnButton(_ identifier: String, waitForDisappearance: Bool = false) { private func tapOnButton(_ identifier: String, waitForDisappearance: Bool = false) {
let button = app.buttons[identifier] let button = app.buttons[identifier]
XCTAssertTrue(button.waitForExistence(timeout: 10.0)) XCTAssertTrue(button.waitForExistence(timeout: 10.0))
button.tapCenter() button.tap(.center)
if waitForDisappearance { if waitForDisappearance {
let doesNotExistPredicate = NSPredicate(format: "exists == 0") let doesNotExistPredicate = NSPredicate(format: "exists == 0")
@@ -250,7 +250,7 @@ class UserFlowTests: XCTestCase {
private func tapOnMenu(_ identifier: String) { private func tapOnMenu(_ identifier: String) {
let button = app.buttons[identifier] let button = app.buttons[identifier]
XCTAssertTrue(button.waitForExistence(timeout: 10.0)) 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. /// 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") { private func tapOnBackButton(_ label: String = "Back") {
let button = app.buttons.matching(NSPredicate(format: "label == %@ && identifier == ''", label)).firstMatch let button = app.buttons.matching(NSPredicate(format: "label == %@ && identifier == ''", label)).firstMatch
XCTAssertTrue(button.waitForExistence(timeout: 10.0)) XCTAssertTrue(button.waitForExistence(timeout: 10.0))
button.tapCenter() button.tap(.center)
} }
} }