Files
Gaze/GazeUITests/OverlayReminderUITests.swift
Michael Freno e065d72d9a christ
2026-01-13 11:43:15 -05:00

261 lines
7.8 KiB
Swift

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