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
|
||||
struct AndromidaApp: App {
|
||||
private let modelContainer: ModelContainer
|
||||
private let launchContext: AppLaunchContext
|
||||
@State private var store: RitualStore
|
||||
@State private var settingsStore: SettingsStore
|
||||
@State private var categoryStore: CategoryStore
|
||||
@ -15,6 +16,10 @@ struct AndromidaApp: App {
|
||||
@State private var isTransitioningToRoot = false
|
||||
|
||||
init() {
|
||||
#if !DEBUG
|
||||
Design.showDebugLogs = false
|
||||
#endif
|
||||
|
||||
// Register app's color theme for Bedrock components
|
||||
Theme.register(
|
||||
text: AppTextColors.self,
|
||||
@ -24,43 +29,14 @@ struct AndromidaApp: App {
|
||||
)
|
||||
Theme.register(border: AppBorder.self)
|
||||
|
||||
let environment = ProcessInfo.processInfo.environment
|
||||
let isRunningTests = environment["XCTestConfigurationFilePath"] != nil
|
||||
let isUITesting = environment["UITEST_MODE"] == "1"
|
||||
|
||||
if isUITesting {
|
||||
if environment["UITEST_RESET_USER_DEFAULTS"] == "1",
|
||||
let 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")
|
||||
}
|
||||
}
|
||||
let launchContext = AppLaunchContext()
|
||||
self.launchContext = launchContext
|
||||
launchContext.applyUserDefaultsOverrides(bundleIdentifier: Bundle.main.bundleIdentifier)
|
||||
|
||||
// Include all models in schema - Ritual, RitualArc, and ArcHabit
|
||||
let schema = Schema([Ritual.self, RitualArc.self, ArcHabit.self])
|
||||
|
||||
let configuration: ModelConfiguration
|
||||
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 configuration = launchContext.modelConfiguration(for: schema)
|
||||
|
||||
let container: ModelContainer
|
||||
do {
|
||||
@ -73,15 +49,13 @@ struct AndromidaApp: App {
|
||||
_settingsStore = State(initialValue: settings)
|
||||
_categoryStore = State(initialValue: CategoryStore())
|
||||
|
||||
let ritualStore = RitualStore(modelContext: container.mainContext, seedService: RitualSeedService(), settingsStore: settings)
|
||||
if isUITesting, environment["UITEST_SEED_THREE_PRESETS"] == "1" {
|
||||
ritualStore.createRitualFromPreset(RitualPresetLibrary.healthPresets[0]) // morning
|
||||
ritualStore.createRitualFromPreset(RitualPresetLibrary.healthPresets[1]) // midday
|
||||
ritualStore.createRitualFromPreset(RitualPresetLibrary.healthPresets[3]) // evening
|
||||
}
|
||||
if isUITesting, environment["UITEST_PRELOAD_DEMO_DATA"] == "1" {
|
||||
ritualStore.preloadDemoData()
|
||||
}
|
||||
let ritualStore = RitualStore(
|
||||
modelContext: container.mainContext,
|
||||
seedService: RitualSeedService(),
|
||||
settingsStore: settings,
|
||||
isRunningTests: launchContext.isRunningTests
|
||||
)
|
||||
launchContext.applyUITestSeeding(to: ritualStore)
|
||||
_store = State(initialValue: ritualStore)
|
||||
}
|
||||
|
||||
@ -92,13 +66,12 @@ struct AndromidaApp: App {
|
||||
.ignoresSafeArea()
|
||||
|
||||
if hasCompletedSetupWizard {
|
||||
let uiTestInitialTab = uiTestRequestedInitialTab()
|
||||
// Main app - start on Rituals tab if just completed wizard
|
||||
RootView(
|
||||
store: store,
|
||||
settingsStore: settingsStore,
|
||||
categoryStore: categoryStore,
|
||||
initialTab: uiTestInitialTab ?? (justCompletedWizard ? .rituals : .today)
|
||||
initialTab: launchContext.initialTabOverride ?? (justCompletedWizard ? .rituals : .today)
|
||||
)
|
||||
.transition(.opacity)
|
||||
} else {
|
||||
@ -126,16 +99,4 @@ struct AndromidaApp: App {
|
||||
.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 UserNotifications
|
||||
import Observation
|
||||
import Bedrock
|
||||
|
||||
/// Reminder time slots based on ritual TimeOfDay values.
|
||||
/// Groups similar times to avoid excessive notifications.
|
||||
@ -85,7 +86,7 @@ final class ReminderScheduler: NSObject, UNUserNotificationCenterDelegate {
|
||||
willPresent notification: UNNotification,
|
||||
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
|
||||
completionHandler([.banner, .list, .sound, .badge])
|
||||
}
|
||||
@ -95,7 +96,7 @@ final class ReminderScheduler: NSObject, UNUserNotificationCenterDelegate {
|
||||
didReceive response: UNNotificationResponse,
|
||||
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
|
||||
clearBadge()
|
||||
shouldNavigateToToday = true
|
||||
@ -127,7 +128,7 @@ final class ReminderScheduler: NSObject, UNUserNotificationCenterDelegate {
|
||||
await refreshAuthorizationStatus()
|
||||
return granted
|
||||
} catch {
|
||||
print("Notification authorization error: \(error)")
|
||||
Design.debugLog("Notification authorization error: \(error)")
|
||||
return false
|
||||
}
|
||||
}
|
||||
@ -147,7 +148,7 @@ final class ReminderScheduler: NSObject, UNUserNotificationCenterDelegate {
|
||||
|
||||
/// Schedules a test notification to appear in 5 seconds.
|
||||
func scheduleTestNotification() {
|
||||
print("🔔 Attempting to schedule test notification...")
|
||||
Design.debugLog("🔔 Attempting to schedule test notification...")
|
||||
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = String(localized: "Test Notification")
|
||||
@ -164,9 +165,9 @@ final class ReminderScheduler: NSObject, UNUserNotificationCenterDelegate {
|
||||
|
||||
UNUserNotificationCenter.current().add(request) { error in
|
||||
if let error = error {
|
||||
print("❌ Failed to schedule test notification: \(error)")
|
||||
Design.debugLog("❌ Failed to schedule test notification: \(error)")
|
||||
} 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 {
|
||||
try await UNUserNotificationCenter.current().add(request)
|
||||
} 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 {
|
||||
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,
|
||||
seedService: RitualSeedProviding,
|
||||
settingsStore: any RitualFeedbackSettingsProviding,
|
||||
isRunningTests: Bool,
|
||||
calendar: Calendar = .current,
|
||||
now: @escaping () -> Date = Date.init
|
||||
) {
|
||||
@ -67,7 +68,7 @@ final class RitualStore: RitualStoreProviding {
|
||||
self.settingsStore = settingsStore
|
||||
self.calendar = calendar
|
||||
self.nowProvider = now
|
||||
self.isRunningTests = ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil
|
||||
self.isRunningTests = isRunningTests
|
||||
self.dayFormatter = DateFormatter()
|
||||
self.displayFormatter = DateFormatter()
|
||||
dayFormatter.calendar = calendar
|
||||
@ -1556,7 +1557,7 @@ final class RitualStore: RitualStoreProviding {
|
||||
// Find the first ritual with an active arc
|
||||
guard let ritual = currentRituals.first,
|
||||
let arc = ritual.activeArc(on: now()) else {
|
||||
print("No active arcs to complete")
|
||||
Design.debugLog("No active arcs to complete")
|
||||
return
|
||||
}
|
||||
|
||||
@ -1573,7 +1574,7 @@ final class RitualStore: RitualStoreProviding {
|
||||
// Trigger the completion check - this will set ritualNeedingRenewal
|
||||
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
|
||||
}
|
||||
|
||||
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(
|
||||
modelContext: container.mainContext,
|
||||
seedService: EmptySeedService(),
|
||||
settingsStore: TestFeedbackSettings()
|
||||
settingsStore: TestFeedbackSettings(),
|
||||
isRunningTests: true
|
||||
)
|
||||
|
||||
store.createQuickRitual()
|
||||
|
||||
@ -433,6 +433,7 @@ private func makeStore(now: @escaping () -> Date = Date.init) -> RitualStore {
|
||||
modelContext: container.mainContext,
|
||||
seedService: EmptySeedService(),
|
||||
settingsStore: TestFeedbackSettings(),
|
||||
isRunningTests: true,
|
||||
now: now
|
||||
)
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user