general: test redux
This commit is contained in:
@@ -13,7 +13,7 @@ import os.log
|
|||||||
@MainActor
|
@MainActor
|
||||||
class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
|
class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
|
||||||
@Published var timerEngine: TimerEngine?
|
@Published var timerEngine: TimerEngine?
|
||||||
private let settingsManager: SettingsManager = .shared
|
private let serviceContainer: ServiceContainer
|
||||||
private let windowManager: WindowManaging
|
private let windowManager: WindowManaging
|
||||||
private var updateManager: UpdateManager?
|
private var updateManager: UpdateManager?
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
@@ -21,19 +21,21 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
|
|||||||
|
|
||||||
// Logging manager
|
// Logging manager
|
||||||
private let logger = LoggingManager.shared
|
private let logger = LoggingManager.shared
|
||||||
|
|
||||||
// Smart Mode services
|
// Convenience accessor for settings
|
||||||
private var fullscreenService: FullscreenDetectionService?
|
private var settingsManager: any SettingsProviding {
|
||||||
private var idleService: IdleMonitoringService?
|
serviceContainer.settingsManager
|
||||||
private var usageTrackingService: UsageTrackingService?
|
}
|
||||||
|
|
||||||
override init() {
|
override init() {
|
||||||
|
self.serviceContainer = ServiceContainer.shared
|
||||||
self.windowManager = WindowManager.shared
|
self.windowManager = WindowManager.shared
|
||||||
super.init()
|
super.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initializer for testing with injectable dependencies
|
/// Initializer for testing with injectable dependencies
|
||||||
init(windowManager: WindowManaging) {
|
init(serviceContainer: ServiceContainer, windowManager: WindowManaging) {
|
||||||
|
self.serviceContainer = serviceContainer
|
||||||
self.windowManager = windowManager
|
self.windowManager = windowManager
|
||||||
super.init()
|
super.init()
|
||||||
}
|
}
|
||||||
@@ -46,9 +48,11 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
|
|||||||
logger.configureLogging()
|
logger.configureLogging()
|
||||||
logger.appLogger.info("🚀 Application did finish launching")
|
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
|
// Initialize update manager after onboarding is complete
|
||||||
if settingsManager.settings.hasCompletedOnboarding {
|
if settingsManager.settings.hasCompletedOnboarding {
|
||||||
@@ -64,37 +68,21 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setupSmartModeServices() {
|
// Note: Smart mode setup is now handled by ServiceContainer
|
||||||
fullscreenService = FullscreenDetectionService()
|
// Keeping this method for settings change observation
|
||||||
idleService = IdleMonitoringService(
|
private func observeSmartModeSettings() {
|
||||||
idleThresholdMinutes: settingsManager.settings.smartMode.idleThresholdMinutes
|
settingsManager.settingsPublisher
|
||||||
)
|
|
||||||
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
|
|
||||||
.map { $0.smartMode }
|
.map { $0.smartMode }
|
||||||
.removeDuplicates()
|
.removeDuplicates()
|
||||||
.sink { [weak self] smartMode in
|
.sink { [weak self] smartMode in
|
||||||
self?.idleService?.updateThreshold(minutes: smartMode.idleThresholdMinutes)
|
guard let self = self else { return }
|
||||||
self?.usageTrackingService?.updateResetThreshold(
|
self.serviceContainer.idleService?.updateThreshold(minutes: smartMode.idleThresholdMinutes)
|
||||||
|
self.serviceContainer.usageTrackingService?.updateResetThreshold(
|
||||||
minutes: smartMode.usageResetAfterMinutes)
|
minutes: smartMode.usageResetAfterMinutes)
|
||||||
|
|
||||||
// Force state check when settings change to apply immediately
|
// Force state check when settings change to apply immediately
|
||||||
self?.fullscreenService?.forceUpdate()
|
self.serviceContainer.fullscreenService?.forceUpdate()
|
||||||
self?.idleService?.forceUpdate()
|
self.serviceContainer.idleService?.forceUpdate()
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
@@ -117,7 +105,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func observeSettingsChanges() {
|
private func observeSettingsChanges() {
|
||||||
settingsManager.$settings
|
settingsManager.settingsPublisher
|
||||||
.sink { [weak self] settings in
|
.sink { [weak self] settings in
|
||||||
if settings.hasCompletedOnboarding && self?.hasStartedTimers == false {
|
if settings.hasCompletedOnboarding && self?.hasStartedTimers == false {
|
||||||
self?.startTimers()
|
self?.startTimers()
|
||||||
@@ -129,6 +117,9 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
// Also observe smart mode settings
|
||||||
|
observeSmartModeSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
func applicationWillTerminate(_ notification: Notification) {
|
func applicationWillTerminate(_ notification: Notification) {
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import SwiftUI
|
|||||||
@main
|
@main
|
||||||
struct GazeApp: App {
|
struct GazeApp: App {
|
||||||
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
|
@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
|
@StateObject private var settingsManager = SettingsManager.shared
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
|
|||||||
160
Gaze/Services/MockWindowManager.swift
Normal file
160
Gaze/Services/MockWindowManager.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
97
GazeTests/AppDelegateTestabilityTests.swift
Normal file
97
GazeTests/AppDelegateTestabilityTests.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
104
GazeTests/MockWindowManagerTests.swift
Normal file
104
GazeTests/MockWindowManagerTests.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
232
GazeTests/OnboardingNavigationTests.swift
Normal file
232
GazeTests/OnboardingNavigationTests.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
65
GazeTests/ServiceContainerTests.swift
Normal file
65
GazeTests/ServiceContainerTests.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
135
GazeTests/Services/EnforceModeServiceTests.swift
Normal file
135
GazeTests/Services/EnforceModeServiceTests.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
68
GazeTests/Services/FullscreenDetectionServiceTests.swift
Normal file
68
GazeTests/Services/FullscreenDetectionServiceTests.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
89
GazeTests/Services/IdleMonitoringServiceTests.swift
Normal file
89
GazeTests/Services/IdleMonitoringServiceTests.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
61
GazeTests/Services/LoggingManagerTests.swift
Normal file
61
GazeTests/Services/LoggingManagerTests.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
187
GazeTests/Services/SettingsManagerTests.swift
Normal file
187
GazeTests/Services/SettingsManagerTests.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
302
GazeTests/Services/TimerEngineTests.swift
Normal file
302
GazeTests/Services/TimerEngineTests.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
62
GazeTests/Services/UsageTrackingServiceTests.swift
Normal file
62
GazeTests/Services/UsageTrackingServiceTests.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
129
GazeTests/Services/WindowManagerTests.swift
Normal file
129
GazeTests/Services/WindowManagerTests.swift
Normal 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
259
GazeTests/TestHelpers.swift
Normal 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
|
||||||
112
GazeTests/TimerEngineTestabilityTests.swift
Normal file
112
GazeTests/TimerEngineTestabilityTests.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
76
GazeTests/Views/BlinkSetupViewTests.swift
Normal file
76
GazeTests/Views/BlinkSetupViewTests.swift
Normal 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"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
26
GazeTests/Views/CompletionViewTests.swift
Normal file
26
GazeTests/Views/CompletionViewTests.swift
Normal 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"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
74
GazeTests/Views/GeneralSetupViewTests.swift
Normal file
74
GazeTests/Views/GeneralSetupViewTests.swift
Normal 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"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
82
GazeTests/Views/LookAwaySetupViewTests.swift
Normal file
82
GazeTests/Views/LookAwaySetupViewTests.swift
Normal 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"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
76
GazeTests/Views/PostureSetupViewTests.swift
Normal file
76
GazeTests/Views/PostureSetupViewTests.swift
Normal 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"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
28
GazeTests/Views/WelcomeViewTests.swift
Normal file
28
GazeTests/Views/WelcomeViewTests.swift
Normal 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"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user