reset: prep for new test suite
This commit is contained in:
39
GazeTests/ExampleUnitTests.swift
Normal file
39
GazeTests/ExampleUnitTests.swift
Normal file
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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<ScreenCaptureAuthorizationStatus, Never> {
|
||||
Just(authorizationStatus).eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
init(status: ScreenCaptureAuthorizationStatus) {
|
||||
self.authorizationStatus = status
|
||||
}
|
||||
|
||||
func refreshStatus() {}
|
||||
func requestAuthorizationIfNeeded() {}
|
||||
func openSystemSettings() {}
|
||||
}
|
||||
@@ -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.
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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<AppSettings>.Publisher {
|
||||
$settings
|
||||
}
|
||||
|
||||
private let timerConfigKeyPaths: [TimerType: WritableKeyPath<AppSettings, TimerConfiguration>] = [
|
||||
.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 = []
|
||||
}
|
||||
}
|
||||
@@ -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: View>(_ 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) }
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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"))
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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<AnyCancellable>!
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user