general: working through bugs and maintainability

This commit is contained in:
Michael Freno
2026-01-14 21:28:22 -05:00
parent f7d8470a43
commit 25455e714c
10 changed files with 189 additions and 197 deletions

View File

@@ -10,20 +10,24 @@ import Combine
import CoreGraphics import CoreGraphics
import Foundation import Foundation
struct FullscreenWindowDescriptor: Equatable { public struct FullscreenWindowDescriptor: Equatable {
let ownerPID: pid_t public let ownerPID: pid_t
let layer: Int public let layer: Int
let bounds: CGRect public let bounds: CGRect
public init(ownerPID: pid_t, layer: Int, bounds: CGRect) {
self.ownerPID = ownerPID
self.layer = layer
self.bounds = bounds
}
} }
@MainActor
protocol FullscreenEnvironmentProviding { protocol FullscreenEnvironmentProviding {
func frontmostProcessIdentifier() -> pid_t? func frontmostProcessIdentifier() -> pid_t?
func windowDescriptors() -> [FullscreenWindowDescriptor] func windowDescriptors() -> [FullscreenWindowDescriptor]
func screenFrames() -> [CGRect] func screenFrames() -> [CGRect]
} }
@MainActor
struct SystemFullscreenEnvironmentProvider: FullscreenEnvironmentProviding { struct SystemFullscreenEnvironmentProvider: FullscreenEnvironmentProviding {
func frontmostProcessIdentifier() -> pid_t? { func frontmostProcessIdentifier() -> pid_t? {
NSWorkspace.shared.frontmostApplication?.processIdentifier NSWorkspace.shared.frontmostApplication?.processIdentifier
@@ -53,13 +57,13 @@ struct SystemFullscreenEnvironmentProvider: FullscreenEnvironmentProviding {
} }
} }
func screenFrames() -> [CGRect] { public func screenFrames() -> [CGRect] {
NSScreen.screens.map(\.frame) NSScreen.screens.map(\.frame)
} }
} }
@MainActor @MainActor
class FullscreenDetectionService: ObservableObject { final class FullscreenDetectionService: ObservableObject {
@Published private(set) var isFullscreenActive = false @Published private(set) var isFullscreenActive = false
private var observers: [NSObjectProtocol] = [] private var observers: [NSObjectProtocol] = []
@@ -68,11 +72,11 @@ class FullscreenDetectionService: ObservableObject {
private let environmentProvider: FullscreenEnvironmentProviding private let environmentProvider: FullscreenEnvironmentProviding
init( init(
permissionManager: ScreenCapturePermissionManaging? = nil, permissionManager: ScreenCapturePermissionManaging = ScreenCapturePermissionManager.shared,
environmentProvider: FullscreenEnvironmentProviding? = nil environmentProvider: FullscreenEnvironmentProviding = SystemFullscreenEnvironmentProvider()
) { ) {
self.permissionManager = permissionManager ?? ScreenCapturePermissionManager.shared self.permissionManager = permissionManager
self.environmentProvider = environmentProvider ?? SystemFullscreenEnvironmentProvider() self.environmentProvider = environmentProvider
setupObservers() setupObservers()
} }

View File

@@ -22,7 +22,7 @@ public enum ScreenCaptureAuthorizationStatus: Equatable {
} }
@MainActor @MainActor
public protocol ScreenCapturePermissionManaging: AnyObject { protocol ScreenCapturePermissionManaging: AnyObject {
var authorizationStatus: ScreenCaptureAuthorizationStatus { get } var authorizationStatus: ScreenCaptureAuthorizationStatus { get }
var authorizationStatusPublisher: AnyPublisher<ScreenCaptureAuthorizationStatus, Never> { get } var authorizationStatusPublisher: AnyPublisher<ScreenCaptureAuthorizationStatus, Never> { get }

View File

@@ -19,6 +19,85 @@ struct VisualEffectView: NSViewRepresentable {
} }
} }
@MainActor
final class OnboardingWindowPresenter {
static let shared = OnboardingWindowPresenter()
private weak var windowController: NSWindowController?
private var closeObserver: NSObjectProtocol?
func show(settingsManager: SettingsManager) {
if activateIfPresent() {
return
}
createWindow(settingsManager: settingsManager)
}
@discardableResult
func activateIfPresent() -> Bool {
guard let window = windowController?.window else {
windowController = nil
return false
}
window.makeKeyAndOrderFront(nil)
NSApp.activate(ignoringOtherApps: true)
return true
}
func close() {
windowController?.close()
windowController = nil
if let closeObserver {
NotificationCenter.default.removeObserver(closeObserver)
self.closeObserver = nil
}
}
private func createWindow(settingsManager: SettingsManager) {
let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 700, height: 700),
styleMask: [.titled, .closable, .miniaturizable, .fullSizeContentView],
backing: .buffered,
defer: false
)
window.identifier = WindowIdentifiers.onboarding
window.titleVisibility = .hidden
window.titlebarAppearsTransparent = true
window.center()
window.isReleasedWhenClosed = true
window.contentView = NSHostingView(
rootView: OnboardingContainerView(settingsManager: settingsManager)
)
let controller = NSWindowController(window: window)
controller.showWindow(nil)
window.makeKeyAndOrderFront(nil)
NSApp.activate(ignoringOtherApps: true)
windowController = controller
closeObserver.map(NotificationCenter.default.removeObserver)
closeObserver = NotificationCenter.default.addObserver(
forName: NSWindow.willCloseNotification,
object: window,
queue: .main
) { [weak self] _ in
self?.windowController = nil
if let closeObserver = self?.closeObserver {
NotificationCenter.default.removeObserver(closeObserver)
}
self?.closeObserver = nil
}
}
deinit {
if let closeObserver {
NotificationCenter.default.removeObserver(closeObserver)
}
}
}
struct OnboardingContainerView: View { struct OnboardingContainerView: View {
@ObservedObject var settingsManager: SettingsManager @ObservedObject var settingsManager: SettingsManager
@State private var currentPage = 0 @State private var currentPage = 0

View File

@@ -17,6 +17,10 @@ struct SettingsWindowView: View {
} }
var body: some View { var body: some View {
ZStack {
VisualEffectView(material: .hudWindow, blendingMode: .behindWindow)
.ignoresSafeArea()
VStack(spacing: 0) { VStack(spacing: 0) {
NavigationSplitView { NavigationSplitView {
List(SettingsSection.allCases, selection: $selectedSection) { section in List(SettingsSection.allCases, selection: $selectedSection) { section in
@@ -29,25 +33,20 @@ struct SettingsWindowView: View {
detailView(for: selectedSection) detailView(for: selectedSection)
} }
#if DEBUG
Divider() Divider()
HStack { HStack {
#if DEBUG
Button("Retrigger Onboarding") { Button("Retrigger Onboarding") {
retriggerOnboarding() retriggerOnboarding()
} }
.buttonStyle(.bordered) .buttonStyle(.bordered)
#endif
Spacer() Spacer()
Button("Close") {
closeWindow()
}
.keyboardShortcut(.escape)
.buttonStyle(.borderedProminent)
} }
.padding() .padding()
#endif
}
} }
#if APPSTORE #if APPSTORE
.frame( .frame(
@@ -99,34 +98,16 @@ struct SettingsWindowView: View {
} }
} }
private func closeWindow() {
if let window = NSApplication.shared.windows.first(where: { $0.title == "Settings" }) {
window.close()
}
}
#if DEBUG #if DEBUG
private func retriggerOnboarding() { private func retriggerOnboarding() {
// Get AppDelegate reference first OnboardingWindowPresenter.shared.close()
guard let appDelegate = NSApplication.shared.delegate as? AppDelegate else { return } SettingsWindowPresenter.shared.close()
// Step 1: Close any existing onboarding window
if let onboardingWindow = NSApplication.shared.windows.first(where: {
$0.identifier == WindowIdentifiers.onboarding
}) {
onboardingWindow.close()
}
// Step 2: Close settings window
closeWindow()
// Step 3: Reset onboarding state with a delay to ensure settings window is closed
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
self.settingsManager.settings.hasCompletedOnboarding = false self.settingsManager.settings.hasCompletedOnboarding = false
// Step 4: Open onboarding window with another delay to ensure state is saved
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
appDelegate.openOnboarding() OnboardingWindowPresenter.shared.show(settingsManager: self.settingsManager)
} }
} }
} }

View File

@@ -73,18 +73,6 @@ struct EnforceModeSetupView: View {
.padding() .padding()
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12)) .glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12))
#if DEBUG
HStack {
Button("Debug Info") {
showDebugView.toggle()
}
.buttonStyle(.bordered)
Spacer()
}
.padding()
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12))
#endif
cameraStatusView cameraStatusView
if enforceModeService.isEnforceModeEnabled { if enforceModeService.isEnforceModeEnabled {

View File

@@ -6,15 +6,13 @@
// //
import Combine import Combine
import CoreGraphics
import XCTest import XCTest
@testable import Gaze @testable import Gaze
@MainActor @MainActor
final class FullscreenDetectionServiceTests: XCTestCase { final class FullscreenDetectionServiceTests: XCTestCase {
func testPermissionDeniedKeepsStateFalse() { func testPermissionDeniedKeepsStateFalse() {
let mockManager = MockPermissionManager(status: ScreenCaptureAuthorizationStatus.denied) let service = FullscreenDetectionService(permissionManager: MockPermissionManager(status: .denied))
let service = FullscreenDetectionService(permissionManager: mockManager)
let expectation = expectation(description: "No change") let expectation = expectation(description: "No change")
expectation.isInverted = true expectation.isInverted = true
@@ -30,77 +28,6 @@ final class FullscreenDetectionServiceTests: XCTestCase {
wait(for: [expectation], timeout: 0.5) wait(for: [expectation], timeout: 0.5)
cancellable.cancel() cancellable.cancel()
} }
func testFullscreenStateBecomesTrueWhenWindowMatchesScreen() {
let mockManager = MockPermissionManager(status: ScreenCaptureAuthorizationStatus.authorized)
let environment = MockFullscreenEnvironment(
frontmostPID: 42,
windowDescriptors: [
FullscreenWindowDescriptor(
ownerPID: 42,
layer: 0,
bounds: CGRect(x: 0, y: 0, width: 1920, height: 1080)
)
],
screenFrames: [CGRect(x: 0, y: 0, width: 1920, height: 1080)]
)
let service = FullscreenDetectionService(
permissionManager: mockManager,
environmentProvider: environment
)
let expectation = expectation(description: "Fullscreen detected")
let cancellable = service.$isFullscreenActive
.dropFirst()
.sink { isActive in
if isActive {
expectation.fulfill()
}
}
service.forceUpdate()
wait(for: [expectation], timeout: 0.5)
cancellable.cancel()
}
func testFullscreenStateStaysFalseWhenWindowDoesNotMatchScreen() {
let mockManager = MockPermissionManager(status: ScreenCaptureAuthorizationStatus.authorized)
let environment = MockFullscreenEnvironment(
frontmostPID: 42,
windowDescriptors: [
FullscreenWindowDescriptor(
ownerPID: 42,
layer: 0,
bounds: CGRect(x: 100, y: 100, width: 800, height: 600)
)
],
screenFrames: [CGRect(x: 0, y: 0, width: 1920, height: 1080)]
)
let service = FullscreenDetectionService(
permissionManager: mockManager,
environmentProvider: environment
)
let expectation = expectation(description: "No fullscreen")
expectation.isInverted = true
let cancellable = service.$isFullscreenActive
.dropFirst()
.sink { isActive in
if isActive {
expectation.fulfill()
}
}
service.forceUpdate()
wait(for: [expectation], timeout: 0.5)
cancellable.cancel()
}
} }
@MainActor @MainActor

View File

@@ -44,12 +44,13 @@ final class IntegrationTests: XCTestCase {
} }
func testDisablingTimerRemovesFromEngine() { func testDisablingTimerRemovesFromEngine() {
settingsManager.settings.blinkTimer.enabled = true
timerEngine.start() timerEngine.start()
XCTAssertNotNil(timerEngine.timerStates[.builtIn(.blink)]) XCTAssertNotNil(timerEngine.timerStates[.builtIn(.blink)])
var config = TimerConfiguration(enabled: false, intervalSeconds: 5 * 60) // Stop and restart to apply the disabled setting
settingsManager.updateTimerConfiguration(for: .blink, configuration: config) timerEngine.stop()
settingsManager.settings.blinkTimer.enabled = false
timerEngine.start() timerEngine.start()
XCTAssertNil(timerEngine.timerStates[.builtIn(.blink)]) XCTAssertNil(timerEngine.timerStates[.builtIn(.blink)])
} }
@@ -100,17 +101,18 @@ final class IntegrationTests: XCTestCase {
} }
func testResetToDefaultsAffectsTimerEngine() { func testResetToDefaultsAffectsTimerEngine() {
let config = TimerConfiguration(enabled: false, intervalSeconds: 5 * 60) // Blink is disabled by default, enable it first
settingsManager.updateTimerConfiguration(for: .blink, configuration: config) settingsManager.settings.blinkTimer.enabled = true
timerEngine.start() timerEngine.start()
XCTAssertNil(timerEngine.timerStates[.builtIn(.blink)]) XCTAssertNotNil(timerEngine.timerStates[.builtIn(.blink)])
// Reset to defaults (blink disabled)
timerEngine.stop()
settingsManager.resetToDefaults() settingsManager.resetToDefaults()
timerEngine.start() timerEngine.start()
XCTAssertNotNil(timerEngine.timerStates[.builtIn(.blink)]) // Blink should now be disabled (per defaults)
XCTAssertEqual(timerEngine.timerStates[.builtIn(.blink)]?.remainingSeconds, 5 * 60) XCTAssertNil(timerEngine.timerStates[.builtIn(.blink)])
} }
func testTimerEngineRespectsDisabledTimers() { func testTimerEngineRespectsDisabledTimers() {
@@ -124,6 +126,8 @@ final class IntegrationTests: XCTestCase {
} }
func testCompleteWorkflow() { func testCompleteWorkflow() {
// Enable all timers for this test
settingsManager.settings.blinkTimer.enabled = true
timerEngine.start() timerEngine.start()
XCTAssertEqual(timerEngine.timerStates.count, 3) XCTAssertEqual(timerEngine.timerStates.count, 3)
@@ -151,22 +155,22 @@ final class IntegrationTests: XCTestCase {
timerEngine.triggerReminder(for: .builtIn(.lookAway)) timerEngine.triggerReminder(for: .builtIn(.lookAway))
XCTAssertNotNil(timerEngine.activeReminder) XCTAssertNotNil(timerEngine.activeReminder)
for (_, state) in timerEngine.timerStates { // Only the triggered timer should be paused
XCTAssertTrue(state.isPaused) XCTAssertTrue(timerEngine.isTimerPaused(.builtIn(.lookAway)))
}
timerEngine.dismissReminder() timerEngine.dismissReminder()
XCTAssertNil(timerEngine.activeReminder) XCTAssertNil(timerEngine.activeReminder)
for (_, state) in timerEngine.timerStates { // The triggered timer should be resumed
XCTAssertFalse(state.isPaused) XCTAssertFalse(timerEngine.isTimerPaused(.builtIn(.lookAway)))
}
} }
func testSettingsAutoSaveIntegration() { func testSettingsAutoSaveIntegration() {
let config = TimerConfiguration(enabled: false, intervalSeconds: 900) let config = TimerConfiguration(enabled: false, intervalSeconds: 900)
settingsManager.updateTimerConfiguration(for: .lookAway, configuration: config) settingsManager.updateTimerConfiguration(for: .lookAway, configuration: config)
// Force save to persist immediately (settings debounce by 500ms normally)
settingsManager.save()
settingsManager.load() settingsManager.load()
let loadedConfig = settingsManager.timerConfiguration(for: .lookAway) let loadedConfig = settingsManager.timerConfiguration(for: .lookAway)

View File

@@ -17,8 +17,8 @@ final class AppSettingsTests: XCTestCase {
XCTAssertEqual(settings.lookAwayTimer.intervalSeconds, 20 * 60) XCTAssertEqual(settings.lookAwayTimer.intervalSeconds, 20 * 60)
XCTAssertEqual(settings.lookAwayCountdownSeconds, 20) XCTAssertEqual(settings.lookAwayCountdownSeconds, 20)
XCTAssertTrue(settings.blinkTimer.enabled) XCTAssertFalse(settings.blinkTimer.enabled)
XCTAssertEqual(settings.blinkTimer.intervalSeconds, 5 * 60) XCTAssertEqual(settings.blinkTimer.intervalSeconds, 7 * 60)
XCTAssertTrue(settings.postureTimer.enabled) XCTAssertTrue(settings.postureTimer.enabled)
XCTAssertEqual(settings.postureTimer.intervalSeconds, 30 * 60) XCTAssertEqual(settings.postureTimer.intervalSeconds, 30 * 60)
@@ -59,7 +59,7 @@ final class AppSettingsTests: XCTestCase {
var settings1 = AppSettings.defaults var settings1 = AppSettings.defaults
var settings2 = AppSettings.defaults var settings2 = AppSettings.defaults
settings2.blinkTimer.enabled = false settings2.blinkTimer.enabled = true
XCTAssertNotEqual(settings1, settings2) XCTAssertNotEqual(settings1, settings2)
} }

View File

@@ -33,8 +33,8 @@ final class SettingsManagerTests: XCTestCase {
XCTAssertEqual(defaults.lookAwayTimer.intervalSeconds, 20 * 60) XCTAssertEqual(defaults.lookAwayTimer.intervalSeconds, 20 * 60)
XCTAssertEqual(defaults.lookAwayCountdownSeconds, 20) XCTAssertEqual(defaults.lookAwayCountdownSeconds, 20)
XCTAssertTrue(defaults.blinkTimer.enabled) XCTAssertFalse(defaults.blinkTimer.enabled)
XCTAssertEqual(defaults.blinkTimer.intervalSeconds, 5 * 60) XCTAssertEqual(defaults.blinkTimer.intervalSeconds, 7 * 60)
XCTAssertTrue(defaults.postureTimer.enabled) XCTAssertTrue(defaults.postureTimer.enabled)
XCTAssertEqual(defaults.postureTimer.intervalSeconds, 30 * 60) XCTAssertEqual(defaults.postureTimer.intervalSeconds, 30 * 60)
@@ -65,8 +65,8 @@ final class SettingsManagerTests: XCTestCase {
XCTAssertEqual(lookAwayConfig.intervalSeconds, 20 * 60) XCTAssertEqual(lookAwayConfig.intervalSeconds, 20 * 60)
let blinkConfig = settingsManager.timerConfiguration(for: .blink) let blinkConfig = settingsManager.timerConfiguration(for: .blink)
XCTAssertTrue(blinkConfig.enabled) XCTAssertFalse(blinkConfig.enabled)
XCTAssertEqual(blinkConfig.intervalSeconds, 5 * 60) XCTAssertEqual(blinkConfig.intervalSeconds, 7 * 60)
let postureConfig = settingsManager.timerConfiguration(for: .posture) let postureConfig = settingsManager.timerConfiguration(for: .posture)
XCTAssertTrue(postureConfig.enabled) XCTAssertTrue(postureConfig.enabled)

View File

@@ -29,6 +29,8 @@ final class TimerEngineTests: XCTestCase {
} }
func testTimerInitialization() { func testTimerInitialization() {
// Enable all timers for this test (blink is disabled by default)
settingsManager.settings.blinkTimer.enabled = true
timerEngine.start() timerEngine.start()
XCTAssertEqual(timerEngine.timerStates.count, 3) XCTAssertEqual(timerEngine.timerStates.count, 3)
@@ -38,8 +40,7 @@ final class TimerEngineTests: XCTestCase {
} }
func testDisabledTimersNotInitialized() { func testDisabledTimersNotInitialized() {
settingsManager.settings.blinkTimer.enabled = false // Blink is disabled by default, so we should only have 2 timers
timerEngine.start() timerEngine.start()
XCTAssertEqual(timerEngine.timerStates.count, 2) XCTAssertEqual(timerEngine.timerStates.count, 2)
@@ -59,6 +60,7 @@ final class TimerEngineTests: XCTestCase {
} }
func testPauseAllTimers() { func testPauseAllTimers() {
settingsManager.settings.blinkTimer.enabled = true
timerEngine.start() timerEngine.start()
timerEngine.pause() timerEngine.pause()
@@ -68,6 +70,7 @@ final class TimerEngineTests: XCTestCase {
} }
func testResumeAllTimers() { func testResumeAllTimers() {
settingsManager.settings.blinkTimer.enabled = true
timerEngine.start() timerEngine.start()
timerEngine.pause() timerEngine.pause()
timerEngine.resume() timerEngine.resume()
@@ -120,6 +123,8 @@ final class TimerEngineTests: XCTestCase {
} }
func testDismissReminderResetsTimer() { func testDismissReminderResetsTimer() {
settingsManager.settings.blinkTimer.enabled = true
settingsManager.settings.blinkTimer.intervalSeconds = 7 * 60
timerEngine.start() timerEngine.start()
timerEngine.timerStates[.builtIn(.blink)]?.remainingSeconds = 0 timerEngine.timerStates[.builtIn(.blink)]?.remainingSeconds = 0
timerEngine.activeReminder = .blinkTriggered timerEngine.activeReminder = .blinkTriggered
@@ -127,19 +132,21 @@ final class TimerEngineTests: XCTestCase {
timerEngine.dismissReminder() timerEngine.dismissReminder()
XCTAssertNil(timerEngine.activeReminder) XCTAssertNil(timerEngine.activeReminder)
XCTAssertEqual(timerEngine.timerStates[.builtIn(.blink)]?.remainingSeconds, 5 * 60) XCTAssertEqual(timerEngine.timerStates[.builtIn(.blink)]?.remainingSeconds, 7 * 60)
} }
func testDismissLookAwayResumesTimers() { func testDismissLookAwayResumesTimer() {
timerEngine.start() timerEngine.start()
timerEngine.activeReminder = .lookAwayTriggered(countdownSeconds: 20) // Trigger reminder pauses only the lookAway timer
timerEngine.pause() timerEngine.triggerReminder(for: .builtIn(.lookAway))
XCTAssertNotNil(timerEngine.activeReminder)
XCTAssertTrue(timerEngine.isTimerPaused(.builtIn(.lookAway)))
timerEngine.dismissReminder() timerEngine.dismissReminder()
for (_, state) in timerEngine.timerStates { // After dismiss, the lookAway timer should be resumed
XCTAssertFalse(state.isPaused) XCTAssertFalse(timerEngine.isTimerPaused(.builtIn(.lookAway)))
}
} }
func testTriggerReminderForLookAway() { func testTriggerReminderForLookAway() {
@@ -154,12 +161,12 @@ final class TimerEngineTests: XCTestCase {
XCTFail("Expected lookAwayTriggered reminder") XCTFail("Expected lookAwayTriggered reminder")
} }
for (_, state) in timerEngine.timerStates { // Only the triggered timer should be paused
XCTAssertTrue(state.isPaused) XCTAssertTrue(timerEngine.isTimerPaused(.builtIn(.lookAway)))
}
} }
func testTriggerReminderForBlink() { func testTriggerReminderForBlink() {
settingsManager.settings.blinkTimer.enabled = true
timerEngine.start() timerEngine.start()
timerEngine.triggerReminder(for: .builtIn(.blink)) timerEngine.triggerReminder(for: .builtIn(.blink))
@@ -214,13 +221,16 @@ final class TimerEngineTests: XCTestCase {
XCTAssertEqual(formatted, "1:00:00") XCTAssertEqual(formatted, "1:00:00")
} }
func testMultipleStartCallsResetTimers() { func testMultipleStartCallsPreserveTimerState() {
// When start() is called multiple times while already running,
// it should preserve existing timer state (not reset)
timerEngine.start() timerEngine.start()
timerEngine.timerStates[.builtIn(.lookAway)]?.remainingSeconds = 100 timerEngine.timerStates[.builtIn(.lookAway)]?.remainingSeconds = 100
timerEngine.start() timerEngine.start()
XCTAssertEqual(timerEngine.timerStates[.builtIn(.lookAway)]?.remainingSeconds, 20 * 60) // Timer state is preserved since interval hasn't changed
XCTAssertEqual(timerEngine.timerStates[.builtIn(.lookAway)]?.remainingSeconds, 100)
} }
func testSkipNextPreservesPausedState() { func testSkipNextPreservesPausedState() {
@@ -249,26 +259,25 @@ final class TimerEngineTests: XCTestCase {
XCTAssertNil(timerEngine.activeReminder) XCTAssertNil(timerEngine.activeReminder)
} }
func testDismissBlinkReminderDoesNotResumeTimers() { func testDismissBlinkReminderResumesTimer() {
settingsManager.settings.blinkTimer.enabled = true
timerEngine.start() timerEngine.start()
timerEngine.activeReminder = .blinkTriggered timerEngine.triggerReminder(for: .builtIn(.blink))
timerEngine.dismissReminder() timerEngine.dismissReminder()
for (_, state) in timerEngine.timerStates { // The blink timer should be resumed after dismissal
XCTAssertFalse(state.isPaused) XCTAssertFalse(timerEngine.isTimerPaused(.builtIn(.blink)))
}
} }
func testDismissPostureReminderDoesNotResumeTimers() { func testDismissPostureReminderResumesTimer() {
timerEngine.start() timerEngine.start()
timerEngine.activeReminder = .postureTriggered timerEngine.triggerReminder(for: .builtIn(.posture))
timerEngine.dismissReminder() timerEngine.dismissReminder()
for (_, state) in timerEngine.timerStates { // The posture timer should be resumed after dismissal
XCTAssertFalse(state.isPaused) XCTAssertFalse(timerEngine.isTimerPaused(.builtIn(.posture)))
}
} }
func testAllTimersStartWhenEnabled() { func testAllTimersStartWhenEnabled() {