general: working through bugs and maintainability
This commit is contained in:
@@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
Reference in New Issue
Block a user