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>()
@@ -22,18 +22,20 @@ 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
}
}
}

View File

@@ -0,0 +1,97 @@
//
// AppDelegateTestabilityTests.swift
// GazeTests
//
// Tests demonstrating AppDelegate testability with dependency injection.
//
import XCTest
@testable import Gaze
@MainActor
final class AppDelegateTestabilityTests: XCTestCase {
var testEnv: TestEnvironment!
override func setUp() async throws {
testEnv = TestEnvironment(settings: .onboardingCompleted)
}
override func tearDown() async throws {
testEnv = nil
}
func testAppDelegateCreationWithMocks() {
let appDelegate = testEnv.createAppDelegate()
XCTAssertNotNil(appDelegate)
}
func testWindowManagerReceivesReminderEvents() async throws {
let appDelegate = testEnv.createAppDelegate()
// Simulate app launch
let notification = Notification(name: NSApplication.didFinishLaunchingNotification)
appDelegate.applicationDidFinishLaunching(notification)
// Give time for setup
try await Task.sleep(nanoseconds: 100_000_000) // 100ms
// Trigger a reminder through timer engine
if let timerEngine = appDelegate.timerEngine {
let timerId = TimerIdentifier.builtIn(.blink)
timerEngine.triggerReminder(for: timerId)
// Give time for reminder to propagate
try await Task.sleep(nanoseconds: 100_000_000) // 100ms
// Verify window manager received the show command
XCTAssertTrue(testEnv.windowManager.didPerformOperation(.showSubtleReminder))
} else {
XCTFail("TimerEngine not initialized")
}
}
func testSettingsChangesPropagate() async throws {
let appDelegate = testEnv.createAppDelegate()
// Change a setting
testEnv.settingsManager.settings.lookAwayTimer.enabled = false
// Give time for observation
try await Task.sleep(nanoseconds: 50_000_000) // 50ms
// Verify the change propagated
XCTAssertFalse(testEnv.settingsManager.settings.lookAwayTimer.enabled)
}
func testOpenSettingsUsesWindowManager() {
let appDelegate = testEnv.createAppDelegate()
appDelegate.openSettings(tab: 2)
// Give time for async dispatch
let expectation = XCTestExpectation(description: "Settings opened")
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
XCTAssertTrue(self.testEnv.windowManager.didPerformOperation(.showSettings(initialTab: 2)))
expectation.fulfill()
}
wait(for: [expectation], timeout: 1.0)
}
func testOpenOnboardingUsesWindowManager() {
let appDelegate = testEnv.createAppDelegate()
appDelegate.openOnboarding()
// Give time for async dispatch
let expectation = XCTestExpectation(description: "Onboarding opened")
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
XCTAssertTrue(self.testEnv.windowManager.didPerformOperation(.showOnboarding))
expectation.fulfill()
}
wait(for: [expectation], timeout: 1.0)
}
}

View File

@@ -0,0 +1,104 @@
//
// MockWindowManagerTests.swift
// GazeTests
//
// Tests for MockWindowManager functionality.
//
import SwiftUI
import XCTest
@testable import Gaze
@MainActor
final class MockWindowManagerTests: XCTestCase {
var windowManager: MockWindowManager!
override func setUp() async throws {
windowManager = MockWindowManager()
}
override func tearDown() async throws {
windowManager = nil
}
func testShowOverlayReminder() {
XCTAssertFalse(windowManager.isOverlayReminderVisible)
let view = Text("Test Overlay")
windowManager.showReminderWindow(view, windowType: .overlay)
XCTAssertTrue(windowManager.isOverlayReminderVisible)
XCTAssertTrue(windowManager.didPerformOperation(.showOverlayReminder))
}
func testShowSubtleReminder() {
XCTAssertFalse(windowManager.isSubtleReminderVisible)
let view = Text("Test Subtle")
windowManager.showReminderWindow(view, windowType: .subtle)
XCTAssertTrue(windowManager.isSubtleReminderVisible)
XCTAssertTrue(windowManager.didPerformOperation(.showSubtleReminder))
}
func testDismissOverlayReminder() {
let view = Text("Test")
windowManager.showReminderWindow(view, windowType: .overlay)
XCTAssertTrue(windowManager.isOverlayReminderVisible)
windowManager.dismissOverlayReminder()
XCTAssertFalse(windowManager.isOverlayReminderVisible)
XCTAssertTrue(windowManager.didPerformOperation(.dismissOverlayReminder))
}
func testDismissAllReminders() {
let view = Text("Test")
windowManager.showReminderWindow(view, windowType: .overlay)
windowManager.showReminderWindow(view, windowType: .subtle)
XCTAssertTrue(windowManager.isOverlayReminderVisible)
XCTAssertTrue(windowManager.isSubtleReminderVisible)
windowManager.dismissAllReminders()
XCTAssertFalse(windowManager.isOverlayReminderVisible)
XCTAssertFalse(windowManager.isSubtleReminderVisible)
}
func testOperationTracking() {
let view = Text("Test")
windowManager.showReminderWindow(view, windowType: .overlay)
windowManager.showReminderWindow(view, windowType: .overlay)
windowManager.dismissOverlayReminder()
XCTAssertEqual(windowManager.operationCount(.showOverlayReminder), 2)
XCTAssertEqual(windowManager.operationCount(.dismissOverlayReminder), 1)
}
func testCallbacks() {
var overlayShown = false
windowManager.onShowOverlayReminder = {
overlayShown = true
}
let view = Text("Test")
windowManager.showReminderWindow(view, windowType: .overlay)
XCTAssertTrue(overlayShown)
}
func testReset() {
let view = Text("Test")
windowManager.showReminderWindow(view, windowType: .overlay)
windowManager.onShowOverlayReminder = { }
windowManager.reset()
XCTAssertFalse(windowManager.isOverlayReminderVisible)
XCTAssertEqual(windowManager.operations.count, 0)
XCTAssertNil(windowManager.onShowOverlayReminder)
}
}

View File

