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 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 } /// Submit the registration form after filling it. Uses keyboard "Go" button /// or falls back to dismissing keyboard and tapping the register button. private func submitRegistrationForm() { let goButton = app.keyboards.buttons["Go"] if goButton.waitForExistence(timeout: 2) && goButton.isHittable { goButton.tap() return } // Fallback: dismiss keyboard, then tap register button dismissKeyboard() let registerButton = app.buttons[AccessibilityIdentifiers.Authentication.registerButton] registerButton.waitForExistenceOrFail(timeout: 5) if !registerButton.isHittable { let scrollView = app.scrollViews.firstMatch if scrollView.exists { registerButton.scrollIntoView(in: scrollView) } } registerButton.forceTap() } /// Dismiss keyboard safely by tapping a neutral area. 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 } // Try navigation bar (works on most screens) let navBar = app.navigationBars.firstMatch if navBar.exists && navBar.isHittable { navBar.tap() _ = app.keyboards.firstMatch.waitForNonExistence(timeout: 2) return } // Fallback: tap top-center of the app app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.1)).tap() _ = app.keyboards.firstMatch.waitForNonExistence(timeout: 2) } /// Fill registration form with given credentials. /// Uses Return key (\n) to trigger SwiftUI's .onSubmit / @FocusState field /// transitions. Direct field taps fail on iOS 26 when transitioning from /// TextField to SecureTextField (keyboard never appears). private func fillRegistrationForm(username: String, email: String, password: String, confirmPassword: String) { // iOS 26 bug: SecureTextField won't gain keyboard focus when tapped directly. // Workaround: toggle password visibility first to convert SecureField → TextField. let scrollView = app.scrollViews.firstMatch if scrollView.exists { scrollView.swipeUp() } let toggleButtons = app.buttons.matching(NSPredicate(format: "label == 'Toggle password visibility'")) for i in 0..