Version bump to v0.4.0

This commit is contained in:
Michael Freno
2026-01-13 10:28:04 -05:00
parent 338e79c6c6
commit 2d0db8af2a
5 changed files with 268 additions and 80 deletions

View File

@@ -424,7 +424,7 @@
CODE_SIGN_ENTITLEMENTS = Gaze/Gaze.entitlements; CODE_SIGN_ENTITLEMENTS = Gaze/Gaze.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 5; CURRENT_PROJECT_VERSION = 6;
DEVELOPMENT_TEAM = 6GK4F9L62V; DEVELOPMENT_TEAM = 6GK4F9L62V;
ENABLE_APP_SANDBOX = YES; ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
@@ -438,7 +438,7 @@
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 13.0; MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 0.3.0; MARKETING_VERSION = 0.4.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mikefreno.Gaze; PRODUCT_BUNDLE_IDENTIFIER = com.mikefreno.Gaze;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES; REGISTER_APP_GROUPS = YES;
@@ -460,7 +460,7 @@
CODE_SIGN_ENTITLEMENTS = Gaze/Gaze.entitlements; CODE_SIGN_ENTITLEMENTS = Gaze/Gaze.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 5; CURRENT_PROJECT_VERSION = 6;
DEVELOPMENT_TEAM = 6GK4F9L62V; DEVELOPMENT_TEAM = 6GK4F9L62V;
ENABLE_APP_SANDBOX = YES; ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
@@ -474,7 +474,7 @@
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 13.0; MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 0.3.0; MARKETING_VERSION = 0.4.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mikefreno.Gaze; PRODUCT_BUNDLE_IDENTIFIER = com.mikefreno.Gaze;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES; REGISTER_APP_GROUPS = YES;
@@ -492,11 +492,11 @@
buildSettings = { buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 6;
DEVELOPMENT_TEAM = 6GK4F9L62V; DEVELOPMENT_TEAM = 6GK4F9L62V;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MACOSX_DEPLOYMENT_TARGET = 26.2; MACOSX_DEPLOYMENT_TARGET = 26.2;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 0.4.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mikefreno.GazeTests; PRODUCT_BUNDLE_IDENTIFIER = com.mikefreno.GazeTests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO; STRING_CATALOG_GENERATE_SYMBOLS = NO;
@@ -513,11 +513,11 @@
buildSettings = { buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 6;
DEVELOPMENT_TEAM = 6GK4F9L62V; DEVELOPMENT_TEAM = 6GK4F9L62V;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MACOSX_DEPLOYMENT_TARGET = 26.2; MACOSX_DEPLOYMENT_TARGET = 26.2;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 0.4.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mikefreno.GazeTests; PRODUCT_BUNDLE_IDENTIFIER = com.mikefreno.GazeTests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO; STRING_CATALOG_GENERATE_SYMBOLS = NO;
@@ -533,10 +533,10 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 6;
DEVELOPMENT_TEAM = 6GK4F9L62V; DEVELOPMENT_TEAM = 6GK4F9L62V;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 0.4.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mikefreno.GazeUITests; PRODUCT_BUNDLE_IDENTIFIER = com.mikefreno.GazeUITests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO; STRING_CATALOG_GENERATE_SYMBOLS = NO;
@@ -552,10 +552,10 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 6;
DEVELOPMENT_TEAM = 6GK4F9L62V; DEVELOPMENT_TEAM = 6GK4F9L62V;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 0.4.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mikefreno.GazeUITests; PRODUCT_BUNDLE_IDENTIFIER = com.mikefreno.GazeUITests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO; STRING_CATALOG_GENERATE_SYMBOLS = NO;

View File

@@ -37,14 +37,16 @@ struct SettingsWindowView: View {
Label("Posture", systemImage: "figure.stand") Label("Posture", systemImage: "figure.stand")
} }
UserTimersView(userTimers: Binding( UserTimersView(
get: { settingsManager.settings.userTimers }, userTimers: Binding(
set: { settingsManager.settings.userTimers = $0 } get: { settingsManager.settings.userTimers },
)) set: { settingsManager.settings.userTimers = $0 }
.tag(3) )
.tabItem { )
Label("User Timers", systemImage: "plus.circle") .tag(3)
} .tabItem {
Label("User Timers", systemImage: "plus.circle")
}
GeneralSetupView( GeneralSetupView(
settingsManager: settingsManager, settingsManager: settingsManager,
@@ -60,10 +62,10 @@ struct SettingsWindowView: View {
HStack { HStack {
#if DEBUG #if DEBUG
Button("Retrigger Onboarding") { Button("Retrigger Onboarding") {
retriggerOnboarding() retriggerOnboarding()
} }
.buttonStyle(.bordered) .buttonStyle(.bordered)
#endif #endif
Spacer() Spacer()
@@ -96,19 +98,19 @@ struct SettingsWindowView: View {
} }
#if DEBUG #if DEBUG
private func retriggerOnboarding() { private func retriggerOnboarding() {
// Close settings window first // Close settings window first
closeWindow() closeWindow()
// Get AppDelegate and open onboarding // Get AppDelegate and open onboarding
if let appDelegate = NSApplication.shared.delegate as? AppDelegate { if let appDelegate = NSApplication.shared.delegate as? AppDelegate {
// Reset onboarding state so it shows as fresh // Reset onboarding state so it shows as fresh
settingsManager.settings.hasCompletedOnboarding = false settingsManager.settings.hasCompletedOnboarding = false
// Open onboarding window // Open onboarding window
appDelegate.openOnboarding() appDelegate.openOnboarding()
}
} }
}
#endif #endif
} }

View File

@@ -32,9 +32,9 @@ final class TimerEngineTests: XCTestCase {
timerEngine.start() timerEngine.start()
XCTAssertEqual(timerEngine.timerStates.count, 3) XCTAssertEqual(timerEngine.timerStates.count, 3)
XCTAssertNotNil(timerEngine.timerStates[.lookAway]) XCTAssertNotNil(timerEngine.timerStates[.builtIn(.lookAway)])
XCTAssertNotNil(timerEngine.timerStates[.blink]) XCTAssertNotNil(timerEngine.timerStates[.builtIn(.blink)])
XCTAssertNotNil(timerEngine.timerStates[.posture]) XCTAssertNotNil(timerEngine.timerStates[.builtIn(.posture)])
} }
func testDisabledTimersNotInitialized() { func testDisabledTimersNotInitialized() {
@@ -43,16 +43,16 @@ final class TimerEngineTests: XCTestCase {
timerEngine.start() timerEngine.start()
XCTAssertEqual(timerEngine.timerStates.count, 2) XCTAssertEqual(timerEngine.timerStates.count, 2)
XCTAssertNotNil(timerEngine.timerStates[.lookAway]) XCTAssertNotNil(timerEngine.timerStates[.builtIn(.lookAway)])
XCTAssertNil(timerEngine.timerStates[.blink]) XCTAssertNil(timerEngine.timerStates[.builtIn(.blink)])
XCTAssertNotNil(timerEngine.timerStates[.posture]) XCTAssertNotNil(timerEngine.timerStates[.builtIn(.posture)])
} }
func testTimerStateInitialValues() { func testTimerStateInitialValues() {
timerEngine.start() timerEngine.start()
let lookAwayState = timerEngine.timerStates[.lookAway]! let lookAwayState = timerEngine.timerStates[.builtIn(.lookAway)]!
XCTAssertEqual(lookAwayState.type, .lookAway) XCTAssertEqual(lookAwayState.identifier, .builtIn(.lookAway))
XCTAssertEqual(lookAwayState.remainingSeconds, 20 * 60) XCTAssertEqual(lookAwayState.remainingSeconds, 20 * 60)
XCTAssertFalse(lookAwayState.isPaused) XCTAssertFalse(lookAwayState.isPaused)
XCTAssertTrue(lookAwayState.isActive) XCTAssertTrue(lookAwayState.isActive)
@@ -81,33 +81,33 @@ final class TimerEngineTests: XCTestCase {
settingsManager.settings.lookAwayTimer.intervalSeconds = 60 settingsManager.settings.lookAwayTimer.intervalSeconds = 60
timerEngine.start() timerEngine.start()
timerEngine.timerStates[.lookAway]?.remainingSeconds = 10 timerEngine.timerStates[.builtIn(.lookAway)]?.remainingSeconds = 10
timerEngine.skipNext(type: .lookAway) timerEngine.skipNext(identifier: .builtIn(.lookAway))
XCTAssertEqual(timerEngine.timerStates[.lookAway]?.remainingSeconds, 60) XCTAssertEqual(timerEngine.timerStates[.builtIn(.lookAway)]?.remainingSeconds, 60)
} }
func testGetTimeRemaining() { func testGetTimeRemaining() {
timerEngine.start() timerEngine.start()
let timeRemaining = timerEngine.getTimeRemaining(for: .lookAway) let timeRemaining = timerEngine.getTimeRemaining(for: .builtIn(.lookAway))
XCTAssertEqual(timeRemaining, TimeInterval(20 * 60)) XCTAssertEqual(timeRemaining, TimeInterval(20 * 60))
} }
func testGetFormattedTimeRemaining() { func testGetFormattedTimeRemaining() {
timerEngine.start() timerEngine.start()
timerEngine.timerStates[.lookAway]?.remainingSeconds = 125 timerEngine.timerStates[.builtIn(.lookAway)]?.remainingSeconds = 125
let formatted = timerEngine.getFormattedTimeRemaining(for: .lookAway) let formatted = timerEngine.getFormattedTimeRemaining(for: .builtIn(.lookAway))
XCTAssertEqual(formatted, "2:05") XCTAssertEqual(formatted, "2:05")
} }
func testGetFormattedTimeRemainingWithHours() { func testGetFormattedTimeRemainingWithHours() {
timerEngine.start() timerEngine.start()
timerEngine.timerStates[.lookAway]?.remainingSeconds = 3665 timerEngine.timerStates[.builtIn(.lookAway)]?.remainingSeconds = 3665
let formatted = timerEngine.getFormattedTimeRemaining(for: .lookAway) let formatted = timerEngine.getFormattedTimeRemaining(for: .builtIn(.lookAway))
XCTAssertEqual(formatted, "1:01:05") XCTAssertEqual(formatted, "1:01:05")
} }
@@ -121,13 +121,13 @@ final class TimerEngineTests: XCTestCase {
func testDismissReminderResetsTimer() { func testDismissReminderResetsTimer() {
timerEngine.start() timerEngine.start()
timerEngine.timerStates[.blink]?.remainingSeconds = 0 timerEngine.timerStates[.builtIn(.blink)]?.remainingSeconds = 0
timerEngine.activeReminder = .blinkTriggered timerEngine.activeReminder = .blinkTriggered
timerEngine.dismissReminder() timerEngine.dismissReminder()
XCTAssertNil(timerEngine.activeReminder) XCTAssertNil(timerEngine.activeReminder)
XCTAssertEqual(timerEngine.timerStates[.blink]?.remainingSeconds, 5 * 60) XCTAssertEqual(timerEngine.timerStates[.builtIn(.blink)]?.remainingSeconds, 5 * 60)
} }
func testDismissLookAwayResumesTimers() { func testDismissLookAwayResumesTimers() {
@@ -145,7 +145,7 @@ final class TimerEngineTests: XCTestCase {
func testTriggerReminderForLookAway() { func testTriggerReminderForLookAway() {
timerEngine.start() timerEngine.start()
timerEngine.triggerReminder(for: .lookAway) timerEngine.triggerReminder(for: .builtIn(.lookAway))
XCTAssertNotNil(timerEngine.activeReminder) XCTAssertNotNil(timerEngine.activeReminder)
if case .lookAwayTriggered(let countdown) = timerEngine.activeReminder { if case .lookAwayTriggered(let countdown) = timerEngine.activeReminder {
@@ -162,7 +162,7 @@ final class TimerEngineTests: XCTestCase {
func testTriggerReminderForBlink() { func testTriggerReminderForBlink() {
timerEngine.start() timerEngine.start()
timerEngine.triggerReminder(for: .blink) timerEngine.triggerReminder(for: .builtIn(.blink))
XCTAssertNotNil(timerEngine.activeReminder) XCTAssertNotNil(timerEngine.activeReminder)
if case .blinkTriggered = timerEngine.activeReminder { if case .blinkTriggered = timerEngine.activeReminder {
@@ -175,7 +175,7 @@ final class TimerEngineTests: XCTestCase {
func testTriggerReminderForPosture() { func testTriggerReminderForPosture() {
timerEngine.start() timerEngine.start()
timerEngine.triggerReminder(for: .posture) timerEngine.triggerReminder(for: .builtIn(.posture))
XCTAssertNotNil(timerEngine.activeReminder) XCTAssertNotNil(timerEngine.activeReminder)
if case .postureTriggered = timerEngine.activeReminder { if case .postureTriggered = timerEngine.activeReminder {
@@ -186,58 +186,58 @@ final class TimerEngineTests: XCTestCase {
} }
func testGetTimeRemainingForNonExistentTimer() { func testGetTimeRemainingForNonExistentTimer() {
let timeRemaining = timerEngine.getTimeRemaining(for: .lookAway) let timeRemaining = timerEngine.getTimeRemaining(for: .builtIn(.lookAway))
XCTAssertEqual(timeRemaining, 0) XCTAssertEqual(timeRemaining, 0)
} }
func testGetFormattedTimeRemainingZeroSeconds() { func testGetFormattedTimeRemainingZeroSeconds() {
timerEngine.start() timerEngine.start()
timerEngine.timerStates[.lookAway]?.remainingSeconds = 0 timerEngine.timerStates[.builtIn(.lookAway)]?.remainingSeconds = 0
let formatted = timerEngine.getFormattedTimeRemaining(for: .lookAway) let formatted = timerEngine.getFormattedTimeRemaining(for: .builtIn(.lookAway))
XCTAssertEqual(formatted, "0:00") XCTAssertEqual(formatted, "0:00")
} }
func testGetFormattedTimeRemainingLessThanMinute() { func testGetFormattedTimeRemainingLessThanMinute() {
timerEngine.start() timerEngine.start()
timerEngine.timerStates[.lookAway]?.remainingSeconds = 45 timerEngine.timerStates[.builtIn(.lookAway)]?.remainingSeconds = 45
let formatted = timerEngine.getFormattedTimeRemaining(for: .lookAway) let formatted = timerEngine.getFormattedTimeRemaining(for: .builtIn(.lookAway))
XCTAssertEqual(formatted, "0:45") XCTAssertEqual(formatted, "0:45")
} }
func testGetFormattedTimeRemainingExactHour() { func testGetFormattedTimeRemainingExactHour() {
timerEngine.start() timerEngine.start()
timerEngine.timerStates[.lookAway]?.remainingSeconds = 3600 timerEngine.timerStates[.builtIn(.lookAway)]?.remainingSeconds = 3600
let formatted = timerEngine.getFormattedTimeRemaining(for: .lookAway) let formatted = timerEngine.getFormattedTimeRemaining(for: .builtIn(.lookAway))
XCTAssertEqual(formatted, "1:00:00") XCTAssertEqual(formatted, "1:00:00")
} }
func testMultipleStartCallsResetTimers() { func testMultipleStartCallsResetTimers() {
timerEngine.start() timerEngine.start()
timerEngine.timerStates[.lookAway]?.remainingSeconds = 100 timerEngine.timerStates[.builtIn(.lookAway)]?.remainingSeconds = 100
timerEngine.start() timerEngine.start()
XCTAssertEqual(timerEngine.timerStates[.lookAway]?.remainingSeconds, 20 * 60) XCTAssertEqual(timerEngine.timerStates[.builtIn(.lookAway)]?.remainingSeconds, 20 * 60)
} }
func testSkipNextPreservesPausedState() { func testSkipNextPreservesPausedState() {
timerEngine.start() timerEngine.start()
timerEngine.pause() timerEngine.pause()
timerEngine.skipNext(type: .lookAway) timerEngine.skipNext(identifier: .builtIn(.lookAway))
XCTAssertTrue(timerEngine.timerStates[.lookAway]?.isPaused ?? false) XCTAssertTrue(timerEngine.timerStates[.builtIn(.lookAway)]?.isPaused ?? false)
} }
func testSkipNextPreservesActiveState() { func testSkipNextPreservesActiveState() {
timerEngine.start() timerEngine.start()
timerEngine.skipNext(type: .lookAway) timerEngine.skipNext(identifier: .builtIn(.lookAway))
XCTAssertTrue(timerEngine.timerStates[.lookAway]?.isActive ?? false) XCTAssertTrue(timerEngine.timerStates[.builtIn(.lookAway)]?.isActive ?? false)
} }
func testDismissReminderWithNoActiveReminder() { func testDismissReminderWithNoActiveReminder() {
@@ -279,8 +279,8 @@ final class TimerEngineTests: XCTestCase {
timerEngine.start() timerEngine.start()
XCTAssertEqual(timerEngine.timerStates.count, 3) XCTAssertEqual(timerEngine.timerStates.count, 3)
for timerType in TimerType.allCases { for builtInTimer in TimerType.allCases {
XCTAssertNotNil(timerEngine.timerStates[timerType]) XCTAssertNotNil(timerEngine.timerStates[.builtIn(builtInTimer)])
} }
} }
@@ -302,8 +302,8 @@ final class TimerEngineTests: XCTestCase {
timerEngine.start() timerEngine.start()
XCTAssertEqual(timerEngine.timerStates.count, 2) XCTAssertEqual(timerEngine.timerStates.count, 2)
XCTAssertNotNil(timerEngine.timerStates[.lookAway]) XCTAssertNotNil(timerEngine.timerStates[.builtIn(.lookAway)])
XCTAssertNil(timerEngine.timerStates[.blink]) XCTAssertNil(timerEngine.timerStates[.builtIn(.blink)])
XCTAssertNotNil(timerEngine.timerStates[.posture]) XCTAssertNotNil(timerEngine.timerStates[.builtIn(.posture)])
} }
} }

View File

@@ -0,0 +1,175 @@
//
// 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

@@ -111,11 +111,18 @@ ask_version_bump() {
echo "New version: v${NEW_VERSION}" echo "New version: v${NEW_VERSION}"
# Get current build number for display
DISPLAY_CURRENT_BUILD=$(grep -A 1 "CURRENT_PROJECT_VERSION" Gaze.xcodeproj/project.pbxproj | grep -o '[0-9]\+' | head -1)
if [ -z "$DISPLAY_CURRENT_BUILD" ]; then
DISPLAY_CURRENT_BUILD=0
fi
DISPLAY_NEW_BUILD=$((DISPLAY_CURRENT_BUILD + 1))
# Ask for confirmation to proceed with version bumping # Ask for confirmation to proceed with version bumping
echo "" echo ""
echo "This will:" echo "This will:"
echo " 1. Update project.pbxproj → MARKETING_VERSION = ${NEW_VERSION}" echo " 1. Update project.pbxproj → MARKETING_VERSION = ${NEW_VERSION}"
echo " 2. Update project.pbxproj → CURRENT_PROJECT_VERSION = $(($(date +%Y%m%d)))" echo " 2. Update project.pbxproj → CURRENT_PROJECT_VERSION = ${DISPLAY_NEW_BUILD} (currently ${DISPLAY_CURRENT_BUILD})"
echo " 3. Create git tag v${NEW_VERSION}" echo " 3. Create git tag v${NEW_VERSION}"
echo "" echo ""
read -p "Proceed with version bump? (y/n) " -n 1 -r read -p "Proceed with version bump? (y/n) " -n 1 -r
@@ -132,8 +139,12 @@ ask_version_bump() {
sed -i.bak "s/MARKETING_VERSION = [0-9.]*;/MARKETING_VERSION = ${NEW_VERSION};/" Gaze.xcodeproj/project.pbxproj sed -i.bak "s/MARKETING_VERSION = [0-9.]*;/MARKETING_VERSION = ${NEW_VERSION};/" Gaze.xcodeproj/project.pbxproj
rm -f Gaze.xcodeproj/project.pbxproj.bak rm -f Gaze.xcodeproj/project.pbxproj.bak
# Update CURRENT_PROJECT_VERSION (build number) # Update CURRENT_PROJECT_VERSION (build number) - increment by 1
BUILD_NUMBER=$(date +%Y%m%d) CURRENT_BUILD=$(grep -A 1 "CURRENT_PROJECT_VERSION" Gaze.xcodeproj/project.pbxproj | grep -o '[0-9]\+' | head -1)
if [ -z "$CURRENT_BUILD" ]; then
CURRENT_BUILD=0
fi
BUILD_NUMBER=$((CURRENT_BUILD + 1))
sed -i.bak "s/CURRENT_PROJECT_VERSION = [0-9]*;/CURRENT_PROJECT_VERSION = ${BUILD_NUMBER};/" Gaze.xcodeproj/project.pbxproj sed -i.bak "s/CURRENT_PROJECT_VERSION = [0-9]*;/CURRENT_PROJECT_VERSION = ${BUILD_NUMBER};/" Gaze.xcodeproj/project.pbxproj
rm -f Gaze.xcodeproj/project.pbxproj.bak rm -f Gaze.xcodeproj/project.pbxproj.bak