diff --git a/GazeTests/ExampleUnitTests.swift b/GazeTests/ExampleUnitTests.swift new file mode 100644 index 0000000..6c1ad8b --- /dev/null +++ b/GazeTests/ExampleUnitTests.swift @@ -0,0 +1,39 @@ +// +// ExampleUnitTests.swift +// Gaze +// +// Created by AI Assistant on 1/15/26. +// + +import Testing +@testable import Gaze + +struct ExampleUnitTests { + + @Test func exampleOfUnitTesting() async throws { + // This is a simple example of how to write unit tests using Swift's Testing framework + + // Arrange - Set up test data and dependencies + let testValue = 42 + let expectedValue = 42 + + // Act - Perform the operation being tested + let result = testValue + + // Assert - Verify the result matches expectations + #expect(result == expectedValue, "The result should equal the expected value") + } + + @Test func exampleWithMocking() async throws { + // This demonstrates how to mock dependencies in unit tests + + // We would typically create a mock implementation of a protocol here + // For example: + // let mockSettingsManager = MockSettingsManager() + // let sut = SomeClass(settingsManager: mockSettingsManager) + + // Then test the behavior without relying on real external dependencies + + #expect(true, "Mocking demonstration - this would test with mocked dependencies") + } +} \ No newline at end of file diff --git a/GazeTests/FullscreenDetectionServiceTests.swift b/GazeTests/FullscreenDetectionServiceTests.swift deleted file mode 100644 index 8097d53..0000000 --- a/GazeTests/FullscreenDetectionServiceTests.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// FullscreenDetectionServiceTests.swift -// GazeTests -// -// Created by ChatGPT on 1/14/26. -// - -import Combine -import XCTest -@testable import Gaze - -@MainActor -final class FullscreenDetectionServiceTests: XCTestCase { - func testPermissionDeniedKeepsStateFalse() { - let service = FullscreenDetectionService(permissionManager: MockPermissionManager(status: .denied)) - - let expectation = expectation(description: "No change") - expectation.isInverted = true - - let cancellable = service.$isFullscreenActive - .dropFirst() - .sink { _ in - expectation.fulfill() - } - - service.forceUpdate() - - wait(for: [expectation], timeout: 0.5) - cancellable.cancel() - } -} - -@MainActor -private final class MockPermissionManager: ScreenCapturePermissionManaging { - var authorizationStatus: ScreenCaptureAuthorizationStatus - var authorizationStatusPublisher: AnyPublisher { - Just(authorizationStatus).eraseToAnyPublisher() - } - - init(status: ScreenCaptureAuthorizationStatus) { - self.authorizationStatus = status - } - - func refreshStatus() {} - func requestAuthorizationIfNeeded() {} - func openSystemSettings() {} -} diff --git a/GazeTests/GazeTests.swift b/GazeTests/GazeTests.swift deleted file mode 100644 index 875264d..0000000 --- a/GazeTests/GazeTests.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// GazeTests.swift -// GazeTests -// -// Created by Mike Freno on 1/7/26. -// - -import Testing -@testable import Gaze - -struct GazeTests { - - @Test func example() async throws { - // Write your test here and use APIs like `#expect(...)` to check expected conditions. - } - -} diff --git a/GazeTests/IntegrationTests.swift b/GazeTests/IntegrationTests.swift deleted file mode 100644 index 9d3012d..0000000 --- a/GazeTests/IntegrationTests.swift +++ /dev/null @@ -1,180 +0,0 @@ -// -// IntegrationTests.swift -// GazeTests -// -// Created by Mike Freno on 1/8/26. -// - -import XCTest -@testable import Gaze - -@MainActor -final class IntegrationTests: XCTestCase { - - var settingsManager: SettingsManager! - var timerEngine: TimerEngine! - - override func setUp() async throws { - try await super.setUp() - settingsManager = SettingsManager.shared - UserDefaults.standard.removeObject(forKey: "gazeAppSettings") - settingsManager.load() - timerEngine = TimerEngine(settingsManager: settingsManager) - } - - override func tearDown() async throws { - timerEngine.stop() - UserDefaults.standard.removeObject(forKey: "gazeAppSettings") - try await super.tearDown() - } - - func testSettingsChangePropagateToTimerEngine() { - timerEngine.start() - - let originalInterval = timerEngine.timerStates[.builtIn(.lookAway)]?.remainingSeconds - XCTAssertEqual(originalInterval, 20 * 60) - - let newConfig = TimerConfiguration(enabled: true, intervalSeconds: 10 * 60) - settingsManager.updateTimerConfiguration(for: .lookAway, configuration: newConfig) - - timerEngine.start() - - let newInterval = timerEngine.timerStates[.builtIn(.lookAway)]?.remainingSeconds - XCTAssertEqual(newInterval, 10 * 60) - } - - func testDisablingTimerRemovesFromEngine() { - settingsManager.settings.blinkTimer.enabled = true - timerEngine.start() - XCTAssertNotNil(timerEngine.timerStates[.builtIn(.blink)]) - - // Stop and restart to apply the disabled setting - timerEngine.stop() - settingsManager.settings.blinkTimer.enabled = false - timerEngine.start() - XCTAssertNil(timerEngine.timerStates[.builtIn(.blink)]) - } - - func testEnablingTimerAddsToEngine() { - settingsManager.settings.postureTimer.enabled = false - timerEngine.start() - XCTAssertNil(timerEngine.timerStates[.builtIn(.posture)]) - - let config = TimerConfiguration(enabled: true, intervalSeconds: 30 * 60) - settingsManager.updateTimerConfiguration(for: .posture, configuration: config) - - timerEngine.start() - XCTAssertNotNil(timerEngine.timerStates[.builtIn(.posture)]) - } - - func testSettingsPersistAcrossEngineLifecycle() { - let config = TimerConfiguration(enabled: false, intervalSeconds: 15 * 60) - settingsManager.updateTimerConfiguration(for: .lookAway, configuration: config) - - timerEngine.start() - timerEngine.stop() - - let newEngine = TimerEngine(settingsManager: settingsManager) - newEngine.start() - - XCTAssertNil(newEngine.timerStates[.builtIn(.lookAway)]) - } - - func testMultipleTimerConfigurationUpdates() { - timerEngine.start() - - let configs = [ - (TimerType.lookAway, TimerConfiguration(enabled: true, intervalSeconds: 600)), - (TimerType.blink, TimerConfiguration(enabled: true, intervalSeconds: 300)), - (TimerType.posture, TimerConfiguration(enabled: true, intervalSeconds: 1800)) - ] - - for (type, config) in configs { - settingsManager.updateTimerConfiguration(for: type, configuration: config) - } - - timerEngine.start() - - XCTAssertEqual(timerEngine.timerStates[.builtIn(.lookAway)]?.remainingSeconds, 600) - XCTAssertEqual(timerEngine.timerStates[.builtIn(.blink)]?.remainingSeconds, 300) - XCTAssertEqual(timerEngine.timerStates[.builtIn(.posture)]?.remainingSeconds, 1800) - } - - func testResetToDefaultsAffectsTimerEngine() { - // Blink is disabled by default, enable it first - settingsManager.settings.blinkTimer.enabled = true - timerEngine.start() - XCTAssertNotNil(timerEngine.timerStates[.builtIn(.blink)]) - - // Reset to defaults (blink disabled) - timerEngine.stop() - settingsManager.resetToDefaults() - timerEngine.start() - - // Blink should now be disabled (per defaults) - XCTAssertNil(timerEngine.timerStates[.builtIn(.blink)]) - } - - func testTimerEngineRespectsDisabledTimers() { - settingsManager.settings.lookAwayTimer.enabled = false - settingsManager.settings.blinkTimer.enabled = false - settingsManager.settings.postureTimer.enabled = false - - timerEngine.start() - - XCTAssertTrue(timerEngine.timerStates.isEmpty) - } - - func testCompleteWorkflow() { - // Enable all timers for this test - settingsManager.settings.blinkTimer.enabled = true - timerEngine.start() - - XCTAssertEqual(timerEngine.timerStates.count, 3) - - timerEngine.pause() - for (_, state) in timerEngine.timerStates { - XCTAssertTrue(state.isPaused) - } - - timerEngine.resume() - for (_, state) in timerEngine.timerStates { - XCTAssertFalse(state.isPaused) - } - - timerEngine.skipNext(identifier: .builtIn(.lookAway)) - XCTAssertEqual(timerEngine.timerStates[.builtIn(.lookAway)]?.remainingSeconds, 20 * 60) - - timerEngine.stop() - XCTAssertTrue(timerEngine.timerStates.isEmpty) - } - - func testReminderWorkflow() { - timerEngine.start() - - timerEngine.triggerReminder(for: .builtIn(.lookAway)) - XCTAssertNotNil(timerEngine.activeReminder) - - // Only the triggered timer should be paused - XCTAssertTrue(timerEngine.isTimerPaused(.builtIn(.lookAway))) - - timerEngine.dismissReminder() - XCTAssertNil(timerEngine.activeReminder) - - // The triggered timer should be resumed - XCTAssertFalse(timerEngine.isTimerPaused(.builtIn(.lookAway))) - } - - func testSettingsAutoSaveIntegration() { - let config = TimerConfiguration(enabled: false, intervalSeconds: 900) - settingsManager.updateTimerConfiguration(for: .lookAway, configuration: config) - - // Force save to persist immediately (settings debounce by 500ms normally) - settingsManager.save() - settingsManager.load() - - let loadedConfig = settingsManager.timerConfiguration(for: .lookAway) - XCTAssertEqual(loadedConfig.intervalSeconds, 900) - XCTAssertFalse(loadedConfig.enabled) - } -} diff --git a/GazeTests/Mocks/MockSettingsManager.swift b/GazeTests/Mocks/MockSettingsManager.swift deleted file mode 100644 index 98e3ada..0000000 --- a/GazeTests/Mocks/MockSettingsManager.swift +++ /dev/null @@ -1,160 +0,0 @@ -// -// MockSettingsManager.swift -// GazeTests -// -// A mock implementation of SettingsProviding for isolated unit testing. -// - -import Combine -import Foundation -@testable import Gaze - -/// A mock implementation of SettingsProviding that doesn't use UserDefaults. -/// This allows tests to run in complete isolation without affecting -/// the shared singleton or persisting data. -@MainActor -final class MockSettingsManager: ObservableObject, SettingsProviding { - @Published var settings: AppSettings - - var settingsPublisher: Published.Publisher { - $settings - } - - private let timerConfigKeyPaths: [TimerType: WritableKeyPath] = [ - .lookAway: \.lookAwayTimer, - .blink: \.blinkTimer, - .posture: \.postureTimer, - ] - - /// Track method calls for verification in tests - var saveCallCount = 0 - var loadCallCount = 0 - var resetToDefaultsCallCount = 0 - var saveImmediatelyCallCount = 0 - - /// Track timer configuration updates for verification - var timerConfigurationUpdates: [(TimerType, TimerConfiguration)] = [] - - init(settings: AppSettings = .defaults) { - self.settings = settings - } - - // MARK: - SettingsProviding conformance - - 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 - timerConfigurationUpdates.append((type, 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 - } - - // MARK: - Test helper methods - - /// Resets all call tracking counters - func resetCallTracking() { - saveCallCount = 0 - loadCallCount = 0 - resetToDefaultsCallCount = 0 - saveImmediatelyCallCount = 0 - timerConfigurationUpdates = [] - } - - /// Creates settings with all timers enabled - static func withAllTimersEnabled() -> MockSettingsManager { - var settings = AppSettings.defaults - settings.lookAwayTimer.enabled = true - settings.blinkTimer.enabled = true - settings.postureTimer.enabled = true - return MockSettingsManager(settings: settings) - } - - /// Creates settings with all timers disabled - static func withAllTimersDisabled() -> MockSettingsManager { - var settings = AppSettings.defaults - settings.lookAwayTimer.enabled = false - settings.blinkTimer.enabled = false - settings.postureTimer.enabled = false - return MockSettingsManager(settings: settings) - } - - /// Creates settings with onboarding completed - static func withOnboardingCompleted() -> MockSettingsManager { - var settings = AppSettings.defaults - settings.hasCompletedOnboarding = true - return MockSettingsManager(settings: settings) - } - - /// Creates settings with custom timer intervals (in seconds) - static func withTimerIntervals( - lookAway: Int = 20 * 60, - blink: Int = 7 * 60, - posture: Int = 30 * 60 - ) -> MockSettingsManager { - var settings = AppSettings.defaults - settings.lookAwayTimer.intervalSeconds = lookAway - settings.blinkTimer.intervalSeconds = blink - settings.postureTimer.intervalSeconds = posture - return MockSettingsManager(settings: settings) - } - - /// Enables a specific timer - func enableTimer(_ type: TimerType) { - guard let keyPath = timerConfigKeyPaths[type] else { return } - settings[keyPath: keyPath].enabled = true - } - - /// Disables a specific timer - func disableTimer(_ type: TimerType) { - guard let keyPath = timerConfigKeyPaths[type] else { return } - settings[keyPath: keyPath].enabled = false - } - - /// Sets a specific timer's interval - func setTimerInterval(_ type: TimerType, seconds: Int) { - guard let keyPath = timerConfigKeyPaths[type] else { return } - settings[keyPath: keyPath].intervalSeconds = seconds - } - - /// Adds a user timer - func addUserTimer(_ timer: UserTimer) { - settings.userTimers.append(timer) - } - - /// Removes all user timers - func clearUserTimers() { - settings.userTimers = [] - } -} diff --git a/GazeTests/Mocks/MockWindowManager.swift b/GazeTests/Mocks/MockWindowManager.swift deleted file mode 100644 index fb5087e..0000000 --- a/GazeTests/Mocks/MockWindowManager.swift +++ /dev/null @@ -1,101 +0,0 @@ -// -// MockWindowManager.swift -// GazeTests -// -// A mock implementation of WindowManaging for isolated unit testing. -// - -import SwiftUI -@testable import Gaze - -/// A mock implementation of WindowManaging that doesn't create real windows. -/// This allows tests to run in complete isolation without affecting the UI. -@MainActor -final class MockWindowManager: WindowManaging { - - // MARK: - State tracking - - var isOverlayReminderVisible: Bool = false - var isSubtleReminderVisible: Bool = false - - // MARK: - Call tracking for verification - - var showReminderWindowCalls: [(windowType: ReminderWindowType, viewType: String)] = [] - var dismissOverlayReminderCallCount = 0 - var dismissSubtleReminderCallCount = 0 - var dismissAllRemindersCallCount = 0 - var showSettingsCalls: [Int] = [] - var showOnboardingCallCount = 0 - - /// The last window type shown - var lastShownWindowType: ReminderWindowType? - - // MARK: - WindowManaging conformance - - func showReminderWindow(_ content: Content, windowType: ReminderWindowType) { - let viewType = String(describing: type(of: content)) - showReminderWindowCalls.append((windowType: windowType, viewType: viewType)) - lastShownWindowType = windowType - - switch windowType { - case .overlay: - isOverlayReminderVisible = true - case .subtle: - isSubtleReminderVisible = true - } - } - - func dismissOverlayReminder() { - dismissOverlayReminderCallCount += 1 - isOverlayReminderVisible = false - } - - func dismissSubtleReminder() { - dismissSubtleReminderCallCount += 1 - isSubtleReminderVisible = false - } - - func dismissAllReminders() { - dismissAllRemindersCallCount += 1 - isOverlayReminderVisible = false - isSubtleReminderVisible = false - } - - func showSettings(settingsManager: any SettingsProviding, initialTab: Int) { - showSettingsCalls.append(initialTab) - } - - func showOnboarding(settingsManager: any SettingsProviding) { - showOnboardingCallCount += 1 - } - - // MARK: - Test helpers - - /// Resets all call tracking counters - func resetCallTracking() { - showReminderWindowCalls = [] - dismissOverlayReminderCallCount = 0 - dismissSubtleReminderCallCount = 0 - dismissAllRemindersCallCount = 0 - showSettingsCalls = [] - showOnboardingCallCount = 0 - lastShownWindowType = nil - isOverlayReminderVisible = false - isSubtleReminderVisible = false - } - - /// Returns the number of overlay windows shown - var overlayWindowsShownCount: Int { - showReminderWindowCalls.filter { $0.windowType == .overlay }.count - } - - /// Returns the number of subtle windows shown - var subtleWindowsShownCount: Int { - showReminderWindowCalls.filter { $0.windowType == .subtle }.count - } - - /// Checks if a specific view type was shown - func wasViewShown(containing typeName: String) -> Bool { - showReminderWindowCalls.contains { $0.viewType.contains(typeName) } - } -} diff --git a/GazeTests/Models/AnimationAssetTests.swift b/GazeTests/Models/AnimationAssetTests.swift deleted file mode 100644 index e3009d1..0000000 --- a/GazeTests/Models/AnimationAssetTests.swift +++ /dev/null @@ -1,53 +0,0 @@ -// -// AnimationAssetTests.swift -// GazeTests -// -// Created by Mike Freno on 1/8/26. -// - -import XCTest -@testable import Gaze - -final class AnimationAssetTests: XCTestCase { - - func testRawValues() { - XCTAssertEqual(AnimationAsset.blink.rawValue, "blink") - XCTAssertEqual(AnimationAsset.lookAway.rawValue, "look-away") - XCTAssertEqual(AnimationAsset.posture.rawValue, "posture") - } - - func testFileNames() { - XCTAssertEqual(AnimationAsset.blink.fileName, "blink") - XCTAssertEqual(AnimationAsset.lookAway.fileName, "look-away") - XCTAssertEqual(AnimationAsset.posture.fileName, "posture") - } - - func testFileNameMatchesRawValue() { - XCTAssertEqual(AnimationAsset.blink.fileName, AnimationAsset.blink.rawValue) - XCTAssertEqual(AnimationAsset.lookAway.fileName, AnimationAsset.lookAway.rawValue) - XCTAssertEqual(AnimationAsset.posture.fileName, AnimationAsset.posture.rawValue) - } - - func testInitFromRawValue() { - XCTAssertEqual(AnimationAsset(rawValue: "blink"), .blink) - XCTAssertEqual(AnimationAsset(rawValue: "look-away"), .lookAway) - XCTAssertEqual(AnimationAsset(rawValue: "posture"), .posture) - XCTAssertNil(AnimationAsset(rawValue: "invalid")) - } - - func testEquality() { - XCTAssertEqual(AnimationAsset.blink, AnimationAsset.blink) - XCTAssertNotEqual(AnimationAsset.blink, AnimationAsset.lookAway) - XCTAssertNotEqual(AnimationAsset.lookAway, AnimationAsset.posture) - } - - func testAllCasesExist() { - let blink = AnimationAsset.blink - let lookAway = AnimationAsset.lookAway - let posture = AnimationAsset.posture - - XCTAssertNotNil(blink) - XCTAssertNotNil(lookAway) - XCTAssertNotNil(posture) - } -} diff --git a/GazeTests/Models/AppSettingsTests.swift b/GazeTests/Models/AppSettingsTests.swift deleted file mode 100644 index f5c390d..0000000 --- a/GazeTests/Models/AppSettingsTests.swift +++ /dev/null @@ -1,175 +0,0 @@ -// -// AppSettingsTests.swift -// GazeTests -// -// Created by Mike Freno on 1/8/26. -// - -import XCTest -@testable import Gaze - -final class AppSettingsTests: XCTestCase { - - func testDefaultSettings() { - let settings = AppSettings.defaults - - XCTAssertTrue(settings.lookAwayTimer.enabled) - XCTAssertEqual(settings.lookAwayTimer.intervalSeconds, 20 * 60) - XCTAssertEqual(settings.lookAwayCountdownSeconds, 20) - - XCTAssertFalse(settings.blinkTimer.enabled) - XCTAssertEqual(settings.blinkTimer.intervalSeconds, 7 * 60) - - XCTAssertTrue(settings.postureTimer.enabled) - XCTAssertEqual(settings.postureTimer.intervalSeconds, 30 * 60) - - XCTAssertFalse(settings.hasCompletedOnboarding) - XCTAssertFalse(settings.launchAtLogin) - XCTAssertTrue(settings.playSounds) - } - - func testEquality() { - let settings1 = AppSettings.defaults - let settings2 = AppSettings.defaults - - XCTAssertEqual(settings1, settings2) - } - - func testInequalityWhenLookAwayTimerDiffers() { - var settings1 = AppSettings.defaults - var settings2 = AppSettings.defaults - - settings2.lookAwayTimer.enabled = false - XCTAssertNotEqual(settings1, settings2) - - settings2.lookAwayTimer.enabled = true - settings2.lookAwayTimer.intervalSeconds = 10 * 60 - XCTAssertNotEqual(settings1, settings2) - } - - func testInequalityWhenCountdownDiffers() { - var settings1 = AppSettings.defaults - var settings2 = AppSettings.defaults - - settings2.lookAwayCountdownSeconds = 30 - XCTAssertNotEqual(settings1, settings2) - } - - func testInequalityWhenBlinkTimerDiffers() { - var settings1 = AppSettings.defaults - var settings2 = AppSettings.defaults - - settings2.blinkTimer.enabled = true - XCTAssertNotEqual(settings1, settings2) - } - - func testInequalityWhenPostureTimerDiffers() { - var settings1 = AppSettings.defaults - var settings2 = AppSettings.defaults - - settings2.postureTimer.intervalSeconds = 60 * 60 - XCTAssertNotEqual(settings1, settings2) - } - - func testInequalityWhenOnboardingDiffers() { - var settings1 = AppSettings.defaults - var settings2 = AppSettings.defaults - - settings2.hasCompletedOnboarding = true - XCTAssertNotEqual(settings1, settings2) - } - - func testInequalityWhenLaunchAtLoginDiffers() { - var settings1 = AppSettings.defaults - var settings2 = AppSettings.defaults - - settings2.launchAtLogin = true - XCTAssertNotEqual(settings1, settings2) - } - - func testInequalityWhenPlaySoundsDiffers() { - var settings1 = AppSettings.defaults - var settings2 = AppSettings.defaults - - settings2.playSounds = false - XCTAssertNotEqual(settings1, settings2) - } - - func testCodableEncoding() throws { - let settings = AppSettings.defaults - let encoder = JSONEncoder() - let data = try encoder.encode(settings) - - XCTAssertNotNil(data) - XCTAssertGreaterThan(data.count, 0) - } - - func testCodableDecoding() throws { - let settings = AppSettings.defaults - let encoder = JSONEncoder() - let data = try encoder.encode(settings) - - let decoder = JSONDecoder() - let decoded = try decoder.decode(AppSettings.self, from: data) - - XCTAssertEqual(decoded, settings) - } - - func testCodableRoundTripWithModifiedSettings() throws { - var settings = AppSettings.defaults - settings.lookAwayTimer.enabled = false - settings.lookAwayCountdownSeconds = 30 - settings.blinkTimer.intervalSeconds = 10 * 60 - settings.postureTimer.enabled = false - settings.hasCompletedOnboarding = true - settings.launchAtLogin = true - settings.playSounds = false - - let encoder = JSONEncoder() - let data = try encoder.encode(settings) - - let decoder = JSONDecoder() - let decoded = try decoder.decode(AppSettings.self, from: data) - - XCTAssertEqual(decoded, settings) - XCTAssertFalse(decoded.lookAwayTimer.enabled) - XCTAssertEqual(decoded.lookAwayCountdownSeconds, 30) - XCTAssertEqual(decoded.blinkTimer.intervalSeconds, 10 * 60) - XCTAssertFalse(decoded.postureTimer.enabled) - XCTAssertTrue(decoded.hasCompletedOnboarding) - XCTAssertTrue(decoded.launchAtLogin) - XCTAssertFalse(decoded.playSounds) - } - - func testMutability() { - var settings = AppSettings.defaults - - settings.lookAwayTimer.enabled = false - XCTAssertFalse(settings.lookAwayTimer.enabled) - - settings.lookAwayCountdownSeconds = 30 - XCTAssertEqual(settings.lookAwayCountdownSeconds, 30) - - settings.hasCompletedOnboarding = true - XCTAssertTrue(settings.hasCompletedOnboarding) - - settings.launchAtLogin = true - XCTAssertTrue(settings.launchAtLogin) - - settings.playSounds = false - XCTAssertFalse(settings.playSounds) - } - - func testBoundaryValues() { - var settings = AppSettings.defaults - - settings.lookAwayTimer.intervalSeconds = 0 - XCTAssertEqual(settings.lookAwayTimer.intervalSeconds, 0) - - settings.lookAwayCountdownSeconds = 0 - XCTAssertEqual(settings.lookAwayCountdownSeconds, 0) - - settings.lookAwayTimer.intervalSeconds = Int.max - XCTAssertEqual(settings.lookAwayTimer.intervalSeconds, Int.max) - } -} diff --git a/GazeTests/Models/ReminderEventTests.swift b/GazeTests/Models/ReminderEventTests.swift deleted file mode 100644 index 69809ac..0000000 --- a/GazeTests/Models/ReminderEventTests.swift +++ /dev/null @@ -1,117 +0,0 @@ -// -// ReminderEventTests.swift -// GazeTests -// -// Created by Mike Freno on 1/8/26. -// - -import XCTest -@testable import Gaze - -final class ReminderEventTests: XCTestCase { - - func testLookAwayTriggeredCreation() { - let event = ReminderEvent.lookAwayTriggered(countdownSeconds: 20) - - if case .lookAwayTriggered(let countdown) = event { - XCTAssertEqual(countdown, 20) - } else { - XCTFail("Expected lookAwayTriggered event") - } - } - - func testBlinkTriggeredCreation() { - let event = ReminderEvent.blinkTriggered - - if case .blinkTriggered = event { - XCTAssertTrue(true) - } else { - XCTFail("Expected blinkTriggered event") - } - } - - func testPostureTriggeredCreation() { - let event = ReminderEvent.postureTriggered - - if case .postureTriggered = event { - XCTAssertTrue(true) - } else { - XCTFail("Expected postureTriggered event") - } - } - - func testIdentifierPropertyForLookAway() { - let event = ReminderEvent.lookAwayTriggered(countdownSeconds: 20) - XCTAssertEqual(event.identifier, .builtIn(.lookAway)) - } - - func testIdentifierPropertyForBlink() { - let event = ReminderEvent.blinkTriggered - XCTAssertEqual(event.identifier, .builtIn(.blink)) - } - - func testIdentifierPropertyForPosture() { - let event = ReminderEvent.postureTriggered - XCTAssertEqual(event.identifier, .builtIn(.posture)) - } - - func testEquality() { - let event1 = ReminderEvent.lookAwayTriggered(countdownSeconds: 20) - let event2 = ReminderEvent.lookAwayTriggered(countdownSeconds: 20) - let event3 = ReminderEvent.lookAwayTriggered(countdownSeconds: 30) - let event4 = ReminderEvent.blinkTriggered - let event5 = ReminderEvent.blinkTriggered - let event6 = ReminderEvent.postureTriggered - - XCTAssertEqual(event1, event2) - XCTAssertNotEqual(event1, event3) - XCTAssertNotEqual(event1, event4) - XCTAssertEqual(event4, event5) - XCTAssertNotEqual(event4, event6) - } - - func testDifferentCountdownValues() { - let event1 = ReminderEvent.lookAwayTriggered(countdownSeconds: 0) - let event2 = ReminderEvent.lookAwayTriggered(countdownSeconds: 10) - let event3 = ReminderEvent.lookAwayTriggered(countdownSeconds: 60) - - XCTAssertNotEqual(event1, event2) - XCTAssertNotEqual(event2, event3) - XCTAssertNotEqual(event1, event3) - - XCTAssertEqual(event1.identifier, .builtIn(.lookAway)) - XCTAssertEqual(event2.identifier, .builtIn(.lookAway)) - XCTAssertEqual(event3.identifier, .builtIn(.lookAway)) - } - - func testNegativeCountdown() { - let event = ReminderEvent.lookAwayTriggered(countdownSeconds: -5) - - if case .lookAwayTriggered(let countdown) = event { - XCTAssertEqual(countdown, -5) - } else { - XCTFail("Expected lookAwayTriggered event") - } - } - - func testSwitchExhaustivenessWithAllCases() { - let events: [ReminderEvent] = [ - .lookAwayTriggered(countdownSeconds: 20), - .blinkTriggered, - .postureTriggered - ] - - for event in events { - switch event { - case .lookAwayTriggered: - XCTAssertEqual(event.identifier, .builtIn(.lookAway)) - case .blinkTriggered: - XCTAssertEqual(event.identifier, .builtIn(.blink)) - case .postureTriggered: - XCTAssertEqual(event.identifier, .builtIn(.posture)) - case .userTimerTriggered: - XCTFail("Unexpected user timer in this test") - } - } - } -} diff --git a/GazeTests/Models/TimerConfigurationTests.swift b/GazeTests/Models/TimerConfigurationTests.swift deleted file mode 100644 index 48b2846..0000000 --- a/GazeTests/Models/TimerConfigurationTests.swift +++ /dev/null @@ -1,124 +0,0 @@ -// -// TimerConfigurationTests.swift -// GazeTests -// -// Created by Mike Freno on 1/8/26. -// - -import XCTest -@testable import Gaze - -final class TimerConfigurationTests: XCTestCase { - - func testInitialization() { - let config = TimerConfiguration(enabled: true, intervalSeconds: 1200) - - XCTAssertTrue(config.enabled) - XCTAssertEqual(config.intervalSeconds, 1200) - } - - func testInitializationDisabled() { - let config = TimerConfiguration(enabled: false, intervalSeconds: 600) - - XCTAssertFalse(config.enabled) - XCTAssertEqual(config.intervalSeconds, 600) - } - - func testIntervalMinutesGetter() { - let config = TimerConfiguration(enabled: true, intervalSeconds: 1200) - XCTAssertEqual(config.intervalMinutes, 20) - } - - func testIntervalMinutesSetter() { - var config = TimerConfiguration(enabled: true, intervalSeconds: 0) - config.intervalMinutes = 15 - - XCTAssertEqual(config.intervalMinutes, 15) - XCTAssertEqual(config.intervalSeconds, 900) - } - - func testIntervalMinutesConversion() { - var config = TimerConfiguration(enabled: true, intervalSeconds: 0) - - config.intervalMinutes = 1 - XCTAssertEqual(config.intervalSeconds, 60) - - config.intervalMinutes = 60 - XCTAssertEqual(config.intervalSeconds, 3600) - - config.intervalMinutes = 0 - XCTAssertEqual(config.intervalSeconds, 0) - } - - func testEquality() { - let config1 = TimerConfiguration(enabled: true, intervalSeconds: 1200) - let config2 = TimerConfiguration(enabled: true, intervalSeconds: 1200) - let config3 = TimerConfiguration(enabled: false, intervalSeconds: 1200) - let config4 = TimerConfiguration(enabled: true, intervalSeconds: 600) - - XCTAssertEqual(config1, config2) - XCTAssertNotEqual(config1, config3) - XCTAssertNotEqual(config1, config4) - } - - func testCodableEncoding() throws { - let config = TimerConfiguration(enabled: true, intervalSeconds: 1200) - let encoder = JSONEncoder() - let data = try encoder.encode(config) - - XCTAssertNotNil(data) - XCTAssertGreaterThan(data.count, 0) - } - - func testCodableDecoding() throws { - let config = TimerConfiguration(enabled: true, intervalSeconds: 1200) - let encoder = JSONEncoder() - let data = try encoder.encode(config) - - let decoder = JSONDecoder() - let decoded = try decoder.decode(TimerConfiguration.self, from: data) - - XCTAssertEqual(decoded, config) - } - - func testCodableRoundTrip() throws { - let configs = [ - TimerConfiguration(enabled: true, intervalSeconds: 300), - TimerConfiguration(enabled: false, intervalSeconds: 1200), - TimerConfiguration(enabled: true, intervalSeconds: 1800), - TimerConfiguration(enabled: false, intervalSeconds: 0) - ] - - let encoder = JSONEncoder() - let decoder = JSONDecoder() - - for config in configs { - let data = try encoder.encode(config) - let decoded = try decoder.decode(TimerConfiguration.self, from: data) - XCTAssertEqual(decoded, config) - } - } - - func testMutability() { - var config = TimerConfiguration(enabled: true, intervalSeconds: 1200) - - config.enabled = false - XCTAssertFalse(config.enabled) - - config.intervalSeconds = 600 - XCTAssertEqual(config.intervalSeconds, 600) - XCTAssertEqual(config.intervalMinutes, 10) - } - - func testZeroInterval() { - let config = TimerConfiguration(enabled: true, intervalSeconds: 0) - XCTAssertEqual(config.intervalSeconds, 0) - XCTAssertEqual(config.intervalMinutes, 0) - } - - func testLargeInterval() { - let config = TimerConfiguration(enabled: true, intervalSeconds: 86400) - XCTAssertEqual(config.intervalSeconds, 86400) - XCTAssertEqual(config.intervalMinutes, 1440) - } -} diff --git a/GazeTests/Models/TimerStateTests.swift b/GazeTests/Models/TimerStateTests.swift deleted file mode 100644 index 972e5ab..0000000 --- a/GazeTests/Models/TimerStateTests.swift +++ /dev/null @@ -1,93 +0,0 @@ -// -// TimerStateTests.swift -// GazeTests -// -// Created by Mike Freno on 1/8/26. -// - -import XCTest -@testable import Gaze - -final class TimerStateTests: XCTestCase { - - func testInitialization() { - let state = TimerState(identifier: .builtIn(.lookAway), intervalSeconds: 1200) - - XCTAssertEqual(state.identifier, .builtIn(.lookAway)) - XCTAssertEqual(state.remainingSeconds, 1200) - XCTAssertFalse(state.isPaused) - XCTAssertTrue(state.isActive) - } - - func testInitializationWithPausedState() { - let state = TimerState(identifier: .builtIn(.blink), intervalSeconds: 300, isPaused: true) - - XCTAssertEqual(state.identifier, .builtIn(.blink)) - XCTAssertEqual(state.remainingSeconds, 300) - XCTAssertTrue(state.isPaused) - XCTAssertTrue(state.isActive) - } - - func testInitializationWithInactiveState() { - let state = TimerState(identifier: .builtIn(.posture), intervalSeconds: 1800, isPaused: false, isActive: false) - - XCTAssertEqual(state.identifier, .builtIn(.posture)) - XCTAssertEqual(state.remainingSeconds, 1800) - XCTAssertFalse(state.isPaused) - XCTAssertFalse(state.isActive) - } - - func testMutability() { - var state = TimerState(identifier: .builtIn(.lookAway), intervalSeconds: 1200) - - state.remainingSeconds = 600 - XCTAssertEqual(state.remainingSeconds, 600) - - state.isPaused = true - XCTAssertTrue(state.isPaused) - - state.isActive = false - XCTAssertFalse(state.isActive) - } - - func testEquality() { - let state1 = TimerState(identifier: .builtIn(.lookAway), intervalSeconds: 1200) - let state2 = TimerState(identifier: .builtIn(.lookAway), intervalSeconds: 1200) - let state3 = TimerState(identifier: .builtIn(.blink), intervalSeconds: 1200) - let state4 = TimerState(identifier: .builtIn(.lookAway), intervalSeconds: 600) - - XCTAssertEqual(state1, state2) - XCTAssertNotEqual(state1, state3) - XCTAssertNotEqual(state1, state4) - } - - func testEqualityWithDifferentPausedState() { - let state1 = TimerState(identifier: .builtIn(.lookAway), intervalSeconds: 1200, isPaused: false) - let state2 = TimerState(identifier: .builtIn(.lookAway), intervalSeconds: 1200, isPaused: true) - - XCTAssertNotEqual(state1, state2) - } - - func testEqualityWithDifferentActiveState() { - let state1 = TimerState(identifier: .builtIn(.lookAway), intervalSeconds: 1200, isActive: true) - let state2 = TimerState(identifier: .builtIn(.lookAway), intervalSeconds: 1200, isActive: false) - - XCTAssertNotEqual(state1, state2) - } - - func testZeroRemainingSeconds() { - let state = TimerState(identifier: .builtIn(.lookAway), intervalSeconds: 0) - XCTAssertEqual(state.remainingSeconds, 0) - } - - func testNegativeRemainingSeconds() { - var state = TimerState(identifier: .builtIn(.lookAway), intervalSeconds: 10) - state.remainingSeconds = -5 - XCTAssertEqual(state.remainingSeconds, -5) - } - - func testLargeIntervalSeconds() { - let state = TimerState(identifier: .builtIn(.posture), intervalSeconds: 86400) - XCTAssertEqual(state.remainingSeconds, 86400) - } -} diff --git a/GazeTests/Models/TimerTypeTests.swift b/GazeTests/Models/TimerTypeTests.swift deleted file mode 100644 index 443e9dc..0000000 --- a/GazeTests/Models/TimerTypeTests.swift +++ /dev/null @@ -1,68 +0,0 @@ -// -// TimerTypeTests.swift -// GazeTests -// -// Created by Mike Freno on 1/8/26. -// - -import XCTest -@testable import Gaze - -final class TimerTypeTests: XCTestCase { - - func testAllCases() { - let allCases = TimerType.allCases - XCTAssertEqual(allCases.count, 3) - XCTAssertTrue(allCases.contains(.lookAway)) - XCTAssertTrue(allCases.contains(.blink)) - XCTAssertTrue(allCases.contains(.posture)) - } - - func testRawValues() { - XCTAssertEqual(TimerType.lookAway.rawValue, "lookAway") - XCTAssertEqual(TimerType.blink.rawValue, "blink") - XCTAssertEqual(TimerType.posture.rawValue, "posture") - } - - func testDisplayNames() { - XCTAssertEqual(TimerType.lookAway.displayName, "Look Away") - XCTAssertEqual(TimerType.blink.displayName, "Blink") - XCTAssertEqual(TimerType.posture.displayName, "Posture") - } - - func testIconNames() { - XCTAssertEqual(TimerType.lookAway.iconName, "eye.fill") - XCTAssertEqual(TimerType.blink.iconName, "eye.circle") - XCTAssertEqual(TimerType.posture.iconName, "figure.stand") - } - - func testIdentifiable() { - XCTAssertEqual(TimerType.lookAway.id, "lookAway") - XCTAssertEqual(TimerType.blink.id, "blink") - XCTAssertEqual(TimerType.posture.id, "posture") - } - - func testCodable() throws { - let encoder = JSONEncoder() - let decoder = JSONDecoder() - - for timerType in TimerType.allCases { - let encoded = try encoder.encode(timerType) - let decoded = try decoder.decode(TimerType.self, from: encoded) - XCTAssertEqual(decoded, timerType) - } - } - - func testEquality() { - XCTAssertEqual(TimerType.lookAway, TimerType.lookAway) - XCTAssertNotEqual(TimerType.lookAway, TimerType.blink) - XCTAssertNotEqual(TimerType.blink, TimerType.posture) - } - - func testInitFromRawValue() { - XCTAssertEqual(TimerType(rawValue: "lookAway"), .lookAway) - XCTAssertEqual(TimerType(rawValue: "blink"), .blink) - XCTAssertEqual(TimerType(rawValue: "posture"), .posture) - XCTAssertNil(TimerType(rawValue: "invalid")) - } -} diff --git a/GazeTests/Services/CameraAccessServiceTests.swift b/GazeTests/Services/CameraAccessServiceTests.swift deleted file mode 100644 index c55b3a3..0000000 --- a/GazeTests/Services/CameraAccessServiceTests.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// CameraAccessServiceTests.swift -// GazeTests -// -// Created by Mike Freno on 1/13/26. -// - -import XCTest -@testable import Gaze - -@MainActor -final class CameraAccessServiceTests: XCTestCase { - var cameraService: CameraAccessService! - - override func setUp() async throws { - cameraService = CameraAccessService.shared - } - - func testCameraServiceInitialization() { - XCTAssertNotNil(cameraService) - } - - func testCheckCameraAuthorizationStatus() { - cameraService.checkCameraAuthorizationStatus() - - XCTAssertFalse(cameraService.isCameraAuthorized || cameraService.cameraError != nil) - } - - func testIsFaceDetectionAvailable() { - let isAvailable = cameraService.isFaceDetectionAvailable() - - XCTAssertEqual(isAvailable, cameraService.isCameraAuthorized) - } -} diff --git a/GazeTests/Services/EnforceModeServiceTests.swift b/GazeTests/Services/EnforceModeServiceTests.swift deleted file mode 100644 index c4db965..0000000 --- a/GazeTests/Services/EnforceModeServiceTests.swift +++ /dev/null @@ -1,59 +0,0 @@ -// -// EnforceModeServiceTests.swift -// GazeTests -// -// Created by Mike Freno on 1/13/26. -// - -import XCTest -@testable import Gaze - -@MainActor -final class EnforceModeServiceTests: XCTestCase { - var enforceModeService: EnforceModeService! - var settingsManager: SettingsManager! - - override func setUp() async throws { - settingsManager = SettingsManager.shared - enforceModeService = EnforceModeService.shared - } - - override func tearDown() async throws { - enforceModeService.disableEnforceMode() - settingsManager.settings.enforcementMode = false - } - - func testEnforceModeServiceInitialization() { - XCTAssertNotNil(enforceModeService) - XCTAssertFalse(enforceModeService.isEnforceModeEnabled) - XCTAssertFalse(enforceModeService.isCameraActive) - XCTAssertFalse(enforceModeService.userCompliedWithBreak) - } - - func testDisableEnforceModeResetsState() { - enforceModeService.disableEnforceMode() - - XCTAssertFalse(enforceModeService.isEnforceModeEnabled) - XCTAssertFalse(enforceModeService.isCameraActive) - XCTAssertFalse(enforceModeService.userCompliedWithBreak) - } - - func testShouldEnforceBreakOnlyForLookAwayTimer() { - settingsManager.settings.enforcementMode = true - - let shouldEnforceLookAway = enforceModeService.shouldEnforceBreak(for: .builtIn(.lookAway)) - XCTAssertFalse(shouldEnforceLookAway) - - let shouldEnforceBlink = enforceModeService.shouldEnforceBreak(for: .builtIn(.blink)) - XCTAssertFalse(shouldEnforceBlink) - - let shouldEnforcePosture = enforceModeService.shouldEnforceBreak(for: .builtIn(.posture)) - XCTAssertFalse(shouldEnforcePosture) - } - - func testCheckUserComplianceWhenNotActive() { - enforceModeService.checkUserCompliance() - - XCTAssertFalse(enforceModeService.userCompliedWithBreak) - } -} diff --git a/GazeTests/Services/EyeTrackingServiceTests.swift b/GazeTests/Services/EyeTrackingServiceTests.swift deleted file mode 100644 index fff7092..0000000 --- a/GazeTests/Services/EyeTrackingServiceTests.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// EyeTrackingServiceTests.swift -// GazeTests -// -// Created by Mike Freno on 1/13/26. -// - -import XCTest -@testable import Gaze - -@MainActor -final class EyeTrackingServiceTests: XCTestCase { - var eyeTrackingService: EyeTrackingService! - - override func setUp() async throws { - eyeTrackingService = EyeTrackingService.shared - } - - override func tearDown() async throws { - eyeTrackingService.stopEyeTracking() - } - - func testEyeTrackingServiceInitialization() { - XCTAssertNotNil(eyeTrackingService) - XCTAssertFalse(eyeTrackingService.isEyeTrackingActive) - XCTAssertFalse(eyeTrackingService.isEyesClosed) - XCTAssertTrue(eyeTrackingService.userLookingAtScreen) - XCTAssertFalse(eyeTrackingService.faceDetected) - } - - func testStopEyeTrackingResetsState() { - eyeTrackingService.stopEyeTracking() - - XCTAssertFalse(eyeTrackingService.isEyeTrackingActive) - XCTAssertFalse(eyeTrackingService.isEyesClosed) - XCTAssertTrue(eyeTrackingService.userLookingAtScreen) - XCTAssertFalse(eyeTrackingService.faceDetected) - } -} diff --git a/GazeTests/Services/LaunchAtLoginManagerTests.swift b/GazeTests/Services/LaunchAtLoginManagerTests.swift deleted file mode 100644 index ea198aa..0000000 --- a/GazeTests/Services/LaunchAtLoginManagerTests.swift +++ /dev/null @@ -1,71 +0,0 @@ -// -// LaunchAtLoginManagerTests.swift -// GazeTests -// -// Created by Mike Freno on 1/8/26. -// - -import XCTest -@testable import Gaze - -final class LaunchAtLoginManagerTests: XCTestCase { - - func testIsEnabledReturnsBool() { - let isEnabled = LaunchAtLoginManager.isEnabled - XCTAssertNotNil(isEnabled) - } - - func testIsEnabledOnMacOS13AndLater() { - if #available(macOS 13.0, *) { - let isEnabled = LaunchAtLoginManager.isEnabled - XCTAssert(isEnabled == true || isEnabled == false) - } - } - - func testIsEnabledOnOlderMacOS() { - if #unavailable(macOS 13.0) { - let isEnabled = LaunchAtLoginManager.isEnabled - XCTAssertFalse(isEnabled) - } - } - - func testEnableThrowsOnUnsupportedOS() { - if #unavailable(macOS 13.0) { - XCTAssertThrowsError(try LaunchAtLoginManager.enable()) { error in - XCTAssertTrue(error is LaunchAtLoginError) - if let launchError = error as? LaunchAtLoginError { - XCTAssertEqual(launchError, .unsupportedOS) - } - } - } - } - - func testDisableThrowsOnUnsupportedOS() { - if #unavailable(macOS 13.0) { - XCTAssertThrowsError(try LaunchAtLoginManager.disable()) { error in - XCTAssertTrue(error is LaunchAtLoginError) - if let launchError = error as? LaunchAtLoginError { - XCTAssertEqual(launchError, .unsupportedOS) - } - } - } - } - - func testToggleDoesNotCrash() { - LaunchAtLoginManager.toggle() - } - - func testLaunchAtLoginErrorCases() { - let unsupportedError = LaunchAtLoginError.unsupportedOS - let registrationError = LaunchAtLoginError.registrationFailed - - XCTAssertNotEqual(unsupportedError, registrationError) - } - - func testLaunchAtLoginErrorEquality() { - let error1 = LaunchAtLoginError.unsupportedOS - let error2 = LaunchAtLoginError.unsupportedOS - - XCTAssertEqual(error1, error2) - } -} diff --git a/GazeTests/Services/MigrationManagerTests.swift b/GazeTests/Services/MigrationManagerTests.swift deleted file mode 100644 index 0bcf617..0000000 --- a/GazeTests/Services/MigrationManagerTests.swift +++ /dev/null @@ -1,137 +0,0 @@ -// -// MigrationManagerTests.swift -// GazeTests -// -// Created by Mike Freno on 1/8/26. -// - -import XCTest -@testable import Gaze - -final class MigrationManagerTests: XCTestCase { - - var migrationManager: MigrationManager! - - override func setUp() { - super.setUp() - migrationManager = MigrationManager() - UserDefaults.standard.removeObject(forKey: "app_version") - UserDefaults.standard.removeObject(forKey: "gazeAppSettings") - UserDefaults.standard.removeObject(forKey: "gazeAppSettings_backup") - } - - override func tearDown() { - UserDefaults.standard.removeObject(forKey: "app_version") - UserDefaults.standard.removeObject(forKey: "gazeAppSettings") - UserDefaults.standard.removeObject(forKey: "gazeAppSettings_backup") - super.tearDown() - } - - func testGetCurrentVersionDefaultsToZero() { - let version = migrationManager.getCurrentVersion() - XCTAssertEqual(version, "0.0.0") - } - - func testSetCurrentVersion() { - migrationManager.setCurrentVersion("1.2.3") - let version = migrationManager.getCurrentVersion() - XCTAssertEqual(version, "1.2.3") - } - - func testMigrateSettingsReturnsNilWhenNoSettings() throws { - let result = try migrationManager.migrateSettingsIfNeeded() - XCTAssertNil(result) - } - - func testMigrateSettingsReturnsExistingDataWhenUpToDate() throws { - let testData: [String: Any] = ["test": "value"] - let jsonData = try JSONSerialization.data(withJSONObject: testData) - UserDefaults.standard.set(jsonData, forKey: "gazeAppSettings") - - if let bundleVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String { - migrationManager.setCurrentVersion(bundleVersion) - } - - let result = try migrationManager.migrateSettingsIfNeeded() - XCTAssertNotNil(result) - XCTAssertEqual(result?["test"] as? String, "value") - } - - func testMigrationErrorTypes() { - let migrationFailed = MigrationError.migrationFailed("test") - let invalidData = MigrationError.invalidDataStructure - let versionMismatch = MigrationError.versionMismatch - let noBackup = MigrationError.noBackupAvailable - - switch migrationFailed { - case .migrationFailed(let message): - XCTAssertEqual(message, "test") - default: - XCTFail("Expected migrationFailed error") - } - - XCTAssertNotNil(invalidData.errorDescription) - XCTAssertNotNil(versionMismatch.errorDescription) - XCTAssertNotNil(noBackup.errorDescription) - } - - func testMigrationErrorDescriptions() { - let errors: [MigrationError] = [ - .migrationFailed("test message"), - .invalidDataStructure, - .versionMismatch, - .noBackupAvailable - ] - - for error in errors { - XCTAssertNotNil(error.errorDescription) - XCTAssertFalse(error.errorDescription!.isEmpty) - } - } - - func testVersion101MigrationTargetVersion() { - let migration = Version101Migration() - XCTAssertEqual(migration.targetVersion, "1.0.1") - } - - func testVersion101MigrationPreservesData() throws { - let migration = Version101Migration() - let testData: [String: Any] = ["key1": "value1", "key2": 42] - - let result = try migration.migrate(testData) - - XCTAssertEqual(result["key1"] as? String, "value1") - XCTAssertEqual(result["key2"] as? Int, 42) - } - - func testMigrationThrowsOnInvalidData() { - UserDefaults.standard.set(Data("invalid json".utf8), forKey: "gazeAppSettings") - migrationManager.setCurrentVersion("0.0.0") - - XCTAssertThrowsError(try migrationManager.migrateSettingsIfNeeded()) { error in - XCTAssertTrue(error is MigrationError) - } - } - - func testVersionComparison() throws { - migrationManager.setCurrentVersion("1.0.0") - let current = migrationManager.getCurrentVersion() - XCTAssertEqual(current, "1.0.0") - - migrationManager.setCurrentVersion("1.2.3") - let updated = migrationManager.getCurrentVersion() - XCTAssertEqual(updated, "1.2.3") - } - - func testBackupIsCreatedDuringMigration() throws { - let testData: [String: Any] = ["test": "backup"] - let jsonData = try JSONSerialization.data(withJSONObject: testData) - UserDefaults.standard.set(jsonData, forKey: "gazeAppSettings") - migrationManager.setCurrentVersion("0.0.0") - - _ = try? migrationManager.migrateSettingsIfNeeded() - - let backupData = UserDefaults.standard.data(forKey: "gazeAppSettings_backup") - XCTAssertNotNil(backupData) - } -} diff --git a/GazeTests/Services/UpdateManagerTests.swift b/GazeTests/Services/UpdateManagerTests.swift deleted file mode 100644 index 2a6effb..0000000 --- a/GazeTests/Services/UpdateManagerTests.swift +++ /dev/null @@ -1,148 +0,0 @@ -// -// UpdateManagerTests.swift -// GazeTests -// -// Created by Mike Freno on 1/11/26. -// - -import XCTest -import Combine -@testable import Gaze - -@MainActor -final class UpdateManagerTests: XCTestCase { - - var sut: UpdateManager! - var cancellables: Set! - - override func setUp() async throws { - try await super.setUp() - sut = UpdateManager.shared - cancellables = [] - } - - override func tearDown() async throws { - cancellables = nil - sut = nil - try await super.tearDown() - } - - // MARK: - Singleton Tests - - func testSingletonInstance() { - // Arrange & Act - let instance1 = UpdateManager.shared - let instance2 = UpdateManager.shared - - // Assert - XCTAssertTrue(instance1 === instance2, "UpdateManager should be a singleton") - } - - // MARK: - Initialization Tests - - func testInitialization() { - // Assert - XCTAssertNotNil(sut, "UpdateManager should initialize") - } - - func testInitialObservableProperties() { - // Assert - Check that properties are initialized (values may vary) - // automaticallyChecksForUpdates could be true or false based on Info.plist - // Just verify it's a valid boolean - XCTAssertTrue( - sut.automaticallyChecksForUpdates == true || sut.automaticallyChecksForUpdates == false, - "automaticallyChecksForUpdates should be a valid boolean" - ) - } - - // MARK: - Observable Property Tests - - func testAutomaticallyChecksForUpdatesIsPublished() async throws { - // Arrange - let expectation = expectation(description: "automaticallyChecksForUpdates property change observed") - var observedValue: Bool? - - // Act - Subscribe to published property - sut.$automaticallyChecksForUpdates - .dropFirst() // Skip initial value - .sink { newValue in - observedValue = newValue - expectation.fulfill() - } - .store(in: &cancellables) - - // Toggle the value (toggle to ensure change regardless of initial value) - let originalValue = sut.automaticallyChecksForUpdates - sut.automaticallyChecksForUpdates = !originalValue - - // Assert - await fulfillment(of: [expectation], timeout: 2.0) - XCTAssertNotNil(observedValue, "Should observe a value change") - XCTAssertEqual(observedValue, !originalValue, "Observed value should match the new value") - } - - func testLastUpdateCheckDateIsPublished() async throws { - // Arrange - let expectation = expectation(description: "lastUpdateCheckDate property change observed") - var observedValue: Date? - var changeDetected = false - - // Act - Subscribe to published property - sut.$lastUpdateCheckDate - .dropFirst() // Skip initial value - .sink { newValue in - observedValue = newValue - changeDetected = true - expectation.fulfill() - } - .store(in: &cancellables) - - // Set a new date - let testDate = Date(timeIntervalSince1970: 1000000) - sut.lastUpdateCheckDate = testDate - - // Assert - await fulfillment(of: [expectation], timeout: 2.0) - XCTAssertTrue(changeDetected, "Should detect property change") - XCTAssertEqual(observedValue, testDate, "Observed date should match the set date") - } - - // MARK: - Update Check Tests - - func testCheckForUpdatesDoesNotCrash() { - // Arrange - method should be callable without crash - - // Act & Assert - XCTAssertNoThrow( - sut.checkForUpdates(), - "checkForUpdates should not throw or crash" - ) - } - - func testCheckForUpdatesIsCallable() { - // Arrange - var didComplete = false - - // Act - sut.checkForUpdates() - didComplete = true - - // Assert - XCTAssertTrue(didComplete, "checkForUpdates should complete synchronously") - } - - // MARK: - Integration Tests - - func testCheckForUpdatesIsAvailableAfterInitialization() { - // Arrange & Act - // checkForUpdates should be available immediately after initialization - var didExecute = false - - // Act - Call the method - sut.checkForUpdates() - didExecute = true - - // Assert - XCTAssertTrue(didExecute, "checkForUpdates should be callable after initialization") - } -} diff --git a/GazeTests/SettingsManagerTests.swift b/GazeTests/SettingsManagerTests.swift deleted file mode 100644 index eeb1618..0000000 --- a/GazeTests/SettingsManagerTests.swift +++ /dev/null @@ -1,222 +0,0 @@ -// -// SettingsManagerTests.swift -// GazeTests -// -// Created by Mike Freno on 1/7/26. -// - -import XCTest -@testable import Gaze - -@MainActor -final class SettingsManagerTests: XCTestCase { - - var settingsManager: SettingsManager! - - override func setUp() async throws { - try await super.setUp() - settingsManager = SettingsManager.shared - // Clear any existing settings - UserDefaults.standard.removeObject(forKey: "gazeAppSettings") - settingsManager.load() - } - - override func tearDown() async throws { - UserDefaults.standard.removeObject(forKey: "gazeAppSettings") - try await super.tearDown() - } - - func testDefaultSettings() { - let defaults = AppSettings.defaults - - XCTAssertTrue(defaults.lookAwayTimer.enabled) - XCTAssertEqual(defaults.lookAwayTimer.intervalSeconds, 20 * 60) - XCTAssertEqual(defaults.lookAwayCountdownSeconds, 20) - - XCTAssertFalse(defaults.blinkTimer.enabled) - XCTAssertEqual(defaults.blinkTimer.intervalSeconds, 7 * 60) - - XCTAssertTrue(defaults.postureTimer.enabled) - XCTAssertEqual(defaults.postureTimer.intervalSeconds, 30 * 60) - - XCTAssertFalse(defaults.hasCompletedOnboarding) - XCTAssertFalse(defaults.launchAtLogin) - XCTAssertTrue(defaults.playSounds) - } - - func testSaveAndLoad() { - var settings = AppSettings.defaults - settings.lookAwayTimer.enabled = false - settings.lookAwayCountdownSeconds = 30 - settings.hasCompletedOnboarding = true - - settingsManager.settings = settings - - settingsManager.load() - - XCTAssertFalse(settingsManager.settings.lookAwayTimer.enabled) - XCTAssertEqual(settingsManager.settings.lookAwayCountdownSeconds, 30) - XCTAssertTrue(settingsManager.settings.hasCompletedOnboarding) - } - - func testTimerConfigurationRetrieval() { - let lookAwayConfig = settingsManager.timerConfiguration(for: .lookAway) - XCTAssertTrue(lookAwayConfig.enabled) - XCTAssertEqual(lookAwayConfig.intervalSeconds, 20 * 60) - - let blinkConfig = settingsManager.timerConfiguration(for: .blink) - XCTAssertFalse(blinkConfig.enabled) - XCTAssertEqual(blinkConfig.intervalSeconds, 7 * 60) - - let postureConfig = settingsManager.timerConfiguration(for: .posture) - XCTAssertTrue(postureConfig.enabled) - XCTAssertEqual(postureConfig.intervalSeconds, 30 * 60) - } - - func testUpdateTimerConfiguration() { - let newConfig = TimerConfiguration(enabled: false, intervalSeconds: 10 * 60) - settingsManager.updateTimerConfiguration(for: .lookAway, configuration: newConfig) - - let retrieved = settingsManager.timerConfiguration(for: .lookAway) - XCTAssertFalse(retrieved.enabled) - XCTAssertEqual(retrieved.intervalSeconds, 10 * 60) - } - - func testResetToDefaults() { - settingsManager.settings.lookAwayTimer.enabled = false - settingsManager.settings.hasCompletedOnboarding = true - - settingsManager.resetToDefaults() - - XCTAssertTrue(settingsManager.settings.lookAwayTimer.enabled) - XCTAssertFalse(settingsManager.settings.hasCompletedOnboarding) - } - - func testCodableEncoding() { - let settings = AppSettings.defaults - - let encoder = JSONEncoder() - let data = try? encoder.encode(settings) - - XCTAssertNotNil(data) - } - - func testCodableDecoding() { - let settings = AppSettings.defaults - - let encoder = JSONEncoder() - let data = try! encoder.encode(settings) - - let decoder = JSONDecoder() - let decoded = try? decoder.decode(AppSettings.self, from: data) - - XCTAssertNotNil(decoded) - XCTAssertEqual(decoded, settings) - } - - func testTimerConfigurationIntervalMinutes() { - var config = TimerConfiguration(enabled: true, intervalSeconds: 600) - - XCTAssertEqual(config.intervalMinutes, 10) - - config.intervalMinutes = 20 - XCTAssertEqual(config.intervalSeconds, 1200) - } - - func testSettingsAutoSaveOnChange() { - var settings = AppSettings.defaults - settings.playSounds = false - - settingsManager.settings = settings - - let savedData = UserDefaults.standard.data(forKey: "gazeAppSettings") - XCTAssertNotNil(savedData) - } - - func testMultipleTimerConfigurationUpdates() { - let config1 = TimerConfiguration(enabled: false, intervalSeconds: 600) - settingsManager.updateTimerConfiguration(for: .lookAway, configuration: config1) - - let config2 = TimerConfiguration(enabled: true, intervalSeconds: 900) - settingsManager.updateTimerConfiguration(for: .blink, configuration: config2) - - let config3 = TimerConfiguration(enabled: false, intervalSeconds: 2400) - settingsManager.updateTimerConfiguration(for: .posture, configuration: config3) - - XCTAssertEqual(settingsManager.timerConfiguration(for: .lookAway).intervalSeconds, 600) - XCTAssertEqual(settingsManager.timerConfiguration(for: .blink).intervalSeconds, 900) - XCTAssertEqual(settingsManager.timerConfiguration(for: .posture).intervalSeconds, 2400) - - XCTAssertFalse(settingsManager.timerConfiguration(for: .lookAway).enabled) - XCTAssertTrue(settingsManager.timerConfiguration(for: .blink).enabled) - XCTAssertFalse(settingsManager.timerConfiguration(for: .posture).enabled) - } - - func testSettingsPersistenceAcrossReloads() { - var settings = AppSettings.defaults - settings.lookAwayCountdownSeconds = 45 - settings.playSounds = false - - settingsManager.settings = settings - settingsManager.load() - - XCTAssertEqual(settingsManager.settings.lookAwayCountdownSeconds, 45) - XCTAssertFalse(settingsManager.settings.playSounds) - } - - func testInvalidDataDoesNotCrashLoad() { - UserDefaults.standard.set(Data("invalid".utf8), forKey: "gazeAppSettings") - - settingsManager.load() - - XCTAssertEqual(settingsManager.settings, .defaults) - } - - func testAllTimerTypesHaveConfiguration() { - for timerType in TimerType.allCases { - let config = settingsManager.timerConfiguration(for: timerType) - XCTAssertNotNil(config) - } - } - - func testUpdateTimerConfigurationPersists() { - let newConfig = TimerConfiguration(enabled: false, intervalSeconds: 7200) - settingsManager.updateTimerConfiguration(for: .posture, configuration: newConfig) - - settingsManager.load() - - let retrieved = settingsManager.timerConfiguration(for: .posture) - XCTAssertEqual(retrieved.intervalSeconds, 7200) - XCTAssertFalse(retrieved.enabled) - } - - func testResetToDefaultsClearsAllChanges() { - settingsManager.settings.lookAwayTimer.enabled = false - settingsManager.settings.lookAwayCountdownSeconds = 60 - settingsManager.settings.blinkTimer.intervalSeconds = 10 * 60 - settingsManager.settings.postureTimer.enabled = false - settingsManager.settings.hasCompletedOnboarding = true - settingsManager.settings.launchAtLogin = true - settingsManager.settings.playSounds = false - - settingsManager.resetToDefaults() - - let defaults = AppSettings.defaults - XCTAssertEqual(settingsManager.settings, defaults) - } - - func testConcurrentAccessDoesNotCrash() { - let expectation = XCTestExpectation(description: "Concurrent access") - expectation.expectedFulfillmentCount = 10 - - for i in 0..<10 { - Task { @MainActor in - let config = TimerConfiguration(enabled: true, intervalSeconds: 300 * (i + 1)) - settingsManager.updateTimerConfiguration(for: .lookAway, configuration: config) - expectation.fulfill() - } - } - - wait(for: [expectation], timeout: 2.0) - } -} diff --git a/GazeTests/TimerEngineTests.swift b/GazeTests/TimerEngineTests.swift deleted file mode 100644 index f2dd510..0000000 --- a/GazeTests/TimerEngineTests.swift +++ /dev/null @@ -1,454 +0,0 @@ -// -// TimerEngineTests.swift -// GazeTests -// -// Created by Mike Freno on 1/7/26. -// - -import XCTest -@testable import Gaze - -@MainActor -final class TimerEngineTests: XCTestCase { - - var timerEngine: TimerEngine! - var mockSettings: MockSettingsManager! - - override func setUp() async throws { - try await super.setUp() - mockSettings = MockSettingsManager() - timerEngine = TimerEngine(settingsManager: mockSettings, enforceModeService: nil) - } - - override func tearDown() async throws { - timerEngine.stop() - mockSettings = nil - try await super.tearDown() - } - - func testTimerInitialization() { - // Enable all timers for this test (blink is disabled by default) - mockSettings.enableTimer(.blink) - timerEngine.start() - - XCTAssertEqual(timerEngine.timerStates.count, 3) - XCTAssertNotNil(timerEngine.timerStates[.builtIn(.lookAway)]) - XCTAssertNotNil(timerEngine.timerStates[.builtIn(.blink)]) - XCTAssertNotNil(timerEngine.timerStates[.builtIn(.posture)]) - } - - func testDisabledTimersNotInitialized() { - // Blink is disabled by default, so we should only have 2 timers - timerEngine.start() - - XCTAssertEqual(timerEngine.timerStates.count, 2) - XCTAssertNotNil(timerEngine.timerStates[.builtIn(.lookAway)]) - XCTAssertNil(timerEngine.timerStates[.builtIn(.blink)]) - XCTAssertNotNil(timerEngine.timerStates[.builtIn(.posture)]) - } - - func testTimerStateInitialValues() { - timerEngine.start() - - let lookAwayState = timerEngine.timerStates[.builtIn(.lookAway)]! - XCTAssertEqual(lookAwayState.identifier, .builtIn(.lookAway)) - XCTAssertEqual(lookAwayState.remainingSeconds, 20 * 60) - XCTAssertFalse(lookAwayState.isPaused) - XCTAssertTrue(lookAwayState.isActive) - } - - func testPauseAllTimers() { - mockSettings.enableTimer(.blink) - timerEngine.start() - timerEngine.pause() - - for (_, state) in timerEngine.timerStates { - XCTAssertTrue(state.isPaused) - } - } - - func testResumeAllTimers() { - mockSettings.enableTimer(.blink) - timerEngine.start() - timerEngine.pause() - timerEngine.resume() - - for (_, state) in timerEngine.timerStates { - XCTAssertFalse(state.isPaused) - } - } - - func testSkipNext() { - mockSettings.setTimerInterval(.lookAway, seconds: 60) - timerEngine.start() - - timerEngine.timerStates[.builtIn(.lookAway)]?.remainingSeconds = 10 - - timerEngine.skipNext(identifier: .builtIn(.lookAway)) - - XCTAssertEqual(timerEngine.timerStates[.builtIn(.lookAway)]?.remainingSeconds, 60) - } - - func testGetTimeRemaining() { - timerEngine.start() - - let timeRemaining = timerEngine.getTimeRemaining(for: .builtIn(.lookAway)) - XCTAssertEqual(timeRemaining, TimeInterval(20 * 60)) - } - - func testGetFormattedTimeRemaining() { - timerEngine.start() - timerEngine.timerStates[.builtIn(.lookAway)]?.remainingSeconds = 125 - - let formatted = timerEngine.getFormattedTimeRemaining(for: .builtIn(.lookAway)) - XCTAssertEqual(formatted, "2:05") - } - - func testGetFormattedTimeRemainingWithHours() { - timerEngine.start() - timerEngine.timerStates[.builtIn(.lookAway)]?.remainingSeconds = 3665 - - let formatted = timerEngine.getFormattedTimeRemaining(for: .builtIn(.lookAway)) - XCTAssertEqual(formatted, "1:01:05") - } - - func testStop() { - timerEngine.start() - XCTAssertFalse(timerEngine.timerStates.isEmpty) - - timerEngine.stop() - XCTAssertTrue(timerEngine.timerStates.isEmpty) - } - - func testDismissReminderResetsTimer() { - mockSettings.enableTimer(.blink) - mockSettings.setTimerInterval(.blink, seconds: 7 * 60) - timerEngine.start() - timerEngine.timerStates[.builtIn(.blink)]?.remainingSeconds = 0 - timerEngine.activeReminder = .blinkTriggered - - timerEngine.dismissReminder() - - XCTAssertNil(timerEngine.activeReminder) - XCTAssertEqual(timerEngine.timerStates[.builtIn(.blink)]?.remainingSeconds, 7 * 60) - } - - func testDismissLookAwayResumesTimer() { - timerEngine.start() - // Trigger reminder pauses only the lookAway timer - timerEngine.triggerReminder(for: .builtIn(.lookAway)) - - XCTAssertNotNil(timerEngine.activeReminder) - XCTAssertTrue(timerEngine.isTimerPaused(.builtIn(.lookAway))) - - timerEngine.dismissReminder() - - // After dismiss, the lookAway timer should be resumed - XCTAssertFalse(timerEngine.isTimerPaused(.builtIn(.lookAway))) - } - - func testTriggerReminderForLookAway() { - timerEngine.start() - - timerEngine.triggerReminder(for: .builtIn(.lookAway)) - - XCTAssertNotNil(timerEngine.activeReminder) - if case .lookAwayTriggered(let countdown) = timerEngine.activeReminder { - XCTAssertEqual(countdown, mockSettings.settings.lookAwayCountdownSeconds) - } else { - XCTFail("Expected lookAwayTriggered reminder") - } - - // Only the triggered timer should be paused - XCTAssertTrue(timerEngine.isTimerPaused(.builtIn(.lookAway))) - } - - func testTriggerReminderForBlink() { - mockSettings.enableTimer(.blink) - timerEngine.start() - - timerEngine.triggerReminder(for: .builtIn(.blink)) - - XCTAssertNotNil(timerEngine.activeReminder) - if case .blinkTriggered = timerEngine.activeReminder { - XCTAssertTrue(true) - } else { - XCTFail("Expected blinkTriggered reminder") - } - } - - func testTriggerReminderForPosture() { - timerEngine.start() - - timerEngine.triggerReminder(for: .builtIn(.posture)) - - XCTAssertNotNil(timerEngine.activeReminder) - if case .postureTriggered = timerEngine.activeReminder { - XCTAssertTrue(true) - } else { - XCTFail("Expected postureTriggered reminder") - } - } - - func testGetTimeRemainingForNonExistentTimer() { - let timeRemaining = timerEngine.getTimeRemaining(for: .builtIn(.lookAway)) - XCTAssertEqual(timeRemaining, 0) - } - - func testGetFormattedTimeRemainingZeroSeconds() { - timerEngine.start() - timerEngine.timerStates[.builtIn(.lookAway)]?.remainingSeconds = 0 - - let formatted = timerEngine.getFormattedTimeRemaining(for: .builtIn(.lookAway)) - XCTAssertEqual(formatted, "0:00") - } - - func testGetFormattedTimeRemainingLessThanMinute() { - timerEngine.start() - timerEngine.timerStates[.builtIn(.lookAway)]?.remainingSeconds = 45 - - let formatted = timerEngine.getFormattedTimeRemaining(for: .builtIn(.lookAway)) - XCTAssertEqual(formatted, "0:45") - } - - func testGetFormattedTimeRemainingExactHour() { - timerEngine.start() - timerEngine.timerStates[.builtIn(.lookAway)]?.remainingSeconds = 3600 - - let formatted = timerEngine.getFormattedTimeRemaining(for: .builtIn(.lookAway)) - XCTAssertEqual(formatted, "1:00:00") - } - - func testMultipleStartCallsPreserveTimerState() { - // When start() is called multiple times while already running, - // it should preserve existing timer state (not reset) - timerEngine.start() - timerEngine.timerStates[.builtIn(.lookAway)]?.remainingSeconds = 100 - - timerEngine.start() - - // Timer state is preserved since interval hasn't changed - XCTAssertEqual(timerEngine.timerStates[.builtIn(.lookAway)]?.remainingSeconds, 100) - } - - func testSkipNextPreservesPausedState() { - timerEngine.start() - timerEngine.pause() - - timerEngine.skipNext(identifier: .builtIn(.lookAway)) - - XCTAssertTrue(timerEngine.timerStates[.builtIn(.lookAway)]?.isPaused ?? false) - } - - func testSkipNextPreservesActiveState() { - timerEngine.start() - - timerEngine.skipNext(identifier: .builtIn(.lookAway)) - - XCTAssertTrue(timerEngine.timerStates[.builtIn(.lookAway)]?.isActive ?? false) - } - - func testDismissReminderWithNoActiveReminder() { - timerEngine.start() - XCTAssertNil(timerEngine.activeReminder) - - timerEngine.dismissReminder() - - XCTAssertNil(timerEngine.activeReminder) - } - - func testDismissBlinkReminderResumesTimer() { - mockSettings.enableTimer(.blink) - timerEngine.start() - timerEngine.triggerReminder(for: .builtIn(.blink)) - - timerEngine.dismissReminder() - - // The blink timer should be resumed after dismissal - XCTAssertFalse(timerEngine.isTimerPaused(.builtIn(.blink))) - } - - func testDismissPostureReminderResumesTimer() { - timerEngine.start() - timerEngine.triggerReminder(for: .builtIn(.posture)) - - timerEngine.dismissReminder() - - // The posture timer should be resumed after dismissal - XCTAssertFalse(timerEngine.isTimerPaused(.builtIn(.posture))) - } - - func testAllTimersStartWhenEnabled() { - mockSettings.enableTimer(.lookAway) - mockSettings.enableTimer(.blink) - mockSettings.enableTimer(.posture) - - timerEngine.start() - - XCTAssertEqual(timerEngine.timerStates.count, 3) - for builtInTimer in TimerType.allCases { - XCTAssertNotNil(timerEngine.timerStates[.builtIn(builtInTimer)]) - } - } - - func testAllTimersDisabled() { - mockSettings.disableTimer(.lookAway) - mockSettings.disableTimer(.blink) - mockSettings.disableTimer(.posture) - - timerEngine.start() - - XCTAssertEqual(timerEngine.timerStates.count, 0) - } - - func testPartialTimersEnabled() { - mockSettings.enableTimer(.lookAway) - mockSettings.disableTimer(.blink) - mockSettings.enableTimer(.posture) - - timerEngine.start() - - XCTAssertEqual(timerEngine.timerStates.count, 2) - XCTAssertNotNil(timerEngine.timerStates[.builtIn(.lookAway)]) - XCTAssertNil(timerEngine.timerStates[.builtIn(.blink)]) - XCTAssertNotNil(timerEngine.timerStates[.builtIn(.posture)]) - } - - func testMultipleReminderTypesCanTriggerSimultaneously() { - // Setup: Create a user timer with overlay type (focus-stealing) - let overlayTimer = UserTimer( - title: "Water Break", - type: .overlay, - timeOnScreenSeconds: 10, - intervalMinutes: 1, - message: "Drink water" - ) - mockSettings.addUserTimer(overlayTimer) - - timerEngine.start() - - // Trigger an overlay reminder (look away or user timer overlay) - timerEngine.triggerReminder(for: .user(id: overlayTimer.id)) - - // Verify overlay reminder is active - XCTAssertNotNil(timerEngine.activeReminder) - if case .userTimerTriggered(let timer) = timerEngine.activeReminder { - XCTAssertEqual(timer.id, overlayTimer.id) - XCTAssertEqual(timer.type, .overlay) - } else { - XCTFail("Expected userTimerTriggered with overlay type") - } - - // Verify the overlay timer is paused - XCTAssertTrue(timerEngine.isTimerPaused(.user(id: overlayTimer.id))) - - // Now trigger a subtle reminder (blink) while overlay is still active - timerEngine.triggerReminder(for: .builtIn(.blink)) - - // The activeReminder should be replaced with the blink reminder - // This is expected behavior - TimerEngine only tracks one activeReminder - XCTAssertNotNil(timerEngine.activeReminder) - if case .blinkTriggered = timerEngine.activeReminder { - XCTAssertTrue(true) - } else { - XCTFail("Expected blinkTriggered reminder") - } - - // Both timers should be paused (the one that triggered their reminder) - XCTAssertTrue(timerEngine.isTimerPaused(.user(id: overlayTimer.id))) - XCTAssertTrue(timerEngine.isTimerPaused(.builtIn(.blink))) - } - - func testOverlayReminderDoesNotBlockSubtleReminders() { - // Setup overlay user timer - let overlayTimer = UserTimer( - title: "Stand Up", - type: .overlay, - timeOnScreenSeconds: 10, - intervalMinutes: 1 - ) - mockSettings.addUserTimer(overlayTimer) - mockSettings.enableTimer(.blink) - mockSettings.setTimerInterval(.blink, seconds: 60) - - timerEngine.start() - - // Trigger overlay reminder first - timerEngine.triggerReminder(for: .user(id: overlayTimer.id)) - XCTAssertNotNil(timerEngine.activeReminder) - XCTAssertTrue(timerEngine.isTimerPaused(.user(id: overlayTimer.id))) - - // Trigger subtle reminder while overlay is active - timerEngine.triggerReminder(for: .builtIn(.blink)) - - // The blink reminder should now be active - if case .blinkTriggered = timerEngine.activeReminder { - XCTAssertTrue(true) - } else { - XCTFail("Expected blinkTriggered reminder") - } - - // Both timers should be paused - XCTAssertTrue(timerEngine.isTimerPaused(.user(id: overlayTimer.id))) - XCTAssertTrue(timerEngine.isTimerPaused(.builtIn(.blink))) - - // Dismiss the blink reminder - timerEngine.dismissReminder() - - // After dismissing blink, the reminder should be cleared - XCTAssertNil(timerEngine.activeReminder) - - // Blink timer should be reset and resumed - XCTAssertFalse(timerEngine.isTimerPaused(.builtIn(.blink))) - XCTAssertEqual(timerEngine.timerStates[.builtIn(.blink)]?.remainingSeconds, 60) - - // The overlay timer should still be paused - XCTAssertTrue(timerEngine.isTimerPaused(.user(id: overlayTimer.id))) - } - - // MARK: - Tests using injectable time provider - - func testTimerEngineWithMockTimeProvider() { - let mockTime = MockTimeProvider(startTime: Date()) - let engine = TimerEngine( - settingsManager: mockSettings, - enforceModeService: nil, - timeProvider: mockTime - ) - - engine.start() - XCTAssertNotNil(engine.timerStates[.builtIn(.lookAway)]) - - engine.stop() - } - - func testSystemSleepWakeWithMockTime() { - let startDate = Date() - let mockTime = MockTimeProvider(startTime: startDate) - let engine = TimerEngine( - settingsManager: mockSettings, - enforceModeService: nil, - timeProvider: mockTime - ) - - engine.start() - let initialRemaining = engine.timerStates[.builtIn(.lookAway)]?.remainingSeconds ?? 0 - - // Simulate sleep - engine.handleSystemSleep() - XCTAssertTrue(engine.isTimerPaused(.builtIn(.lookAway))) - - // Advance mock time by 5 minutes - mockTime.advance(by: 300) - - // Simulate wake - engine.handleSystemWake() - - // Timer should resume and have adjusted remaining time - XCTAssertFalse(engine.isTimerPaused(.builtIn(.lookAway))) - let newRemaining = engine.timerStates[.builtIn(.lookAway)]?.remainingSeconds ?? 0 - XCTAssertEqual(newRemaining, initialRemaining - 300) - - engine.stop() - } -} diff --git a/GazeUITests/AccessibilityUITests.swift b/GazeUITests/AccessibilityUITests.swift deleted file mode 100644 index 0d54c6a..0000000 --- a/GazeUITests/AccessibilityUITests.swift +++ /dev/null @@ -1,86 +0,0 @@ -// -// AccessibilityUITests.swift -// GazeUITests -// -// Created by Mike Freno on 1/8/26. -// - -import XCTest - -@MainActor -final class AccessibilityUITests: XCTestCase { - - var app: XCUIApplication! - - override func setUpWithError() throws { - continueAfterFailure = false - app = XCUIApplication() - app.launchArguments.append("--skip-onboarding") - app.launch() - } - - override func tearDownWithError() throws { - app = nil - } - - func testMenuBarAccessibilityLabels() throws { - let menuBar = app.menuBarItems.firstMatch - if menuBar.waitForExistence(timeout: 5) { - menuBar.click() - - for button in app.buttons.allElementsBoundByIndex { - XCTAssertFalse(button.label.isEmpty, "Button should have accessibility label") - } - } - } - - func testKeyboardNavigation() throws { - let menuBar = app.menuBarItems.firstMatch - if menuBar.waitForExistence(timeout: 5) { - menuBar.click() - - app.typeKey(XCUIKeyboardKey.tab, modifierFlags: []) - - let focusedElement = app.descendants(matching: .any).element(matching: NSPredicate(format: "hasKeyboardFocus == true")) - XCTAssertTrue(focusedElement.exists || app.buttons.count > 0) - } - } - - func testAllButtonsAreHittable() throws { - let menuBar = app.menuBarItems.firstMatch - if menuBar.waitForExistence(timeout: 5) { - menuBar.click() - - sleep(1) - - let buttons = app.buttons.allElementsBoundByIndex - for button in buttons where button.exists && button.isEnabled { - XCTAssertTrue(button.isHittable || !button.isEnabled, "Enabled button should be hittable: \(button.label)") - } - } - } - - func testVoiceOverElementsHaveLabels() throws { - let menuBar = app.menuBarItems.firstMatch - if menuBar.waitForExistence(timeout: 5) { - menuBar.click() - - let staticTexts = app.staticTexts.allElementsBoundByIndex - for text in staticTexts where text.exists { - XCTAssertFalse(text.label.isEmpty, "Static text should have label") - } - } - } - - func testImagesHaveAccessibilityTraits() throws { - let menuBar = app.menuBarItems.firstMatch - if menuBar.waitForExistence(timeout: 5) { - menuBar.click() - - let images = app.images.allElementsBoundByIndex - for image in images where image.exists { - XCTAssertFalse(image.label.isEmpty, "Image should have accessibility label") - } - } - } -} diff --git a/GazeUITests/EnhancedOnboardingUITests.swift b/GazeUITests/EnhancedOnboardingUITests.swift deleted file mode 100644 index e756cc0..0000000 --- a/GazeUITests/EnhancedOnboardingUITests.swift +++ /dev/null @@ -1,175 +0,0 @@ -// -// EnhancedOnboardingUITests.swift -// GazeUITests -// -// Created by Gaze Team on 1/13/26. -// - -import XCTest - -@MainActor -final class EnhancedOnboardingUITests: XCTestCase { - - var app: XCUIApplication! - - override func setUpWithError() throws { - continueAfterFailure = false - app = XCUIApplication() - app.launchArguments.append("--reset-onboarding") - app.launch() - } - - override func tearDownWithError() throws { - app = nil - } - - func testOnboardingCompleteFlowWithUserTimers() throws { - // Navigate through the complete onboarding flow - let continueButtons = app.buttons.matching(identifier: "Continue") - let nextButtons = app.buttons.matching(identifier: "Next") - - var currentStep = 0 - let maxSteps = 15 - - while currentStep < maxSteps { - if continueButtons.firstMatch.exists && continueButtons.firstMatch.isHittable { - continueButtons.firstMatch.tap() - currentStep += 1 - sleep(1) - } else if nextButtons.firstMatch.exists && nextButtons.firstMatch.isHittable { - nextButtons.firstMatch.tap() - currentStep += 1 - sleep(1) - } else if app.buttons["Get Started"].exists { - app.buttons["Get Started"].tap() - break - } else if app.buttons["Done"].exists { - app.buttons["Done"].tap() - break - } else { - break - } - } - - // Verify onboarding completed successfully - XCTAssertLessThan(currentStep, maxSteps, "Onboarding flow should complete") - - // Verify main application UI is visible (menubar should be active) - XCTAssertTrue(app.menuBarItems.firstMatch.exists, "Menubar should be available after onboarding") - } - - func testUserTimerCreationInOnboarding() throws { - // Reset to fresh onboarding state - app.terminate() - app = XCUIApplication() - app.launchArguments.append("--reset-onboarding") - app.launch() - - // Navigate to user timer setup section (assumes it's at the end) - let continueButtons = app.buttons.matching(identifier: "Continue") - let nextButtons = app.buttons.matching(identifier: "Next") - - // Skip through initial screens - var currentStep = 0 - while currentStep < 8 && (continueButtons.firstMatch.exists || nextButtons.firstMatch.exists) { - if continueButtons.firstMatch.exists && continueButtons.firstMatch.isHittable { - continueButtons.firstMatch.tap() - currentStep += 1 - sleep(1) - } else if nextButtons.firstMatch.exists && nextButtons.firstMatch.isHittable { - nextButtons.firstMatch.tap() - currentStep += 1 - sleep(1) - } - } - - // Look for timer creation UI or related elements - let timerSetupElement = app.staticTexts.containing(NSPredicate(format: "label CONTAINS 'Timer' OR label CONTAINS 'Custom'")).firstMatch - XCTAssertTrue(timerSetupElement.exists, "User timer setup section should be available during onboarding") - - // If we can create a timer in onboarding, test that flow - if app.buttons["Add Timer"].exists { - app.buttons["Add Timer"].tap() - - // Fill out timer details - this would be specific to the actual UI structure - let titleField = app.textFields["Timer Title"] - if titleField.exists { - titleField.typeText("Test Timer") - } - - let intervalField = app.textFields["Interval (minutes)"] - if intervalField.exists { - intervalField.typeText("10") - } - - // Submit the timer - app.buttons["Save"].tap() - } - } - - func testSettingsPersistenceAfterOnboarding() throws { - // Reset to fresh onboarding state - app.terminate() - app = XCUIApplication() - app.launchArguments.append("--reset-onboarding") - app.launch() - - // Complete onboarding flow - let continueButtons = app.buttons.matching(identifier: "Continue") - let nextButtons = app.buttons.matching(identifier: "Next") - - while continueButtons.firstMatch.exists || nextButtons.firstMatch.exists { - if continueButtons.firstMatch.exists && continueButtons.firstMatch.isHittable { - continueButtons.firstMatch.tap() - sleep(1) - } else if nextButtons.firstMatch.exists && nextButtons.firstMatch.isHittable { - nextButtons.firstMatch.tap() - sleep(1) - } - } - - // Get to the end and complete onboarding - app.buttons["Get Started"].tap() - - // Verify that settings are properly initialized - let menuBar = app.menuBarItems.firstMatch - XCTAssertTrue(menuBar.exists, "Menubar should exist after onboarding") - - // Re-launch the app to verify settings persistence - app.terminate() - let newApp = XCUIApplication() - newApp.launchArguments.append("--skip-onboarding") - newApp.launch() - - XCTAssertTrue(newApp.menuBarItems.firstMatch.exists, "Application should maintain state after restart") - newApp.terminate() - } - - func testOnboardingNavigationEdgeCases() throws { - // Test that navigation buttons work properly at each step - let continueButton = app.buttons["Continue"] - if continueButton.waitForExistence(timeout: 2) { - continueButton.tap() - - // Verify we moved to the next screen - let nextScreen = app.staticTexts.containing(NSPredicate(format: "label CONTAINS 'Setup' OR label CONTAINS 'Configure'")).firstMatch - XCTAssertTrue(nextScreen.exists, "Should navigate to next screen on Continue") - } - - // Test back navigation - let backButton = app.buttons["Back"] - if backButton.waitForExistence(timeout: 1) { - backButton.tap() - - // Should return to previous screen - XCTAssertTrue(app.staticTexts.containing(NSPredicate(format: "label CONTAINS 'Welcome'")).firstMatch.exists) - } - - // Test that we can go forward again - let continueButton2 = app.buttons["Continue"] - if continueButton2.waitForExistence(timeout: 1) { - continueButton2.tap() - XCTAssertTrue(app.staticTexts.containing(NSPredicate(format: "label CONTAINS 'Setup'")).firstMatch.exists) - } - } -} \ No newline at end of file diff --git a/GazeUITests/GazeUITests.swift b/GazeUITests/ExampleUITests.swift similarity index 63% rename from GazeUITests/GazeUITests.swift rename to GazeUITests/ExampleUITests.swift index c3c34fe..372e317 100644 --- a/GazeUITests/GazeUITests.swift +++ b/GazeUITests/ExampleUITests.swift @@ -1,13 +1,13 @@ // -// GazeUITests.swift -// GazeUITests +// ExampleUITests.swift +// Gaze // -// Created by Mike Freno on 1/7/26. +// Created by AI Assistant on 1/15/26. // import XCTest -final class GazeUITests: XCTestCase { +final class ExampleUITests: XCTestCase { override func setUpWithError() throws { // Put setup code here. This method is called before the invocation of each test method in the class. @@ -15,7 +15,7 @@ final class GazeUITests: XCTestCase { // In UI tests it is usually best to stop immediately when a failure occurs. continueAfterFailure = false - // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. + // In UI tests it's important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. } override func tearDownWithError() throws { @@ -23,12 +23,17 @@ final class GazeUITests: XCTestCase { } @MainActor - func testExample() throws { + func testExampleOfUITesting() throws { // UI tests must launch the application that they test. let app = XCUIApplication() app.launch() // Use XCTAssert and related functions to verify your tests produce the correct results. + // For example: + // XCTAssertEqual(app.windows.count, 1) + // XCTAssertTrue(app.buttons["Start"].exists) + + XCTAssertTrue(true, "UI testing example - this would verify UI elements") } @MainActor @@ -38,4 +43,4 @@ final class GazeUITests: XCTestCase { XCUIApplication().launch() } } -} +} \ No newline at end of file diff --git a/GazeUITests/GazeUITestsLaunchTests.swift b/GazeUITests/GazeUITestsLaunchTests.swift deleted file mode 100644 index 4757c60..0000000 --- a/GazeUITests/GazeUITestsLaunchTests.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// GazeUITestsLaunchTests.swift -// GazeUITests -// -// Created by Mike Freno on 1/7/26. -// - -import XCTest - -final class GazeUITestsLaunchTests: XCTestCase { - - override class var runsForEachTargetApplicationUIConfiguration: Bool { - true - } - - override func setUpWithError() throws { - continueAfterFailure = false - } - - @MainActor - func testLaunch() throws { - let app = XCUIApplication() - app.launch() - - // Insert steps here to perform after app launch but before taking a screenshot, - // such as logging into a test account or navigating somewhere in the app - - let attachment = XCTAttachment(screenshot: app.screenshot()) - attachment.name = "Launch Screen" - attachment.lifetime = .keepAlways - add(attachment) - } -} diff --git a/GazeUITests/MenuBarUITests.swift b/GazeUITests/MenuBarUITests.swift deleted file mode 100644 index 472feca..0000000 --- a/GazeUITests/MenuBarUITests.swift +++ /dev/null @@ -1,136 +0,0 @@ -// -// MenuBarUITests.swift -// GazeUITests -// -// Created by Mike Freno on 1/8/26. -// - -import XCTest - -@MainActor -final class MenuBarUITests: XCTestCase { - - var app: XCUIApplication! - - override func setUpWithError() throws { - continueAfterFailure = false - app = XCUIApplication() - app.launchArguments.append("--skip-onboarding") - app.launch() - } - - override func tearDownWithError() throws { - app = nil - } - - func testMenuBarExtraExists() throws { - let menuBar = app.menuBarItems.firstMatch - XCTAssertTrue(menuBar.waitForExistence(timeout: 5)) - } - - func testMenuBarCanBeOpened() throws { - let menuBar = app.menuBarItems.firstMatch - if menuBar.waitForExistence(timeout: 5) { - menuBar.click() - - let gazeTitle = app.staticTexts["Gaze"] - XCTAssertTrue(gazeTitle.waitForExistence(timeout: 2) || app.staticTexts.count > 0) - } - } - - func testMenuBarHasTimerStatus() throws { - let menuBar = app.menuBarItems.firstMatch - if menuBar.waitForExistence(timeout: 5) { - menuBar.click() - - let activeTimersText = app.staticTexts["Active Timers"] - let hasTimerInfo = activeTimersText.exists || app.staticTexts.count > 3 - - XCTAssertTrue(hasTimerInfo) - } - } - - func testMenuBarHasPauseResumeControl() throws { - let menuBar = app.menuBarItems.firstMatch - if menuBar.waitForExistence(timeout: 5) { - menuBar.click() - - let pauseButton = app.buttons.containing(NSPredicate(format: "label CONTAINS 'Pause' OR label CONTAINS 'Resume'")).firstMatch - XCTAssertTrue(pauseButton.waitForExistence(timeout: 2)) - } - } - - func testMenuBarHasSettingsButton() throws { - let menuBar = app.menuBarItems.firstMatch - if menuBar.waitForExistence(timeout: 5) { - menuBar.click() - - let settingsButton = app.buttons["Settings"] - let settingsMenuItem = app.menuItems["Settings"] - - XCTAssertTrue(settingsButton.exists || settingsMenuItem.exists) - } - } - - func testMenuBarHasQuitButton() throws { - let menuBar = app.menuBarItems.firstMatch - if menuBar.waitForExistence(timeout: 5) { - menuBar.click() - - let quitButton = app.buttons.containing(NSPredicate(format: "label CONTAINS 'Quit'")).firstMatch - XCTAssertTrue(quitButton.waitForExistence(timeout: 2)) - } - } - - func testPauseResumeToggle() throws { - let menuBar = app.menuBarItems.firstMatch - if menuBar.waitForExistence(timeout: 5) { - menuBar.click() - - let pauseButton = app.buttons.containing(NSPredicate(format: "label CONTAINS 'Pause'")).firstMatch - let resumeButton = app.buttons.containing(NSPredicate(format: "label CONTAINS 'Resume'")).firstMatch - - if pauseButton.exists && pauseButton.isHittable { - pauseButton.tap() - XCTAssertTrue(resumeButton.waitForExistence(timeout: 2)) - } else if resumeButton.exists && resumeButton.isHittable { - resumeButton.tap() - XCTAssertTrue(pauseButton.waitForExistence(timeout: 2)) - } - } - } - - func testCompleteOnboardingButtonVisibleWhenOnboardingIncomplete() throws { - // Relaunch app without skip-onboarding flag - app.terminate() - let newApp = XCUIApplication() - newApp.launchArguments.append("--reset-onboarding") - newApp.launch() - - let menuBar = newApp.menuBarItems.firstMatch - if menuBar.waitForExistence(timeout: 5) { - menuBar.click() - - let completeOnboardingButton = newApp.buttons["Complete Onboarding"] - XCTAssertTrue( - completeOnboardingButton.waitForExistence(timeout: 2), - "Complete Onboarding button should be visible when onboarding is incomplete" - ) - } - - newApp.terminate() - } - - func testCompleteOnboardingButtonNotVisibleWhenOnboardingComplete() throws { - let menuBar = app.menuBarItems.firstMatch - if menuBar.waitForExistence(timeout: 5) { - menuBar.click() - - let completeOnboardingButton = app.buttons["Complete Onboarding"] - XCTAssertFalse( - completeOnboardingButton.exists, - "Complete Onboarding button should not be visible when onboarding is complete" - ) - } - } -} diff --git a/GazeUITests/OnboardingUITests.swift b/GazeUITests/OnboardingUITests.swift deleted file mode 100644 index aafb009..0000000 --- a/GazeUITests/OnboardingUITests.swift +++ /dev/null @@ -1,92 +0,0 @@ -// -// OnboardingUITests.swift -// GazeUITests -// -// Created by Mike Freno on 1/8/26. -// - -import XCTest - -@MainActor -final class OnboardingUITests: XCTestCase { - - var app: XCUIApplication! - - override func setUpWithError() throws { - continueAfterFailure = false - app = XCUIApplication() - app.launchArguments.append("--reset-onboarding") - app.launch() - } - - override func tearDownWithError() throws { - app = nil - } - - func testOnboardingWelcomeScreen() throws { - XCTAssertTrue(app.staticTexts["Welcome to Gaze"].exists || app.staticTexts.containing(NSPredicate(format: "label CONTAINS 'Welcome'")).firstMatch.exists) - } - - func testOnboardingNavigationFromWelcome() throws { - let continueButton = app.buttons["Continue"] - - if continueButton.waitForExistence(timeout: 2) { - continueButton.tap() - - let nextScreen = app.staticTexts.containing(NSPredicate(format: "label CONTAINS 'Setup' OR label CONTAINS 'Configure'")).firstMatch - XCTAssertTrue(nextScreen.waitForExistence(timeout: 2)) - } - } - - func testOnboardingBackNavigation() throws { - let continueButton = app.buttons["Continue"] - if continueButton.waitForExistence(timeout: 2) { - continueButton.tap() - - let backButton = app.buttons["Back"] - if backButton.waitForExistence(timeout: 1) { - backButton.tap() - - XCTAssertTrue(app.staticTexts.containing(NSPredicate(format: "label CONTAINS 'Welcome'")).firstMatch.waitForExistence(timeout: 1)) - } - } - } - - func testOnboardingCompleteFlow() throws { - let continueButtons = app.buttons.matching(identifier: "Continue") - let nextButtons = app.buttons.matching(identifier: "Next") - - var currentStep = 0 - let maxSteps = 10 - - while currentStep < maxSteps { - if continueButtons.firstMatch.exists && continueButtons.firstMatch.isHittable { - continueButtons.firstMatch.tap() - currentStep += 1 - sleep(1) - } else if nextButtons.firstMatch.exists && nextButtons.firstMatch.isHittable { - nextButtons.firstMatch.tap() - currentStep += 1 - sleep(1) - } else if app.buttons["Get Started"].exists { - app.buttons["Get Started"].tap() - break - } else if app.buttons["Done"].exists { - app.buttons["Done"].tap() - break - } else { - break - } - } - - XCTAssertLessThan(currentStep, maxSteps, "Onboarding flow should complete") - } - - func testOnboardingHasRequiredElements() throws { - let hasText = app.staticTexts.count > 0 - let hasButtons = app.buttons.count > 0 - - XCTAssertTrue(hasText, "Onboarding should have text elements") - XCTAssertTrue(hasButtons, "Onboarding should have buttons") - } -} diff --git a/GazeUITests/OverlayReminderUITests.swift b/GazeUITests/OverlayReminderUITests.swift deleted file mode 100644 index 23d313f..0000000 --- a/GazeUITests/OverlayReminderUITests.swift +++ /dev/null @@ -1,260 +0,0 @@ -// -// OverlayReminderUITests.swift -// GazeUITests -// -// Created by OpenCode on 1/13/26. -// - -import XCTest - -/// Comprehensive UI tests for overlay and reminder system -/// -/// NOTE: macOS MenuBarExtra UI testing limitations: -/// - MenuBarExtras created with MenuBarExtra {} don't reliably appear in XCUITest accessibility hierarchy -/// - This is a known limitation of XCUITest with SwiftUI MenuBarExtra -/// - Therefore, these tests focus on what can be tested: window lifecycle, dismissal, and cleanup -/// -/// These tests verify: -/// - No overlays get stuck on screen -/// - Window cleanup happens properly -/// - App remains responsive after overlay cycles -@MainActor -final class OverlayReminderUITests: XCTestCase { - - var app: XCUIApplication! - - override func setUpWithError() throws { - continueAfterFailure = false - app = XCUIApplication() - app.launchArguments.append("--skip-onboarding") - app.launchArguments.append("--ui-testing") - app.launch() - - // Wait for app to be ready - sleep(UInt32(2)) - } - - override func tearDownWithError() throws { - // Ensure app is terminated cleanly - app.terminate() - app = nil - } - - // MARK: - Helper Methods - - /// Verifies that no overlay is currently visible - private func verifyNoOverlay() { - let overlayTexts = ["Look Away", "Blink", "Posture", "User Reminder"] - - for text in overlayTexts { - XCTAssertFalse( - app.staticTexts[text].exists, - "Overlay '\(text)' should not be visible" - ) - } - } - - /// Counts the number of windows - private func countWindows() -> Int { - return app.windows.count - } - - // MARK: - App Lifecycle Tests - - func testAppLaunchesSuccessfully() throws { - // Basic test to ensure app launches and is responsive - XCTAssertTrue(app.exists, "App should launch successfully") - - // Verify no stuck overlays from previous sessions - verifyNoOverlay() - } - - func testAppRemainsResponsiveAfterLaunch() throws { - // Wait a bit and verify app didn't crash - sleep(UInt32(3)) - - XCTAssertTrue(app.exists, "App should remain running") - - // Verify no unexpected overlays appeared - verifyNoOverlay() - } - - func testNoStuckWindowsAfterAppLaunch() throws { - let initialWindowCount = countWindows() - - // Wait to ensure no delayed windows appear - sleep(UInt32(5)) - - let finalWindowCount = countWindows() - - // Window count should be stable (menu bar doesn't create visible windows) - XCTAssertLessThanOrEqual( - finalWindowCount, - initialWindowCount + 1, // Allow for menu bar if it appears - "No unexpected windows should appear after launch" - ) - - verifyNoOverlay() - } - - // MARK: - Window Lifecycle Tests - - func testWindowCleanupVerification() throws { - let initialWindowCount = countWindows() - - // Let the app run for a while - sleep(UInt32(10)) - - let finalWindowCount = countWindows() - - // Ensure window count hasn't grown unexpectedly - XCTAssertLessThanOrEqual( - finalWindowCount, - initialWindowCount + 2, // Allow some leeway for system windows - "Window count should remain stable during normal operation" - ) - } - - // MARK: - Overlay Detection Tests - - func testNoOverlaysAppearWithoutTrigger() throws { - // Run for a period and ensure no overlays appear - // (with our UI testing timers disabled or set to very long intervals) - - for i in 1...5 { - print("Checking for stuck overlays - iteration \(i)/5") - sleep(UInt32(2)) - verifyNoOverlay() - } - - print("✅ No stuck overlays detected during test period") - } - - func testAppStabilityOverTime() throws { - // Extended stability test - run for 30 seconds - let testDuration: Int = 30 - let checkInterval: Int = 5 - let iterations = testDuration / checkInterval - - for i in 1...iterations { - print("Stability check \(i)/\(iterations)") - sleep(UInt32(checkInterval)) - - XCTAssertTrue(app.exists, "App should continue running") - verifyNoOverlay() - } - - print("✅ App remained stable for \(testDuration) seconds") - } - - // MARK: - Regression Tests - - func testNoStuckOverlaysAfterAppStart() throws { - // This test specifically checks for the bug where overlays don't dismiss - - // Wait for initial app startup - sleep(UInt32(3)) - - verifyNoOverlay() - - // Check multiple times to ensure stability - for i in 1...10 { - sleep(UInt32(1)) - verifyNoOverlay() - - if i % 3 == 0 { - print("No stuck overlays detected - check \(i)/10") - } - } - - XCTAssertTrue( - app.exists, - "App should still be running after extended monitoring" - ) - } - - // MARK: - Documentation Tests - - func testDocumentedLimitations() throws { - // This test documents the UI testing limitations we discovered - - print(""" - - ==================== UI Testing Limitations ==================== - - MenuBarExtra Accessibility: - - SwiftUI MenuBarExtra items don't reliably appear in XCUITest - - This is a known Apple limitation as of macOS 13+ - - MenuBarItem queries return system menu bars (Apple, etc.) not app extras - - Workarounds Attempted: - - Searching by index (unreliable, system dependent) - - Using accessibility identifiers (not exposed for MenuBarExtra) - - Iterating through menu bar items (finds wrong items) - - What We Can Test: - - App launch and stability - - Window lifecycle and cleanup - - No stuck overlays appear unexpectedly - - App remains responsive - - What Requires Manual Testing: - - Overlay appearance when triggered - - ESC/Space/Button dismissal methods - - Countdown functionality - - Rapid trigger/dismiss cycles - - Multiple reminder types in sequence - - Recommendation: - - Use unit tests for TimerEngine logic - - Use integration tests for reminder triggering - - Use manual testing for UI overlay behavior - - Use these UI tests for regression detection of stuck overlays - - ================================================================ - - """) - - XCTAssertTrue(true, "Limitations documented") - } -} - -// MARK: - Manual Test Checklist -/* - Manual testing checklist for overlay reminders: - - Look Away Overlay: - ☐ Appears when triggered - ☐ Shows countdown - ☐ Dismisses with ESC key - ☐ Dismisses with Space key - ☐ Dismisses with X button - ☐ Auto-dismisses after countdown - ☐ Doesn't appear when timers paused - - User Timer Overlay: - ☐ Appears when triggered - ☐ Shows custom message - ☐ Shows correct color - ☐ Dismisses properly with all methods - - Subtle Reminders (Blink, Posture, User Timer Subtle): - ☐ Appear in corner - ☐ Auto-dismiss after 3 seconds - ☐ Don't block UI interaction - - Edge Cases: - ☐ Rapid triggering (10x in a row) - ☐ Trigger while countdown active - ☐ Trigger while paused - ☐ System sleep during overlay - ☐ Multiple monitors - ☐ Window cleanup after dismissal - - Regression: - ☐ No overlays get stuck on screen - ☐ All dismissal methods work reliably - ☐ Window count returns to baseline after dismissal - ☐ App remains responsive after many overlay cycles - */ - diff --git a/GazeUITests/PerformanceUITests.swift b/GazeUITests/PerformanceUITests.swift deleted file mode 100644 index f6de7ea..0000000 --- a/GazeUITests/PerformanceUITests.swift +++ /dev/null @@ -1,88 +0,0 @@ -// -// PerformanceUITests.swift -// GazeUITests -// -// Created by Mike Freno on 1/8/26. -// - -import XCTest - -@MainActor -final class PerformanceUITests: XCTestCase { - - var app: XCUIApplication! - - override func setUpWithError() throws { - continueAfterFailure = false - app = XCUIApplication() - app.launchArguments.append("--skip-onboarding") - } - - override func tearDownWithError() throws { - app = nil - } - - func testAppLaunchPerformance() throws { - measure(metrics: [XCTApplicationLaunchMetric()]) { - app.launch() - app.terminate() - } - } - - func testMenuBarOpenPerformance() throws { - app.launch() - - measure { - let menuBar = app.menuBarItems.firstMatch - if menuBar.waitForExistence(timeout: 5) { - menuBar.click() - _ = app.staticTexts["Gaze"].waitForExistence(timeout: 2) - } - } - } - - func testSettingsWindowOpenPerformance() throws { - app.launch() - - let menuBar = app.menuBarItems.firstMatch - if menuBar.waitForExistence(timeout: 5) { - menuBar.click() - - measure { - let settingsButton = app.menuItems["Settings"] - if settingsButton.waitForExistence(timeout: 2) { - settingsButton.click() - - let settingsWindow = app.windows["Settings"] - _ = settingsWindow.waitForExistence(timeout: 3) - - if settingsWindow.exists { - let closeButton = settingsWindow.buttons[XCUIIdentifierCloseWindow] - if closeButton.exists { - closeButton.click() - } - } - } - - menuBar.click() - } - } - } - - func testMemoryUsageDuringOperation() throws { - app.launch() - - let menuBar = app.menuBarItems.firstMatch - if menuBar.waitForExistence(timeout: 5) { - measure(metrics: [XCTMemoryMetric()]) { - for _ in 0..<5 { - menuBar.click() - sleep(1) - - menuBar.click() - sleep(1) - } - } - } - } -} diff --git a/GazeUITests/SettingsUITests.swift b/GazeUITests/SettingsUITests.swift deleted file mode 100644 index 0032181..0000000 --- a/GazeUITests/SettingsUITests.swift +++ /dev/null @@ -1,83 +0,0 @@ -// -// SettingsUITests.swift -// GazeUITests -// -// Created by Mike Freno on 1/8/26. -// - -import XCTest - -@MainActor -final class SettingsUITests: XCTestCase { - - var app: XCUIApplication! - - override func setUpWithError() throws { - continueAfterFailure = false - app = XCUIApplication() - app.launchArguments.append("--skip-onboarding") - app.launch() - } - - override func tearDownWithError() throws { - app = nil - } - - func testOpenSettingsWindow() throws { - let menuBar = app.menuBarItems.firstMatch - if menuBar.exists { - menuBar.click() - - let settingsButton = app.menuItems["Settings"] - if settingsButton.waitForExistence(timeout: 2) { - settingsButton.click() - - let settingsWindow = app.windows["Settings"] - XCTAssertTrue(settingsWindow.waitForExistence(timeout: 3)) - } - } - } - - func testSettingsWindowHasTimerControls() throws { - let menuBar = app.menuBarItems.firstMatch - if menuBar.exists { - menuBar.click() - - let settingsButton = app.menuItems["Settings"] - if settingsButton.waitForExistence(timeout: 2) { - settingsButton.click() - - sleep(1) - - let hasSliders = app.sliders.count > 0 - let hasTextFields = app.textFields.count > 0 - let hasSwitches = app.switches.count > 0 - - let hasControls = hasSliders || hasTextFields || hasSwitches - XCTAssertTrue(hasControls, "Settings should have timer controls") - } - } - } - - func testSettingsWindowCanBeClosed() throws { - let menuBar = app.menuBarItems.firstMatch - if menuBar.exists { - menuBar.click() - - let settingsButton = app.menuItems["Settings"] - if settingsButton.waitForExistence(timeout: 2) { - settingsButton.click() - - let settingsWindow = app.windows["Settings"] - if settingsWindow.waitForExistence(timeout: 3) { - let closeButton = settingsWindow.buttons[XCUIIdentifierCloseWindow] - if closeButton.exists { - closeButton.click() - - XCTAssertFalse(settingsWindow.exists) - } - } - } - } - } -}