- 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>
136 lines
4.8 KiB
Swift
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
|
|
}
|
|
}
|
|
}
|