import XCTest /// Base class for all tests that require a logged-in user. class AuthenticatedUITestCase: BaseUITestCase { // MARK: - Configuration (override in subclasses) var testCredentials: (username: String, password: String) { ("testuser", "TestPass123!") } var needsAPISession: Bool { false } var apiCredentials: (username: String, password: String) { ("admin", "Test1234") } // MARK: - API Session private(set) var session: TestSession! private(set) var cleaner: TestDataCleaner! // MARK: - Lifecycle override class func setUp() { super.setUp() guard TestAccountAPIClient.isBackendReachable() else { return } // Ensure both known test accounts exist (covers all subclass credential overrides) if TestAccountAPIClient.login(username: "testuser", password: "TestPass123!") == nil { _ = TestAccountAPIClient.createVerifiedAccount(username: "testuser", email: "testuser@honeydue.com", password: "TestPass123!") } if TestAccountAPIClient.login(username: "admin", password: "Test1234") == nil { _ = TestAccountAPIClient.createVerifiedAccount(username: "admin", email: "admin@honeydue.com", password: "Test1234") } } override func setUpWithError() throws { guard TestAccountAPIClient.isBackendReachable() else { throw XCTSkip("Backend not reachable at \(TestAccountAPIClient.baseURL)") } try super.setUpWithError() // If already logged in (tab bar visible), skip the login flow let tabBar = app.tabBars.firstMatch if tabBar.waitForExistence(timeout: defaultTimeout) { // Already logged in — just set up API session if needed if needsAPISession { guard let apiSession = TestAccountManager.loginSeededAccount( username: apiCredentials.username, password: apiCredentials.password ) else { XCTFail("Could not login API account '\(apiCredentials.username)'") return } session = apiSession cleaner = TestDataCleaner(token: apiSession.token) } return } // Not logged in — do the full login flow UITestHelpers.ensureLoggedOut(app: app) loginToMainApp() if needsAPISession { guard let apiSession = TestAccountManager.loginSeededAccount( username: apiCredentials.username, password: apiCredentials.password ) else { XCTFail("Could not login API account '\(apiCredentials.username)'") return } session = apiSession cleaner = TestDataCleaner(token: apiSession.token) } } override func tearDownWithError() throws { cleaner?.cleanAll() try super.tearDownWithError() } // MARK: - Login func loginToMainApp() { let creds = testCredentials UITestHelpers.ensureOnLoginScreen(app: app) let login = LoginScreenObject(app: app) login.waitForLoad(timeout: loginTimeout) login.enterUsername(creds.username) login.enterPassword(creds.password) let loginButton = app.buttons[AccessibilityIdentifiers.Authentication.loginButton] loginButton.waitForExistenceOrFail(timeout: defaultTimeout) loginButton.tap() waitForMainApp() } func waitForMainApp() { let tabBar = app.tabBars.firstMatch let verification = VerificationScreen(app: app) let deadline = Date().addingTimeInterval(loginTimeout) while Date() < deadline { if tabBar.exists { break } if verification.codeField.exists { verification.enterCode(TestAccountAPIClient.debugVerificationCode) verification.submitCode() _ = tabBar.waitForExistence(timeout: loginTimeout) break } RunLoop.current.run(until: Date().addingTimeInterval(0.5)) } XCTAssertTrue(tabBar.exists, "Expected tab bar after login with '\(testCredentials.username)'") } // MARK: - Tab Navigation func navigateToTab(_ label: String) { let tabBar = app.tabBars.firstMatch tabBar.waitForExistenceOrFail(timeout: navigationTimeout) let tab = tabBar.buttons.containing( NSPredicate(format: "label CONTAINS[c] %@", label) ).firstMatch tab.waitForExistenceOrFail(timeout: navigationTimeout) tab.tap() // Verify navigation happened — wait for isSelected (best effort, won't fail) // sidebarAdaptable tabs sometimes need a moment let selected = NSPredicate(format: "isSelected == true") let exp = XCTNSPredicateExpectation(predicate: selected, object: tab) let result = XCTWaiter().wait(for: [exp], timeout: navigationTimeout) // If first tap didn't register, tap again if result != .completed { tab.tap() _ = XCTWaiter().wait(for: [XCTNSPredicateExpectation(predicate: selected, object: tab)], timeout: navigationTimeout) } } func navigateToResidences() { navigateToTab("Residences") } func navigateToTasks() { navigateToTab("Tasks") } func navigateToContractors() { navigateToTab("Contractors") } func navigateToDocuments() { navigateToTab("Doc") } func navigateToProfile() { navigateToTab("Profile") } // MARK: - Pull to Refresh func pullToRefresh() { let scrollable = app.collectionViews.firstMatch.exists ? app.collectionViews.firstMatch : app.scrollViews.firstMatch guard scrollable.waitForExistence(timeout: defaultTimeout) else { return } let start = scrollable.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.15)) let end = scrollable.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.85)) start.press(forDuration: 0.3, thenDragTo: end) _ = app.activityIndicators.firstMatch.waitForNonExistence(timeout: navigationTimeout) } func pullToRefreshUntilVisible(_ element: XCUIElement, maxRetries: Int = 3) { for _ in 0..