From e065d72d9ad0bd375465cca42f74124cdff393a7 Mon Sep 17 00:00:00 2001 From: Michael Freno Date: Tue, 13 Jan 2026 11:43:15 -0500 Subject: [PATCH] christ --- Gaze.xcodeproj/project.pbxproj | 22 -- Gaze/Constants/TestingEnvironment.swift | 38 +++ Gaze/Views/MenuBar/MenuBarContentView.swift | 1 + GazeUITests/OverlayReminderUITests.swift | 260 ++++++++++++++++++++ 4 files changed, 299 insertions(+), 22 deletions(-) create mode 100644 Gaze/Constants/TestingEnvironment.swift create mode 100644 GazeUITests/OverlayReminderUITests.swift diff --git a/Gaze.xcodeproj/project.pbxproj b/Gaze.xcodeproj/project.pbxproj index e56439c..01cecfc 100644 --- a/Gaze.xcodeproj/project.pbxproj +++ b/Gaze.xcodeproj/project.pbxproj @@ -121,7 +121,6 @@ buildPhases = ( 27A21B382F0F69DC0018C4F3 /* Sources */, 27A21B392F0F69DC0018C4F3 /* Frameworks */, - 27D081082F16AA7100FF3A31 /* Run Script */, 27A21B3A2F0F69DC0018C4F3 /* Resources */, ); buildRules = ( @@ -258,27 +257,6 @@ }; /* End PBXResourcesBuildPhase section */ -/* Begin PBXShellScriptBuildPhase section */ - 27D081082F16AA7100FF3A31 /* Run Script */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - name = "Run Script"; - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = " #!/bin/bash\n if [[ \"${OTHER_SWIFT_FLAGS}\" == *\"APPSTORE\"* ]]; then\n echo \"Removing Sparkle framework for App Store build...\"\n rm -rf \"${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/Sparkle.framework\"\n echo \"Sparkle framework removed successfully\"\n fi\n"; - }; -/* End PBXShellScriptBuildPhase section */ - /* Begin PBXSourcesBuildPhase section */ 27A21B382F0F69DC0018C4F3 /* Sources */ = { isa = PBXSourcesBuildPhase; diff --git a/Gaze/Constants/TestingEnvironment.swift b/Gaze/Constants/TestingEnvironment.swift new file mode 100644 index 0000000..ae42e0a --- /dev/null +++ b/Gaze/Constants/TestingEnvironment.swift @@ -0,0 +1,38 @@ +// +// TestingEnvironment.swift +// Gaze +// +// Created by OpenCode on 1/13/26. +// + +import Foundation + +/// Detects and manages testing environment states +enum TestingEnvironment { + /// Check if app is running in UI testing mode + static var isUITesting: Bool { + return ProcessInfo.processInfo.arguments.contains("--ui-testing") + } + + /// Check if app should skip onboarding + static var shouldSkipOnboarding: Bool { + return ProcessInfo.processInfo.arguments.contains("--skip-onboarding") + } + + /// Check if app should reset onboarding + static var shouldResetOnboarding: Bool { + return ProcessInfo.processInfo.arguments.contains("--reset-onboarding") + } + + /// Check if running in any test mode (unit tests or UI tests) + static var isAnyTestMode: Bool { + return isUITesting || ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil + } + + #if DEBUG + /// Check if dev triggers should be visible + static var shouldShowDevTriggers: Bool { + return isUITesting || isAnyTestMode + } + #endif +} diff --git a/Gaze/Views/MenuBar/MenuBarContentView.swift b/Gaze/Views/MenuBar/MenuBarContentView.swift index b97952e..b674ccf 100644 --- a/Gaze/Views/MenuBar/MenuBarContentView.swift +++ b/Gaze/Views/MenuBar/MenuBarContentView.swift @@ -436,6 +436,7 @@ struct TimerStatusRowWithIndividualControls: View { in: .circle ) .help("Trigger \(displayName) reminder now (dev)") + .accessibilityIdentifier("trigger_\(displayName.replacingOccurrences(of: " ", with: "_"))") .onHover { hovering in isHoveredDevTrigger = hovering } diff --git a/GazeUITests/OverlayReminderUITests.swift b/GazeUITests/OverlayReminderUITests.swift new file mode 100644 index 0000000..23d313f --- /dev/null +++ b/GazeUITests/OverlayReminderUITests.swift @@ -0,0 +1,260 @@ +// +// 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 + */ +