@@ -0,0 +1,232 @@
//
// OnboardingNavigationTests.swift
// GazeTests
//
// Comprehensive tests for onboarding flow navigation.
//
import SwiftUI
import XCTest
@testable import Gaze
@MainActor
final class OnboardingNavigationTests: XCTestCase {
var testEnv: TestEnvironment!
override func setUp() async throws {
var settings = AppSettings.defaults
settings.hasCompletedOnboarding = false
testEnv = TestEnvironment(settings: settings)
}
override func tearDown() async throws {
testEnv = nil
}
// MARK: - Navigation Tests
func testOnboardingStartsAtWelcomePage() {
let onboarding = OnboardingContainerView(settingsManager: testEnv.settingsManager as! SettingsManager)
// Verify initial state
XCTAssertFalse(testEnv.settingsManager.settings.hasCompletedOnboarding)
}
func testNavigationForwardThroughAllPages() async throws {
var settings = testEnv.settingsManager.settings
// Simulate moving through pages
let pages = [
"Welcome", // 0
"LookAway", // 1
"Blink", // 2
"Posture", // 3
"General", // 4
"Completion" // 5
]
for (index, pageName) in pages.enumerated() {
// Verify we can track page progression
XCTAssertEqual(index, index, "Should be on page \(index): \(pageName)")
}
}
func testNavigationBackward() {
// Start from page 3 (Posture)
var currentPage = 3
// Navigate backward
currentPage -= 1
XCTAssertEqual(currentPage, 2, "Should navigate back to Blink page")
currentPage -= 1
XCTAssertEqual(currentPage, 1, "Should navigate back to LookAway page")
currentPage -= 1
XCTAssertEqual(currentPage, 0, "Should navigate back to Welcome page")
}
func testCannotNavigateBackFromWelcome() {
let currentPage = 0
// Should not be able to go below 0
XCTAssertEqual(currentPage, 0, "Should stay on Welcome page")
}
func testSettingsPersistDuringNavigation() {
// Configure lookaway timer
var config = testEnv.settingsManager.settings.lookAwayTimer
config.enabled = true
config.intervalSeconds = 1200
testEnv.settingsManager.updateTimerConfiguration(for: .lookAway, configuration: config)
// Verify settings persisted
let retrieved = testEnv.settingsManager.timerConfiguration(for: .lookAway)
XCTAssertTrue(retrieved.enabled)
XCTAssertEqual(retrieved.intervalSeconds, 1200)
// Configure blink timer
var blinkConfig = testEnv.settingsManager.settings.blinkTimer
blinkConfig.enabled = false
blinkConfig.intervalSeconds = 300
testEnv.settingsManager.updateTimerConfiguration(for: .blink, configuration: blinkConfig)
// Verify both settings persist
let lookAway = testEnv.settingsManager.timerConfiguration(for: .lookAway)
let blink = testEnv.settingsManager.timerConfiguration(for: .blink)
XCTAssertTrue(lookAway.enabled)
XCTAssertEqual(lookAway.intervalSeconds, 1200)
XCTAssertFalse(blink.enabled)
XCTAssertEqual(blink.intervalSeconds, 300)
}
func testOnboardingCompletion() {
// Start with onboarding incomplete
XCTAssertFalse(testEnv.settingsManager.settings.hasCompletedOnboarding)
// Complete onboarding
testEnv.settingsManager.settings.hasCompletedOnboarding = true
// Verify completion
XCTAssertTrue(testEnv.settingsManager.settings.hasCompletedOnboarding)
}
func testAllTimersConfiguredDuringOnboarding() {
// Configure all three built-in timers
var lookAwayConfig = testEnv.settingsManager.settings.lookAwayTimer
lookAwayConfig.enabled = true
lookAwayConfig.intervalSeconds = 1200
testEnv.settingsManager.updateTimerConfiguration(for: .lookAway, configuration: lookAwayConfig)
var blinkConfig = testEnv.settingsManager.settings.blinkTimer
blinkConfig.enabled = true
blinkConfig.intervalSeconds = 300
testEnv.settingsManager.updateTimerConfiguration(for: .blink, configuration: blinkConfig)
var postureConfig = testEnv.settingsManager.settings.postureTimer
postureConfig.enabled = true
postureConfig.intervalSeconds = 1800
testEnv.settingsManager.updateTimerConfiguration(for: .posture, configuration: postureConfig)
// Verify all configurations
let allConfigs = testEnv.settingsManager.allTimerConfigurations()
XCTAssertEqual(allConfigs[.lookAway]?.intervalSeconds, 1200)
XCTAssertEqual(allConfigs[.blink]?.intervalSeconds, 300)
XCTAssertEqual(allConfigs[.posture]?.intervalSeconds, 1800)
XCTAssertTrue(allConfigs[.lookAway]?.enabled ?? false)
XCTAssertTrue(allConfigs[.blink]?.enabled ?? false)
XCTAssertTrue(allConfigs[.posture]?.enabled ?? false)
}
func testNavigationWithPartialConfiguration() {
// Configure only some timers
var lookAwayConfig = testEnv.settingsManager.settings.lookAwayTimer
lookAwayConfig.enabled = true
testEnv.settingsManager.updateTimerConfiguration(for: .lookAway, configuration: lookAwayConfig)
var blinkConfig = testEnv.settingsManager.settings.blinkTimer
blinkConfig.enabled = false
testEnv.settingsManager.updateTimerConfiguration(for: .blink, configuration: blinkConfig)
// Should still be able to complete onboarding
testEnv.settingsManager.settings.hasCompletedOnboarding = true
XCTAssertTrue(testEnv.settingsManager.settings.hasCompletedOnboarding)
}
func testGeneralSettingsConfigurationDuringOnboarding() {
// Configure general settings
testEnv.settingsManager.settings.playSounds = true
testEnv.settingsManager.settings.launchAtLogin = true
XCTAssertTrue(testEnv.settingsManager.settings.playSounds)
XCTAssertTrue(testEnv.settingsManager.settings.launchAtLogin)
}
func testOnboardingFlowFromStartToFinish() {
// Complete simulation of onboarding flow
XCTAssertFalse(testEnv.settingsManager.settings.hasCompletedOnboarding)
// Page 0: Welcome - no configuration needed
// Page 1: LookAway Setup
var lookAwayConfig = testEnv.settingsManager.settings.lookAwayTimer
lookAwayConfig.enabled = true
lookAwayConfig.intervalSeconds = 1200
testEnv.settingsManager.updateTimerConfiguration(for: .lookAway, configuration: lookAwayConfig)
// Page 2: Blink Setup
var blinkConfig = testEnv.settingsManager.settings.blinkTimer
blinkConfig.enabled = true
blinkConfig.intervalSeconds = 300
testEnv.settingsManager.updateTimerConfiguration(for: .blink, configuration: blinkConfig)
// Page 3: Posture Setup
var postureConfig = testEnv.settingsManager.settings.postureTimer
postureConfig.enabled = false // User chooses to disable this one
testEnv.settingsManager.updateTimerConfiguration(for: .posture, configuration: postureConfig)
// Page 4: General Settings
testEnv.settingsManager.settings.playSounds = true
testEnv.settingsManager.settings.launchAtLogin = false
// Page 5: Completion - mark as done
testEnv.settingsManager.settings.hasCompletedOnboarding = true
// Verify final state
XCTAssertTrue(testEnv.settingsManager.settings.hasCompletedOnboarding)
let finalConfigs = testEnv.settingsManager.allTimerConfigurations()
XCTAssertTrue(finalConfigs[.lookAway]?.enabled ?? false)
XCTAssertTrue(finalConfigs[.blink]?.enabled ?? false)
XCTAssertFalse(finalConfigs[.posture]?.enabled ?? true)
XCTAssertTrue(testEnv.settingsManager.settings.playSounds)
XCTAssertFalse(testEnv.settingsManager.settings.launchAtLogin)
}
func testNavigatingBackPreservesSettings() {
// Configure on page 1
var lookAwayConfig = testEnv.settingsManager.settings.lookAwayTimer
lookAwayConfig.intervalSeconds = 1500
testEnv.settingsManager.updateTimerConfiguration(for: .lookAway, configuration: lookAwayConfig)
// Move forward to page 2
var blinkConfig = testEnv.settingsManager.settings.blinkTimer
blinkConfig.intervalSeconds = 250
testEnv.settingsManager.updateTimerConfiguration(for: .blink, configuration: blinkConfig)
// Navigate back to page 1
// Verify lookaway settings still exist
let lookAway = testEnv.settingsManager.timerConfiguration(for: .lookAway)
XCTAssertEqual(lookAway.intervalSeconds, 1500)
// Navigate forward again to page 2
// Verify blink settings still exist
let blink = testEnv.settingsManager.timerConfiguration(for: .blink)
XCTAssertEqual(blink.intervalSeconds, 250)
}
}

View File

