Refactor iOS UI tests to blueprint architecture
This commit is contained in:
@@ -1,11 +1,654 @@
|
||||
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 {
|
||||
func testSuite1_OpenAndDismissRegister() {
|
||||
let register = TestFlows.openRegisterFromLogin(app: app)
|
||||
register.tapCancel()
|
||||
override var includeResetStateLaunchArgument: Bool { false }
|
||||
|
||||
let login = LoginScreen(app: app)
|
||||
login.waitForLoad(timeout: defaultTimeout)
|
||||
|
||||
// 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 {
|
||||
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 {
|
||||
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() {
|
||||
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
|
||||
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")
|
||||
|
||||
// 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 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.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.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
|
||||
)
|
||||
|
||||
dismissKeyboard()
|
||||
app.buttons[AccessibilityIdentifiers.Authentication.registerButton].tap()
|
||||
|
||||
// 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
|
||||
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")
|
||||
|
||||
dismissKeyboard()
|
||||
codeField.tap()
|
||||
codeField.typeText(testVerificationCode)
|
||||
|
||||
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: 10), "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(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
|
||||
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.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)
|
||||
dismissKeyboard()
|
||||
codeField.tap()
|
||||
codeField.typeText("000000") // 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)
|
||||
dismissKeyboard()
|
||||
codeField.tap()
|
||||
codeField.typeText("123") // 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.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
|
||||
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
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user