// // UITestMode.swift // Reflect (iOS) // // Handles launch arguments for UI testing mode. // When --ui-testing is passed, the app uses deterministic settings. // import Foundation #if canImport(UIKit) import UIKit #endif enum UITestMode { /// Whether the app was launched in UI testing mode static var isUITesting: Bool { ProcessInfo.processInfo.arguments.contains("--ui-testing") } /// Whether to reset all state before the test run static var shouldResetState: Bool { ProcessInfo.processInfo.arguments.contains("--reset-state") } /// Whether to disable animations for faster, more deterministic tests static var disableAnimations: Bool { ProcessInfo.processInfo.arguments.contains("--disable-animations") } /// Whether to bypass the subscription paywall static var bypassSubscription: Bool { ProcessInfo.processInfo.arguments.contains("--bypass-subscription") } /// Whether to skip onboarding static var skipOnboarding: Bool { ProcessInfo.processInfo.arguments.contains("--skip-onboarding") } /// Whether to force the trial to be expired (sets firstLaunchDate to 31 days ago) static var expireTrial: Bool { ProcessInfo.processInfo.arguments.contains("--expire-trial") } /// Unique session ID for parallel test isolation. /// Each test class gets its own session, ensuring no shared state between parallel test runners. static var sessionID: String? { ProcessInfo.processInfo.environment["UI_TEST_SESSION_ID"] } /// Seed fixture name if provided (via environment variable) static var seedFixture: String? { ProcessInfo.processInfo.environment["UI_TEST_FIXTURE"] } /// Apply all UI test mode settings. Called early in app startup. @MainActor static func configureIfNeeded() { guard isUITesting else { return } #if canImport(UIKit) if disableAnimations { UIView.setAnimationsEnabled(false) } #endif if shouldResetState { resetAppState() } if skipOnboarding { GroupUserDefaults.groupDefaults.set(false, forKey: UserDefaultsStore.Keys.needsOnboarding.rawValue) } if expireTrial { // Set firstLaunchDate to 31 days ago so the 30-day trial is expired. // Must run BEFORE IAPManager.shared is accessed so the async status // check sees the expired date. let expiredDate = Calendar.current.date(byAdding: .day, value: -31, to: Date())! GroupUserDefaults.groupDefaults.set(expiredDate, forKey: UserDefaultsStore.Keys.firstLaunchDate.rawValue) GroupUserDefaults.groupDefaults.synchronize() } #if DEBUG IAPManager.shared.bypassSubscription = bypassSubscription // Reset subscription state to discard stale cached state from previous test runs. // IAPManager.shared was already initialized (as @StateObject in ReflectApp) before // configureIfNeeded runs, so it may have restored stale subscription data. IAPManager.shared.resetForTesting() #endif // Seed fixture data if requested if let fixture = seedFixture { seedData(fixture: fixture) } } /// Reset all user defaults and persisted state for a clean test run @MainActor private static func resetAppState() { let defaults = GroupUserDefaults.groupDefaults // Clear group user defaults using the session-specific or shared suite domain name let suiteName = GroupUserDefaults.currentSuiteName defaults.removePersistentDomain(forName: suiteName) // Explicitly clear subscription cache keys that may survive removePersistentDomain // on app group suites (known reliability issue). defaults.removeObject(forKey: UserDefaultsStore.Keys.cachedSubscriptionExpiration.rawValue) defaults.removeObject(forKey: UserDefaultsStore.Keys.hasActiveSubscription.rawValue) defaults.removeObject(forKey: UserDefaultsStore.Keys.firstLaunchDate.rawValue) // Reset key defaults explicitly (true = fresh install state where onboarding is needed) defaults.set(true, forKey: UserDefaultsStore.Keys.needsOnboarding.rawValue) defaults.set(0, forKey: UserDefaultsStore.Keys.votingLayoutStyle.rawValue) // horizontal defaults.synchronize() // Clear standard user defaults UserDefaults.standard.set(false, forKey: "debug_bypassSubscription") // Clear all mood data DataController.shared.clearDB() } /// Seed the database with fixture data for deterministic tests @MainActor private static func seedData(fixture: String) { switch fixture { case "single_mood": // One entry for today with mood "Great" DataController.shared.add(mood: .great, forDate: Calendar.current.startOfDay(for: Date()), entryType: .listView) case "week_of_moods": // One mood per day for the last 7 days let moods: [Mood] = [.great, .good, .average, .bad, .horrible, .good, .great] for (offset, mood) in moods.enumerated() { let date = Calendar.current.date(byAdding: .day, value: -offset, to: Calendar.current.startOfDay(for: Date()))! DataController.shared.add(mood: mood, forDate: date, entryType: .listView) } case "empty": // No data — already cleared in resetAppState break default: break } } }