@@ -0,0 +1,65 @@
//
// ServiceContainerTests.swift
// GazeTests
//
// Tests for the dependency injection infrastructure.
//
import XCTest
@testable import Gaze
@MainActor
final class ServiceContainerTests: XCTestCase {
func testProductionContainerCreation() {
let container = ServiceContainer.shared
XCTAssertFalse(container.isTestEnvironment)
XCTAssertNotNil(container.settingsManager)
XCTAssertNotNil(container.enforceModeService)
}
func testTestContainerCreation() {
let settings = AppSettings.onlyLookAwayEnabled
let container = ServiceContainer.forTesting(settings: settings)
XCTAssertTrue(container.isTestEnvironment)
XCTAssertEqual(container.settingsManager.settings.lookAwayTimer.enabled, true)
XCTAssertEqual(container.settingsManager.settings.blinkTimer.enabled, false)
}
func testTimerEngineCreation() {
let container = ServiceContainer.forTesting()
let timerEngine = container.timerEngine
XCTAssertNotNil(timerEngine)
// Second access should return the same instance
XCTAssertTrue(container.timerEngine === timerEngine)
}
func testCustomTimerEngineInjection() {
let container = ServiceContainer.forTesting()
let mockSettings = EnhancedMockSettingsManager(settings: .shortIntervals)
let customEngine = TimerEngine(
settingsManager: mockSettings,
timeProvider: MockTimeProvider()
)
container.setTimerEngine(customEngine)
XCTAssertTrue(container.timerEngine === customEngine)
}
func testContainerReset() {
let container = ServiceContainer.forTesting()
// Access timer engine to create it
_ = container.timerEngine
// Reset should clear the timer engine
container.reset()
// Accessing again should create a new instance
let newEngine = container.timerEngine
XCTAssertNotNil(newEngine)
}
}

View File

@@ -0,0 +1,135 @@
//
// EnforceModeServiceTests.swift
// GazeTests
//
// Unit tests for EnforceModeService.
//
import XCTest
@testable import Gaze
@MainActor
final class EnforceModeServiceTests: XCTestCase {
var service: EnforceModeService!
override func setUp() async throws {
service = EnforceModeService.shared
}
override func tearDown() async throws {
service.disableEnforceMode()
service = nil
}
// MARK: - Initialization Tests
func testServiceInitialization() {
XCTAssertNotNil(service)
}
func testInitialState() {
XCTAssertFalse(service.isEnforceModeEnabled)
XCTAssertFalse(service.isCameraActive)
XCTAssertFalse(service.userCompliedWithBreak)
}
// MARK: - Enable/Disable Tests
func testEnableEnforceMode() async {
await service.enableEnforceMode()
// May or may not be enabled depending on camera permissions
// Just verify the method doesn't crash
XCTAssertNotNil(service)
}
func testDisableEnforceMode() {
service.disableEnforceMode()
XCTAssertFalse(service.isEnforceModeEnabled)
XCTAssertFalse(service.isCameraActive)
}
func testEnableDisableCycle() async {
await service.enableEnforceMode()
service.disableEnforceMode()
XCTAssertFalse(service.isEnforceModeEnabled)
}
// MARK: - Timer Engine Integration Tests
func testSetTimerEngine() {
let testEnv = TestEnvironment()
let timerEngine = testEnv.container.timerEngine
service.setTimerEngine(timerEngine)
// Should not crash
XCTAssertNotNil(service)
}
// MARK: - Should Enforce Break Tests
func testShouldEnforceBreakWhenDisabled() {
service.disableEnforceMode()
let shouldEnforce = service.shouldEnforceBreak(for: .builtIn(.lookAway))
XCTAssertFalse(shouldEnforce)
}
// MARK: - Camera Tests
func testStopCamera() {
service.stopCamera()
XCTAssertFalse(service.isCameraActive)
}
// MARK: - Compliance Tests
func testCheckUserCompliance() {
service.checkUserCompliance()
// Should not crash
XCTAssertNotNil(service)
}
func testHandleReminderDismissed() {
service.handleReminderDismissed()
// Should not crash
XCTAssertNotNil(service)
}
// MARK: - Test Mode Tests
func testStartTestMode() async {
await service.startTestMode()
XCTAssertTrue(service.isTestMode)
}
func testStopTestMode() {
service.stopTestMode()
XCTAssertFalse(service.isTestMode)
}
func testTestModeCycle() async {
await service.startTestMode()
XCTAssertTrue(service.isTestMode)
service.stopTestMode()
XCTAssertFalse(service.isTestMode)
}
// MARK: - Protocol Conformance Tests
func testConformsToEnforceModeProviding() {
let providing: EnforceModeProviding = service
XCTAssertNotNil(providing)
XCTAssertFalse(providing.isEnforceModeEnabled)
}
}

View File

@@ -0,0 +1,68 @@
//
// FullscreenDetectionServiceTests.swift
// GazeTests
//
// Unit tests for FullscreenDetectionService.
//
import Combine
import XCTest
@testable import Gaze
@MainActor
final class FullscreenDetectionServiceTests: XCTestCase {
var service: FullscreenDetectionService!
var cancellables: Set<AnyCancellable>!
override func setUp() async throws {
service = FullscreenDetectionService()
cancellables = []
}
override func tearDown() async throws {
cancellables = nil
service = nil
}
// MARK: - Initialization Tests
func testServiceInitialization() {
XCTAssertNotNil(service)
}
func testInitialFullscreenState() {
// Initially should not be in fullscreen (unless actually in fullscreen)
XCTAssertNotNil(service.isFullscreenActive)
}
// MARK: - Publisher Tests
func testFullscreenStatePublisher() async throws {
let expectation = XCTestExpectation(description: "Fullscreen state published")
service.$isFullscreenActive
.sink { isFullscreen in
expectation.fulfill()
}
.store(in: &cancellables)
await fulfillment(of: [expectation], timeout: 0.1)
}
// MARK: - Force Update Tests
func testForceUpdate() {
// Should not crash
service.forceUpdate()
XCTAssertNotNil(service.isFullscreenActive)
}
// MARK: - Protocol Conformance Tests
func testConformsToFullscreenDetectionProviding() {
let providing: FullscreenDetectionProviding = service
XCTAssertNotNil(providing)
XCTAssertNotNil(providing.isFullscreenActive)
}
}

View File

@@ -0,0 +1,89 @@
//
// IdleMonitoringServiceTests.swift
// GazeTests
//
// Unit tests for IdleMonitoringService.
//
import Combine
import XCTest
@testable import Gaze
@MainActor
final class IdleMonitoringServiceTests: XCTestCase {
var service: IdleMonitoringService!
var cancellables: Set<AnyCancellable>!
override func setUp() async throws {
service = IdleMonitoringService(idleThresholdMinutes: 5)
cancellables = []
}
override func tearDown() async throws {
cancellables = nil
service = nil
}
// MARK: - Initialization Tests
func testServiceInitialization() {
XCTAssertNotNil(service)
}
func testInitialIdleState() {
// Initially should not be idle
XCTAssertFalse(service.isIdle)
}
func testInitializationWithCustomThreshold() {
let customService = IdleMonitoringService(idleThresholdMinutes: 10)
XCTAssertNotNil(customService)
}
// MARK: - Threshold Tests
func testUpdateThreshold() {
service.updateThreshold(minutes: 15)
// Should not crash
XCTAssertNotNil(service)
}
func testUpdateThresholdMultipleTimes() {
service.updateThreshold(minutes: 5)
service.updateThreshold(minutes: 10)
service.updateThreshold(minutes: 3)
XCTAssertNotNil(service)
}
// MARK: - Publisher Tests
func testIdleStatePublisher() async throws {
let expectation = XCTestExpectation(description: "Idle state published")
service.$isIdle
.sink { isIdle in
expectation.fulfill()
}
.store(in: &cancellables)
await fulfillment(of: [expectation], timeout: 0.1)
}
// MARK: - Force Update Tests
func testForceUpdate() {
service.forceUpdate()
XCTAssertNotNil(service.isIdle)
}
// MARK: - Protocol Conformance Tests
func testConformsToIdleMonitoringProviding() {
let providing: IdleMonitoringProviding = service
XCTAssertNotNil(providing)
XCTAssertNotNil(providing.isIdle)
}
}

View File

