Files
honeyDueKMP/iosApp/HoneyDueUITests/Suite1_RegistrationTests.swift
Trey T 4df8707b92 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>
2026-03-23 15:05:37 -05:00

698 lines
33 KiB
Swift

import XCTest
/// Comprehensive registration flow tests with strict, failure-first assertions
/// Tests verify both positive AND negative conditions to ensure robust validation
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
private var testUsername: String {
return "testuser_\(Int(Date().timeIntervalSince1970))"
}
private var testEmail: String {
return "test_\(Int(Date().timeIntervalSince1970))@example.com"
}
private let testPassword = "Pass1234"
/// Fixed test verification code - Go API uses this code when DEBUG=true
private let testVerificationCode = "123456"
override func setUpWithError() throws {
// Force clean app launch registration tests leave sheet state that persists
app.terminate()
try super.setUpWithError()
let loginScreen = app.textFields[AccessibilityIdentifiers.Authentication.usernameField]
if !loginScreen.waitForExistence(timeout: 3) {
ensureLoggedOut()
}
XCTAssertTrue(loginScreen.waitForExistence(timeout: 10), "PRECONDITION FAILED: Must start on login screen")
}
override func tearDownWithError() throws {
ensureLoggedOut()
try super.tearDownWithError()
}
// MARK: - Strict Helper Methods
private func ensureLoggedOut() {
UITestHelpers.ensureLoggedOut(app: app)
}
/// Navigate to registration screen with strict verification
/// Note: Registration is presented as a sheet, so login screen elements still exist underneath
private func navigateToRegistration() {
let welcomeText = app.textFields[AccessibilityIdentifiers.Authentication.usernameField]
XCTAssertTrue(welcomeText.exists, "PRECONDITION: Must be on login screen to navigate to registration")
let signUpButton = app.buttons[AccessibilityIdentifiers.Authentication.signUpButton].firstMatch
XCTAssertTrue(signUpButton.waitForExistence(timeout: 5), "Sign Up button must exist on login screen")
// 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)
// Note: Login screen still exists underneath the sheet, so we verify registration elements instead
let usernameField = app.textFields[AccessibilityIdentifiers.Authentication.registerUsernameField]
XCTAssertTrue(usernameField.waitForExistence(timeout: 5), "Registration username field must appear")
XCTAssertTrue(waitForElementToBeHittable(usernameField, timeout: 5), "Registration username field must be tappable")
// Keep action buttons visible for strict assertions and interactions.
let createAccountButton = app.buttons[AccessibilityIdentifiers.Authentication.registerButton]
if createAccountButton.exists && !createAccountButton.isHittable {
let scrollView = app.scrollViews.firstMatch
if scrollView.exists {
createAccountButton.scrollIntoView(in: scrollView, maxSwipes: 5)
}
}
// STRICT: The Sign Up button should no longer be hittable (covered by sheet)
XCTAssertFalse(signUpButton.isHittable, "Login Sign Up button should be covered by registration sheet")
}
/// Dismisses iOS Strong Password suggestion overlay
private func dismissStrongPasswordSuggestion() {
let chooseOwnPassword = app.buttons["Choose My Own Password"]
if chooseOwnPassword.waitForExistence(timeout: 1) {
chooseOwnPassword.tap()
return
}
let notNowButton = app.buttons["Not Now"]
if notNowButton.exists && notNowButton.isHittable {
notNowButton.tap()
return
}
// Dismiss by tapping elsewhere
let strongPasswordText = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'Strong Password'")).firstMatch
if strongPasswordText.exists {
app.tap()
}
}
/// Wait for element to disappear - CRITICAL for strict testing
private func waitForElementToDisappear(_ element: XCUIElement, timeout: TimeInterval) -> Bool {
let expectation = XCTNSPredicateExpectation(
predicate: NSPredicate(format: "exists == false"),
object: element
)
let result = XCTWaiter.wait(for: [expectation], timeout: timeout)
return result == .completed
}
/// Wait for element to become hittable (visible AND interactive)
private func waitForElementToBeHittable(_ element: XCUIElement, timeout: TimeInterval) -> Bool {
let expectation = XCTNSPredicateExpectation(
predicate: NSPredicate(format: "isHittable == true"),
object: element
)
let result = XCTWaiter.wait(for: [expectation], timeout: timeout)
return result == .completed
}
/// Verification screen readiness check based on stable accessibility IDs.
private func waitForVerificationScreen(timeout: TimeInterval) -> Bool {
let authCodeField = app.textFields[AccessibilityIdentifiers.Authentication.verificationCodeField]
let onboardingCodeField = app.textFields[AccessibilityIdentifiers.Onboarding.verificationCodeField]
let authVerifyButton = app.buttons[AccessibilityIdentifiers.Authentication.verifyButton]
let onboardingVerifyButton = app.buttons[AccessibilityIdentifiers.Onboarding.verifyButton]
return authCodeField.waitForExistence(timeout: timeout)
|| onboardingCodeField.waitForExistence(timeout: timeout)
|| authVerifyButton.waitForExistence(timeout: timeout)
|| onboardingVerifyButton.waitForExistence(timeout: timeout)
}
private func verificationCodeField() -> XCUIElement {
let authCodeField = app.textFields[AccessibilityIdentifiers.Authentication.verificationCodeField]
if authCodeField.exists {
return authCodeField
}
return app.textFields[AccessibilityIdentifiers.Onboarding.verificationCodeField]
}
private func verificationButton() -> XCUIElement {
let authVerifyButton = app.buttons[AccessibilityIdentifiers.Authentication.verifyButton]
if authVerifyButton.exists {
return authVerifyButton
}
let onboardingVerifyButton = app.buttons[AccessibilityIdentifiers.Onboarding.verifyButton]
if onboardingVerifyButton.exists {
return onboardingVerifyButton
}
return app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Verify'")).firstMatch
}
/// 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() {
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
}
// 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
private func fillRegistrationForm(username: String, email: String, password: String, confirmPassword: String) {
let usernameField = app.textFields[AccessibilityIdentifiers.Authentication.registerUsernameField]
let emailField = app.textFields[AccessibilityIdentifiers.Authentication.registerEmailField]
let passwordField = app.secureTextFields[AccessibilityIdentifiers.Authentication.registerPasswordField]
let confirmPasswordField = app.secureTextFields[AccessibilityIdentifiers.Authentication.registerConfirmPasswordField]
// STRICT: All fields must exist and be hittable
XCTAssertTrue(usernameField.isHittable, "Username field must be hittable")
XCTAssertTrue(emailField.isHittable, "Email field must be hittable")
XCTAssertTrue(passwordField.isHittable, "Password field must be hittable")
XCTAssertTrue(confirmPasswordField.isHittable, "Confirm password field must be hittable")
usernameField.focusAndType(username, app: app)
emailField.focusAndType(email, app: app)
// SecureTextFields: tap, handle strong password suggestion, type directly
passwordField.tap()
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)
// 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)
func test01_registrationScreenElements() {
navigateToRegistration()
// STRICT: All form elements must exist AND be hittable
let usernameField = app.textFields[AccessibilityIdentifiers.Authentication.registerUsernameField]
let emailField = app.textFields[AccessibilityIdentifiers.Authentication.registerEmailField]
let passwordField = app.secureTextFields[AccessibilityIdentifiers.Authentication.registerPasswordField]
let confirmPasswordField = app.secureTextFields[AccessibilityIdentifiers.Authentication.registerConfirmPasswordField]
let createAccountButton = app.buttons[AccessibilityIdentifiers.Authentication.registerButton]
let cancelButton = app.buttons[AccessibilityIdentifiers.Authentication.registerCancelButton]
XCTAssertTrue(usernameField.exists && usernameField.isHittable, "Username field must be visible and tappable")
XCTAssertTrue(emailField.exists && emailField.isHittable, "Email field must be visible and tappable")
XCTAssertTrue(passwordField.exists && passwordField.isHittable, "Password field must be visible and tappable")
XCTAssertTrue(confirmPasswordField.exists && confirmPasswordField.isHittable, "Confirm password field must be visible and tappable")
XCTAssertTrue(createAccountButton.exists && createAccountButton.isHittable, "Create Account button must be visible and tappable")
XCTAssertTrue(cancelButton.exists && cancelButton.isHittable, "Cancel button must be visible and tappable")
// NEGATIVE CHECK: Should NOT see verification screen elements as hittable
let verifyTitle = app.staticTexts["Verify Your Email"]
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[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")
}
}
func test02_cancelRegistration() {
navigateToRegistration()
// Capture that we're on registration screen
let usernameField = app.textFields[AccessibilityIdentifiers.Authentication.registerUsernameField]
XCTAssertTrue(usernameField.isHittable, "PRECONDITION: Must be on registration screen")
let cancelButton = app.buttons[AccessibilityIdentifiers.Authentication.registerCancelButton]
XCTAssertTrue(cancelButton.isHittable, "Cancel button must be tappable")
dismissKeyboard()
cancelButton.tap()
// STRICT: Registration sheet must dismiss - username field should no longer be hittable
XCTAssertTrue(waitForElementToDisappear(usernameField, timeout: 5), "Registration form must disappear after cancel")
// STRICT: Login screen must now be interactive again
let welcomeText = app.textFields[AccessibilityIdentifiers.Authentication.usernameField]
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[AccessibilityIdentifiers.Authentication.signUpButton].firstMatch
XCTAssertTrue(waitForElementToBeHittable(signUpButton, timeout: 5), "Sign Up button must be tappable after cancel")
}
// MARK: - 2. Client-Side Validation Tests (no API calls, fail locally)
func test03_registrationWithEmptyFields() {
navigateToRegistration()
let createAccountButton = app.buttons[AccessibilityIdentifiers.Authentication.registerButton]
XCTAssertTrue(createAccountButton.isHittable, "Create Account button must be tappable")
// Capture current state
let verifyTitle = app.staticTexts["Verify Your Email"]
XCTAssertFalse(verifyTitle.exists, "PRECONDITION: Should not be on verification screen")
dismissKeyboard()
createAccountButton.tap()
// STRICT: Must show error message
let errorPredicate = NSPredicate(format: "label CONTAINS[c] 'required' OR label CONTAINS[c] 'Username'")
let errorMessage = app.staticTexts.containing(errorPredicate).firstMatch
XCTAssertTrue(errorMessage.waitForExistence(timeout: 3), "Error message must appear for empty fields")
// NEGATIVE CHECK: Should NOT navigate away from registration
// XCTAssertFalse(verifyTitle.exists, "Should NOT navigate to verification screen with empty fields")
// STRICT: Registration form should still be visible and interactive
// let usernameField = app.textFields[AccessibilityIdentifiers.Authentication.registerUsernameField]
// XCTAssertTrue(usernameField.isHittable, "Username field should still be tappable after error")
}
func test04_registrationWithInvalidEmail() {
navigateToRegistration()
fillRegistrationForm(
username: "testuser",
email: "invalid-email", // Invalid format
password: testPassword,
confirmPassword: testPassword
)
let createAccountButton = app.buttons[AccessibilityIdentifiers.Authentication.registerButton]
dismissKeyboard()
createAccountButton.tap()
// STRICT: Must show email-specific error
let errorPredicate = NSPredicate(format: "label CONTAINS[c] 'email' OR label CONTAINS[c] 'invalid'")
let errorMessage = app.staticTexts.containing(errorPredicate).firstMatch
XCTAssertTrue(errorMessage.waitForExistence(timeout: 3), "Error message must appear for invalid email format")
// NEGATIVE CHECK: Should NOT proceed to verification
let verifyTitle = app.staticTexts["Verify Your Email"]
XCTAssertFalse(verifyTitle.exists, "Should NOT navigate to verification with invalid email")
}
func test05_registrationWithMismatchedPasswords() {
navigateToRegistration()
fillRegistrationForm(
username: "testuser",
email: "test@example.com",
password: "Password123!",
confirmPassword: "DifferentPassword123!" // Mismatched
)
let createAccountButton = app.buttons[AccessibilityIdentifiers.Authentication.registerButton]
dismissKeyboard()
createAccountButton.tap()
// STRICT: Must show password mismatch error
let errorPredicate = NSPredicate(format: "label CONTAINS[c] 'match' OR label CONTAINS[c] 'password'")
let errorMessage = app.staticTexts.containing(errorPredicate).firstMatch
XCTAssertTrue(errorMessage.waitForExistence(timeout: 3), "Error message must appear for mismatched passwords")
// NEGATIVE CHECK: Should NOT proceed to verification
let verifyTitle = app.staticTexts["Verify Your Email"]
XCTAssertFalse(verifyTitle.exists, "Should NOT navigate to verification with mismatched passwords")
}
func test06_registrationWithWeakPassword() {
navigateToRegistration()
fillRegistrationForm(
username: "testuser",
email: "test@example.com",
password: "weak", // Too weak
confirmPassword: "weak"
)
let createAccountButton = app.buttons[AccessibilityIdentifiers.Authentication.registerButton]
dismissKeyboard()
createAccountButton.tap()
// STRICT: Must show password strength error
let errorPredicate = NSPredicate(format: "label CONTAINS[c] 'password' OR label CONTAINS[c] 'character' OR label CONTAINS[c] 'strong' OR label CONTAINS[c] '8'")
let errorMessage = app.staticTexts.containing(errorPredicate).firstMatch
XCTAssertTrue(errorMessage.waitForExistence(timeout: 3), "Error message must appear for weak password")
// NEGATIVE CHECK: Should NOT proceed
let verifyTitle = app.staticTexts["Verify Your Email"]
XCTAssertFalse(verifyTitle.exists, "Should NOT navigate to verification with weak password")
}
// MARK: - 3. Full Registration Flow Tests (creates new users - MUST RUN BEFORE tests that need existing users)
func test07_successfulRegistrationAndVerification() {
let username = testUsername
let email = testEmail
// Use the proven RegisterScreenObject approach (navigates + fills via screen object)
let login = TestFlows.navigateToLoginFromOnboarding(app: app)
login.waitForLoad(timeout: defaultTimeout)
login.tapSignUp()
let register = RegisterScreenObject(app: app)
register.waitForLoad(timeout: navigationTimeout)
register.fill(username: username, email: email, password: testPassword)
// 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()
}
// 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")
// NEGATIVE CHECK: Tab bar should NOT be hittable while on verification
let tabBar = app.tabBars.firstMatch
if tabBar.exists {
XCTAssertFalse(tabBar.isHittable, "Tab bar should NOT be interactive while verification is required")
}
// Enter verification code
let codeField = verificationCodeField()
XCTAssertTrue(codeField.waitForExistence(timeout: 5), "Verification code field must exist")
XCTAssertTrue(codeField.isHittable, "Verification code field must be tappable")
codeField.focusAndType(testVerificationCode, app: app)
dismissKeyboard()
let verifyButton = verificationButton()
XCTAssertTrue(verifyButton.exists && verifyButton.isHittable, "Verify button must be tappable")
verifyButton.tap()
// STRICT: Verification screen must DISAPPEAR
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: 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
XCTAssertFalse(codeField.exists, "Verification code field must NOT exist after successful verification")
// Verify we can interact with the app (tap tab)
dismissKeyboard()
residencesTab.tap()
// Cleanup: Logout via settings button on Residences tab
dismissKeyboard()
residencesTab.tap()
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()
let alertLogout = app.alerts.buttons["Log Out"]
if alertLogout.waitForExistence(timeout: 3) {
dismissKeyboard()
alertLogout.tap()
}
// STRICT: Must return to login screen
let welcomeText = app.textFields[AccessibilityIdentifiers.Authentication.usernameField]
XCTAssertTrue(welcomeText.waitForExistence(timeout: 5), "Must return to login screen after logout")
}
// MARK: - 4. Server-Side Validation Tests (NOW a user exists from test07)
// func test08_registrationWithExistingUsername() {
// // NOTE: test07 created a user, so now we can test duplicate username rejection
// // We use 'testuser' which should be seeded, OR we could use the username from test07
// navigateToRegistration()
//
// fillRegistrationForm(
// username: "testuser", // Existing username (seeded in test DB)
// email: "newemail_\(Int(Date().timeIntervalSince1970))@example.com",
// password: testPassword,
// confirmPassword: testPassword
// )
//
// dismissKeyboard()
// app.buttons[AccessibilityIdentifiers.Authentication.registerButton].tap()
//
// // STRICT: Must show "already exists" error
// let errorPredicate = NSPredicate(format: "label CONTAINS[c] 'exists' OR label CONTAINS[c] 'already' OR label CONTAINS[c] 'taken'")
// let errorMessage = app.staticTexts.containing(errorPredicate).firstMatch
// XCTAssertTrue(errorMessage.waitForExistence(timeout: 5), "Error message must appear for existing username")
//
// // NEGATIVE CHECK: Should NOT proceed to verification
// let verifyTitle = app.staticTexts["Verify Your Email"]
// XCTAssertFalse(verifyTitle.exists, "Should NOT navigate to verification with existing username")
//
// // STRICT: Should still be on registration form
// let usernameField = app.textFields[AccessibilityIdentifiers.Authentication.registerUsernameField]
// XCTAssertTrue(usernameField.exists && usernameField.isHittable, "Registration form should still be active")
// }
// MARK: - 5. Verification Screen Tests
func test09_registrationWithInvalidVerificationCode() {
let username = testUsername
let email = testEmail
navigateToRegistration()
fillRegistrationForm(
username: username,
email: email,
password: testPassword,
confirmPassword: testPassword
)
dismissKeyboard()
app.buttons[AccessibilityIdentifiers.Authentication.registerButton].tap()
// Wait for verification screen
XCTAssertTrue(waitForVerificationScreen(timeout: 10), "Must navigate to verification screen")
// Enter INVALID code
let codeField = verificationCodeField()
XCTAssertTrue(codeField.waitForExistence(timeout: 5) && codeField.isHittable)
codeField.focusAndType("000000", app: app) // Wrong code
let verifyButton = verificationButton()
dismissKeyboard()
verifyButton.tap()
// STRICT: Error message must appear
let errorPredicate = NSPredicate(format: "label CONTAINS[c] 'invalid' OR label CONTAINS[c] 'error' OR label CONTAINS[c] 'incorrect' OR label CONTAINS[c] 'wrong'")
let errorMessage = app.staticTexts.containing(errorPredicate).firstMatch
XCTAssertTrue(errorMessage.waitForExistence(timeout: 5), "Error message MUST appear for invalid verification code")
}
func test10_verificationCodeFieldValidation() {
let username = testUsername
let email = testEmail
navigateToRegistration()
fillRegistrationForm(
username: username,
email: email,
password: testPassword,
confirmPassword: testPassword
)
dismissKeyboard()
app.buttons[AccessibilityIdentifiers.Authentication.registerButton].tap()
XCTAssertTrue(waitForVerificationScreen(timeout: 10))
// Enter incomplete code (only 3 digits)
let codeField = verificationCodeField()
XCTAssertTrue(codeField.waitForExistence(timeout: 5) && codeField.isHittable)
codeField.focusAndType("123", app: app) // Incomplete
let verifyButton = verificationButton()
// Button might be disabled with incomplete code
if verifyButton.isEnabled {
dismissKeyboard()
verifyButton.tap()
}
// STRICT: Must still be on verification screen
XCTAssertTrue(codeField.exists && codeField.isHittable, "Must remain on verification screen with incomplete code")
// NEGATIVE CHECK: Should NOT have navigated to main app
let residencesTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Residences'")).firstMatch
if residencesTab.exists {
XCTAssertFalse(residencesTab.isHittable, "Tab bar MUST NOT be accessible with incomplete verification")
}
}
func test11_appRelaunchWithUnverifiedUser() {
// This test verifies the fix for: user kills app on verification screen, relaunches, should see verification again
let username = testUsername
let email = testEmail
navigateToRegistration()
fillRegistrationForm(
username: username,
email: email,
password: testPassword,
confirmPassword: testPassword
)
dismissKeyboard()
app.buttons[AccessibilityIdentifiers.Authentication.registerButton].tap()
// Wait for verification screen
XCTAssertTrue(waitForVerificationScreen(timeout: 10), "Must reach verification screen")
// Simulate app kill and relaunch (terminate and launch)
app.terminate()
app.launch()
// STRICT: After relaunch, unverified user MUST see verification screen, NOT main app
let authCodeFieldAfterRelaunch = app.textFields[AccessibilityIdentifiers.Authentication.verificationCodeField]
let onboardingCodeFieldAfterRelaunch = app.textFields[AccessibilityIdentifiers.Onboarding.verificationCodeField]
let loginScreen = app.textFields[AccessibilityIdentifiers.Authentication.usernameField]
let tabBar = app.tabBars.firstMatch
// Wait for app to settle
_ = authCodeFieldAfterRelaunch.waitForExistence(timeout: 10)
|| onboardingCodeFieldAfterRelaunch.waitForExistence(timeout: 10)
|| loginScreen.waitForExistence(timeout: 10)
// User should either be on verification screen OR login screen (if token expired)
// They should NEVER be on main app with unverified email
if tabBar.exists && tabBar.isHittable {
// If tab bar is accessible, that's a FAILURE - unverified user should not access main app
XCTFail("CRITICAL: Unverified user should NOT have access to main app after relaunch. Tab bar is hittable!")
}
// Acceptable states: verification screen OR login screen
let onVerificationScreen =
(authCodeFieldAfterRelaunch.exists && authCodeFieldAfterRelaunch.isHittable)
|| (onboardingCodeFieldAfterRelaunch.exists && onboardingCodeFieldAfterRelaunch.isHittable)
let onLoginScreen = loginScreen.exists && loginScreen.isHittable
XCTAssertTrue(onVerificationScreen || onLoginScreen,
"After relaunch, unverified user must be on verification screen or login screen, NOT main app")
// Cleanup
if onVerificationScreen {
let logoutButton = app.buttons[AccessibilityIdentifiers.Profile.logoutButton].firstMatch
if logoutButton.exists && logoutButton.isHittable {
dismissKeyboard()
logoutButton.tap()
}
}
}
func test12_logoutFromVerificationScreen() {
let username = testUsername
let email = testEmail
navigateToRegistration()
fillRegistrationForm(
username: username,
email: email,
password: testPassword,
confirmPassword: testPassword
)
dismissKeyboard()
app.buttons[AccessibilityIdentifiers.Authentication.registerButton].tap()
// Wait for verification screen
XCTAssertTrue(waitForVerificationScreen(timeout: 10), "Must navigate to verification screen")
// STRICT: Logout button must exist and be tappable
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")
dismissKeyboard()
logoutButton.tap()
// STRICT: Verification screen must disappear
let codeField = verificationCodeField()
XCTAssertTrue(waitForElementToDisappear(codeField, timeout: 5), "Verification screen must disappear after logout")
// STRICT: Must return to login screen
let welcomeText = app.textFields[AccessibilityIdentifiers.Authentication.usernameField]
XCTAssertTrue(welcomeText.waitForExistence(timeout: 5), "Must return to login screen after logout")
XCTAssertTrue(welcomeText.isHittable, "Login screen must be interactive")
// NEGATIVE CHECK: Verification screen elements should be gone
XCTAssertFalse(codeField.exists, "Verification code field should NOT exist after logout")
}
}
// MARK: - XCUIElement Extension
extension XCUIElement {
var hasKeyboardFocus: Bool {
return (value(forKey: "hasKeyboardFocus") as? Bool) ?? false
}
}