Files
Reflect/Shared/UITestMode.swift
Trey t c286294cd3 Fix remaining 12 UI test failures: subscription state, hittability, tab selection
- IAPManager: add resetForTesting() to discard stale cached subscription state
- UITestMode: call resetForTesting() after clearing defaults (fixes 5 banner tests)
- StabilityTests: use NSPredicate wait for isSelected (iOS 26 Liquid Glass)
- SettingsActionTests: use coordinate tap for clear data and analytics toggle
- IconPackTests: add horizontal scroll fallback for off-screen icon packs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 17:43:28 -06:00

136 lines
4.8 KiB
Swift

//
// UITestMode.swift
// Feels (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")
}
/// 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 FeelsApp) 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() {
// Clear group user defaults using the correct suite name
let defaults = GroupUserDefaults.groupDefaults
defaults.removePersistentDomain(forName: Constants.currentGroupShareId)
// 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
}
}
}