@@ -0,0 +1,61 @@
//
// LoggingManagerTests.swift
// GazeTests
//
// Unit tests for LoggingManager.
//
import os.log
import XCTest
@testable import Gaze
final class LoggingManagerTests: XCTestCase {
var loggingManager: LoggingManager!
override func setUp() {
loggingManager = LoggingManager.shared
}
override func tearDown() {
loggingManager = nil
}
// MARK: - Initialization Tests
func testLoggingManagerInitialization() {
XCTAssertNotNil(loggingManager)
}
func testLoggersExist() {
XCTAssertNotNil(loggingManager.appLogger)
XCTAssertNotNil(loggingManager.timerLogger)
XCTAssertNotNil(loggingManager.systemLogger)
}
// MARK: - Configuration Tests
func testConfigureLogging() {
// Should not crash
loggingManager.configureLogging()
XCTAssertNotNil(loggingManager)
}
// MARK: - Logger Usage Tests
func testAppLoggerLogging() {
// Should not crash
loggingManager.appLogger.info("Test app log")
XCTAssertNotNil(loggingManager.appLogger)
}
func testTimerLoggerLogging() {
loggingManager.timerLogger.info("Test timer log")
XCTAssertNotNil(loggingManager.timerLogger)
}
func testSystemLoggerLogging() {
loggingManager.systemLogger.info("Test system log")
XCTAssertNotNil(loggingManager.systemLogger)
}
}

View File

@@ -0,0 +1,187 @@
//
// SettingsManagerTests.swift
// GazeTests
//
// Unit tests for SettingsManager service.
//
import Combine
import XCTest
@testable import Gaze
@MainActor
final class SettingsManagerTests: XCTestCase {
var settingsManager: SettingsManager!
var cancellables: Set<AnyCancellable>!
override func setUp() async throws {
settingsManager = SettingsManager.shared
cancellables = []
// Reset to defaults for testing
settingsManager.resetToDefaults()
}
override func tearDown() async throws {
cancellables = nil
settingsManager = nil
}
// MARK: - Initialization Tests
func testSettingsManagerInitialization() {
XCTAssertNotNil(settingsManager)
XCTAssertNotNil(settingsManager.settings)
}
func testDefaultSettingsValues() {
let defaults = AppSettings.defaults
XCTAssertTrue(defaults.lookAwayTimer.enabled)
XCTAssertTrue(defaults.blinkTimer.enabled)
XCTAssertTrue(defaults.postureTimer.enabled)
XCTAssertFalse(defaults.hasCompletedOnboarding)
}
// MARK: - Timer Configuration Tests
func testGetTimerConfiguration() {
let lookAwayConfig = settingsManager.timerConfiguration(for: .lookAway)
XCTAssertNotNil(lookAwayConfig)
XCTAssertTrue(lookAwayConfig.enabled)
}
func testUpdateTimerConfiguration() {
var config = settingsManager.timerConfiguration(for: .lookAway)
config.intervalSeconds = 1500
config.enabled = false
settingsManager.updateTimerConfiguration(for: .lookAway, configuration: config)
let updated = settingsManager.timerConfiguration(for: .lookAway)
XCTAssertEqual(updated.intervalSeconds, 1500)
XCTAssertFalse(updated.enabled)
}
func testAllTimerConfigurations() {
let allConfigs = settingsManager.allTimerConfigurations()
XCTAssertEqual(allConfigs.count, 3)
XCTAssertNotNil(allConfigs[.lookAway])
XCTAssertNotNil(allConfigs[.blink])
XCTAssertNotNil(allConfigs[.posture])
}
func testUpdateMultipleTimerConfigurations() {
var lookAway = settingsManager.timerConfiguration(for: .lookAway)
lookAway.intervalSeconds = 1000
settingsManager.updateTimerConfiguration(for: .lookAway, configuration: lookAway)
var blink = settingsManager.timerConfiguration(for: .blink)
blink.intervalSeconds = 250
settingsManager.updateTimerConfiguration(for: .blink, configuration: blink)
XCTAssertEqual(settingsManager.timerConfiguration(for: .lookAway).intervalSeconds, 1000)
XCTAssertEqual(settingsManager.timerConfiguration(for: .blink).intervalSeconds, 250)
}
// MARK: - Settings Publisher Tests
func testSettingsPublisherEmitsChanges() async throws {
let expectation = XCTestExpectation(description: "Settings changed")
var receivedSettings: AppSettings?
settingsManager.$settings
.dropFirst() // Skip initial value
.sink { settings in
receivedSettings = settings
expectation.fulfill()
}
.store(in: &cancellables)
// Trigger change
settingsManager.settings.playSounds = !settingsManager.settings.playSounds
await fulfillment(of: [expectation], timeout: 1.0)
XCTAssertNotNil(receivedSettings)
}
// MARK: - Save/Load Tests
func testSave() {
settingsManager.settings.playSounds = false
settingsManager.save()
// Save is debounced, so just verify it doesn't crash
XCTAssertFalse(settingsManager.settings.playSounds)
}
func testSaveImmediately() {
settingsManager.settings.launchAtLogin = true
settingsManager.saveImmediately()
// Verify the setting persisted
XCTAssertTrue(settingsManager.settings.launchAtLogin)
}
func testLoad() {
// Load should restore settings from UserDefaults
settingsManager.load()
XCTAssertNotNil(settingsManager.settings)
}
// MARK: - Reset Tests
func testResetToDefaults() {
// Modify settings
settingsManager.settings.playSounds = false
settingsManager.settings.launchAtLogin = true
var config = settingsManager.timerConfiguration(for: .lookAway)
config.intervalSeconds = 5000
settingsManager.updateTimerConfiguration(for: .lookAway, configuration: config)
// Reset
settingsManager.resetToDefaults()
// Verify reset to defaults
let defaults = AppSettings.defaults
XCTAssertEqual(settingsManager.settings.playSounds, defaults.playSounds)
XCTAssertEqual(settingsManager.settings.launchAtLogin, defaults.launchAtLogin)
}
// MARK: - Onboarding Tests
func testOnboardingCompletion() {
XCTAssertFalse(settingsManager.settings.hasCompletedOnboarding)
settingsManager.settings.hasCompletedOnboarding = true
XCTAssertTrue(settingsManager.settings.hasCompletedOnboarding)
}
// MARK: - General Settings Tests
func testPlaySoundsToggle() {
let initial = settingsManager.settings.playSounds
settingsManager.settings.playSounds = !initial
XCTAssertNotEqual(settingsManager.settings.playSounds, initial)
}
func testLaunchAtLoginToggle() {
let initial = settingsManager.settings.launchAtLogin
settingsManager.settings.launchAtLogin = !initial
XCTAssertNotEqual(settingsManager.settings.launchAtLogin, initial)
}
// MARK: - Smart Mode Settings Tests
func testSmartModeSettings() {
settingsManager.settings.smartMode.autoPauseOnFullscreen = true
settingsManager.settings.smartMode.autoPauseOnIdle = true
settingsManager.settings.smartMode.idleThresholdMinutes = 10
XCTAssertTrue(settingsManager.settings.smartMode.autoPauseOnFullscreen)
XCTAssertTrue(settingsManager.settings.smartMode.autoPauseOnIdle)
XCTAssertEqual(settingsManager.settings.smartMode.idleThresholdMinutes, 10)
}
}

View File

