import XCTest /// Base class for tests requiring a logged-in session against the real local backend. /// /// By default, creates a fresh verified account via the API, launches the app /// (without `--ui-test-mock-auth`), and drives the UI through login. /// /// Override `useSeededAccount` to log in with a pre-existing database account instead. /// Override `performUILogin` to skip the UI login step (if you only need the API session). /// /// ## Data Seeding & Cleanup /// Use the `cleaner` property to seed data that auto-cleans in tearDown: /// ``` /// let residence = cleaner.seedResidence(name: "My Test Home") /// let task = cleaner.seedTask(residenceId: residence.id) /// ``` /// Or seed without tracking via `TestDataSeeder` and track manually: /// ``` /// let res = TestDataSeeder.createResidence(token: session.token) /// cleaner.trackResidence(res.id) /// ``` class AuthenticatedTestCase: BaseUITestCase { /// The active test session, populated during setUp. var session: TestSession! /// Tracks and cleans up resources created during the test. /// Initialized in setUp after the session is established. private(set) var cleaner: TestDataCleaner! /// Override to `true` in subclasses that should use the pre-seeded admin account. var useSeededAccount: Bool { false } /// Seeded account credentials. Override in subclasses that use a different seeded user. var seededUsername: String { "admin" } var seededPassword: String { "test1234" } /// Override to `false` to skip driving the app through the login UI. var performUILogin: Bool { true } /// No mock auth - we're testing against the real backend. override var additionalLaunchArguments: [String] { [] } // MARK: - Setup override func setUpWithError() throws { // Check backend reachability before anything else guard TestAccountAPIClient.isBackendReachable() else { throw XCTSkip("Local backend is not reachable at \(TestAccountAPIClient.baseURL)") } // Create or login account via API if useSeededAccount { guard let s = TestAccountManager.loginSeededAccount( username: seededUsername, password: seededPassword ) else { throw XCTSkip("Could not login seeded account '\(seededUsername)'") } session = s } else { guard let s = TestAccountManager.createVerifiedAccount() else { throw XCTSkip("Could not create verified test account") } session = s } // Initialize the cleaner with the session token cleaner = TestDataCleaner(token: session.token) // Launch the app (calls BaseUITestCase.setUpWithError which launches and waits for ready) try super.setUpWithError() // Drive the UI through login if needed if performUILogin { loginViaUI() } } override func tearDownWithError() throws { // Clean up all tracked test data cleaner?.cleanAll() try super.tearDownWithError() } // MARK: - UI Login /// Navigate from onboarding welcome → login screen → type credentials → wait for main tabs. func loginViaUI() { let login = TestFlows.navigateToLoginFromOnboarding(app: app) login.enterUsername(session.username) login.enterPassword(session.password) // Tap the login button let loginButton = app.buttons[UITestID.Auth.loginButton] loginButton.waitUntilHittable(timeout: defaultTimeout).tap() // Wait for either main tabs or verification screen let mainTabs = app.otherElements[UITestID.Root.mainTabs] let tabBar = app.tabBars.firstMatch let deadline = Date().addingTimeInterval(longTimeout) while Date() < deadline { if mainTabs.exists || tabBar.exists { return } // Check for email verification gate - if we hit it, enter the debug code let verificationScreen = VerificationScreen(app: app) if verificationScreen.codeField.exists { verificationScreen.enterCode(TestAccountAPIClient.debugVerificationCode) verificationScreen.submitCode() // Wait for main tabs after verification if mainTabs.waitForExistence(timeout: longTimeout) || tabBar.waitForExistence(timeout: 5) { return } } RunLoop.current.run(until: Date().addingTimeInterval(0.5)) } XCTFail("Failed to reach main app after login. Debug tree:\n\(app.debugDescription)") } // MARK: - Tab Navigation func navigateToTab(_ tab: String) { let tabButton = app.buttons[tab] if tabButton.waitForExistence(timeout: defaultTimeout) { tabButton.forceTap() } else { // Fallback: search tab bar buttons by label let label = tab.replacingOccurrences(of: "TabBar.", with: "") let byLabel = app.tabBars.buttons.containing( NSPredicate(format: "label CONTAINS[c] %@", label) ).firstMatch byLabel.waitForExistenceOrFail(timeout: defaultTimeout) byLabel.forceTap() } } func navigateToResidences() { navigateToTab(AccessibilityIdentifiers.Navigation.residencesTab) } func navigateToTasks() { navigateToTab(AccessibilityIdentifiers.Navigation.tasksTab) } func navigateToContractors() { navigateToTab(AccessibilityIdentifiers.Navigation.contractorsTab) } func navigateToDocuments() { navigateToTab(AccessibilityIdentifiers.Navigation.documentsTab) } func navigateToProfile() { navigateToTab(AccessibilityIdentifiers.Navigation.profileTab) } }