christ
This commit is contained in:
@@ -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;
|
||||
|
||||
38
Gaze/Constants/TestingEnvironment.swift
Normal file
38
Gaze/Constants/TestingEnvironment.swift
Normal file
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
260
GazeUITests/OverlayReminderUITests.swift
Normal file
260
GazeUITests/OverlayReminderUITests.swift
Normal file
@@ -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
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user