@@ -0,0 +1,302 @@
//
// TimerEngineTests.swift
// GazeTests
//
// Unit tests for TimerEngine service.
//
import Combine
import XCTest
@testable import Gaze
@MainActor
final class TimerEngineTests: XCTestCase {
var testEnv: TestEnvironment!
var timerEngine: TimerEngine!
var cancellables: Set<AnyCancellable>!
override func setUp() async throws {
testEnv = TestEnvironment(settings: .defaults)
timerEngine = testEnv.container.timerEngine
cancellables = []
}
override func tearDown() async throws {
timerEngine?.stop()
cancellables = nil
timerEngine = nil
testEnv = nil
}
// MARK: - Initialization Tests
func testTimerEngineInitialization() {
XCTAssertNotNil(timerEngine)
XCTAssertEqual(timerEngine.timerStates.count, 0)
XCTAssertNil(timerEngine.activeReminder)
}
func testTimerEngineWithCustomTimeProvider() {
let timeProvider = MockTimeProvider()
let engine = TimerEngine(
settingsManager: testEnv.settingsManager,
timeProvider: timeProvider
)
XCTAssertNotNil(engine)
}
// MARK: - Start/Stop Tests
func testStartTimers() {
timerEngine.start()
// Should create timer states for enabled timers
XCTAssertGreaterThan(timerEngine.timerStates.count, 0)
}
func testStopTimers() {
timerEngine.start()
let initialCount = timerEngine.timerStates.count
XCTAssertGreaterThan(initialCount, 0)
timerEngine.stop()
// Timers should be cleared
XCTAssertEqual(timerEngine.timerStates.count, 0)
}
func testRestartTimers() {
timerEngine.start()
let firstCount = timerEngine.timerStates.count
timerEngine.stop()
XCTAssertEqual(timerEngine.timerStates.count, 0)
timerEngine.start()
let secondCount = timerEngine.timerStates.count
XCTAssertEqual(firstCount, secondCount)
}
// MARK: - Pause/Resume Tests
func testPauseAllTimers() {
timerEngine.start()
timerEngine.pause()
for (_, state) in timerEngine.timerStates {
XCTAssertTrue(state.isPaused)
}
}
func testResumeAllTimers() {
timerEngine.start()
timerEngine.pause()
timerEngine.resume()
for (_, state) in timerEngine.timerStates {
XCTAssertFalse(state.isPaused)
}
}
func testPauseSpecificTimer() {
timerEngine.start()
guard let firstTimer = timerEngine.timerStates.keys.first else {
XCTFail("No timers available")
return
}
timerEngine.pauseTimer(identifier: firstTimer)
let state = timerEngine.timerStates[firstTimer]
XCTAssertTrue(state?.isPaused ?? false)
}
func testResumeSpecificTimer() {
timerEngine.start()
guard let firstTimer = timerEngine.timerStates.keys.first else {
XCTFail("No timers available")
return
}
timerEngine.pauseTimer(identifier: firstTimer)
XCTAssertTrue(timerEngine.isTimerPaused(firstTimer))
timerEngine.resumeTimer(identifier: firstTimer)
XCTAssertFalse(timerEngine.isTimerPaused(firstTimer))
}
// MARK: - Skip Tests
func testSkipNext() {
timerEngine.start()
guard let firstTimer = timerEngine.timerStates.keys.first else {
XCTFail("No timers available")
return
}
timerEngine.skipNext(identifier: firstTimer)
// Timer should be reset
XCTAssertNotNil(timerEngine.timerStates[firstTimer])
}
// MARK: - Reminder Tests
func testTriggerReminder() async throws {
timerEngine.start()
guard let firstTimer = timerEngine.timerStates.keys.first else {
XCTFail("No timers available")
return
}
timerEngine.triggerReminder(for: firstTimer)
// Give time for async operations
try await Task.sleep(nanoseconds: 50_000_000)
XCTAssertNotNil(timerEngine.activeReminder)
}
func testDismissReminder() {
timerEngine.start()
guard let firstTimer = timerEngine.timerStates.keys.first else {
XCTFail("No timers available")
return
}
timerEngine.triggerReminder(for: firstTimer)
XCTAssertNotNil(timerEngine.activeReminder)
timerEngine.dismissReminder()
XCTAssertNil(timerEngine.activeReminder)
}
// MARK: - Time Remaining Tests
func testGetTimeRemaining() {
timerEngine.start()
guard let firstTimer = timerEngine.timerStates.keys.first else {
XCTFail("No timers available")
return
}
let remaining = timerEngine.getTimeRemaining(for: firstTimer)
XCTAssertGreaterThan(remaining, 0)
}
func testGetFormattedTimeRemaining() {
timerEngine.start()
guard let firstTimer = timerEngine.timerStates.keys.first else {
XCTFail("No timers available")
return
}
let formatted = timerEngine.getFormattedTimeRemaining(for: firstTimer)
XCTAssertFalse(formatted.isEmpty)
XCTAssertTrue(formatted.contains(":"))
}
// MARK: - Timer State Publisher Tests
func testTimerStatesPublisher() async throws {
let expectation = XCTestExpectation(description: "Timer states changed")
timerEngine.$timerStates
.dropFirst()
.sink { states in
if !states.isEmpty {
expectation.fulfill()
}
}
.store(in: &cancellables)
timerEngine.start()
await fulfillment(of: [expectation], timeout: 1.0)
}
func testActiveReminderPublisher() async throws {
let expectation = XCTestExpectation(description: "Active reminder changed")
timerEngine.$activeReminder
.dropFirst()
.sink { reminder in
if reminder != nil {
expectation.fulfill()
}
}
.store(in: &cancellables)
timerEngine.start()
guard let firstTimer = timerEngine.timerStates.keys.first else {
XCTFail("No timers available")
return
}
timerEngine.triggerReminder(for: firstTimer)
await fulfillment(of: [expectation], timeout: 1.0)
}
// MARK: - System Sleep/Wake Tests
func testHandleSystemSleep() {
timerEngine.start()
let statesBefore = timerEngine.timerStates.count
timerEngine.handleSystemSleep()
// States should still exist
XCTAssertEqual(timerEngine.timerStates.count, statesBefore)
}
func testHandleSystemWake() {
timerEngine.start()
timerEngine.handleSystemSleep()
timerEngine.handleSystemWake()
// Should handle wake event without crashing
XCTAssertGreaterThan(timerEngine.timerStates.count, 0)
}
// MARK: - Disabled Timer Tests
func testDisabledTimersNotInitialized() {
var settings = AppSettings.defaults
settings.lookAwayTimer.enabled = false
settings.blinkTimer.enabled = false
settings.postureTimer.enabled = false
let settingsManager = EnhancedMockSettingsManager(settings: settings)
let engine = TimerEngine(settingsManager: settingsManager)
engine.start()
XCTAssertEqual(engine.timerStates.count, 0)
}
func testPartiallyEnabledTimers() {
var settings = AppSettings.defaults
settings.lookAwayTimer.enabled = true
settings.blinkTimer.enabled = false
settings.postureTimer.enabled = false
let settingsManager = EnhancedMockSettingsManager(settings: settings)
let engine = TimerEngine(settingsManager: settingsManager)
engine.start()
XCTAssertEqual(engine.timerStates.count, 1)
}
}

View File

@@ -0,0 +1,62 @@
//
// UsageTrackingServiceTests.swift
// GazeTests
//
// Unit tests for UsageTrackingService.
//
import XCTest
@testable import Gaze
@MainActor
final class UsageTrackingServiceTests: XCTestCase {
var service: UsageTrackingService!
override func setUp() async throws {
service = UsageTrackingService(resetThresholdMinutes: 60)
}
override func tearDown() async throws {
service = nil
}
// MARK: - Initialization Tests
func testServiceInitialization() {
XCTAssertNotNil(service)
}
func testInitializationWithCustomThreshold() {
let customService = UsageTrackingService(resetThresholdMinutes: 120)
XCTAssertNotNil(customService)
}
// MARK: - Threshold Tests
func testUpdateResetThreshold() {
service.updateResetThreshold(minutes: 90)
// Should not crash
XCTAssertNotNil(service)
}
func testUpdateThresholdMultipleTimes() {
service.updateResetThreshold(minutes: 30)
service.updateResetThreshold(minutes: 60)
service.updateResetThreshold(minutes: 120)
XCTAssertNotNil(service)
}
// MARK: - Idle Monitoring Integration Tests
func testSetupIdleMonitoring() {
let idleService = IdleMonitoringService(idleThresholdMinutes: 5)
service.setupIdleMonitoring(idleService)
// Should not crash
XCTAssertNotNil(service)
}
}

View File

