import XCTest class BaseUITestCase: XCTestCase { let app = XCUIApplication() let shortTimeout: TimeInterval = 5 let defaultTimeout: TimeInterval = 15 let longTimeout: TimeInterval = 30 var includeResetStateLaunchArgument: Bool { true } /// Override to `true` in tests that need the standalone login screen /// (skips onboarding). Default is `false` so tests that navigate from /// onboarding or test onboarding screens work without extra config. var completeOnboarding: Bool { false } var additionalLaunchArguments: [String] { [] } override func setUpWithError() throws { continueAfterFailure = false XCUIDevice.shared.orientation = .portrait // Auto-dismiss any system alerts (notifications, tracking, etc.) addUIInterruptionMonitor(withDescription: "System Alert") { alert in let buttons = ["Allow", "OK", "Don't Allow", "Not Now", "Dismiss", "Allow While Using App"] for label in buttons { let button = alert.buttons[label] if button.exists { button.tap() return true } } return false } var launchArguments = [ "--ui-testing", "--disable-animations" ] if completeOnboarding { launchArguments.append("--complete-onboarding") } if includeResetStateLaunchArgument { launchArguments.append("--reset-state") } launchArguments.append(contentsOf: additionalLaunchArguments) app.launchArguments = launchArguments app.launch() app.otherElements["ui.app.ready"].waitForExistenceOrFail(timeout: longTimeout) } override func tearDownWithError() throws { if let run = testRun, !run.hasSucceeded { let attachment = XCTAttachment(screenshot: app.screenshot()) attachment.name = "Failure-\(name)" attachment.lifetime = .keepAlways add(attachment) } } } extension XCUIElement { @discardableResult func waitForExistenceOrFail( timeout: TimeInterval, message: String? = nil, file: StaticString = #filePath, line: UInt = #line ) -> XCUIElement { if !waitForExistence(timeout: timeout) { XCTFail(message ?? "Expected element to exist: \(self)", file: file, line: line) } return self } @discardableResult func waitUntilHittable( timeout: TimeInterval, message: String? = nil, file: StaticString = #filePath, line: UInt = #line ) -> XCUIElement { let predicate = NSPredicate(format: "exists == true AND hittable == true") let expectation = XCTNSPredicateExpectation(predicate: predicate, object: self) let result = XCTWaiter().wait(for: [expectation], timeout: timeout) if result != .completed { XCTFail(message ?? "Expected element to become hittable: \(self)", file: file, line: line) } return self } @discardableResult func waitForNonExistence( timeout: TimeInterval, message: String? = nil, file: StaticString = #filePath, line: UInt = #line ) -> Bool { let predicate = NSPredicate(format: "exists == false") let expectation = XCTNSPredicateExpectation(predicate: predicate, object: self) let result = XCTWaiter().wait(for: [expectation], timeout: timeout) if result != .completed { XCTFail(message ?? "Expected element to disappear: \(self)", file: file, line: line) return false } return true } func scrollIntoView( in scrollView: XCUIElement, maxSwipes: Int = 8, file: StaticString = #filePath, line: UInt = #line ) { if isHittable { return } for _ in 0..