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:
Trey T
2026-03-23 15:05:37 -05:00
parent 0ca4a44bac
commit 4df8707b92
67 changed files with 3085 additions and 4853 deletions

View File

@@ -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")