// // BaseUITestCase.swift // Tests iOS // // Base class for all UI tests. Handles launch arguments, // parallel test isolation, and screenshot capture on failure. // import XCTest class BaseUITestCase: XCTestCase { var app: XCUIApplication! /// Element on current screen — if it's not there in 2s, the app is broken let defaultTimeout: TimeInterval = 2 /// Screen transitions, tab switches let navigationTimeout: TimeInterval = 5 // MARK: - Parallel Test Isolation /// Unique session ID for this test class instance. /// Passed to the app via environment so each parallel runner gets /// its own UserDefaults suite and in-memory SwiftData container. private(set) var testSessionID: String = UUID().uuidString // MARK: - Configuration (override in subclasses) /// Fixture to seed. Override to use a specific data set. var seedFixture: String? { nil } /// Whether to bypass the subscription paywall. Default: true. var bypassSubscription: Bool { true } /// Whether to skip onboarding. Default: true. var skipOnboarding: Bool { true } /// Whether to force the trial to be expired. Default: false. var expireTrial: Bool { false } /// Override to change the test locale/language. Default: English (US). var localeArguments: [String] { ["-AppleLanguages", "(en)", "-AppleLocale", "en_US"] } /// Extra launch arguments (accessibility sizes, reduce motion, etc.). var extraLaunchArguments: [String] { [] } // MARK: - Lifecycle override func setUp() { super.setUp() continueAfterFailure = false app = launchApp(resetState: true) } override func tearDown() { if let failure = testRun?.failureCount, failure > 0 { captureScreenshot(name: "FAILURE-\(name)") } app = nil super.tearDown() } // MARK: - Launch Configuration private func buildLaunchArguments(resetState: Bool) -> [String] { var args = ["--ui-testing", "--disable-animations"] args.append(contentsOf: localeArguments) if resetState { args.append("--reset-state") } if bypassSubscription { args.append("--bypass-subscription") } if skipOnboarding { args.append("--skip-onboarding") } if expireTrial { args.append("--expire-trial") } args.append(contentsOf: extraLaunchArguments) return args } private func buildLaunchEnvironment() -> [String: String] { var env = [String: String]() env["UI_TEST_SESSION_ID"] = testSessionID if let fixture = seedFixture { env["UI_TEST_FIXTURE"] = fixture } return env } // MARK: - Screenshots func captureScreenshot(name: String) { let screenshot = XCTAttachment(screenshot: app.screenshot()) screenshot.name = name screenshot.lifetime = .keepAlways add(screenshot) } // MARK: - Launch Helpers @discardableResult func launchApp(resetState: Bool) -> XCUIApplication { let application = XCUIApplication() application.launchArguments = buildLaunchArguments(resetState: resetState) application.launchEnvironment = buildLaunchEnvironment() application.launch() return application } /// Relaunch with a different bypass setting, preserving session ID. @discardableResult func relaunchApp(resetState: Bool, bypassSubscription overrideBypass: Bool) -> XCUIApplication { app.terminate() let application = XCUIApplication() var args = ["--ui-testing", "--disable-animations"] args.append(contentsOf: localeArguments) if resetState { args.append("--reset-state") } if overrideBypass { args.append("--bypass-subscription") } if skipOnboarding { args.append("--skip-onboarding") } if expireTrial { args.append("--expire-trial") } args.append(contentsOf: extraLaunchArguments) application.launchArguments = args application.launchEnvironment = buildLaunchEnvironment() application.launch() app = application return application } @discardableResult func relaunchPreservingState() -> XCUIApplication { app.terminate() let relaunched = launchApp(resetState: false) app = relaunched return relaunched } }