import XCTest /// Mock scenarios matching the main app's UITestScenario values. /// These are passed via launch environment to configure app behavior. enum UITestScenario: String, CaseIterable { case emptyDashboard case populatedDashboard case authenticated case unauthenticated case darkWatchPopulated case voicePrintPopulated case spamShieldPopulated case homeTitlePopulated case removeBrokersPopulated case settingsPopulated case authError case signupSuccess case forgotPasswordSuccess } /// Base class for all Kordant UI tests. /// Provides common setup, teardown, and helper methods. class UITestBase: XCTestCase { var app: XCUIApplication! /// The mock scenario to use for this test class. /// Subclasses can override to change the scenario. class var scenario: UITestScenario { .populatedDashboard } override func setUpWithError() throws { continueAfterFailure = false app = XCUIApplication() // Configure testing mode via launch arguments and environment app.launchArguments = ["-UITesting"] app.launchEnvironment["UITestScenario"] = Self.scenario.rawValue // Disable animations for faster, more reliable tests app.launchEnvironment["UITestDisableAnimations"] = "YES" // Reset any persisted state for clean test runs app.launch() // Wait for the app to settle after launch let _ = app.wait(for: .runningForeground, timeout: 5) } override func tearDownWithError() throws { // Capture screenshot on failure if testRun?.hasSucceeded == false { let screenshot = app.windows.firstMatch.screenshot() let attachment = XCTAttachment(screenshot: screenshot) attachment.name = "Failure - \(name)" attachment.lifetime = .keepAlways add(attachment) } app = nil super.tearDownWithError() } // MARK: - Navigation Helpers /// Navigate to a specific tab in the main tab bar func navigateToTab(_ tab: TabBarItem) { app.tabBars.buttons[tab.rawValue].tap() } enum TabBarItem: String { case dashboard = "Dashboard" case services = "Services" case alerts = "Alerts" case settings = "Settings" case account = "Account" } // MARK: - Element Queries /// Find a button by its accessibility label or title func button(_ label: String) -> XCUIElement { app.buttons[label] } /// Find a text field by its placeholder or label func textField(_ label: String) -> XCUIElement { let field = app.textFields[label] if field.exists { return field } return app.textFields.containing(.staticText, identifier: label).element } /// Find a secure text field by its placeholder or label func secureTextField(_ label: String) -> XCUIElement { let field = app.secureTextFields[label] if field.exists { return field } return app.secureTextFields.containing(.staticText, identifier: label).element } /// Find static text by label func text(_ label: String) -> XCUIElement { app.staticTexts[label] } /// Find a navigation bar by its title func navigationBar(_ title: String) -> XCUIElement { app.navigationBars[title] } /// Find a toggle/switch by its label func `switch`(_ label: String) -> XCUIElement { app.switches[label] } // MARK: - Waiting Helpers /// Wait for an element to appear with a timeout @discardableResult func waitForElement(_ element: XCUIElement, timeout: TimeInterval = 5) -> Bool { let predicate = NSPredicate(format: "exists == true") let expectation = XCTNSPredicateExpectation(predicate: predicate, object: element) let result = XCTWaiter.wait(for: [expectation], timeout: timeout) return result == .completed } /// Wait for an element to be hittable (visible and interactable) @discardableResult func waitForHittable(_ element: XCUIElement, timeout: TimeInterval = 5) -> Bool { let predicate = NSPredicate(format: "hittable == true") let expectation = XCTNSPredicateExpectation(predicate: predicate, object: element) let result = XCTWaiter.wait(for: [expectation], timeout: timeout) return result == .completed } /// Wait for an element to disappear @discardableResult func waitForAbsence(_ element: XCUIElement, timeout: TimeInterval = 5) -> Bool { let predicate = NSPredicate(format: "exists == false") let expectation = XCTNSPredicateExpectation(predicate: predicate, object: element) let result = XCTWaiter.wait(for: [expectation], timeout: timeout) return result == .completed } // MARK: - Gesture Helpers /// Pull to refresh on a scroll view func pullToRefresh() { let window = app.windows.firstMatch let start = window.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.1)) let end = window.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.6)) start.press(forDuration: 0, thenDragTo: end) } /// Scroll down in a scroll view func scrollDown(times: Int = 1) { for _ in 0..