@@ -0,0 +1,129 @@
//
// WindowManagerTests.swift
// GazeTests
//
// Unit tests for WindowManager service.
//
import SwiftUI
import XCTest
@testable import Gaze
@MainActor
final class WindowManagerTests: XCTestCase {
var windowManager: WindowManager!
override func setUp() async throws {
windowManager = WindowManager.shared
}
override func tearDown() async throws {
windowManager.dismissAllReminders()
windowManager = nil
}
// MARK: - Initialization Tests
func testWindowManagerInitialization() {
XCTAssertNotNil(windowManager)
}
func testInitialState() {
XCTAssertFalse(windowManager.isOverlayReminderVisible)
XCTAssertFalse(windowManager.isSubtleReminderVisible)
}
// MARK: - Window Visibility Tests
func testOverlayReminderVisibility() {
XCTAssertFalse(windowManager.isOverlayReminderVisible)
let view = Text("Test Overlay")
windowManager.showReminderWindow(view, windowType: .overlay)
XCTAssertTrue(windowManager.isOverlayReminderVisible)
windowManager.dismissOverlayReminder()
XCTAssertFalse(windowManager.isOverlayReminderVisible)
}
func testSubtleReminderVisibility() {
XCTAssertFalse(windowManager.isSubtleReminderVisible)
let view = Text("Test Subtle")
windowManager.showReminderWindow(view, windowType: .subtle)
XCTAssertTrue(windowManager.isSubtleReminderVisible)
windowManager.dismissSubtleReminder()
XCTAssertFalse(windowManager.isSubtleReminderVisible)
}
// MARK: - Multiple Window Tests
func testShowBothWindowTypes() {
let overlayView = Text("Overlay")
let subtleView = Text("Subtle")
windowManager.showReminderWindow(overlayView, windowType: .overlay)
windowManager.showReminderWindow(subtleView, windowType: .subtle)
XCTAssertTrue(windowManager.isOverlayReminderVisible)
XCTAssertTrue(windowManager.isSubtleReminderVisible)
}
func testDismissAllReminders() {
let overlayView = Text("Overlay")
let subtleView = Text("Subtle")
windowManager.showReminderWindow(overlayView, windowType: .overlay)
windowManager.showReminderWindow(subtleView, windowType: .subtle)
windowManager.dismissAllReminders()
XCTAssertFalse(windowManager.isOverlayReminderVisible)
XCTAssertFalse(windowManager.isSubtleReminderVisible)
}
// MARK: - Window Replacement Tests
func testReplaceOverlayWindow() {
let firstView = Text("First Overlay")
let secondView = Text("Second Overlay")
windowManager.showReminderWindow(firstView, windowType: .overlay)
XCTAssertTrue(windowManager.isOverlayReminderVisible)
// Showing a new overlay should replace the old one
windowManager.showReminderWindow(secondView, windowType: .overlay)
XCTAssertTrue(windowManager.isOverlayReminderVisible)
}
func testReplaceSubtleWindow() {
let firstView = Text("First Subtle")
let secondView = Text("Second Subtle")
windowManager.showReminderWindow(firstView, windowType: .subtle)
XCTAssertTrue(windowManager.isSubtleReminderVisible)
windowManager.showReminderWindow(secondView, windowType: .subtle)
XCTAssertTrue(windowManager.isSubtleReminderVisible)
}
// MARK: - Integration with Settings Tests
func testShowSettingsWithSettingsManager() {
let settingsManager = SettingsManager.shared
// Should not crash
windowManager.showSettings(settingsManager: settingsManager, initialTab: 0)
}
func testShowOnboardingWithSettingsManager() {
let settingsManager = SettingsManager.shared
// Should not crash
windowManager.showOnboarding(settingsManager: settingsManager)
}
}

259
GazeTests/TestHelpers.swift Normal file
View File

@@ -0,0 +1,259 @@
//
// TestHelpers.swift
// GazeTests
//
// Test helpers and utilities for unit testing.
//
import Foundation
import XCTest
@testable import Gaze
// MARK: - Enhanced MockSettingsManager
/// Enhanced mock settings manager with full control over state
@MainActor
final class EnhancedMockSettingsManager: ObservableObject, SettingsProviding {
@Published var settings: AppSettings
var settingsPublisher: Published<AppSettings>.Publisher {
$settings
}
private let timerConfigKeyPaths: [TimerType: WritableKeyPath<AppSettings, TimerConfiguration>] = [
.lookAway: \.lookAwayTimer,
.blink: \.blinkTimer,
.posture: \.postureTimer,
]
// Track method calls for verification
private(set) var saveCallCount = 0
private(set) var saveImmediatelyCallCount = 0
private(set) var loadCallCount = 0
private(set) var resetToDefaultsCallCount = 0
init(settings: AppSettings = .defaults) {
self.settings = settings
}
func timerConfiguration(for type: TimerType) -> TimerConfiguration {
guard let keyPath = timerConfigKeyPaths[type] else {
preconditionFailure("Unknown timer type: \(type)")
}
return settings[keyPath: keyPath]
}
func updateTimerConfiguration(for type: TimerType, configuration: TimerConfiguration) {
guard let keyPath = timerConfigKeyPaths[type] else {
preconditionFailure("Unknown timer type: \(type)")
}
settings[keyPath: keyPath] = configuration
}
func allTimerConfigurations() -> [TimerType: TimerConfiguration] {
var configs: [TimerType: TimerConfiguration] = [:]
for (type, keyPath) in timerConfigKeyPaths {
configs[type] = settings[keyPath: keyPath]
}
return configs
}
func save() {
saveCallCount += 1
}
func saveImmediately() {
saveImmediatelyCallCount += 1
}
func load() {
loadCallCount += 1
}
func resetToDefaults() {
resetToDefaultsCallCount += 1
settings = .defaults
}
// Test helpers
func reset() {
saveCallCount = 0
saveImmediatelyCallCount = 0
loadCallCount = 0
resetToDefaultsCallCount = 0
settings = .defaults
}
}
// MARK: - Mock Smart Mode Services
@MainActor
final class MockFullscreenDetectionService: ObservableObject, FullscreenDetectionProviding {
@Published var isFullscreenActive: Bool = false
var isFullscreenActivePublisher: Published<Bool>.Publisher {
$isFullscreenActive
}
private(set) var forceUpdateCallCount = 0
func forceUpdate() {
forceUpdateCallCount += 1
}
func simulateFullscreen(_ active: Bool) {
isFullscreenActive = active
}
}
@MainActor
final class MockIdleMonitoringService: ObservableObject, IdleMonitoringProviding {
@Published var isIdle: Bool = false
var isIdlePublisher: Published<Bool>.Publisher {
$isIdle
}
private(set) var thresholdMinutes: Int = 5
private(set) var forceUpdateCallCount = 0
func updateThreshold(minutes: Int) {
thresholdMinutes = minutes
}
func forceUpdate() {
forceUpdateCallCount += 1
}
func simulateIdle(_ idle: Bool) {
isIdle = idle
}
}
// MARK: - Test Fixtures
extension AppSettings {
/// Settings with all timers disabled
static var allTimersDisabled: AppSettings {
var settings = AppSettings.defaults
settings.lookAwayTimer.enabled = false
settings.blinkTimer.enabled = false
settings.postureTimer.enabled = false
return settings
}
/// Settings with only lookAway timer enabled
static var onlyLookAwayEnabled: AppSettings {
var settings = AppSettings.defaults
settings.lookAwayTimer.enabled = true
settings.blinkTimer.enabled = false
settings.postureTimer.enabled = false
return settings
}
/// Settings with short intervals for testing
static var shortIntervals: AppSettings {
var settings = AppSettings.defaults
settings.lookAwayTimer.intervalSeconds = 5
settings.blinkTimer.intervalSeconds = 3
settings.postureTimer.intervalSeconds = 7
return settings
}
/// Settings with onboarding completed
static var onboardingCompleted: AppSettings {
var settings = AppSettings.defaults
settings.hasCompletedOnboarding = true
return settings
}
/// Settings with smart mode fully enabled
static var smartModeEnabled: AppSettings {
var settings = AppSettings.defaults
settings.smartMode.autoPauseOnFullscreen = true
settings.smartMode.autoPauseOnIdle = true
settings.smartMode.idleThresholdMinutes = 5
return settings
}
}
// MARK: - Test Utilities
/// Creates a service container configured for testing
@MainActor
func createTestContainer(
settings: AppSettings = .defaults
) -> ServiceContainer {
return ServiceContainer.forTesting(settings: settings)
}
/// Creates a complete test environment with all mocks
@MainActor
struct TestEnvironment {
let container: ServiceContainer
let windowManager: MockWindowManager
let settingsManager: EnhancedMockSettingsManager
let timeProvider: MockTimeProvider
init(settings: AppSettings = .defaults) {
self.settingsManager = EnhancedMockSettingsManager(settings: settings)
self.container = ServiceContainer(settingsManager: settingsManager)
self.windowManager = MockWindowManager()
self.timeProvider = MockTimeProvider()
}
/// Creates an AppDelegate with all test dependencies
func createAppDelegate() -> AppDelegate {
return AppDelegate(serviceContainer: container, windowManager: windowManager)
}
/// Resets all mock state
func reset() {
windowManager.reset()
settingsManager.reset()
}
}
// MARK: - XCTest Extensions
extension XCTestCase {
/// Waits for a condition to be true with timeout
@MainActor
func waitFor(
_ condition: @escaping () -> Bool,
timeout: TimeInterval = 1.0,
message: String = "Condition not met"
) async throws {
let deadline = Date().addingTimeInterval(timeout)
while !condition() {
if Date() > deadline {
XCTFail(message)
return
}
try await Task.sleep(nanoseconds: 10_000_000) // 10ms
}
}
/// Waits for a published value to change
@MainActor
func waitForPublisher<T: Equatable>(
_ publisher: Published<T>.Publisher,
toEqual expectedValue: T,
timeout: TimeInterval = 1.0
) async throws {
let expectation = XCTestExpectation(description: "Publisher value changed")
var cancellable: AnyCancellable?
cancellable = publisher.sink { value in
if value == expectedValue {
expectation.fulfill()
}
}
await fulfillment(of: [expectation], timeout: timeout)
cancellable?.cancel()
}
}
// MARK: - Import Statement for Combine
import Combine

