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