general: test redux

This commit is contained in:
Michael Freno
2026-01-15 15:37:42 -05:00
parent 80edfa8e06
commit 9c6bdaed6a
23 changed files with 2452 additions and 35 deletions

View File

@@ -13,7 +13,7 @@ import os.log
@MainActor
class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
@Published var timerEngine: TimerEngine?
private let settingsManager: SettingsManager = .shared
private let serviceContainer: ServiceContainer
private let windowManager: WindowManaging
private var updateManager: UpdateManager?
private var cancellables = Set<AnyCancellable>()
@@ -21,19 +21,21 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
// Logging manager
private let logger = LoggingManager.shared
// Smart Mode services
private var fullscreenService: FullscreenDetectionService?
private var idleService: IdleMonitoringService?
private var usageTrackingService: UsageTrackingService?
// Convenience accessor for settings
private var settingsManager: any SettingsProviding {
serviceContainer.settingsManager
}
override init() {
self.serviceContainer = ServiceContainer.shared
self.windowManager = WindowManager.shared
super.init()
}
/// Initializer for testing with injectable dependencies
init(windowManager: WindowManaging) {
init(serviceContainer: ServiceContainer, windowManager: WindowManaging) {
self.serviceContainer = serviceContainer
self.windowManager = windowManager
super.init()
}
@@ -46,9 +48,11 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
logger.configureLogging()
logger.appLogger.info("🚀 Application did finish launching")
timerEngine = TimerEngine(settingsManager: settingsManager)
// Get timer engine from service container
timerEngine = serviceContainer.timerEngine
setupSmartModeServices()
// Setup smart mode services through container
serviceContainer.setupSmartModeServices()
// Initialize update manager after onboarding is complete
if settingsManager.settings.hasCompletedOnboarding {
@@ -64,37 +68,21 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
}
}
private func setupSmartModeServices() {
fullscreenService = FullscreenDetectionService()
idleService = IdleMonitoringService(
idleThresholdMinutes: settingsManager.settings.smartMode.idleThresholdMinutes
)
usageTrackingService = UsageTrackingService(
resetThresholdMinutes: settingsManager.settings.smartMode.usageResetAfterMinutes
)
if let idleService = idleService {
usageTrackingService?.setupIdleMonitoring(idleService)
}
// Connect services to timer engine
timerEngine?.setupSmartMode(
fullscreenService: fullscreenService,
idleService: idleService
)
// Observe smart mode settings changes
settingsManager.$settings
// Note: Smart mode setup is now handled by ServiceContainer
// Keeping this method for settings change observation
private func observeSmartModeSettings() {
settingsManager.settingsPublisher
.map { $0.smartMode }
.removeDuplicates()
.sink { [weak self] smartMode in
self?.idleService?.updateThreshold(minutes: smartMode.idleThresholdMinutes)
self?.usageTrackingService?.updateResetThreshold(
guard let self = self else { return }
self.serviceContainer.idleService?.updateThreshold(minutes: smartMode.idleThresholdMinutes)
self.serviceContainer.usageTrackingService?.updateResetThreshold(
minutes: smartMode.usageResetAfterMinutes)
// Force state check when settings change to apply immediately
self?.fullscreenService?.forceUpdate()
self?.idleService?.forceUpdate()
self.serviceContainer.fullscreenService?.forceUpdate()
self.serviceContainer.idleService?.forceUpdate()
}
.store(in: &cancellables)
}
@@ -117,7 +105,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
}
private func observeSettingsChanges() {
settingsManager.$settings
settingsManager.settingsPublisher
.sink { [weak self] settings in
if settings.hasCompletedOnboarding && self?.hasStartedTimers == false {
self?.startTimers()
@@ -129,6 +117,9 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
}
}
.store(in: &cancellables)
// Also observe smart mode settings
observeSmartModeSettings()
}
func applicationWillTerminate(_ notification: Notification) {

View File

@@ -10,6 +10,8 @@ import SwiftUI
@main
struct GazeApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
// Note: SettingsManager.shared is used directly here for SwiftUI view updates
// AppDelegate uses ServiceContainer for dependency injection
@StateObject private var settingsManager = SettingsManager.shared
init() {

View File

@@ -0,0 +1,160 @@
//
// MockWindowManager.swift
// Gaze
//
// Mock implementation of WindowManaging for testing purposes.
//
import SwiftUI
/// Mock window manager that tracks window operations without creating actual windows.
/// Useful for unit testing UI flows and state management.
@MainActor
final class MockWindowManager: WindowManaging {
// MARK: - State Tracking
private(set) var isOverlayReminderVisible = false
private(set) var isSubtleReminderVisible = false
// MARK: - Operation History
struct WindowOperation {
let timestamp: Date
let operation: Operation
enum Operation {
case showOverlayReminder
case showSubtleReminder
case dismissOverlayReminder
case dismissSubtleReminder
case dismissAllReminders
case showSettings(initialTab: Int)
case showOnboarding
}
}
private(set) var operations: [WindowOperation] = []
// MARK: - Callbacks for Testing
var onShowOverlayReminder: (() -> Void)?
var onShowSubtleReminder: (() -> Void)?
var onDismissOverlayReminder: (() -> Void)?
var onDismissSubtleReminder: (() -> Void)?
var onShowSettings: ((Int) -> Void)?
var onShowOnboarding: (() -> Void)?
// MARK: - WindowManaging Implementation
func showReminderWindow<Content: View>(_ content: Content, windowType: ReminderWindowType) {
let operation: WindowOperation.Operation
switch windowType {
case .overlay:
isOverlayReminderVisible = true
operation = .showOverlayReminder
onShowOverlayReminder?()
case .subtle:
isSubtleReminderVisible = true
operation = .showSubtleReminder
onShowSubtleReminder?()
}
operations.append(WindowOperation(timestamp: Date(), operation: operation))
}
func dismissOverlayReminder() {
isOverlayReminderVisible = false
operations.append(WindowOperation(timestamp: Date(), operation: .dismissOverlayReminder))
onDismissOverlayReminder?()
}
func dismissSubtleReminder() {
isSubtleReminderVisible = false
operations.append(WindowOperation(timestamp: Date(), operation: .dismissSubtleReminder))
onDismissSubtleReminder?()
}
func dismissAllReminders() {
isOverlayReminderVisible = false
isSubtleReminderVisible = false
operations.append(WindowOperation(timestamp: Date(), operation: .dismissAllReminders))
onDismissOverlayReminder?()
onDismissSubtleReminder?()
}
func showSettings(settingsManager: any SettingsProviding, initialTab: Int) {
operations.append(WindowOperation(timestamp: Date(), operation: .showSettings(initialTab: initialTab)))
onShowSettings?(initialTab)
}
func showOnboarding(settingsManager: any SettingsProviding) {
operations.append(WindowOperation(timestamp: Date(), operation: .showOnboarding))
onShowOnboarding?()
}
// MARK: - Test Helpers
/// Resets all state for a fresh test
func reset() {
isOverlayReminderVisible = false
isSubtleReminderVisible = false
operations.removeAll()
onShowOverlayReminder = nil
onShowSubtleReminder = nil
onDismissOverlayReminder = nil
onDismissSubtleReminder = nil
onShowSettings = nil
onShowOnboarding = nil
}
/// Returns the number of times a specific operation was performed
func operationCount(_ operationType: WindowOperation.Operation) -> Int {
operations.filter { operation in
switch (operation.operation, operationType) {
case (.showOverlayReminder, .showOverlayReminder),
(.showSubtleReminder, .showSubtleReminder),
(.dismissOverlayReminder, .dismissOverlayReminder),
(.dismissSubtleReminder, .dismissSubtleReminder),
(.dismissAllReminders, .dismissAllReminders),
(.showOnboarding, .showOnboarding):
return true
case (.showSettings(let tab1), .showSettings(let tab2)):
return tab1 == tab2
default:
return false
}
}.count
}
/// Returns true if the operation was performed at least once
func didPerformOperation(_ operationType: WindowOperation.Operation) -> Bool {
operationCount(operationType) > 0
}
/// Returns the last operation performed, if any
var lastOperation: WindowOperation? {
operations.last
}
}
// MARK: - Equatable Conformance for Testing
extension MockWindowManager.WindowOperation.Operation: Equatable {
static func == (lhs: MockWindowManager.WindowOperation.Operation, rhs: MockWindowManager.WindowOperation.Operation) -> Bool {
switch (lhs, rhs) {
case (.showOverlayReminder, .showOverlayReminder),
(.showSubtleReminder, .showSubtleReminder),
(.dismissOverlayReminder, .dismissOverlayReminder),
(.dismissSubtleReminder, .dismissSubtleReminder),
(.dismissAllReminders, .dismissAllReminders),
(.showOnboarding, .showOnboarding):
return true
case (.showSettings(let tab1), .showSettings(let tab2)):
return tab1 == tab2
default:
return false
}
}
}