View File

@@ -0,0 +1,112 @@
//
// TimerEngineTestabilityTests.swift
// GazeTests
//
// Tests demonstrating TimerEngine testability with dependency injection.
//
import Combine
import XCTest
@testable import Gaze
@MainActor
final class TimerEngineTestabilityTests: XCTestCase {
var testEnv: TestEnvironment!
var cancellables: Set<AnyCancellable>!
override func setUp() async throws {
testEnv = TestEnvironment(settings: .shortIntervals)
cancellables = []
}
override func tearDown() async throws {
cancellables = nil
testEnv = nil
}
func testTimerEngineCreationWithMocks() {
let timeProvider = MockTimeProvider()
let timerEngine = TimerEngine(
settingsManager: testEnv.settingsManager,
timeProvider: timeProvider
)
XCTAssertNotNil(timerEngine)
XCTAssertEqual(timerEngine.timerStates.count, 0)
}
func testTimerEngineUsesInjectedSettings() {
var settings = AppSettings.defaults
settings.lookAwayTimer.enabled = true
settings.blinkTimer.enabled = false
settings.postureTimer.enabled = false
testEnv.settingsManager.settings = settings
let timerEngine = testEnv.container.timerEngine
timerEngine.start()
// Only lookAway should be active
let lookAwayTimer = timerEngine.timerStates.first { $0.key == .builtIn(.lookAway) }
let blinkTimer = timerEngine.timerStates.first { $0.key == .builtIn(.blink) }
XCTAssertNotNil(lookAwayTimer)
XCTAssertNil(blinkTimer)
}
func testTimerEngineWithMockTimeProvider() {
let timeProvider = MockTimeProvider(startTime: Date())
let timerEngine = TimerEngine(
settingsManager: testEnv.settingsManager,
timeProvider: timeProvider
)
// Start timers
timerEngine.start()
// Advance time
timeProvider.advance(by: 10)
// Timer engine should use the mocked time
XCTAssertNotNil(timerEngine.timerStates)
}
func testPauseAndResumeWithMocks() {
let timerEngine = testEnv.container.timerEngine
timerEngine.start()
timerEngine.pause()
// Verify all timers are paused
for (_, state) in timerEngine.timerStates {
XCTAssertTrue(state.isPaused)
}
timerEngine.resume()
// Verify all timers are resumed
for (_, state) in timerEngine.timerStates {
XCTAssertFalse(state.isPaused)
}
}
func testReminderEventPublishing() async throws {
let timerEngine = testEnv.container.timerEngine
var receivedReminder: ReminderEvent?
timerEngine.$activeReminder
.sink { reminder in
receivedReminder = reminder
}
.store(in: &cancellables)
let timerId = TimerIdentifier.builtIn(.lookAway)
timerEngine.triggerReminder(for: timerId)
// Give time for publisher to fire
try await Task.sleep(nanoseconds: 10_000_000)
XCTAssertNotNil(receivedReminder)
}
}

View File

@@ -0,0 +1,76 @@
//
// BlinkSetupViewTests.swift
// GazeTests
//
// Tests for BlinkSetupView component.
//
import SwiftUI
import XCTest
@testable import Gaze
@MainActor
final class BlinkSetupViewTests: XCTestCase {
var testEnv: TestEnvironment!
override func setUp() async throws {
testEnv = TestEnvironment()
}
override func tearDown() async throws {
testEnv = nil
}
func testBlinkSetupInitialization() {
let view = BlinkSetupView(
settingsManager: testEnv.settingsManager as! SettingsManager
)
XCTAssertNotNil(view)
}
func testBlinkTimerConfigurationChanges() {
let initial = testEnv.settingsManager.timerConfiguration(for: .blink)
var modified = initial
modified.enabled = true
modified.intervalSeconds = 300
testEnv.settingsManager.updateTimerConfiguration(for: .blink, configuration: modified)
let updated = testEnv.settingsManager.timerConfiguration(for: .blink)
XCTAssertTrue(updated.enabled)
XCTAssertEqual(updated.intervalSeconds, 300)
}
func testBlinkTimerEnableDisable() {
var config = testEnv.settingsManager.timerConfiguration(for: .blink)
config.enabled = true
testEnv.settingsManager.updateTimerConfiguration(for: .blink, configuration: config)
XCTAssertTrue(testEnv.settingsManager.timerConfiguration(for: .blink).enabled)
config.enabled = false
testEnv.settingsManager.updateTimerConfiguration(for: .blink, configuration: config)
XCTAssertFalse(testEnv.settingsManager.timerConfiguration(for: .blink).enabled)
}
func testBlinkIntervalValidation() {
var config = testEnv.settingsManager.timerConfiguration(for: .blink)
let intervals = [180, 240, 300, 360, 600]
for interval in intervals {
config.intervalSeconds = interval
testEnv.settingsManager.updateTimerConfiguration(for: .blink, configuration: config)
let retrieved = testEnv.settingsManager.timerConfiguration(for: .blink)
XCTAssertEqual(retrieved.intervalSeconds, interval)
}
}
func testBlinkAccessibilityIdentifier() {
XCTAssertEqual(
AccessibilityIdentifiers.Onboarding.blinkPage,
"onboarding.page.blink"
)
}
}

View File

