fixes for testing and debug
Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
c1625e4c54
commit
ce27a4473e
@ -5,6 +5,7 @@ import Bedrock
|
|||||||
@main
|
@main
|
||||||
struct AndromidaApp: App {
|
struct AndromidaApp: App {
|
||||||
private let modelContainer: ModelContainer
|
private let modelContainer: ModelContainer
|
||||||
|
private let launchContext: AppLaunchContext
|
||||||
@State private var store: RitualStore
|
@State private var store: RitualStore
|
||||||
@State private var settingsStore: SettingsStore
|
@State private var settingsStore: SettingsStore
|
||||||
@State private var categoryStore: CategoryStore
|
@State private var categoryStore: CategoryStore
|
||||||
@ -15,6 +16,10 @@ struct AndromidaApp: App {
|
|||||||
@State private var isTransitioningToRoot = false
|
@State private var isTransitioningToRoot = false
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
|
#if !DEBUG
|
||||||
|
Design.showDebugLogs = false
|
||||||
|
#endif
|
||||||
|
|
||||||
// Register app's color theme for Bedrock components
|
// Register app's color theme for Bedrock components
|
||||||
Theme.register(
|
Theme.register(
|
||||||
text: AppTextColors.self,
|
text: AppTextColors.self,
|
||||||
@ -23,44 +28,15 @@ struct AndromidaApp: App {
|
|||||||
status: AppStatus.self
|
status: AppStatus.self
|
||||||
)
|
)
|
||||||
Theme.register(border: AppBorder.self)
|
Theme.register(border: AppBorder.self)
|
||||||
|
|
||||||
let environment = ProcessInfo.processInfo.environment
|
|
||||||
let isRunningTests = environment["XCTestConfigurationFilePath"] != nil
|
|
||||||
let isUITesting = environment["UITEST_MODE"] == "1"
|
|
||||||
|
|
||||||
if isUITesting {
|
let launchContext = AppLaunchContext()
|
||||||
if environment["UITEST_RESET_USER_DEFAULTS"] == "1",
|
self.launchContext = launchContext
|
||||||
let bundleIdentifier = Bundle.main.bundleIdentifier {
|
launchContext.applyUserDefaultsOverrides(bundleIdentifier: Bundle.main.bundleIdentifier)
|
||||||
UserDefaults.standard.removePersistentDomain(forName: bundleIdentifier)
|
|
||||||
}
|
|
||||||
if let completedValue = environment["UITEST_HAS_COMPLETED_SETUP_WIZARD"] {
|
|
||||||
let hasCompleted = completedValue == "1" || completedValue.lowercased() == "true"
|
|
||||||
UserDefaults.standard.set(hasCompleted, forKey: "hasCompletedSetupWizard")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Include all models in schema - Ritual, RitualArc, and ArcHabit
|
// Include all models in schema - Ritual, RitualArc, and ArcHabit
|
||||||
let schema = Schema([Ritual.self, RitualArc.self, ArcHabit.self])
|
let schema = Schema([Ritual.self, RitualArc.self, ArcHabit.self])
|
||||||
|
|
||||||
let configuration: ModelConfiguration
|
let configuration = launchContext.modelConfiguration(for: schema)
|
||||||
if isUITesting {
|
|
||||||
// UI tests should always run with isolated in-memory persistence.
|
|
||||||
configuration = ModelConfiguration(
|
|
||||||
schema: schema,
|
|
||||||
isStoredInMemoryOnly: true,
|
|
||||||
cloudKitDatabase: .none
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// Use App Group for shared container between app and widget.
|
|
||||||
// Disable CloudKit mirroring under XCTest to keep simulator tests deterministic.
|
|
||||||
let storeURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: AppIdentifiers.appGroupIdentifier)?
|
|
||||||
.appendingPathComponent("Andromida.sqlite") ?? URL.documentsDirectory.appendingPathComponent("Andromida.sqlite")
|
|
||||||
configuration = ModelConfiguration(
|
|
||||||
schema: schema,
|
|
||||||
url: storeURL,
|
|
||||||
cloudKitDatabase: isRunningTests ? .none : .private(AppIdentifiers.cloudKitContainerIdentifier)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
let container: ModelContainer
|
let container: ModelContainer
|
||||||
do {
|
do {
|
||||||
@ -73,15 +49,13 @@ struct AndromidaApp: App {
|
|||||||
_settingsStore = State(initialValue: settings)
|
_settingsStore = State(initialValue: settings)
|
||||||
_categoryStore = State(initialValue: CategoryStore())
|
_categoryStore = State(initialValue: CategoryStore())
|
||||||
|
|
||||||
let ritualStore = RitualStore(modelContext: container.mainContext, seedService: RitualSeedService(), settingsStore: settings)
|
let ritualStore = RitualStore(
|
||||||
if isUITesting, environment["UITEST_SEED_THREE_PRESETS"] == "1" {
|
modelContext: container.mainContext,
|
||||||
ritualStore.createRitualFromPreset(RitualPresetLibrary.healthPresets[0]) // morning
|
seedService: RitualSeedService(),
|
||||||
ritualStore.createRitualFromPreset(RitualPresetLibrary.healthPresets[1]) // midday
|
settingsStore: settings,
|
||||||
ritualStore.createRitualFromPreset(RitualPresetLibrary.healthPresets[3]) // evening
|
isRunningTests: launchContext.isRunningTests
|
||||||
}
|
)
|
||||||
if isUITesting, environment["UITEST_PRELOAD_DEMO_DATA"] == "1" {
|
launchContext.applyUITestSeeding(to: ritualStore)
|
||||||
ritualStore.preloadDemoData()
|
|
||||||
}
|
|
||||||
_store = State(initialValue: ritualStore)
|
_store = State(initialValue: ritualStore)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,13 +66,12 @@ struct AndromidaApp: App {
|
|||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
|
|
||||||
if hasCompletedSetupWizard {
|
if hasCompletedSetupWizard {
|
||||||
let uiTestInitialTab = uiTestRequestedInitialTab()
|
|
||||||
// Main app - start on Rituals tab if just completed wizard
|
// Main app - start on Rituals tab if just completed wizard
|
||||||
RootView(
|
RootView(
|
||||||
store: store,
|
store: store,
|
||||||
settingsStore: settingsStore,
|
settingsStore: settingsStore,
|
||||||
categoryStore: categoryStore,
|
categoryStore: categoryStore,
|
||||||
initialTab: uiTestInitialTab ?? (justCompletedWizard ? .rituals : .today)
|
initialTab: launchContext.initialTabOverride ?? (justCompletedWizard ? .rituals : .today)
|
||||||
)
|
)
|
||||||
.transition(.opacity)
|
.transition(.opacity)
|
||||||
} else {
|
} else {
|
||||||
@ -126,16 +99,4 @@ struct AndromidaApp: App {
|
|||||||
.preferredColorScheme(settingsStore.theme.colorScheme)
|
.preferredColorScheme(settingsStore.theme.colorScheme)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func uiTestRequestedInitialTab() -> RootView.RootTab? {
|
|
||||||
guard ProcessInfo.processInfo.environment["UITEST_MODE"] == "1" else { return nil }
|
|
||||||
switch ProcessInfo.processInfo.environment["UITEST_INITIAL_TAB"]?.lowercased() {
|
|
||||||
case "today": return .today
|
|
||||||
case "rituals": return .rituals
|
|
||||||
case "insights": return .insights
|
|
||||||
case "history": return .history
|
|
||||||
case "settings": return .settings
|
|
||||||
default: return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import UserNotifications
|
import UserNotifications
|
||||||
import Observation
|
import Observation
|
||||||
|
import Bedrock
|
||||||
|
|
||||||
/// Reminder time slots based on ritual TimeOfDay values.
|
/// Reminder time slots based on ritual TimeOfDay values.
|
||||||
/// Groups similar times to avoid excessive notifications.
|
/// Groups similar times to avoid excessive notifications.
|
||||||
@ -85,7 +86,7 @@ final class ReminderScheduler: NSObject, UNUserNotificationCenterDelegate {
|
|||||||
willPresent notification: UNNotification,
|
willPresent notification: UNNotification,
|
||||||
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
|
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
|
||||||
) {
|
) {
|
||||||
print("🔔 Notification will present in foreground: \(notification.request.identifier)")
|
Design.debugLog("🔔 Notification will present in foreground: \(notification.request.identifier)")
|
||||||
// Show the notification even when the app is in the foreground
|
// Show the notification even when the app is in the foreground
|
||||||
completionHandler([.banner, .list, .sound, .badge])
|
completionHandler([.banner, .list, .sound, .badge])
|
||||||
}
|
}
|
||||||
@ -95,7 +96,7 @@ final class ReminderScheduler: NSObject, UNUserNotificationCenterDelegate {
|
|||||||
didReceive response: UNNotificationResponse,
|
didReceive response: UNNotificationResponse,
|
||||||
withCompletionHandler completionHandler: @escaping () -> Void
|
withCompletionHandler completionHandler: @escaping () -> Void
|
||||||
) {
|
) {
|
||||||
print("🔔 Notification received/tapped: \(response.notification.request.identifier)")
|
Design.debugLog("🔔 Notification received/tapped: \(response.notification.request.identifier)")
|
||||||
// Clear badge when user interacts with notification
|
// Clear badge when user interacts with notification
|
||||||
clearBadge()
|
clearBadge()
|
||||||
shouldNavigateToToday = true
|
shouldNavigateToToday = true
|
||||||
@ -127,7 +128,7 @@ final class ReminderScheduler: NSObject, UNUserNotificationCenterDelegate {
|
|||||||
await refreshAuthorizationStatus()
|
await refreshAuthorizationStatus()
|
||||||
return granted
|
return granted
|
||||||
} catch {
|
} catch {
|
||||||
print("Notification authorization error: \(error)")
|
Design.debugLog("Notification authorization error: \(error)")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -147,7 +148,7 @@ final class ReminderScheduler: NSObject, UNUserNotificationCenterDelegate {
|
|||||||
|
|
||||||
/// Schedules a test notification to appear in 5 seconds.
|
/// Schedules a test notification to appear in 5 seconds.
|
||||||
func scheduleTestNotification() {
|
func scheduleTestNotification() {
|
||||||
print("🔔 Attempting to schedule test notification...")
|
Design.debugLog("🔔 Attempting to schedule test notification...")
|
||||||
|
|
||||||
let content = UNMutableNotificationContent()
|
let content = UNMutableNotificationContent()
|
||||||
content.title = String(localized: "Test Notification")
|
content.title = String(localized: "Test Notification")
|
||||||
@ -164,9 +165,9 @@ final class ReminderScheduler: NSObject, UNUserNotificationCenterDelegate {
|
|||||||
|
|
||||||
UNUserNotificationCenter.current().add(request) { error in
|
UNUserNotificationCenter.current().add(request) { error in
|
||||||
if let error = error {
|
if let error = error {
|
||||||
print("❌ Failed to schedule test notification: \(error)")
|
Design.debugLog("❌ Failed to schedule test notification: \(error)")
|
||||||
} else {
|
} else {
|
||||||
print("✅ Test notification scheduled successfully! It should appear in 5 seconds.")
|
Design.debugLog("✅ Test notification scheduled successfully! It should appear in 5 seconds.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -257,7 +258,7 @@ final class ReminderScheduler: NSObject, UNUserNotificationCenterDelegate {
|
|||||||
do {
|
do {
|
||||||
try await UNUserNotificationCenter.current().add(request)
|
try await UNUserNotificationCenter.current().add(request)
|
||||||
} catch {
|
} catch {
|
||||||
print("Failed to schedule \(slot.rawValue) reminder: \(error)")
|
Design.debugLog("Failed to schedule \(slot.rawValue) reminder: \(error)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,6 +11,11 @@ extension RitualStore {
|
|||||||
} catch {
|
} catch {
|
||||||
fatalError("Preview container failed: \(error)")
|
fatalError("Preview container failed: \(error)")
|
||||||
}
|
}
|
||||||
return RitualStore(modelContext: container.mainContext, seedService: RitualSeedService(), settingsStore: SettingsStore())
|
return RitualStore(
|
||||||
|
modelContext: container.mainContext,
|
||||||
|
seedService: RitualSeedService(),
|
||||||
|
settingsStore: SettingsStore(),
|
||||||
|
isRunningTests: false
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -59,6 +59,7 @@ final class RitualStore: RitualStoreProviding {
|
|||||||
modelContext: ModelContext,
|
modelContext: ModelContext,
|
||||||
seedService: RitualSeedProviding,
|
seedService: RitualSeedProviding,
|
||||||
settingsStore: any RitualFeedbackSettingsProviding,
|
settingsStore: any RitualFeedbackSettingsProviding,
|
||||||
|
isRunningTests: Bool,
|
||||||
calendar: Calendar = .current,
|
calendar: Calendar = .current,
|
||||||
now: @escaping () -> Date = Date.init
|
now: @escaping () -> Date = Date.init
|
||||||
) {
|
) {
|
||||||
@ -67,7 +68,7 @@ final class RitualStore: RitualStoreProviding {
|
|||||||
self.settingsStore = settingsStore
|
self.settingsStore = settingsStore
|
||||||
self.calendar = calendar
|
self.calendar = calendar
|
||||||
self.nowProvider = now
|
self.nowProvider = now
|
||||||
self.isRunningTests = ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil
|
self.isRunningTests = isRunningTests
|
||||||
self.dayFormatter = DateFormatter()
|
self.dayFormatter = DateFormatter()
|
||||||
self.displayFormatter = DateFormatter()
|
self.displayFormatter = DateFormatter()
|
||||||
dayFormatter.calendar = calendar
|
dayFormatter.calendar = calendar
|
||||||
@ -1556,7 +1557,7 @@ final class RitualStore: RitualStoreProviding {
|
|||||||
// Find the first ritual with an active arc
|
// Find the first ritual with an active arc
|
||||||
guard let ritual = currentRituals.first,
|
guard let ritual = currentRituals.first,
|
||||||
let arc = ritual.activeArc(on: now()) else {
|
let arc = ritual.activeArc(on: now()) else {
|
||||||
print("No active arcs to complete")
|
Design.debugLog("No active arcs to complete")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1573,7 +1574,7 @@ final class RitualStore: RitualStoreProviding {
|
|||||||
// Trigger the completion check - this will set ritualNeedingRenewal
|
// Trigger the completion check - this will set ritualNeedingRenewal
|
||||||
checkForCompletedArcs()
|
checkForCompletedArcs()
|
||||||
|
|
||||||
print("Arc '\(ritual.title)' marked as completed. Navigate to Today tab to see renewal prompt.")
|
Design.debugLog("Arc '\(ritual.title)' marked as completed. Navigate to Today tab to see renewal prompt.")
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|||||||
105
Andromida/App/Support/AppLaunchContext.swift
Normal file
105
Andromida/App/Support/AppLaunchContext.swift
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import Foundation
|
||||||
|
import SwiftData
|
||||||
|
|
||||||
|
struct AppLaunchContext {
|
||||||
|
enum LaunchTab: String {
|
||||||
|
case today
|
||||||
|
case rituals
|
||||||
|
case insights
|
||||||
|
case history
|
||||||
|
case settings
|
||||||
|
}
|
||||||
|
|
||||||
|
let isRunningTests: Bool
|
||||||
|
let isUITesting: Bool
|
||||||
|
let shouldResetUserDefaults: Bool
|
||||||
|
let hasCompletedSetupWizardOverride: Bool?
|
||||||
|
let shouldSeedThreePresets: Bool
|
||||||
|
let shouldPreloadDemoData: Bool
|
||||||
|
let requestedInitialTab: LaunchTab?
|
||||||
|
|
||||||
|
init(environment: [String: String] = ProcessInfo.processInfo.environment) {
|
||||||
|
isRunningTests = environment["XCTestConfigurationFilePath"] != nil
|
||||||
|
isUITesting = Self.isEnabled(environment["UITEST_MODE"])
|
||||||
|
shouldResetUserDefaults = Self.isEnabled(environment["UITEST_RESET_USER_DEFAULTS"])
|
||||||
|
hasCompletedSetupWizardOverride = Self.optionalBool(environment["UITEST_HAS_COMPLETED_SETUP_WIZARD"])
|
||||||
|
shouldSeedThreePresets = Self.isEnabled(environment["UITEST_SEED_THREE_PRESETS"])
|
||||||
|
shouldPreloadDemoData = Self.isEnabled(environment["UITEST_PRELOAD_DEMO_DATA"])
|
||||||
|
requestedInitialTab = Self.parseTab(environment["UITEST_INITIAL_TAB"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyUserDefaultsOverrides(bundleIdentifier: String?) {
|
||||||
|
guard isUITesting else { return }
|
||||||
|
|
||||||
|
if shouldResetUserDefaults, let bundleIdentifier {
|
||||||
|
UserDefaults.standard.removePersistentDomain(forName: bundleIdentifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let hasCompletedSetupWizardOverride {
|
||||||
|
UserDefaults.standard.set(hasCompletedSetupWizardOverride, forKey: "hasCompletedSetupWizard")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func modelConfiguration(for schema: Schema) -> ModelConfiguration {
|
||||||
|
if isUITesting {
|
||||||
|
// UI tests should always run with isolated in-memory persistence.
|
||||||
|
return ModelConfiguration(
|
||||||
|
schema: schema,
|
||||||
|
isStoredInMemoryOnly: true,
|
||||||
|
cloudKitDatabase: .none
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use App Group for shared container between app and widget.
|
||||||
|
// Disable CloudKit mirroring under XCTest to keep simulator tests deterministic.
|
||||||
|
let storeURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: AppIdentifiers.appGroupIdentifier)?
|
||||||
|
.appendingPathComponent("Andromida.sqlite") ?? URL.documentsDirectory.appendingPathComponent("Andromida.sqlite")
|
||||||
|
|
||||||
|
return ModelConfiguration(
|
||||||
|
schema: schema,
|
||||||
|
url: storeURL,
|
||||||
|
cloudKitDatabase: isRunningTests ? .none : .private(AppIdentifiers.cloudKitContainerIdentifier)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyUITestSeeding(to ritualStore: RitualStore) {
|
||||||
|
guard isUITesting else { return }
|
||||||
|
|
||||||
|
if shouldSeedThreePresets {
|
||||||
|
ritualStore.createRitualFromPreset(RitualPresetLibrary.healthPresets[0]) // morning
|
||||||
|
ritualStore.createRitualFromPreset(RitualPresetLibrary.healthPresets[1]) // midday
|
||||||
|
ritualStore.createRitualFromPreset(RitualPresetLibrary.healthPresets[3]) // evening
|
||||||
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
if shouldPreloadDemoData {
|
||||||
|
ritualStore.preloadDemoData()
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
var initialTabOverride: RootView.RootTab? {
|
||||||
|
guard let requestedInitialTab else { return nil }
|
||||||
|
switch requestedInitialTab {
|
||||||
|
case .today: return .today
|
||||||
|
case .rituals: return .rituals
|
||||||
|
case .insights: return .insights
|
||||||
|
case .history: return .history
|
||||||
|
case .settings: return .settings
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func isEnabled(_ value: String?) -> Bool {
|
||||||
|
optionalBool(value) ?? false
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func optionalBool(_ value: String?) -> Bool? {
|
||||||
|
guard let value else { return nil }
|
||||||
|
return value == "1" || value.lowercased() == "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func parseTab(_ value: String?) -> LaunchTab? {
|
||||||
|
guard let value else { return nil }
|
||||||
|
return LaunchTab(rawValue: value.lowercased())
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -47,7 +47,8 @@ struct AndromidaTests {
|
|||||||
let store = RitualStore(
|
let store = RitualStore(
|
||||||
modelContext: container.mainContext,
|
modelContext: container.mainContext,
|
||||||
seedService: EmptySeedService(),
|
seedService: EmptySeedService(),
|
||||||
settingsStore: TestFeedbackSettings()
|
settingsStore: TestFeedbackSettings(),
|
||||||
|
isRunningTests: true
|
||||||
)
|
)
|
||||||
|
|
||||||
store.createQuickRitual()
|
store.createQuickRitual()
|
||||||
|
|||||||
@ -433,6 +433,7 @@ private func makeStore(now: @escaping () -> Date = Date.init) -> RitualStore {
|
|||||||
modelContext: container.mainContext,
|
modelContext: container.mainContext,
|
||||||
seedService: EmptySeedService(),
|
seedService: EmptySeedService(),
|
||||||
settingsStore: TestFeedbackSettings(),
|
settingsStore: TestFeedbackSettings(),
|
||||||
|
isRunningTests: true,
|
||||||
now: now
|
now: now
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user