import XCTest /// Consolidated login authentication UI tests. /// /// Merged from four legacy suites: /// - SimpleLoginTest (basic login screen smoke tests) /// - AuthCriticalPathTests (critical-path login / logout / entry navigation) /// - AuthenticationTests (F201–F209 login-screen element + navigation checks) /// - Suite2_AuthenticationRebuildTests (R201–R206 valid-credential landing / logout) /// /// Logged-OUT suite: extends `BaseUITestCase` (no auth). Each test drives the /// login / registration-entry / forgot-password screens itself via the existing /// page objects and helpers (LoginScreenObject, UITestHelpers, TestFlows, …). final class AuthLoginUITests: BaseUITestCase { // Merged override: SimpleLogin and Suite2 both disabled reset-state; the // other two relied on the default. Disabling reset-state is the safe union // because every test here re-establishes its own starting screen state. override var includeResetStateLaunchArgument: Bool { false } // AuthCriticalPath, Authentication, and Suite2 all relaunched between tests. override var relaunchBetweenTests: Bool { true } // AuthCriticalPath, Authentication, and Suite2 all booted with onboarding // completed so a successful login lands on the main tabs (a freshly-seeded // user has no residence; without this it routes to onboarding). override var completeOnboarding: Bool { true } private let validUser = RebuildTestUserFactory.seeded /// The seeded user's Kratos login identifier. Kratos keys honeyDue /// identities by EMAIL, and the app sends the typed identifier straight to /// Kratos (AuthApi.login), so login must use the email — not the bare /// display username. private let seededLoginIdentifier = "testuser@honeydue.com" private enum AuthLandingState { case main case verification } override func setUpWithError() throws { // Force a clean app launch so no stale field text persists between tests // (union of SimpleLogin + Suite2 setUp behavior). app.terminate() try super.setUpWithError() // CRITICAL: Ensure we're logged out before each test UITestHelpers.ensureLoggedOut(app: app) } override func tearDownWithError() throws { try super.tearDownWithError() } // MARK: - Helpers (from SimpleLoginTest) /// Ensures the user is logged out and on the login screen private func ensureLoggedOut() { UITestHelpers.ensureLoggedOut(app: app) } // MARK: - Helpers (from AuthCriticalPathTests) private func navigateToLogin() { let loginField = app.textFields[AccessibilityIdentifiers.Authentication.usernameField] if loginField.waitForExistence(timeout: defaultTimeout) { return } // On onboarding — tap login button let onboardingLogin = app.buttons[AccessibilityIdentifiers.Onboarding.loginButton] if onboardingLogin.waitForExistence(timeout: navigationTimeout) { onboardingLogin.tap() } loginField.waitForExistenceOrFail(timeout: navigationTimeout, message: "Login screen should appear") } private func loginAsTestUser() { navigateToLogin() let login = LoginScreenObject(app: app) // Kratos uses the EMAIL as the login identifier (no username trait). login.enterUsername("testuser@honeydue.com") login.enterPassword("TestPass123!") app.buttons[AccessibilityIdentifiers.Authentication.loginButton].tap() // Wait for main app or verification gate let tabBar = app.tabBars.firstMatch let verification = VerificationScreen(app: app) let deadline = Date().addingTimeInterval(loginTimeout) while Date() < deadline { if tabBar.exists { return } if verification.codeField.exists { verification.enterCode(TestAccountAPIClient.debugVerificationCode) verification.submitCode() _ = tabBar.waitForExistence(timeout: loginTimeout) return } RunLoop.current.run(until: Date().addingTimeInterval(0.5)) } XCTAssertTrue(tabBar.exists, "Should reach main app after login") } // MARK: - Helpers (from Suite2_AuthenticationRebuildTests) private func loginFromLoginScreen(user: RebuildTestUser = RebuildTestUserFactory.seeded) { UITestHelpers.ensureOnLoginScreen(app: app) let login = LoginScreenObject(app: app) login.waitForLoad(timeout: defaultTimeout) login.enterUsername(seededLoginIdentifier) login.enterPassword(user.password) let loginButton = app.buttons[AccessibilityIdentifiers.Authentication.loginButton] loginButton.waitForExistenceOrFail(timeout: defaultTimeout) loginButton.forceTap() } @discardableResult private func loginAndWaitForAuthenticatedLanding(user: RebuildTestUser = RebuildTestUserFactory.seeded) -> AuthLandingState { loginFromLoginScreen(user: user) let mainRoot = app.otherElements[UITestID.Root.mainTabs] if mainRoot.waitForExistence(timeout: loginTimeout) || app.tabBars.firstMatch.waitForExistence(timeout: 2) { return .main } let verification = VerificationScreen(app: app) if verification.codeField.waitForExistence(timeout: defaultTimeout) || verification.verifyButton.waitForExistence(timeout: 2) { return .verification } XCTFail("Expected authenticated landing on main tabs or verification screen") return .verification } private func logoutFromVerificationIfNeeded() { let verification = VerificationScreen(app: app) verification.waitForLoad(timeout: defaultTimeout) verification.tapLogoutIfAvailable() let toolbarLogout = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Logout' OR label CONTAINS[c] 'Log Out'")).firstMatch if toolbarLogout.waitForExistence(timeout: 3) { toolbarLogout.forceTap() } } private func logoutFromMainApp() { UITestHelpers.logout(app: app) } // MARK: - Tests (from SimpleLoginTest) /// Test 1: App launches and shows login screen (or logs out if needed) func testAppLaunchesAndShowsLoginScreen() { let usernameField = app.textFields[AccessibilityIdentifiers.Authentication.usernameField] XCTAssertTrue(usernameField.exists, "Username field should be visible on login screen after logout") } /// Test 2: Can type in username and password fields func testCanTypeInLoginFields() { let usernameField = app.textFields[AccessibilityIdentifiers.Authentication.usernameField] usernameField.waitForExistenceOrFail(timeout: defaultTimeout, message: "Username field should exist on login screen") usernameField.focusAndType("testuser", app: app) let passwordField = app.secureTextFields[AccessibilityIdentifiers.Authentication.passwordField].exists ? app.secureTextFields[AccessibilityIdentifiers.Authentication.passwordField] : app.textFields[AccessibilityIdentifiers.Authentication.passwordField] XCTAssertTrue(passwordField.exists, "Password field should exist on login screen") passwordField.focusAndType("testpass123", app: app) let signInButton = app.buttons[AccessibilityIdentifiers.Authentication.loginButton] XCTAssertTrue(signInButton.exists, "Login button should exist on login screen") } // MARK: - Tests (from AuthCriticalPathTests) // MARK: Login func testLoginWithValidCredentials() { loginAsTestUser() XCTAssertTrue(app.tabBars.firstMatch.exists, "Tab bar should be visible after login") } func testLoginWithInvalidCredentials() { navigateToLogin() let login = LoginScreenObject(app: app) login.enterUsername("invaliduser") login.enterPassword("wrongpassword") app.buttons[AccessibilityIdentifiers.Authentication.loginButton].tap() // Should stay on login screen let loginField = app.textFields[AccessibilityIdentifiers.Authentication.usernameField] XCTAssertTrue(loginField.waitForExistence(timeout: navigationTimeout), "Should remain on login screen after invalid credentials") XCTAssertFalse(app.tabBars.firstMatch.exists, "Tab bar should not appear after failed login") } // MARK: Logout func testLogoutFlow() { loginAsTestUser() UITestHelpers.logout(app: app) let loginField = app.textFields[AccessibilityIdentifiers.Authentication.usernameField] let onboardingLogin = app.buttons[AccessibilityIdentifiers.Onboarding.loginButton] let loggedOut = loginField.waitForExistence(timeout: loginTimeout) || onboardingLogin.waitForExistence(timeout: navigationTimeout) XCTAssertTrue(loggedOut, "Should return to login or onboarding after logout") } // MARK: Registration Entry func testSignUpButtonNavigatesToRegistration() { navigateToLogin() app.buttons[AccessibilityIdentifiers.Authentication.signUpButton].tap() let registerUsername = app.textFields[AccessibilityIdentifiers.Authentication.registerUsernameField] XCTAssertTrue(registerUsername.waitForExistence(timeout: navigationTimeout), "Registration form should appear") } // MARK: Forgot Password func testForgotPasswordButtonExists() { navigateToLogin() let forgotButton = app.buttons[AccessibilityIdentifiers.Authentication.forgotPasswordButton] XCTAssertTrue(forgotButton.waitForExistence(timeout: defaultTimeout), "Forgot password button should exist") } // MARK: - Tests (from AuthenticationTests) func testF201_OnboardingLoginEntryShowsLoginScreen() { let login = TestFlows.navigateToLoginFromOnboarding(app: app) login.waitForLoad(timeout: defaultTimeout) } func testF202_LoginScreenCanTogglePasswordVisibility() { let login = TestFlows.navigateToLoginFromOnboarding(app: app) login.enterUsername("u") login.enterPassword("p") login.tapPasswordVisibilityToggle() login.assertPasswordFieldVisible() } func testF203_RegisterSheetCanOpenAndDismiss() { let login = TestFlows.navigateToLoginFromOnboarding(app: app) login.waitForLoad(timeout: defaultTimeout) login.tapSignUp() let register = RegisterScreenObject(app: app) register.waitForLoad(timeout: navigationTimeout) register.tapCancel() login.waitForLoad(timeout: navigationTimeout) } func testF204_RegisterFormAcceptsInput() { let login = TestFlows.navigateToLoginFromOnboarding(app: app) login.waitForLoad(timeout: defaultTimeout) login.tapSignUp() let register = RegisterScreenObject(app: app) register.waitForLoad(timeout: navigationTimeout) XCTAssertTrue(app.buttons[UITestID.Auth.registerButton].exists, "Register button should exist on register form") } func testF205_LoginButtonDisabledWhenCredentialsAreEmpty() { let login = TestFlows.navigateToLoginFromOnboarding(app: app) login.waitForLoad(timeout: defaultTimeout) let loginButton = app.buttons[UITestID.Auth.loginButton] loginButton.waitForExistenceOrFail(timeout: defaultTimeout) XCTAssertFalse(loginButton.isEnabled, "Login button should be disabled when username/password are empty") } func testF206_ForgotPasswordButtonIsAccessible() { let login = TestFlows.navigateToLoginFromOnboarding(app: app) login.waitForLoad(timeout: defaultTimeout) let forgotButton = app.buttons[UITestID.Auth.forgotPasswordButton] forgotButton.waitForExistenceOrFail(timeout: defaultTimeout) XCTAssertTrue(forgotButton.isHittable, "Forgot password button should be hittable on login screen") } func testF207_LoginScreenShowsAllExpectedElements() { let login = TestFlows.navigateToLoginFromOnboarding(app: app) login.waitForLoad(timeout: defaultTimeout) XCTAssertTrue(app.textFields[UITestID.Auth.usernameField].exists, "Username field should exist") XCTAssertTrue( app.secureTextFields[UITestID.Auth.passwordField].exists || app.textFields[UITestID.Auth.passwordField].exists, "Password field should exist" ) XCTAssertTrue(app.buttons[UITestID.Auth.loginButton].exists, "Login button should exist") XCTAssertTrue(app.buttons[UITestID.Auth.signUpButton].exists, "Sign up button should exist") XCTAssertTrue(app.buttons[UITestID.Auth.forgotPasswordButton].exists, "Forgot password button should exist") XCTAssertTrue(app.buttons[UITestID.Auth.passwordVisibilityToggle].exists, "Password visibility toggle should exist") } func testF208_RegisterFormShowsAllRequiredFields() { let login = TestFlows.navigateToLoginFromOnboarding(app: app) login.waitForLoad(timeout: defaultTimeout) login.tapSignUp() let register = RegisterScreenObject(app: app) register.waitForLoad(timeout: navigationTimeout) XCTAssertTrue(app.textFields[UITestID.Auth.registerUsernameField].exists, "Register username field should exist") XCTAssertTrue(app.textFields[UITestID.Auth.registerEmailField].exists, "Register email field should exist") XCTAssertTrue(app.secureTextFields[UITestID.Auth.registerPasswordField].exists, "Register password field should exist") XCTAssertTrue(app.secureTextFields[UITestID.Auth.registerConfirmPasswordField].exists, "Register confirm password field should exist") XCTAssertTrue(app.buttons[UITestID.Auth.registerButton].exists, "Register button should exist") XCTAssertTrue(app.buttons[UITestID.Auth.registerCancelButton].exists, "Register cancel button should exist") } func testF209_ForgotPasswordNavigatesToResetFlow() { let login = TestFlows.navigateToLoginFromOnboarding(app: app) login.waitForLoad(timeout: defaultTimeout) login.tapForgotPassword() // Verify forgot password screen loaded by checking for its email field (accessibility ID, not label) let emailField = app.textFields[UITestID.PasswordReset.emailField] let sendCodeButton = app.buttons[UITestID.PasswordReset.sendCodeButton] let loaded = emailField.waitForExistence(timeout: navigationTimeout) || sendCodeButton.waitForExistence(timeout: navigationTimeout) XCTAssertTrue(loaded, "Forgot password screen should appear with email field or send code button") } // MARK: - Tests (from Suite2_AuthenticationRebuildTests) func testR201_loginScreenLoadsFromOnboardingEntry() { UITestHelpers.ensureOnLoginScreen(app: app) let login = LoginScreenObject(app: app) login.waitForLoad(timeout: defaultTimeout) } func testR202_validCredentialsSubmitFromLogin() { UITestHelpers.ensureOnLoginScreen(app: app) let login = LoginScreenObject(app: app) login.waitForLoad(timeout: defaultTimeout) login.enterUsername(seededLoginIdentifier) login.enterPassword(validUser.password) let loginButton = app.buttons[AccessibilityIdentifiers.Authentication.loginButton] XCTAssertTrue(loginButton.waitForExistence(timeout: defaultTimeout), "Login button must exist before submit") XCTAssertTrue(loginButton.isHittable, "Login button must be tappable") } func testR203_validLoginTransitionsToMainAppRoot() { let landing = loginAndWaitForAuthenticatedLanding(user: validUser) switch landing { case .main: RebuildSessionAssertions.assertOnMainApp(app, timeout: loginTimeout) case .verification: RebuildSessionAssertions.assertOnVerification(app, timeout: loginTimeout) } } func testR204_mainAppHasExpectedPrimaryTabsAfterLogin() { let landing = loginAndWaitForAuthenticatedLanding(user: validUser) switch landing { case .main: RebuildSessionAssertions.assertOnMainApp(app, timeout: loginTimeout) let tabBar = app.tabBars.firstMatch if tabBar.waitForExistence(timeout: 5) { let residences = tabBar.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Residences'")).firstMatch let tasks = tabBar.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Tasks'")).firstMatch let contractors = tabBar.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Contractors'")).firstMatch let docs = tabBar.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Doc'")).firstMatch XCTAssertTrue(residences.exists, "Residences tab should exist") XCTAssertTrue(tasks.exists, "Tasks tab should exist") XCTAssertTrue(contractors.exists, "Contractors tab should exist") XCTAssertTrue(docs.exists, "Documents tab should exist") } else { XCTAssertTrue(app.otherElements[UITestID.Root.mainTabs].exists, "Main tabs root should exist") } case .verification: let verify = VerificationScreen(app: app) verify.waitForLoad(timeout: defaultTimeout) XCTAssertTrue(verify.codeField.exists, "Verification code field should exist for unverified accounts") } } func testR205_logoutFromMainAppReturnsToLoginRoot() { let landing = loginAndWaitForAuthenticatedLanding(user: validUser) switch landing { case .main: logoutFromMainApp() case .verification: logoutFromVerificationIfNeeded() } RebuildSessionAssertions.assertOnLogin(app, timeout: loginTimeout) } func testR206_postLogoutMainAppIsNoLongerAccessible() { let landing = loginAndWaitForAuthenticatedLanding(user: validUser) switch landing { case .main: logoutFromMainApp() case .verification: logoutFromVerificationIfNeeded() } RebuildSessionAssertions.assertOnLogin(app, timeout: loginTimeout) XCTAssertFalse(app.otherElements[UITestID.Root.mainTabs].exists, "Main app root should not be visible after logout") } }