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 } }