Add XCUITest suite with 27 test files covering unmapped P1 test cases
- Add 8 new test files: HeaderMoodLogging (TC-002), DayViewGrouping (TC-019), AllDayViewStyles (TC-021), MonthViewInteraction (TC-030), PaywallGate (TC-032/039/048), AppTheme (TC-070), IconPack (TC-072), PremiumCustomization (TC-075) - Add accessibility IDs for paywall overlays, icon packs, app theme cards, and day view section headers - Add --expire-trial launch argument to UITestMode for paywall gate testing - Update QA test plan spreadsheet with XCUITest names for 14 test cases - Include existing test infrastructure: screen objects, helpers, base class, and 19 previously written test files Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
132
Shared/UITestMode.swift
Normal file
132
Shared/UITestMode.swift
Normal file
@@ -0,0 +1,132 @@
|
||||
//
|
||||
// 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 bypassSubscription {
|
||||
#if DEBUG
|
||||
IAPManager.shared.bypassSubscription = true
|
||||
#endif
|
||||
}
|
||||
|
||||
if expireTrial {
|
||||
// Set firstLaunchDate to 31 days ago so the 30-day trial is expired
|
||||
let expiredDate = Calendar.current.date(byAdding: .day, value: -31, to: Date())!
|
||||
GroupUserDefaults.groupDefaults.set(expiredDate, forKey: UserDefaultsStore.Keys.firstLaunchDate.rawValue)
|
||||
GroupUserDefaults.groupDefaults.synchronize()
|
||||
}
|
||||
|
||||
// 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
|
||||
let defaults = GroupUserDefaults.groupDefaults
|
||||
if let bundleId = Bundle.main.bundleIdentifier {
|
||||
defaults.removePersistentDomain(forName: bundleId)
|
||||
}
|
||||
// Reset key defaults explicitly
|
||||
defaults.set(false, 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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user