diff --git a/Gaze/AppDelegate.swift b/Gaze/AppDelegate.swift index 1293bf9..86481a9 100644 --- a/Gaze/AppDelegate.swift +++ b/Gaze/AppDelegate.swift @@ -154,7 +154,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { showReminderWindow(contentView) } - private func showReminderWindow(_ content: AnyView) { +private func showReminderWindow(_ content: AnyView) { guard let screen = NSScreen.main else { return } let window = NSWindow( @@ -169,9 +169,14 @@ class AppDelegate: NSObject, NSApplicationDelegate { window.backgroundColor = .clear window.contentView = NSHostingView(rootView: content) window.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary] + // Ensure this window can receive key events + window.acceptsMouseMovedEvents = true + window.makeFirstResponder(window.contentView) let windowController = NSWindowController(window: window) windowController.showWindow(nil) + // Make sure the window is brought to front and made key for key events + window.makeKeyAndOrderFront(nil) reminderWindowController = windowController } @@ -221,4 +226,4 @@ class AppDelegate: NSObject, NSApplicationDelegate { self?.settingsWindowController = nil } } -} +} \ No newline at end of file diff --git a/GazeTests/IntegrationTests.swift b/GazeTests/IntegrationTests.swift new file mode 100644 index 0000000..f944144 --- /dev/null +++ b/GazeTests/IntegrationTests.swift @@ -0,0 +1,176 @@ +// +// 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[.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[.lookAway]?.remainingSeconds + XCTAssertEqual(newInterval, 10 * 60) + } + + func testDisablingTimerRemovesFromEngine() { + timerEngine.start() + XCTAssertNotNil(timerEngine.timerStates[.blink]) + + var config = TimerConfiguration(enabled: false, intervalSeconds: 5 * 60) + settingsManager.updateTimerConfiguration(for: .blink, configuration: config) + + timerEngine.start() + XCTAssertNil(timerEngine.timerStates[.blink]) + } + + func testEnablingTimerAddsToEngine() { + settingsManager.settings.postureTimer.enabled = false + timerEngine.start() + XCTAssertNil(timerEngine.timerStates[.posture]) + + let config = TimerConfiguration(enabled: true, intervalSeconds: 30 * 60) + settingsManager.updateTimerConfiguration(for: .posture, configuration: config) + + timerEngine.start() + XCTAssertNotNil(timerEngine.timerStates[.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[.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[.lookAway]?.remainingSeconds, 600) + XCTAssertEqual(timerEngine.timerStates[.blink]?.remainingSeconds, 300) + XCTAssertEqual(timerEngine.timerStates[.posture]?.remainingSeconds, 1800) + } + + func testResetToDefaultsAffectsTimerEngine() { + let config = TimerConfiguration(enabled: false, intervalSeconds: 5 * 60) + settingsManager.updateTimerConfiguration(for: .blink, configuration: config) + + timerEngine.start() + XCTAssertNil(timerEngine.timerStates[.blink]) + + settingsManager.resetToDefaults() + timerEngine.start() + + XCTAssertNotNil(timerEngine.timerStates[.blink]) + XCTAssertEqual(timerEngine.timerStates[.blink]?.remainingSeconds, 5 * 60) + } + + 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() { + 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(type: .lookAway) + XCTAssertEqual(timerEngine.timerStates[.lookAway]?.remainingSeconds, 20 * 60) + + timerEngine.stop() + XCTAssertTrue(timerEngine.timerStates.isEmpty) + } + + func testReminderWorkflow() { + timerEngine.start() + + timerEngine.triggerReminder(for: .lookAway) + XCTAssertNotNil(timerEngine.activeReminder) + + for (_, state) in timerEngine.timerStates { + XCTAssertTrue(state.isPaused) + } + + timerEngine.dismissReminder() + XCTAssertNil(timerEngine.activeReminder) + + for (_, state) in timerEngine.timerStates { + XCTAssertFalse(state.isPaused) + } + } + + func testSettingsAutoSaveIntegration() { + let config = TimerConfiguration(enabled: false, intervalSeconds: 900) + settingsManager.updateTimerConfiguration(for: .lookAway, configuration: config) + + settingsManager.load() + + let loadedConfig = settingsManager.timerConfiguration(for: .lookAway) + XCTAssertEqual(loadedConfig.intervalSeconds, 900) + XCTAssertFalse(loadedConfig.enabled) + } +} diff --git a/GazeTests/SettingsManagerTests.swift b/GazeTests/SettingsManagerTests.swift index 298edb0..7e8589d 100644 --- a/GazeTests/SettingsManagerTests.swift +++ b/GazeTests/SettingsManagerTests.swift @@ -74,7 +74,7 @@ final class SettingsManagerTests: XCTestCase { } func testUpdateTimerConfiguration() { - var newConfig = TimerConfiguration(enabled: false, intervalSeconds: 10 * 60) + let newConfig = TimerConfiguration(enabled: false, intervalSeconds: 10 * 60) settingsManager.updateTimerConfiguration(for: .lookAway, configuration: newConfig) let retrieved = settingsManager.timerConfiguration(for: .lookAway) diff --git a/GazeTests/TimerEngineTests.swift b/GazeTests/TimerEngineTests.swift index c558dd3..fb13eab 100644 --- a/GazeTests/TimerEngineTests.swift +++ b/GazeTests/TimerEngineTests.swift @@ -141,4 +141,169 @@ final class TimerEngineTests: XCTestCase { XCTAssertFalse(state.isPaused) } } + + func testTriggerReminderForLookAway() { + timerEngine.start() + + timerEngine.triggerReminder(for: .lookAway) + + XCTAssertNotNil(timerEngine.activeReminder) + if case .lookAwayTriggered(let countdown) = timerEngine.activeReminder { + XCTAssertEqual(countdown, settingsManager.settings.lookAwayCountdownSeconds) + } else { + XCTFail("Expected lookAwayTriggered reminder") + } + + for (_, state) in timerEngine.timerStates { + XCTAssertTrue(state.isPaused) + } + } + + func testTriggerReminderForBlink() { + timerEngine.start() + + timerEngine.triggerReminder(for: .blink) + + XCTAssertNotNil(timerEngine.activeReminder) + if case .blinkTriggered = timerEngine.activeReminder { + XCTAssertTrue(true) + } else { + XCTFail("Expected blinkTriggered reminder") + } + } + + func testTriggerReminderForPosture() { + timerEngine.start() + + timerEngine.triggerReminder(for: .posture) + + XCTAssertNotNil(timerEngine.activeReminder) + if case .postureTriggered = timerEngine.activeReminder { + XCTAssertTrue(true) + } else { + XCTFail("Expected postureTriggered reminder") + } + } + + func testGetTimeRemainingForNonExistentTimer() { + let timeRemaining = timerEngine.getTimeRemaining(for: .lookAway) + XCTAssertEqual(timeRemaining, 0) + } + + func testGetFormattedTimeRemainingZeroSeconds() { + timerEngine.start() + timerEngine.timerStates[.lookAway]?.remainingSeconds = 0 + + let formatted = timerEngine.getFormattedTimeRemaining(for: .lookAway) + XCTAssertEqual(formatted, "0:00") + } + + func testGetFormattedTimeRemainingLessThanMinute() { + timerEngine.start() + timerEngine.timerStates[.lookAway]?.remainingSeconds = 45 + + let formatted = timerEngine.getFormattedTimeRemaining(for: .lookAway) + XCTAssertEqual(formatted, "0:45") + } + + func testGetFormattedTimeRemainingExactHour() { + timerEngine.start() + timerEngine.timerStates[.lookAway]?.remainingSeconds = 3600 + + let formatted = timerEngine.getFormattedTimeRemaining(for: .lookAway) + XCTAssertEqual(formatted, "1:00:00") + } + + func testMultipleStartCallsResetTimers() { + timerEngine.start() + timerEngine.timerStates[.lookAway]?.remainingSeconds = 100 + + timerEngine.start() + + XCTAssertEqual(timerEngine.timerStates[.lookAway]?.remainingSeconds, 20 * 60) + } + + func testSkipNextPreservesPausedState() { + timerEngine.start() + timerEngine.pause() + + timerEngine.skipNext(type: .lookAway) + + XCTAssertTrue(timerEngine.timerStates[.lookAway]?.isPaused ?? false) + } + + func testSkipNextPreservesActiveState() { + timerEngine.start() + + timerEngine.skipNext(type: .lookAway) + + XCTAssertTrue(timerEngine.timerStates[.lookAway]?.isActive ?? false) + } + + func testDismissReminderWithNoActiveReminder() { + timerEngine.start() + XCTAssertNil(timerEngine.activeReminder) + + timerEngine.dismissReminder() + + XCTAssertNil(timerEngine.activeReminder) + } + + func testDismissBlinkReminderDoesNotResumeTimers() { + timerEngine.start() + timerEngine.activeReminder = .blinkTriggered + + timerEngine.dismissReminder() + + for (_, state) in timerEngine.timerStates { + XCTAssertFalse(state.isPaused) + } + } + + func testDismissPostureReminderDoesNotResumeTimers() { + timerEngine.start() + timerEngine.activeReminder = .postureTriggered + + timerEngine.dismissReminder() + + for (_, state) in timerEngine.timerStates { + XCTAssertFalse(state.isPaused) + } + } + + func testAllTimersStartWhenEnabled() { + settingsManager.settings.lookAwayTimer.enabled = true + settingsManager.settings.blinkTimer.enabled = true + settingsManager.settings.postureTimer.enabled = true + + timerEngine.start() + + XCTAssertEqual(timerEngine.timerStates.count, 3) + for timerType in TimerType.allCases { + XCTAssertNotNil(timerEngine.timerStates[timerType]) + } + } + + func testAllTimersDisabled() { + settingsManager.settings.lookAwayTimer.enabled = false + settingsManager.settings.blinkTimer.enabled = false + settingsManager.settings.postureTimer.enabled = false + + timerEngine.start() + + XCTAssertEqual(timerEngine.timerStates.count, 0) + } + + func testPartialTimersEnabled() { + settingsManager.settings.lookAwayTimer.enabled = true + settingsManager.settings.blinkTimer.enabled = false + settingsManager.settings.postureTimer.enabled = true + + timerEngine.start() + + XCTAssertEqual(timerEngine.timerStates.count, 2) + XCTAssertNotNil(timerEngine.timerStates[.lookAway]) + XCTAssertNil(timerEngine.timerStates[.blink]) + XCTAssertNotNil(timerEngine.timerStates[.posture]) + } } diff --git a/GazeUITests/AccessibilityUITests.swift b/GazeUITests/AccessibilityUITests.swift new file mode 100644 index 0000000..0d54c6a --- /dev/null +++ b/GazeUITests/AccessibilityUITests.swift @@ -0,0 +1,86 @@ +// +// AccessibilityUITests.swift +// GazeUITests +// +// Created by Mike Freno on 1/8/26. +// + +import XCTest + +@MainActor +final class AccessibilityUITests: XCTestCase { + + var app: XCUIApplication! + + override func setUpWithError() throws { + continueAfterFailure = false + app = XCUIApplication() + app.launchArguments.append("--skip-onboarding") + app.launch() + } + + override func tearDownWithError() throws { + app = nil + } + + func testMenuBarAccessibilityLabels() throws { + let menuBar = app.menuBarItems.firstMatch + if menuBar.waitForExistence(timeout: 5) { + menuBar.click() + + for button in app.buttons.allElementsBoundByIndex { + XCTAssertFalse(button.label.isEmpty, "Button should have accessibility label") + } + } + } + + func testKeyboardNavigation() throws { + let menuBar = app.menuBarItems.firstMatch + if menuBar.waitForExistence(timeout: 5) { + menuBar.click() + + app.typeKey(XCUIKeyboardKey.tab, modifierFlags: []) + + let focusedElement = app.descendants(matching: .any).element(matching: NSPredicate(format: "hasKeyboardFocus == true")) + XCTAssertTrue(focusedElement.exists || app.buttons.count > 0) + } + } + + func testAllButtonsAreHittable() throws { + let menuBar = app.menuBarItems.firstMatch + if menuBar.waitForExistence(timeout: 5) { + menuBar.click() + + sleep(1) + + let buttons = app.buttons.allElementsBoundByIndex + for button in buttons where button.exists && button.isEnabled { + XCTAssertTrue(button.isHittable || !button.isEnabled, "Enabled button should be hittable: \(button.label)") + } + } + } + + func testVoiceOverElementsHaveLabels() throws { + let menuBar = app.menuBarItems.firstMatch + if menuBar.waitForExistence(timeout: 5) { + menuBar.click() + + let staticTexts = app.staticTexts.allElementsBoundByIndex + for text in staticTexts where text.exists { + XCTAssertFalse(text.label.isEmpty, "Static text should have label") + } + } + } + + func testImagesHaveAccessibilityTraits() throws { + let menuBar = app.menuBarItems.firstMatch + if menuBar.waitForExistence(timeout: 5) { + menuBar.click() + + let images = app.images.allElementsBoundByIndex + for image in images where image.exists { + XCTAssertFalse(image.label.isEmpty, "Image should have accessibility label") + } + } + } +} diff --git a/GazeUITests/MenuBarUITests.swift b/GazeUITests/MenuBarUITests.swift new file mode 100644 index 0000000..4a9d703 --- /dev/null +++ b/GazeUITests/MenuBarUITests.swift @@ -0,0 +1,102 @@ +// +// 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)) + } + } + } +} diff --git a/GazeUITests/OnboardingUITests.swift b/GazeUITests/OnboardingUITests.swift new file mode 100644 index 0000000..aafb009 --- /dev/null +++ b/GazeUITests/OnboardingUITests.swift @@ -0,0 +1,92 @@ +// +// OnboardingUITests.swift +// GazeUITests +// +// Created by Mike Freno on 1/8/26. +// + +import XCTest + +@MainActor +final class OnboardingUITests: XCTestCase { + + var app: XCUIApplication! + + override func setUpWithError() throws { + continueAfterFailure = false + app = XCUIApplication() + app.launchArguments.append("--reset-onboarding") + app.launch() + } + + override func tearDownWithError() throws { + app = nil + } + + func testOnboardingWelcomeScreen() throws { + XCTAssertTrue(app.staticTexts["Welcome to Gaze"].exists || app.staticTexts.containing(NSPredicate(format: "label CONTAINS 'Welcome'")).firstMatch.exists) + } + + func testOnboardingNavigationFromWelcome() throws { + let continueButton = app.buttons["Continue"] + + if continueButton.waitForExistence(timeout: 2) { + continueButton.tap() + + let nextScreen = app.staticTexts.containing(NSPredicate(format: "label CONTAINS 'Setup' OR label CONTAINS 'Configure'")).firstMatch + XCTAssertTrue(nextScreen.waitForExistence(timeout: 2)) + } + } + + func testOnboardingBackNavigation() throws { + let continueButton = app.buttons["Continue"] + if continueButton.waitForExistence(timeout: 2) { + continueButton.tap() + + let backButton = app.buttons["Back"] + if backButton.waitForExistence(timeout: 1) { + backButton.tap() + + XCTAssertTrue(app.staticTexts.containing(NSPredicate(format: "label CONTAINS 'Welcome'")).firstMatch.waitForExistence(timeout: 1)) + } + } + } + + func testOnboardingCompleteFlow() throws { + let continueButtons = app.buttons.matching(identifier: "Continue") + let nextButtons = app.buttons.matching(identifier: "Next") + + var currentStep = 0 + let maxSteps = 10 + + while currentStep < maxSteps { + if continueButtons.firstMatch.exists && continueButtons.firstMatch.isHittable { + continueButtons.firstMatch.tap() + currentStep += 1 + sleep(1) + } else if nextButtons.firstMatch.exists && nextButtons.firstMatch.isHittable { + nextButtons.firstMatch.tap() + currentStep += 1 + sleep(1) + } else if app.buttons["Get Started"].exists { + app.buttons["Get Started"].tap() + break + } else if app.buttons["Done"].exists { + app.buttons["Done"].tap() + break + } else { + break + } + } + + XCTAssertLessThan(currentStep, maxSteps, "Onboarding flow should complete") + } + + func testOnboardingHasRequiredElements() throws { + let hasText = app.staticTexts.count > 0 + let hasButtons = app.buttons.count > 0 + + XCTAssertTrue(hasText, "Onboarding should have text elements") + XCTAssertTrue(hasButtons, "Onboarding should have buttons") + } +} diff --git a/GazeUITests/PerformanceUITests.swift b/GazeUITests/PerformanceUITests.swift new file mode 100644 index 0000000..f6de7ea --- /dev/null +++ b/GazeUITests/PerformanceUITests.swift @@ -0,0 +1,88 @@ +// +// PerformanceUITests.swift +// GazeUITests +// +// Created by Mike Freno on 1/8/26. +// + +import XCTest + +@MainActor +final class PerformanceUITests: XCTestCase { + + var app: XCUIApplication! + + override func setUpWithError() throws { + continueAfterFailure = false + app = XCUIApplication() + app.launchArguments.append("--skip-onboarding") + } + + override func tearDownWithError() throws { + app = nil + } + + func testAppLaunchPerformance() throws { + measure(metrics: [XCTApplicationLaunchMetric()]) { + app.launch() + app.terminate() + } + } + + func testMenuBarOpenPerformance() throws { + app.launch() + + measure { + let menuBar = app.menuBarItems.firstMatch + if menuBar.waitForExistence(timeout: 5) { + menuBar.click() + _ = app.staticTexts["Gaze"].waitForExistence(timeout: 2) + } + } + } + + func testSettingsWindowOpenPerformance() throws { + app.launch() + + let menuBar = app.menuBarItems.firstMatch + if menuBar.waitForExistence(timeout: 5) { + menuBar.click() + + measure { + let settingsButton = app.menuItems["Settings"] + if settingsButton.waitForExistence(timeout: 2) { + settingsButton.click() + + let settingsWindow = app.windows["Settings"] + _ = settingsWindow.waitForExistence(timeout: 3) + + if settingsWindow.exists { + let closeButton = settingsWindow.buttons[XCUIIdentifierCloseWindow] + if closeButton.exists { + closeButton.click() + } + } + } + + menuBar.click() + } + } + } + + func testMemoryUsageDuringOperation() throws { + app.launch() + + let menuBar = app.menuBarItems.firstMatch + if menuBar.waitForExistence(timeout: 5) { + measure(metrics: [XCTMemoryMetric()]) { + for _ in 0..<5 { + menuBar.click() + sleep(1) + + menuBar.click() + sleep(1) + } + } + } + } +} diff --git a/GazeUITests/SettingsUITests.swift b/GazeUITests/SettingsUITests.swift new file mode 100644 index 0000000..0032181 --- /dev/null +++ b/GazeUITests/SettingsUITests.swift @@ -0,0 +1,83 @@ +// +// 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) + } + } + } + } + } +}