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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
//
|
//
|
||||||
// GazeUITests.swift
|
// ExampleUITests.swift
|
||||||
// GazeUITests
|
// Gaze
|
||||||
//
|
//
|
||||||
// Created by Mike Freno on 1/7/26.
|
// Created by AI Assistant on 1/15/26.
|
||||||
//
|
//
|
||||||
|
|
||||||
import XCTest
|
import XCTest
|
||||||
|
|
||||||
final class GazeUITests: XCTestCase {
|
final class ExampleUITests: XCTestCase {
|
||||||
|
|
||||||
override func setUpWithError() throws {
|
override func setUpWithError() throws {
|
||||||
// Put setup code here. This method is called before the invocation of each test method in the class.
|
// 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.
|
// In UI tests it is usually best to stop immediately when a failure occurs.
|
||||||
continueAfterFailure = false
|
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 {
|
override func tearDownWithError() throws {
|
||||||
@@ -23,12 +23,17 @@ final class GazeUITests: XCTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func testExample() throws {
|
func testExampleOfUITesting() throws {
|
||||||
// UI tests must launch the application that they test.
|
// UI tests must launch the application that they test.
|
||||||
let app = XCUIApplication()
|
let app = XCUIApplication()
|
||||||
app.launch()
|
app.launch()
|
||||||
|
|
||||||
// Use XCTAssert and related functions to verify your tests produce the correct results.
|
// 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
|
@MainActor
|
||||||
@@ -38,4 +43,4 @@ final class GazeUITests: XCTestCase {
|
|||||||
XCUIApplication().launch()
|
XCUIApplication().launch()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
*/
|
|
||||||
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user