// // BaseUITestCase.swift // Tests iOS // // Base class for all UI tests. Handles launch arguments, // state reset, screenshot capture on failure, and parallel // test isolation via per-session data sandboxing. // import XCTest class BaseUITestCase: XCTestCase { var app: XCUIApplication! // 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). Locale tests override this instead of setUp(). var localeArguments: [String] { ["-AppleLanguages", "(en)", "-AppleLocale", "en_US"] } /// Extra launch arguments for tests needing special settings /// (accessibility sizes, reduce motion, high contrast, etc.). /// Override in subclasses instead of overriding setUp(). 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: - Shared Test Utilities @discardableResult func launchApp(resetState: Bool) -> XCUIApplication { let application = XCUIApplication() application.launchArguments = buildLaunchArguments(resetState: resetState) application.launchEnvironment = buildLaunchEnvironment() application.launch() return application } /// Relaunch the app with custom bypass setting, preserving the session ID. /// Use when a test needs to toggle subscription bypass mid-test. @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 } func assertDayContentVisible(timeout: TimeInterval = 8, file: StaticString = #file, line: UInt = #line) { let hasEntry = app.firstEntryRow.waitForExistence(timeout: timeout) let hasMoodHeader = app.element(UITestID.Day.moodHeader).waitForExistence(timeout: 2) XCTAssertTrue(hasEntry || hasMoodHeader, "Day view should show entry list or mood header", file: file, line: line) } }