import XCTest /// Reusable helper functions for UI tests struct UITestHelpers { private static func loginUsernameField(app: XCUIApplication) -> XCUIElement { app.textFields[AccessibilityIdentifiers.Authentication.usernameField] } // MARK: - Authentication Helpers /// Logs out the user if they are currently logged in /// - Parameter app: The XCUIApplication instance static func logout(app: XCUIApplication) { // Already on login screen. let usernameField = loginUsernameField(app: app) if usernameField.waitForExistence(timeout: 2) { return } // In onboarding flow, navigate to login. let onboardingRoot = app.otherElements[UITestID.Root.onboarding] if onboardingRoot.waitForExistence(timeout: 2) { ensureOnLoginScreen(app: app) return } // Check if we have a tab bar (logged in state) let tabBar = app.tabBars.firstMatch guard tabBar.exists else { return } // Navigate to Residences tab first let residencesTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Residences'")).firstMatch if residencesTab.exists { residencesTab.tap() } // Tap settings button let settingsButton = app.buttons[AccessibilityIdentifiers.Navigation.settingsButton] if settingsButton.waitForExistence(timeout: 3) && settingsButton.isHittable { settingsButton.tap() } // Find and tap logout button — the profile sheet uses a lazy // SwiftUI List so the button may not exist until scrolled into view let logoutButton = app.buttons[AccessibilityIdentifiers.Profile.logoutButton] if !logoutButton.waitForExistence(timeout: 3) { // Scroll down in the profile sheet's CollectionView let collectionView = app.collectionViews.firstMatch if collectionView.exists { for _ in 0..<5 { collectionView.swipeUp() if logoutButton.waitForExistence(timeout: 1) { break } } } } if logoutButton.waitForExistence(timeout: 3) { if logoutButton.isHittable { logoutButton.tap() } else { logoutButton.forceTap() } // Confirm logout in alert if present let alert = app.alerts.firstMatch if alert.waitForExistence(timeout: 3) { let confirmLogout = alert.buttons["Log Out"] if confirmLogout.waitForExistence(timeout: 2) { confirmLogout.tap() } } } // Wait for the app to transition back to login screen after logout let loginRoot = app.otherElements[UITestID.Root.login] let loggedOut = usernameField.waitForExistence(timeout: 15) || loginRoot.waitForExistence(timeout: 5) if !loggedOut { // Check if we landed on onboarding instead (when onboarding state was reset) let onboardingRoot = app.otherElements[UITestID.Root.onboarding] if onboardingRoot.waitForExistence(timeout: 3) { return // Logout succeeded, landed on onboarding } XCTFail("Failed to log out - login username field should appear. App state:\n\(app.debugDescription)") } } /// Logs in a user with the provided credentials /// - Parameters: /// - app: The XCUIApplication instance /// - username: The username/email to use for login /// - password: The password to use for login static func login(app: XCUIApplication, username: String, password: String) { // Find username field by accessibility identifier let usernameField = app.textFields[AccessibilityIdentifiers.Authentication.usernameField] XCTAssertTrue(usernameField.waitForExistence(timeout: 5), "Username field should exist") usernameField.focusAndType(username, app: app) // Find password field - it could be TextField (if visible) or SecureField var passwordField = app.secureTextFields[AccessibilityIdentifiers.Authentication.passwordField] if !passwordField.exists { passwordField = app.textFields[AccessibilityIdentifiers.Authentication.passwordField] } XCTAssertTrue(passwordField.waitForExistence(timeout: 3), "Password field should exist") passwordField.focusAndType(password, app: app) // Find and tap login button let loginButton = app.buttons[AccessibilityIdentifiers.Authentication.loginButton] XCTAssertTrue(loginButton.waitForExistence(timeout: 3), "Login button should exist") loginButton.tap() // Wait for login to complete, handling verification gate if shown let tabBarAfterLogin = app.tabBars.firstMatch let verificationCodeField = app.textFields[AccessibilityIdentifiers.Authentication.verificationCodeField] _ = tabBarAfterLogin.waitForExistence(timeout: 15) || verificationCodeField.waitForExistence(timeout: 3) let onboardingCodeField = app.textFields[AccessibilityIdentifiers.Onboarding.verificationCodeField] if verificationCodeField.waitForExistence(timeout: 3) || onboardingCodeField.waitForExistence(timeout: 2) { let codeField = verificationCodeField.exists ? verificationCodeField : onboardingCodeField codeField.focusAndType("123456", app: app) let verifyButton = app.buttons[AccessibilityIdentifiers.Authentication.verifyButton].exists ? app.buttons[AccessibilityIdentifiers.Authentication.verifyButton] : app.buttons[AccessibilityIdentifiers.Onboarding.verifyButton] if verifyButton.exists { verifyButton.tap() } else { app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Verify'")).firstMatch.tap() } _ = tabBarAfterLogin.waitForExistence(timeout: 15) } } /// Ensures the user is logged out before running a test /// - Parameter app: The XCUIApplication instance static func ensureLoggedOut(app: XCUIApplication) { logout(app: app) ensureOnLoginScreen(app: app) } /// Ensures the user is logged in with test credentials before running a test /// - Parameter app: The XCUIApplication instance /// - Parameter username: Optional username (defaults to "testuser") /// - Parameter password: Optional password (defaults to "TestPass123!") static func ensureLoggedIn(app: XCUIApplication, username: String = "testuser", password: String = "TestPass123!") { // Check if already logged in (tab bar visible) let tabBar = app.tabBars.firstMatch if tabBar.exists { return // Already logged in } ensureOnLoginScreen(app: app) let usernameField = app.textFields[AccessibilityIdentifiers.Authentication.usernameField] if usernameField.waitForExistence(timeout: 5) { login(app: app, username: username, password: password) // Wait for main screen to appear _ = tabBar.waitForExistence(timeout: 10) } } static func ensureOnLoginScreen(app: XCUIApplication) { let usernameField = loginUsernameField(app: app) if usernameField.waitForExistence(timeout: 5) { return } // Handle persisted authenticated sessions first. let mainTabsRoot = app.otherElements[UITestID.Root.mainTabs] if mainTabsRoot.exists || app.tabBars.firstMatch.exists { logout(app: app) if usernameField.waitForExistence(timeout: 10) { return } } // Wait for a stable root state before interacting. let loginRoot = app.otherElements[UITestID.Root.login] let onboardingRoot = app.otherElements[UITestID.Root.onboarding] // Check for standalone login screen first (when --complete-onboarding is active) if loginRoot.waitForExistence(timeout: 8) { _ = usernameField.waitForExistence(timeout: 10) } else if onboardingRoot.waitForExistence(timeout: 5) { // Handle both pure onboarding and onboarding + login sheet. let onboardingLoginButton = app.buttons[AccessibilityIdentifiers.Onboarding.loginButton] if onboardingLoginButton.waitForExistence(timeout: 5) { if onboardingLoginButton.isHittable { onboardingLoginButton.tap() } else { onboardingLoginButton.forceTap() } } else { let welcome = OnboardingWelcomeScreen(app: app) welcome.waitForLoad() welcome.tapAlreadyHaveAccount() } } XCTAssertTrue( usernameField.waitForExistence(timeout: 20), "Expected to reach login screen from current app state" ) } }