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: XCTestCase { var app: XCUIApplication! // 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 = "TestPass123!" /// Fixed test verification code - Go API uses this code when DEBUG=true private let testVerificationCode = "123456" override func setUpWithError() throws { continueAfterFailure = false app = XCUIApplication() app.launch() // STRICT: Verify app launched to a known state let loginScreen = app.staticTexts["Welcome Back"] let tabBar = app.tabBars.firstMatch // Either on login screen OR logged in - handle both if !loginScreen.waitForExistence(timeout: 3) && tabBar.exists { // Logged in - need to logout first 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 { ensureLoggedOut() app = nil } // MARK: - Strict Helper Methods private func ensureLoggedOut() { // Try profile tab logout let profileTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Profile'")).firstMatch if profileTab.exists && profileTab.isHittable { dismissKeyboard() profileTab.tap() let logoutButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Logout' OR label CONTAINS[c] 'Log Out'")).firstMatch if logoutButton.waitForExistence(timeout: 3) && logoutButton.isHittable { dismissKeyboard() logoutButton.tap() // Handle confirmation alert let alertLogout = app.alerts.buttons["Log Out"] if alertLogout.waitForExistence(timeout: 2) { dismissKeyboard() alertLogout.tap() } } } // Try verification screen logout let verifyLogout = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Logout'")).firstMatch if verifyLogout.exists && verifyLogout.isHittable { dismissKeyboard() verifyLogout.tap() } // Wait for login screen _ = app.staticTexts["Welcome Back"].waitForExistence(timeout: 5) } /// 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.staticTexts["Welcome Back"] 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 XCTAssertTrue(signUpButton.waitForExistence(timeout: 5), "Sign Up button must exist on login screen") XCTAssertTrue(signUpButton.isHittable, "Sign Up button must be tappable") dismissKeyboard() 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") // 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 } /// Dismiss keyboard by swiping down on the keyboard area private func dismissKeyboard() { let app = XCUIApplication() if app.keys.element(boundBy: 0).exists { app.typeText("\n") } // Give a moment for keyboard to dismiss Thread.sleep(forTimeInterval: 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.tap() usernameField.typeText(username) emailField.tap() emailField.typeText(email) passwordField.tap() dismissStrongPasswordSuggestion() passwordField.typeText(password) confirmPasswordField.tap() dismissStrongPasswordSuggestion() confirmPasswordField.typeText(confirmPassword) // Dismiss keyboard after filling form so buttons are accessible dismissKeyboard() } // 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.containing(NSPredicate(format: "label CONTAINS[c] 'Sign Up'")).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.staticTexts["Welcome Back"] 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 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 navigateToRegistration() fillRegistrationForm( username: username, email: email, password: testPassword, confirmPassword: testPassword ) // Capture registration form state let usernameField = app.textFields[AccessibilityIdentifiers.Authentication.registerUsernameField] // STRICT: Registration form must disappear XCTAssertTrue(waitForElementToDisappear(usernameField, timeout: 10), "Registration form must disappear after successful registration") // STRICT: Verification screen must appear let verifyTitle = app.staticTexts["Verify Your Email"] XCTAssertTrue(verifyTitle.waitForExistence(timeout: 10), "Verification screen must appear after registration") // STRICT: Verification screen must be the active screen (not behind anything) XCTAssertTrue(verifyTitle.isHittable, "Verification title must be visible and not obscured") // 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 = app.textFields[AccessibilityIdentifiers.Authentication.verificationCodeField] 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) dismissKeyboard() let verifyButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Verify'")).firstMatch XCTAssertTrue(verifyButton.exists && verifyButton.isHittable, "Verify button must be tappable") verifyButton.tap() // STRICT: Verification screen must DISAPPEAR XCTAssertTrue(waitForElementToDisappear(verifyTitle, timeout: 10), "Verification screen 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(waitForElementToBeHittable(residencesTab, timeout: 5), "Residences tab MUST be tappable after verification") // NEGATIVE CHECK: Verification screen should be completely gone XCTAssertFalse(verifyTitle.exists, "Verification screen must NOT exist after successful verification") 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 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") dismissKeyboard() profileTab.tap() let logoutButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Logout' OR label CONTAINS[c] 'Log Out'")).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.staticTexts["Welcome Back"] 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 let verifyTitle = app.staticTexts["Verify Your Email"] XCTAssertTrue(verifyTitle.waitForExistence(timeout: 10), "Must navigate to verification screen") // Enter INVALID code let codeField = app.textFields[AccessibilityIdentifiers.Authentication.verificationCodeField] XCTAssertTrue(codeField.waitForExistence(timeout: 5) && codeField.isHittable) dismissKeyboard() codeField.tap() codeField.typeText("000000") // Wrong code let verifyButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Verify'")).firstMatch 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() // let verifyTitle = app.staticTexts["Verify Your Email"] XCTAssertTrue(verifyTitle.waitForExistence(timeout: 10)) // Enter incomplete code (only 3 digits) let codeField = app.textFields[AccessibilityIdentifiers.Authentication.verificationCodeField] XCTAssertTrue(codeField.waitForExistence(timeout: 5) && codeField.isHittable) dismissKeyboard() codeField.tap() codeField.typeText("123") // Incomplete let verifyButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Verify'")).firstMatch // Button might be disabled with incomplete code if verifyButton.isEnabled { dismissKeyboard() verifyButton.tap() } // STRICT: Must still be on verification screen XCTAssertTrue(verifyTitle.exists && verifyTitle.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 let verifyTitle = app.staticTexts["Verify Your Email"] XCTAssertTrue(verifyTitle.waitForExistence(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 verifyTitleAfterRelaunch = app.staticTexts["Verify Your Email"] let loginScreen = app.staticTexts["Welcome Back"] let tabBar = app.tabBars.firstMatch // Wait for app to settle _ = verifyTitleAfterRelaunch.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 = verifyTitleAfterRelaunch.exists && verifyTitleAfterRelaunch.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.containing(NSPredicate(format: "label CONTAINS[c] 'Logout'")).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 let verifyTitle = app.staticTexts["Verify Your Email"] XCTAssertTrue(verifyTitle.waitForExistence(timeout: 10), "Must navigate to verification screen") XCTAssertTrue(verifyTitle.isHittable, "Verification screen must be active") // STRICT: Logout button must exist and be tappable let logoutButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Logout'")).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 XCTAssertTrue(waitForElementToDisappear(verifyTitle, timeout: 5), "Verification screen must disappear after logout") // STRICT: Must return to login screen let welcomeText = app.staticTexts["Welcome Back"] 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 let codeField = app.textFields[AccessibilityIdentifiers.Authentication.verificationCodeField] 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 } }