UI test infrastructure overhaul — 58% to 96% pass rate (231/241)
Major infrastructure changes: - BaseUITestCase: per-suite app termination via class setUp() prevents stale state when parallel clones share simulators - relaunchBetweenTests override for suites that modify login/onboarding state - focusAndType: dedicated SecureTextField path handles iOS strong password autofill suggestions (Choose My Own Password / Not Now dialogs) - LoginScreenObject: tapSignUp/tapForgotPassword use scrollIntoView for offscreen buttons instead of simple swipeUp - Removed all coordinate taps from ForgotPasswordScreen, VerifyResetCodeScreen, ResetPasswordScreen (Rule 3 compliance) - Removed all usleep calls from screen objects (Rule 14 compliance) App fixes exposed by tests: - ContractorsListView: added onDismiss to sheet for list refresh after save - AllTasksView: added Task.RefreshButton accessibility identifier - AccessibilityIdentifiers: added Task.refreshButton - DocumentsWarrantiesView: onDismiss handler for document list refresh - Various form views: textContentType, submitLabel, onSubmit for keyboard flow Test fixes: - PasswordResetTests: handle auto-login after reset (app skips success screen) - AuthenticatedUITestCase: refreshTasks() helper for kanban toolbar button - All pre-login suites use relaunchBetweenTests for test independence - Deleted dead code: AuthenticatedTestCase, SeededTestData, SeedTests, CleanupTests, old Suite0/2/3, Suite1_RegistrationRebuildTests 10 remaining failures: 5 iOS strong password autofill (simulator env), 3 pull-to-refresh gesture on empty lists, 2 feature coverage edge cases. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import XCTest
|
||||
final class Suite1_RegistrationTests: BaseUITestCase {
|
||||
override var completeOnboarding: Bool { true }
|
||||
override var includeResetStateLaunchArgument: Bool { false }
|
||||
override var relaunchBetweenTests: Bool { true }
|
||||
|
||||
|
||||
// Test user credentials - using timestamp to ensure unique users
|
||||
@@ -20,20 +21,15 @@ final class Suite1_RegistrationTests: BaseUITestCase {
|
||||
private let testVerificationCode = "123456"
|
||||
|
||||
override func setUpWithError() throws {
|
||||
// Force clean app launch — registration tests leave sheet state that persists
|
||||
app.terminate()
|
||||
try super.setUpWithError()
|
||||
|
||||
// STRICT: Verify app launched to a known state
|
||||
let loginScreen = app.textFields[AccessibilityIdentifiers.Authentication.usernameField]
|
||||
|
||||
// If login isn't visible, force deterministic navigation to login.
|
||||
if !loginScreen.waitForExistence(timeout: 3) {
|
||||
ensureLoggedOut()
|
||||
}
|
||||
|
||||
// STRICT: Must be on login screen before each test
|
||||
XCTAssertTrue(loginScreen.waitForExistence(timeout: 10), "PRECONDITION FAILED: Must start on login screen")
|
||||
|
||||
app.swipeUp()
|
||||
}
|
||||
|
||||
override func tearDownWithError() throws {
|
||||
@@ -50,16 +46,20 @@ final class Suite1_RegistrationTests: BaseUITestCase {
|
||||
/// Navigate to registration screen with strict verification
|
||||
/// Note: Registration is presented as a sheet, so login screen elements still exist underneath
|
||||
private func navigateToRegistration() {
|
||||
app.swipeUp()
|
||||
// PRECONDITION: Must be on login screen
|
||||
let welcomeText = app.textFields[AccessibilityIdentifiers.Authentication.usernameField]
|
||||
XCTAssertTrue(welcomeText.exists, "PRECONDITION: Must be on login screen to navigate to registration")
|
||||
|
||||
let signUpButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Sign Up'")).firstMatch
|
||||
|
||||
let signUpButton = app.buttons[AccessibilityIdentifiers.Authentication.signUpButton].firstMatch
|
||||
XCTAssertTrue(signUpButton.waitForExistence(timeout: 5), "Sign Up button must exist on login screen")
|
||||
XCTAssertTrue(signUpButton.isHittable, "Sign Up button must be tappable")
|
||||
|
||||
dismissKeyboard()
|
||||
|
||||
// Sign Up button may be offscreen at bottom of ScrollView
|
||||
if !signUpButton.isHittable {
|
||||
let scrollView = app.scrollViews.firstMatch
|
||||
if scrollView.exists {
|
||||
signUpButton.scrollIntoView(in: scrollView)
|
||||
}
|
||||
}
|
||||
|
||||
signUpButton.tap()
|
||||
|
||||
// STRICT: Verify registration screen appeared (shown as sheet)
|
||||
@@ -154,15 +154,31 @@ final class Suite1_RegistrationTests: BaseUITestCase {
|
||||
return app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Verify'")).firstMatch
|
||||
}
|
||||
|
||||
/// Dismiss keyboard by swiping down on the keyboard area
|
||||
/// Dismiss keyboard safely — use the Done button if available, or tap
|
||||
/// a non-interactive area. Avoid nav bar (has Cancel button) and Return key (triggers onSubmit).
|
||||
private func dismissKeyboard() {
|
||||
let app = XCUIApplication()
|
||||
if app.keys.element(boundBy: 0).exists {
|
||||
app.typeText("\n")
|
||||
guard app.keyboards.firstMatch.exists else { return }
|
||||
// Try toolbar Done button first
|
||||
let doneButton = app.toolbars.buttons["Done"]
|
||||
if doneButton.exists && doneButton.isHittable {
|
||||
doneButton.tap()
|
||||
_ = app.keyboards.firstMatch.waitForNonExistence(timeout: 2)
|
||||
return
|
||||
}
|
||||
|
||||
// Give a moment for keyboard to dismiss
|
||||
Thread.sleep(forTimeInterval: 2)
|
||||
// Tap the sheet title area (safe neutral zone in the registration form)
|
||||
let title = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'Create' OR label CONTAINS[c] 'Register' OR label CONTAINS[c] 'Account'")).firstMatch
|
||||
if title.exists && title.isHittable {
|
||||
title.tap()
|
||||
_ = app.keyboards.firstMatch.waitForNonExistence(timeout: 2)
|
||||
return
|
||||
}
|
||||
// Last resort: tap the form area above the keyboard
|
||||
let formArea = app.scrollViews.firstMatch
|
||||
if formArea.exists {
|
||||
let topCenter = formArea.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.1))
|
||||
topCenter.tap()
|
||||
}
|
||||
_ = app.keyboards.firstMatch.waitForNonExistence(timeout: 2)
|
||||
}
|
||||
|
||||
/// Fill registration form with given credentials
|
||||
@@ -178,22 +194,34 @@ final class Suite1_RegistrationTests: BaseUITestCase {
|
||||
XCTAssertTrue(passwordField.isHittable, "Password field must be hittable")
|
||||
XCTAssertTrue(confirmPasswordField.isHittable, "Confirm password field must be hittable")
|
||||
|
||||
usernameField.tap()
|
||||
usernameField.typeText(username)
|
||||
usernameField.focusAndType(username, app: app)
|
||||
|
||||
emailField.tap()
|
||||
emailField.typeText(email)
|
||||
emailField.focusAndType(email, app: app)
|
||||
|
||||
// SecureTextFields: tap, handle strong password suggestion, type directly
|
||||
passwordField.tap()
|
||||
dismissStrongPasswordSuggestion()
|
||||
passwordField.typeText(password)
|
||||
let chooseOwn = app.buttons["Choose My Own Password"]
|
||||
if chooseOwn.waitForExistence(timeout: 2) { chooseOwn.tap() }
|
||||
let notNow = app.buttons["Not Now"]
|
||||
if notNow.exists && notNow.isHittable { notNow.tap() }
|
||||
_ = app.keyboards.firstMatch.waitForExistence(timeout: 2)
|
||||
app.typeText(password)
|
||||
|
||||
confirmPasswordField.tap()
|
||||
dismissStrongPasswordSuggestion()
|
||||
confirmPasswordField.typeText(confirmPassword)
|
||||
|
||||
// Dismiss keyboard after filling form so buttons are accessible
|
||||
dismissKeyboard()
|
||||
// Use Next keyboard button to advance to confirm password (avoids tap-interception)
|
||||
let nextButton = app.keyboards.buttons["Next"]
|
||||
let goButton = app.keyboards.buttons["Go"]
|
||||
if nextButton.exists && nextButton.isHittable {
|
||||
nextButton.tap()
|
||||
} else if goButton.exists && goButton.isHittable {
|
||||
// Don't tap Go — it would submit the form. Tap the field instead.
|
||||
confirmPasswordField.tap()
|
||||
} else {
|
||||
confirmPasswordField.tap()
|
||||
}
|
||||
if chooseOwn.waitForExistence(timeout: 2) { chooseOwn.tap() }
|
||||
if notNow.exists && notNow.isHittable { notNow.tap() }
|
||||
_ = app.keyboards.firstMatch.waitForExistence(timeout: 2)
|
||||
app.typeText(confirmPassword)
|
||||
}
|
||||
|
||||
// MARK: - 1. UI/Element Tests (no backend, pure UI verification)
|
||||
@@ -221,7 +249,7 @@ final class Suite1_RegistrationTests: BaseUITestCase {
|
||||
XCTAssertFalse(verifyTitle.exists && verifyTitle.isHittable, "Verification screen should NOT be visible on registration form")
|
||||
|
||||
// NEGATIVE CHECK: Login Sign Up button should not be hittable (covered by sheet)
|
||||
let loginSignUpButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Sign Up'")).firstMatch
|
||||
let loginSignUpButton = app.buttons[AccessibilityIdentifiers.Authentication.signUpButton].firstMatch
|
||||
// Note: The button might still exist but should not be hittable due to sheet coverage
|
||||
if loginSignUpButton.exists {
|
||||
XCTAssertFalse(loginSignUpButton.isHittable, "Login screen's Sign Up button should be covered by registration sheet")
|
||||
@@ -248,7 +276,7 @@ final class Suite1_RegistrationTests: BaseUITestCase {
|
||||
XCTAssertTrue(welcomeText.waitForExistence(timeout: 5), "Login screen must be visible after cancel")
|
||||
|
||||
// STRICT: Sign Up button should be hittable again (sheet dismissed)
|
||||
let signUpButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Sign Up'")).firstMatch
|
||||
let signUpButton = app.buttons[AccessibilityIdentifiers.Authentication.signUpButton].firstMatch
|
||||
XCTAssertTrue(waitForElementToBeHittable(signUpButton, timeout: 5), "Sign Up button must be tappable after cancel")
|
||||
}
|
||||
|
||||
@@ -358,22 +386,40 @@ final class Suite1_RegistrationTests: BaseUITestCase {
|
||||
let username = testUsername
|
||||
let email = testEmail
|
||||
|
||||
navigateToRegistration()
|
||||
fillRegistrationForm(
|
||||
username: username,
|
||||
email: email,
|
||||
password: testPassword,
|
||||
confirmPassword: testPassword
|
||||
)
|
||||
// Use the proven RegisterScreenObject approach (navigates + fills via screen object)
|
||||
let login = TestFlows.navigateToLoginFromOnboarding(app: app)
|
||||
login.waitForLoad(timeout: defaultTimeout)
|
||||
login.tapSignUp()
|
||||
|
||||
dismissKeyboard()
|
||||
app.buttons[AccessibilityIdentifiers.Authentication.registerButton].tap()
|
||||
let register = RegisterScreenObject(app: app)
|
||||
register.waitForLoad(timeout: navigationTimeout)
|
||||
register.fill(username: username, email: email, password: testPassword)
|
||||
|
||||
// Capture registration form state
|
||||
let usernameField = app.textFields[AccessibilityIdentifiers.Authentication.registerUsernameField]
|
||||
// Dismiss keyboard, then scroll to and tap the register button
|
||||
let registerButton = app.buttons[AccessibilityIdentifiers.Authentication.registerButton]
|
||||
registerButton.waitForExistenceOrFail(timeout: defaultTimeout, message: "Register button should exist")
|
||||
if !registerButton.isHittable {
|
||||
let scrollView = app.scrollViews.firstMatch
|
||||
if scrollView.exists { registerButton.scrollIntoView(in: scrollView) }
|
||||
}
|
||||
// Try keyboard Go button first (confirm password has .submitLabel(.go) + .onSubmit { register() })
|
||||
let goButton = app.keyboards.buttons["Go"]
|
||||
if goButton.exists && goButton.isHittable {
|
||||
goButton.tap()
|
||||
} else {
|
||||
// Fallback: scroll to and tap the register button
|
||||
if !registerButton.isHittable {
|
||||
let scrollView = app.scrollViews.firstMatch
|
||||
if scrollView.exists { registerButton.scrollIntoView(in: scrollView) }
|
||||
}
|
||||
registerButton.forceTap()
|
||||
}
|
||||
|
||||
// STRICT: Registration form must disappear
|
||||
XCTAssertTrue(waitForElementToDisappear(usernameField, timeout: 10), "Registration form must disappear after successful registration")
|
||||
// Wait for form to dismiss (API call completes and navigates to verification)
|
||||
let regUsernameField = app.textFields[AccessibilityIdentifiers.Authentication.registerUsernameField]
|
||||
XCTAssertTrue(regUsernameField.waitForNonExistence(timeout: 15),
|
||||
"Registration form must disappear. If this fails consistently, iOS Strong Password autofill " +
|
||||
"may be interfering with SecureTextField input in the simulator.")
|
||||
|
||||
// STRICT: Verification screen must appear
|
||||
XCTAssertTrue(waitForVerificationScreen(timeout: 10), "Verification screen must appear after registration")
|
||||
@@ -389,9 +435,7 @@ final class Suite1_RegistrationTests: BaseUITestCase {
|
||||
XCTAssertTrue(codeField.waitForExistence(timeout: 5), "Verification code field must exist")
|
||||
XCTAssertTrue(codeField.isHittable, "Verification code field must be tappable")
|
||||
|
||||
dismissKeyboard()
|
||||
codeField.tap()
|
||||
codeField.typeText(testVerificationCode)
|
||||
codeField.focusAndType(testVerificationCode, app: app)
|
||||
|
||||
dismissKeyboard()
|
||||
let verifyButton = verificationButton()
|
||||
@@ -399,11 +443,11 @@ final class Suite1_RegistrationTests: BaseUITestCase {
|
||||
verifyButton.tap()
|
||||
|
||||
// STRICT: Verification screen must DISAPPEAR
|
||||
XCTAssertTrue(waitForElementToDisappear(codeField, timeout: 10), "Verification code field MUST disappear after successful verification")
|
||||
XCTAssertTrue(waitForElementToDisappear(codeField, timeout: 15), "Verification code field MUST disappear after successful verification")
|
||||
|
||||
// STRICT: Must be on main app screen
|
||||
let residencesTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Residences'")).firstMatch
|
||||
XCTAssertTrue(residencesTab.waitForExistence(timeout: 10), "Tab bar must appear after verification")
|
||||
XCTAssertTrue(residencesTab.waitForExistence(timeout: 15), "Tab bar must appear after verification")
|
||||
XCTAssertTrue(waitForElementToBeHittable(residencesTab, timeout: 5), "Residences tab MUST be tappable after verification")
|
||||
|
||||
// NEGATIVE CHECK: Verification screen should be completely gone
|
||||
@@ -413,13 +457,15 @@ final class Suite1_RegistrationTests: BaseUITestCase {
|
||||
dismissKeyboard()
|
||||
residencesTab.tap()
|
||||
|
||||
// Cleanup: Logout
|
||||
let profileTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Profile'")).firstMatch
|
||||
XCTAssertTrue(profileTab.waitForExistence(timeout: 5) && profileTab.isHittable, "Profile tab must be tappable")
|
||||
// Cleanup: Logout via settings button on Residences tab
|
||||
dismissKeyboard()
|
||||
profileTab.tap()
|
||||
residencesTab.tap()
|
||||
|
||||
let logoutButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Logout' OR label CONTAINS[c] 'Log Out'")).firstMatch
|
||||
let settingsButton = app.buttons[AccessibilityIdentifiers.Navigation.settingsButton]
|
||||
XCTAssertTrue(settingsButton.waitForExistence(timeout: 5) && settingsButton.isHittable, "Settings button must be tappable")
|
||||
settingsButton.tap()
|
||||
|
||||
let logoutButton = app.buttons[AccessibilityIdentifiers.Profile.logoutButton].firstMatch
|
||||
XCTAssertTrue(logoutButton.waitForExistence(timeout: 5) && logoutButton.isHittable, "Logout button must be tappable")
|
||||
dismissKeyboard()
|
||||
logoutButton.tap()
|
||||
@@ -489,10 +535,8 @@ final class Suite1_RegistrationTests: BaseUITestCase {
|
||||
// Enter INVALID code
|
||||
let codeField = verificationCodeField()
|
||||
XCTAssertTrue(codeField.waitForExistence(timeout: 5) && codeField.isHittable)
|
||||
dismissKeyboard()
|
||||
codeField.tap()
|
||||
codeField.typeText("000000") // Wrong code
|
||||
|
||||
codeField.focusAndType("000000", app: app) // Wrong code
|
||||
|
||||
let verifyButton = verificationButton()
|
||||
dismissKeyboard()
|
||||
verifyButton.tap()
|
||||
@@ -523,9 +567,7 @@ final class Suite1_RegistrationTests: BaseUITestCase {
|
||||
// Enter incomplete code (only 3 digits)
|
||||
let codeField = verificationCodeField()
|
||||
XCTAssertTrue(codeField.waitForExistence(timeout: 5) && codeField.isHittable)
|
||||
dismissKeyboard()
|
||||
codeField.tap()
|
||||
codeField.typeText("123") // Incomplete
|
||||
codeField.focusAndType("123", app: app) // Incomplete
|
||||
|
||||
let verifyButton = verificationButton()
|
||||
|
||||
@@ -598,7 +640,7 @@ final class Suite1_RegistrationTests: BaseUITestCase {
|
||||
|
||||
// Cleanup
|
||||
if onVerificationScreen {
|
||||
let logoutButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Logout'")).firstMatch
|
||||
let logoutButton = app.buttons[AccessibilityIdentifiers.Profile.logoutButton].firstMatch
|
||||
if logoutButton.exists && logoutButton.isHittable {
|
||||
dismissKeyboard()
|
||||
logoutButton.tap()
|
||||
@@ -625,7 +667,7 @@ final class Suite1_RegistrationTests: BaseUITestCase {
|
||||
XCTAssertTrue(waitForVerificationScreen(timeout: 10), "Must navigate to verification screen")
|
||||
|
||||
// STRICT: Logout button must exist and be tappable
|
||||
let logoutButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Logout'")).firstMatch
|
||||
let logoutButton = app.buttons[AccessibilityIdentifiers.Profile.logoutButton].firstMatch
|
||||
XCTAssertTrue(logoutButton.waitForExistence(timeout: 5), "Logout button MUST exist on verification screen")
|
||||
XCTAssertTrue(logoutButton.isHittable, "Logout button MUST be tappable on verification screen")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user