feat: add XCUITest suite with 10 critical flow tests and QA test plan
Add comprehensive UI test infrastructure with Page Object pattern, accessibility identifiers, UI test mode (--ui-testing, --reset-state, --disable-animations), and 10 passing tests covering app launch, tab navigation, trip wizard, trip saving, settings, schedule, and accessibility at XXXL Dynamic Type. Also adds a 229-case QA test plan Excel workbook for manual QA handoff. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,15 @@ struct SportsTimeApp: App {
|
||||
private var transactionListener: Task<Void, Never>?
|
||||
|
||||
init() {
|
||||
// UI Test Mode: disable animations and force classic style for deterministic tests
|
||||
if ProcessInfo.isUITesting || ProcessInfo.shouldDisableAnimations {
|
||||
UIView.setAnimationsEnabled(false)
|
||||
}
|
||||
if ProcessInfo.isUITesting {
|
||||
// Force classic (non-animated) home variant for consistent identifiers
|
||||
DesignStyleManager.shared.setStyle(.classic)
|
||||
}
|
||||
|
||||
// Configure sync manager immediately so push/background triggers can sync.
|
||||
BackgroundSyncManager.shared.configure(with: sharedModelContainer)
|
||||
|
||||
@@ -27,7 +36,9 @@ struct SportsTimeApp: App {
|
||||
BackgroundSyncManager.shared.registerTasks()
|
||||
|
||||
// Start listening for transactions immediately
|
||||
transactionListener = StoreManager.shared.listenForTransactions()
|
||||
if !ProcessInfo.isUITesting {
|
||||
transactionListener = StoreManager.shared.listenForTransactions()
|
||||
}
|
||||
}
|
||||
|
||||
var sharedModelContainer: ModelContainer = {
|
||||
@@ -93,7 +104,8 @@ struct BootstrappedContentView: View {
|
||||
@State private var appearanceManager = AppearanceManager.shared
|
||||
|
||||
private var shouldShowOnboardingPaywall: Bool {
|
||||
!UserDefaults.standard.bool(forKey: "hasSeenOnboardingPaywall") && !StoreManager.shared.isPro
|
||||
guard !ProcessInfo.isUITesting else { return false }
|
||||
return !UserDefaults.standard.bool(forKey: "hasSeenOnboardingPaywall") && !StoreManager.shared.isPro
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -136,6 +148,7 @@ struct BootstrappedContentView: View {
|
||||
deepLinkHandler.handleURL(url)
|
||||
}
|
||||
.onChange(of: scenePhase) { _, newPhase in
|
||||
guard !ProcessInfo.isUITesting else { return }
|
||||
switch newPhase {
|
||||
case .active:
|
||||
// Refresh super properties (subscription status may have changed)
|
||||
@@ -170,6 +183,18 @@ struct BootstrappedContentView: View {
|
||||
let bootstrapService = BootstrapService()
|
||||
|
||||
do {
|
||||
// 0. UI Test Mode: reset user data if requested
|
||||
if ProcessInfo.shouldResetState {
|
||||
print("🚀 [BOOT] Step 0: Resetting user data for UI tests...")
|
||||
try context.delete(model: SavedTrip.self)
|
||||
try context.delete(model: StadiumVisit.self)
|
||||
try context.delete(model: Achievement.self)
|
||||
try context.delete(model: LocalTripPoll.self)
|
||||
try context.delete(model: LocalPollVote.self)
|
||||
try context.delete(model: TripVote.self)
|
||||
try context.save()
|
||||
}
|
||||
|
||||
// 1. Bootstrap from bundled JSON if first launch (no data exists)
|
||||
print("🚀 [BOOT] Step 1: Checking if bootstrap needed...")
|
||||
try await bootstrapService.bootstrapIfNeeded(context: context)
|
||||
@@ -192,40 +217,61 @@ struct BootstrappedContentView: View {
|
||||
print("🚀 [BOOT] Loaded \(AppDataProvider.shared.stadiums.count) stadiums")
|
||||
|
||||
// 5. Load store products and entitlements
|
||||
print("🚀 [BOOT] Step 5: Loading store products...")
|
||||
await StoreManager.shared.loadProducts()
|
||||
await StoreManager.shared.updateEntitlements()
|
||||
if ProcessInfo.isUITesting {
|
||||
print("🚀 [BOOT] Step 5: UI Test Mode — forcing Pro, skipping StoreKit")
|
||||
#if DEBUG
|
||||
StoreManager.shared.debugProOverride = true
|
||||
#endif
|
||||
} else {
|
||||
print("🚀 [BOOT] Step 5: Loading store products...")
|
||||
await StoreManager.shared.loadProducts()
|
||||
await StoreManager.shared.updateEntitlements()
|
||||
}
|
||||
|
||||
// 6. Start network monitoring and wire up sync callback
|
||||
print("🚀 [BOOT] Step 6: Starting network monitoring...")
|
||||
NetworkMonitor.shared.onSyncNeeded = {
|
||||
await BackgroundSyncManager.shared.triggerSyncFromNetworkRestoration()
|
||||
if !ProcessInfo.isUITesting {
|
||||
print("🚀 [BOOT] Step 6: Starting network monitoring...")
|
||||
NetworkMonitor.shared.onSyncNeeded = {
|
||||
await BackgroundSyncManager.shared.triggerSyncFromNetworkRestoration()
|
||||
}
|
||||
NetworkMonitor.shared.startMonitoring()
|
||||
} else {
|
||||
print("🚀 [BOOT] Step 6: UI Test Mode — skipping network monitoring")
|
||||
}
|
||||
NetworkMonitor.shared.startMonitoring()
|
||||
|
||||
// 7. Configure analytics
|
||||
print("🚀 [BOOT] Step 7: Configuring analytics...")
|
||||
AnalyticsManager.shared.configure()
|
||||
if !ProcessInfo.isUITesting {
|
||||
print("🚀 [BOOT] Step 7: Configuring analytics...")
|
||||
AnalyticsManager.shared.configure()
|
||||
} else {
|
||||
print("🚀 [BOOT] Step 7: UI Test Mode — skipping analytics")
|
||||
}
|
||||
|
||||
// 8. App is now usable
|
||||
print("🚀 [BOOT] Step 8: Bootstrap complete - app ready")
|
||||
isBootstrapping = false
|
||||
|
||||
// 9. Schedule background tasks for future syncs
|
||||
BackgroundSyncManager.shared.scheduleAllTasks()
|
||||
// 9-10: Background sync (skip in UI test mode)
|
||||
if !ProcessInfo.isUITesting {
|
||||
// 9. Schedule background tasks for future syncs
|
||||
BackgroundSyncManager.shared.scheduleAllTasks()
|
||||
|
||||
// 9b. Ensure CloudKit subscriptions exist for push-driven sync.
|
||||
Task(priority: .utility) {
|
||||
await BackgroundSyncManager.shared.ensureCanonicalSubscriptions()
|
||||
}
|
||||
|
||||
// 10. Background: Try to refresh from CloudKit (non-blocking)
|
||||
print("🚀 [BOOT] Step 10: Starting background CloudKit sync...")
|
||||
Task(priority: .background) {
|
||||
await self.performBackgroundSync(context: self.modelContainer.mainContext)
|
||||
await MainActor.run {
|
||||
self.hasCompletedInitialSync = true
|
||||
// 9b. Ensure CloudKit subscriptions exist for push-driven sync.
|
||||
Task(priority: .utility) {
|
||||
await BackgroundSyncManager.shared.ensureCanonicalSubscriptions()
|
||||
}
|
||||
|
||||
// 10. Background: Try to refresh from CloudKit (non-blocking)
|
||||
print("🚀 [BOOT] Step 10: Starting background CloudKit sync...")
|
||||
Task(priority: .background) {
|
||||
await self.performBackgroundSync(context: self.modelContainer.mainContext)
|
||||
await MainActor.run {
|
||||
self.hasCompletedInitialSync = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
print("🚀 [BOOT] Steps 9-10: UI Test Mode — skipping CloudKit sync")
|
||||
hasCompletedInitialSync = true
|
||||
}
|
||||
} catch {
|
||||
print("❌ [BOOT] Bootstrap failed: \(error.localizedDescription)")
|
||||
|
||||
Reference in New Issue
Block a user