@@ -0,0 +1,26 @@
//
// CompletionViewTests.swift
// GazeTests
//
// Tests for CompletionView component.
//
import SwiftUI
import XCTest
@testable import Gaze
@MainActor
final class CompletionViewTests: XCTestCase {
func testCompletionViewInitialization() {
let view = CompletionView()
XCTAssertNotNil(view)
}
func testCompletionAccessibilityIdentifier() {
XCTAssertEqual(
AccessibilityIdentifiers.Onboarding.completionPage,
"onboarding.page.completion"
)
}
}

View File

@@ -0,0 +1,74 @@
//
// GeneralSetupViewTests.swift
// GazeTests
//
// Tests for GeneralSetupView component.
//
import SwiftUI
import XCTest
@testable import Gaze
@MainActor
final class GeneralSetupViewTests: XCTestCase {
var testEnv: TestEnvironment!
override func setUp() async throws {
testEnv = TestEnvironment()
}
override func tearDown() async throws {
testEnv = nil
}
func testGeneralSetupInitialization() {
let view = GeneralSetupView(
settingsManager: testEnv.settingsManager as! SettingsManager,
isOnboarding: true
)
XCTAssertNotNil(view)
}
func testPlaySoundsToggle() {
// Initial state
let initial = testEnv.settingsManager.settings.playSounds
// Toggle on
testEnv.settingsManager.settings.playSounds = true
XCTAssertTrue(testEnv.settingsManager.settings.playSounds)
// Toggle off
testEnv.settingsManager.settings.playSounds = false
XCTAssertFalse(testEnv.settingsManager.settings.playSounds)
}
func testLaunchAtLoginToggle() {
// Toggle on
testEnv.settingsManager.settings.launchAtLogin = true
XCTAssertTrue(testEnv.settingsManager.settings.launchAtLogin)
// Toggle off
testEnv.settingsManager.settings.launchAtLogin = false
XCTAssertFalse(testEnv.settingsManager.settings.launchAtLogin)
}
func testMultipleSettingsConfiguration() {
testEnv.settingsManager.settings.playSounds = true
testEnv.settingsManager.settings.launchAtLogin = true
XCTAssertTrue(testEnv.settingsManager.settings.playSounds)
XCTAssertTrue(testEnv.settingsManager.settings.launchAtLogin)
testEnv.settingsManager.settings.playSounds = false
XCTAssertFalse(testEnv.settingsManager.settings.playSounds)
XCTAssertTrue(testEnv.settingsManager.settings.launchAtLogin)
}
func testGeneralAccessibilityIdentifier() {
XCTAssertEqual(
AccessibilityIdentifiers.Onboarding.generalPage,
"onboarding.page.general"
)
}
}

View File

@@ -0,0 +1,82 @@
//
// LookAwaySetupViewTests.swift
// GazeTests
//
// Tests for LookAwaySetupView component.
//
import SwiftUI
import XCTest
@testable import Gaze
@MainActor
final class LookAwaySetupViewTests: XCTestCase {
var testEnv: TestEnvironment!
override func setUp() async throws {
testEnv = TestEnvironment()
}
override func tearDown() async throws {
testEnv = nil
}
func testLookAwaySetupInitialization() {
let view = LookAwaySetupView(
settingsManager: testEnv.settingsManager as! SettingsManager
)
XCTAssertNotNil(view)
}
func testLookAwayTimerConfigurationChanges() {
// Start with default
let initial = testEnv.settingsManager.timerConfiguration(for: .lookAway)
// Modify configuration
var modified = initial
modified.enabled = true
modified.intervalSeconds = 1500
testEnv.settingsManager.updateTimerConfiguration(for: .lookAway, configuration: modified)
// Verify changes
let updated = testEnv.settingsManager.timerConfiguration(for: .lookAway)
XCTAssertTrue(updated.enabled)
XCTAssertEqual(updated.intervalSeconds, 1500)
}
func testLookAwayTimerEnableDisable() {
var config = testEnv.settingsManager.timerConfiguration(for: .lookAway)
// Enable
config.enabled = true
testEnv.settingsManager.updateTimerConfiguration(for: .lookAway, configuration: config)
XCTAssertTrue(testEnv.settingsManager.timerConfiguration(for: .lookAway).enabled)
// Disable
config.enabled = false
testEnv.settingsManager.updateTimerConfiguration(for: .lookAway, configuration: config)
XCTAssertFalse(testEnv.settingsManager.timerConfiguration(for: .lookAway).enabled)
}
func testLookAwayIntervalValidation() {
var config = testEnv.settingsManager.timerConfiguration(for: .lookAway)
// Test various intervals
let intervals = [300, 600, 1200, 1800, 3600]
for interval in intervals {
config.intervalSeconds = interval
testEnv.settingsManager.updateTimerConfiguration(for: .lookAway, configuration: config)
let retrieved = testEnv.settingsManager.timerConfiguration(for: .lookAway)
XCTAssertEqual(retrieved.intervalSeconds, interval)
}
}
func testLookAwayAccessibilityIdentifier() {
XCTAssertEqual(
AccessibilityIdentifiers.Onboarding.lookAwayPage,
"onboarding.page.lookAway"
)
}
}

View File

@@ -0,0 +1,76 @@
//
// PostureSetupViewTests.swift
// GazeTests
//
// Tests for PostureSetupView component.
//
import SwiftUI
import XCTest
@testable import Gaze
@MainActor
final class PostureSetupViewTests: XCTestCase {
var testEnv: TestEnvironment!
override func setUp() async throws {
testEnv = TestEnvironment()
}
override func tearDown() async throws {
testEnv = nil
}
func testPostureSetupInitialization() {
let view = PostureSetupView(
settingsManager: testEnv.settingsManager as! SettingsManager
)
XCTAssertNotNil(view)
}
func testPostureTimerConfigurationChanges() {
let initial = testEnv.settingsManager.timerConfiguration(for: .posture)
var modified = initial
modified.enabled = true
modified.intervalSeconds = 1800
testEnv.settingsManager.updateTimerConfiguration(for: .posture, configuration: modified)
let updated = testEnv.settingsManager.timerConfiguration(for: .posture)
XCTAssertTrue(updated.enabled)
XCTAssertEqual(updated.intervalSeconds, 1800)
}
func testPostureTimerEnableDisable() {
var config = testEnv.settingsManager.timerConfiguration(for: .posture)
config.enabled = true
testEnv.settingsManager.updateTimerConfiguration(for: .posture, configuration: config)
XCTAssertTrue(testEnv.settingsManager.timerConfiguration(for: .posture).enabled)
config.enabled = false
testEnv.settingsManager.updateTimerConfiguration(for: .posture, configuration: config)
XCTAssertFalse(testEnv.settingsManager.timerConfiguration(for: .posture).enabled)
}
func testPostureIntervalValidation() {
var config = testEnv.settingsManager.timerConfiguration(for: .posture)
let intervals = [900, 1200, 1800, 2400, 3600]
for interval in intervals {
config.intervalSeconds = interval
testEnv.settingsManager.updateTimerConfiguration(for: .posture, configuration: config)
let retrieved = testEnv.settingsManager.timerConfiguration(for: .posture)
XCTAssertEqual(retrieved.intervalSeconds, interval)
}
}
func testPostureAccessibilityIdentifier() {
XCTAssertEqual(
AccessibilityIdentifiers.Onboarding.posturePage,
"onboarding.page.posture"
)
}
}

View File

@@ -0,0 +1,28 @@
//
// WelcomeViewTests.swift
// GazeTests
//
// Tests for WelcomeView component.
//
import SwiftUI
import XCTest
@testable import Gaze
@MainActor
final class WelcomeViewTests: XCTestCase {
func testWelcomeViewInitialization() {
let view = WelcomeView()
XCTAssertNotNil(view)
}
func testWelcomeViewHasAccessibilityIdentifier() {
// Welcome view should have proper accessibility identifier
// This is a structural test - in real app, verify the view has the identifier
XCTAssertEqual(
AccessibilityIdentifiers.Onboarding.welcomePage,
"onboarding.page.welcome"
)
}
}