reset: prep for new test suite

This commit is contained in:
Michael Freno
2026-01-15 14:51:46 -05:00
parent ec5ac6ed3d
commit 80edfa8e06
29 changed files with 51 additions and 3259 deletions

View 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")
}
}

View File

@@ -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() {}
}

View File

@@ -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.
}
}

View File

@@ -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)
}
}

View File

@@ -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 = []
}
}

View File

@@ -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) }
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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")
}
}
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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"))
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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")
}
}

View File

@@ -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)
}
}

View File

@@ -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()
}
}

View File

@@ -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")
}
}
}
}

View File

@@ -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)
}
}
}

View File

@@ -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 its 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()
} }
} }
} }

View File

@@ -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)
}
}

View File

@@ -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"
)
}
}
}

View File

@@ -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")
}
}

View File

@@ -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
*/

View File

@@ -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)
}
}
}
}
}

View File

@@ -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)
}
}
}
}
}
}