general: basic cleanup
This commit is contained in:
@@ -28,11 +28,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
|
|||||||
self.windowManager = WindowManager.shared
|
self.windowManager = WindowManager.shared
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
// Setup window close observers
|
|
||||||
setupWindowCloseObservers()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initializer for testing with injectable dependencies
|
|
||||||
init(serviceContainer: ServiceContainer, windowManager: WindowManaging) {
|
init(serviceContainer: ServiceContainer, windowManager: WindowManaging) {
|
||||||
self.serviceContainer = serviceContainer
|
self.serviceContainer = serviceContainer
|
||||||
self.windowManager = windowManager
|
self.windowManager = windowManager
|
||||||
@@ -40,16 +37,12 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func applicationDidFinishLaunching(_ notification: Notification) {
|
func applicationDidFinishLaunching(_ notification: Notification) {
|
||||||
// Set activation policy to hide dock icon
|
|
||||||
NSApplication.shared.setActivationPolicy(.accessory)
|
NSApplication.shared.setActivationPolicy(.accessory)
|
||||||
|
|
||||||
logInfo("🚀 Application did finish launching")
|
|
||||||
|
|
||||||
timerEngine = serviceContainer.timerEngine
|
timerEngine = serviceContainer.timerEngine
|
||||||
|
|
||||||
serviceContainer.setupSmartModeServices()
|
serviceContainer.setupSmartModeServices()
|
||||||
|
|
||||||
|
|
||||||
// Initialize update manager after onboarding is complete
|
// Initialize update manager after onboarding is complete
|
||||||
if settingsManager.settings.hasCompletedOnboarding {
|
if settingsManager.settings.hasCompletedOnboarding {
|
||||||
updateManager = UpdateManager.shared
|
updateManager = UpdateManager.shared
|
||||||
@@ -62,24 +55,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
|
|||||||
if settingsManager.settings.hasCompletedOnboarding {
|
if settingsManager.settings.hasCompletedOnboarding {
|
||||||
startTimers()
|
startTimers()
|
||||||
}
|
}
|
||||||
|
|
||||||
// DEBUG: Auto-start eye tracking test mode if launch argument is present
|
|
||||||
#if DEBUG
|
|
||||||
if CommandLine.arguments.contains("--debug-eye-tracking") {
|
|
||||||
NSLog("🔬 DEBUG: Auto-starting eye tracking test mode")
|
|
||||||
Task { @MainActor in
|
|
||||||
// Enable enforce mode if not already
|
|
||||||
if !settingsManager.settings.enforcementMode {
|
|
||||||
settingsManager.settings.enforcementMode = true
|
|
||||||
}
|
|
||||||
// Start test mode after a brief delay
|
|
||||||
try? await Task.sleep(nanoseconds: 1_000_000_000) // 1 second
|
|
||||||
NSLog("🔬 DEBUG: Starting test mode now...")
|
|
||||||
await EnforceModeService.shared.startTestMode()
|
|
||||||
NSLog("🔬 DEBUG: Test mode started")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: Smart mode setup is now handled by ServiceContainer
|
// Note: Smart mode setup is now handled by ServiceContainer
|
||||||
@@ -105,7 +80,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
|
|||||||
func onboardingCompleted() {
|
func onboardingCompleted() {
|
||||||
startTimers()
|
startTimers()
|
||||||
|
|
||||||
// Start update checks after onboarding
|
|
||||||
if updateManager == nil {
|
if updateManager == nil {
|
||||||
updateManager = UpdateManager.shared
|
updateManager = UpdateManager.shared
|
||||||
}
|
}
|
||||||
@@ -249,24 +223,4 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setupWindowCloseObservers() {
|
|
||||||
NotificationCenter.default.addObserver(
|
|
||||||
self,
|
|
||||||
selector: #selector(settingsWindowDidClose),
|
|
||||||
name: Notification.Name("SettingsWindowDidClose"),
|
|
||||||
object: nil
|
|
||||||
)
|
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(
|
|
||||||
self,
|
|
||||||
selector: #selector(onboardingWindowDidClose),
|
|
||||||
name: Notification.Name("OnboardingWindowDidClose"),
|
|
||||||
object: nil
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc private func settingsWindowDidClose() {}
|
|
||||||
|
|
||||||
@objc private func onboardingWindowDidClose() {}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ struct GazeApp: App {
|
|||||||
}
|
}
|
||||||
.windowStyle(.hiddenTitleBar)
|
.windowStyle(.hiddenTitleBar)
|
||||||
.windowResizability(.contentSize)
|
.windowResizability(.contentSize)
|
||||||
.defaultSize(width: 700, height: 700)
|
.defaultSize(width: 1000, height: 700)
|
||||||
.commands {
|
.commands {
|
||||||
CommandGroup(replacing: .newItem) {}
|
CommandGroup(replacing: .newItem) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,22 +5,22 @@
|
|||||||
// Fullscreen overlay view for eye tracking calibration targets.
|
// Fullscreen overlay view for eye tracking calibration targets.
|
||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
import Combine
|
|
||||||
import AVFoundation
|
import AVFoundation
|
||||||
|
import Combine
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
struct CalibrationOverlayView: View {
|
struct CalibrationOverlayView: View {
|
||||||
@StateObject private var calibrationManager = CalibrationManager.shared
|
@StateObject private var calibrationManager = CalibrationManager.shared
|
||||||
@StateObject private var eyeTrackingService = EyeTrackingService.shared
|
@StateObject private var eyeTrackingService = EyeTrackingService.shared
|
||||||
@StateObject private var viewModel = CalibrationOverlayViewModel()
|
@StateObject private var viewModel = CalibrationOverlayViewModel()
|
||||||
|
|
||||||
let onDismiss: () -> Void
|
let onDismiss: () -> Void
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
GeometryReader { geometry in
|
GeometryReader { geometry in
|
||||||
ZStack {
|
ZStack {
|
||||||
Color.black.ignoresSafeArea()
|
Color.black.ignoresSafeArea()
|
||||||
|
|
||||||
// Camera preview at 50% opacity (mirrored for natural feel)
|
// Camera preview at 50% opacity (mirrored for natural feel)
|
||||||
if let previewLayer = eyeTrackingService.previewLayer {
|
if let previewLayer = eyeTrackingService.previewLayer {
|
||||||
CameraPreviewView(previewLayer: previewLayer, borderColor: .clear)
|
CameraPreviewView(previewLayer: previewLayer, borderColor: .clear)
|
||||||
@@ -28,14 +28,16 @@ struct CalibrationOverlayView: View {
|
|||||||
.opacity(0.5)
|
.opacity(0.5)
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
}
|
}
|
||||||
|
|
||||||
if let error = viewModel.showError {
|
if let error = viewModel.showError {
|
||||||
errorView(error)
|
errorView(error)
|
||||||
} else if !viewModel.cameraStarted {
|
} else if !viewModel.cameraStarted {
|
||||||
startingCameraView
|
startingCameraView
|
||||||
} else if calibrationManager.isCalibrating {
|
} else if calibrationManager.isCalibrating {
|
||||||
calibrationContentView(screenSize: geometry.size)
|
calibrationContentView(screenSize: geometry.size)
|
||||||
} else if viewModel.calibrationStarted && calibrationManager.calibrationData.isComplete {
|
} else if viewModel.calibrationStarted
|
||||||
|
&& calibrationManager.calibrationData.isComplete
|
||||||
|
{
|
||||||
// Only show completion if we started calibration this session AND it completed
|
// Only show completion if we started calibration this session AND it completed
|
||||||
completionView
|
completionView
|
||||||
} else if viewModel.calibrationStarted {
|
} else if viewModel.calibrationStarted {
|
||||||
@@ -45,10 +47,12 @@ struct CalibrationOverlayView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.task {
|
.task {
|
||||||
await viewModel.startCamera(eyeTrackingService: eyeTrackingService, calibrationManager: calibrationManager)
|
await viewModel.startCamera(
|
||||||
|
eyeTrackingService: eyeTrackingService, calibrationManager: calibrationManager)
|
||||||
}
|
}
|
||||||
.onDisappear {
|
.onDisappear {
|
||||||
viewModel.cleanup(eyeTrackingService: eyeTrackingService, calibrationManager: calibrationManager)
|
viewModel.cleanup(
|
||||||
|
eyeTrackingService: eyeTrackingService, calibrationManager: calibrationManager)
|
||||||
}
|
}
|
||||||
.onChange(of: calibrationManager.currentStep) { oldStep, newStep in
|
.onChange(of: calibrationManager.currentStep) { oldStep, newStep in
|
||||||
if newStep != nil && oldStep != newStep {
|
if newStep != nil && oldStep != newStep {
|
||||||
@@ -56,38 +60,38 @@ struct CalibrationOverlayView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Starting Camera View
|
// MARK: - Starting Camera View
|
||||||
|
|
||||||
private var startingCameraView: some View {
|
private var startingCameraView: some View {
|
||||||
VStack(spacing: 20) {
|
VStack(spacing: 20) {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
.scaleEffect(2)
|
.scaleEffect(2)
|
||||||
.tint(.white)
|
.tint(.white)
|
||||||
|
|
||||||
Text("Starting camera...")
|
Text("Starting camera...")
|
||||||
.font(.title2)
|
.font(.title2)
|
||||||
.foregroundStyle(.white)
|
.foregroundStyle(.white)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Error View
|
// MARK: - Error View
|
||||||
|
|
||||||
private func errorView(_ message: String) -> some View {
|
private func errorView(_ message: String) -> some View {
|
||||||
VStack(spacing: 20) {
|
VStack(spacing: 20) {
|
||||||
Image(systemName: "exclamationmark.triangle.fill")
|
Image(systemName: "exclamationmark.triangle.fill")
|
||||||
.font(.system(size: 60))
|
.font(.system(size: 60))
|
||||||
.foregroundStyle(.orange)
|
.foregroundStyle(.orange)
|
||||||
|
|
||||||
Text("Camera Error")
|
Text("Camera Error")
|
||||||
.font(.title)
|
.font(.title)
|
||||||
.foregroundStyle(.white)
|
.foregroundStyle(.white)
|
||||||
|
|
||||||
Text(message)
|
Text(message)
|
||||||
.font(.body)
|
.font(.body)
|
||||||
.foregroundStyle(.gray)
|
.foregroundStyle(.gray)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
|
|
||||||
Button("Close") {
|
Button("Close") {
|
||||||
onDismiss()
|
onDismiss()
|
||||||
}
|
}
|
||||||
@@ -96,20 +100,20 @@ struct CalibrationOverlayView: View {
|
|||||||
}
|
}
|
||||||
.padding(40)
|
.padding(40)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Calibration Content
|
// MARK: - Calibration Content
|
||||||
|
|
||||||
private func calibrationContentView(screenSize: CGSize) -> some View {
|
private func calibrationContentView(screenSize: CGSize) -> some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
VStack {
|
VStack {
|
||||||
progressBar
|
progressBar
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
|
|
||||||
if let step = calibrationManager.currentStep {
|
if let step = calibrationManager.currentStep {
|
||||||
calibrationTarget(for: step, screenSize: screenSize)
|
calibrationTarget(for: step, screenSize: screenSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
VStack {
|
VStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
HStack {
|
HStack {
|
||||||
@@ -122,7 +126,7 @@ struct CalibrationOverlayView: View {
|
|||||||
.padding(.horizontal, 40)
|
.padding(.horizontal, 40)
|
||||||
.padding(.bottom, 40)
|
.padding(.bottom, 40)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Face detection indicator
|
// Face detection indicator
|
||||||
VStack {
|
VStack {
|
||||||
HStack {
|
HStack {
|
||||||
@@ -133,9 +137,9 @@ struct CalibrationOverlayView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Progress Bar
|
// MARK: - Progress Bar
|
||||||
|
|
||||||
private var progressBar: some View {
|
private var progressBar: some View {
|
||||||
VStack(spacing: 10) {
|
VStack(spacing: 10) {
|
||||||
HStack {
|
HStack {
|
||||||
@@ -145,7 +149,7 @@ struct CalibrationOverlayView: View {
|
|||||||
Text(calibrationManager.progressText)
|
Text(calibrationManager.progressText)
|
||||||
.foregroundStyle(.white.opacity(0.7))
|
.foregroundStyle(.white.opacity(0.7))
|
||||||
}
|
}
|
||||||
|
|
||||||
ProgressView(value: calibrationManager.progress)
|
ProgressView(value: calibrationManager.progress)
|
||||||
.progressViewStyle(.linear)
|
.progressViewStyle(.linear)
|
||||||
.tint(.blue)
|
.tint(.blue)
|
||||||
@@ -153,15 +157,15 @@ struct CalibrationOverlayView: View {
|
|||||||
.padding()
|
.padding()
|
||||||
.background(Color.black.opacity(0.7))
|
.background(Color.black.opacity(0.7))
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Face Detection Indicator
|
// MARK: - Face Detection Indicator
|
||||||
|
|
||||||
private var faceDetectionIndicator: some View {
|
private var faceDetectionIndicator: some View {
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
Circle()
|
Circle()
|
||||||
.fill(viewModel.stableFaceDetected ? Color.green : Color.red)
|
.fill(viewModel.stableFaceDetected ? Color.green : Color.red)
|
||||||
.frame(width: 12, height: 12)
|
.frame(width: 12, height: 12)
|
||||||
|
|
||||||
Text(viewModel.stableFaceDetected ? "Face detected" : "No face detected")
|
Text(viewModel.stableFaceDetected ? "Face detected" : "No face detected")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.white.opacity(0.8))
|
.foregroundStyle(.white.opacity(0.8))
|
||||||
@@ -173,13 +177,13 @@ struct CalibrationOverlayView: View {
|
|||||||
.padding()
|
.padding()
|
||||||
.animation(.easeInOut(duration: 0.3), value: viewModel.stableFaceDetected)
|
.animation(.easeInOut(duration: 0.3), value: viewModel.stableFaceDetected)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Calibration Target
|
// MARK: - Calibration Target
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private func calibrationTarget(for step: CalibrationStep, screenSize: CGSize) -> some View {
|
private func calibrationTarget(for step: CalibrationStep, screenSize: CGSize) -> some View {
|
||||||
let position = targetPosition(for: step, screenSize: screenSize)
|
let position = targetPosition(for: step, screenSize: screenSize)
|
||||||
|
|
||||||
VStack(spacing: 20) {
|
VStack(spacing: 20) {
|
||||||
ZStack {
|
ZStack {
|
||||||
// Outer ring (pulsing when counting down)
|
// Outer ring (pulsing when counting down)
|
||||||
@@ -188,11 +192,11 @@ struct CalibrationOverlayView: View {
|
|||||||
.frame(width: 100, height: 100)
|
.frame(width: 100, height: 100)
|
||||||
.scaleEffect(viewModel.isCountingDown ? 1.2 : 1.0)
|
.scaleEffect(viewModel.isCountingDown ? 1.2 : 1.0)
|
||||||
.animation(
|
.animation(
|
||||||
viewModel.isCountingDown
|
viewModel.isCountingDown
|
||||||
? .easeInOut(duration: 0.6).repeatForever(autoreverses: true)
|
? .easeInOut(duration: 0.6).repeatForever(autoreverses: true)
|
||||||
: .default,
|
: .default,
|
||||||
value: viewModel.isCountingDown)
|
value: viewModel.isCountingDown)
|
||||||
|
|
||||||
// Progress ring when collecting
|
// Progress ring when collecting
|
||||||
if calibrationManager.isCollectingSamples {
|
if calibrationManager.isCollectingSamples {
|
||||||
Circle()
|
Circle()
|
||||||
@@ -200,15 +204,17 @@ struct CalibrationOverlayView: View {
|
|||||||
.stroke(Color.green, lineWidth: 4)
|
.stroke(Color.green, lineWidth: 4)
|
||||||
.frame(width: 90, height: 90)
|
.frame(width: 90, height: 90)
|
||||||
.rotationEffect(.degrees(-90))
|
.rotationEffect(.degrees(-90))
|
||||||
.animation(.linear(duration: 0.1), value: calibrationManager.samplesCollected)
|
.animation(
|
||||||
|
.linear(duration: 0.1), value: calibrationManager.samplesCollected)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inner circle
|
// Inner circle
|
||||||
Circle()
|
Circle()
|
||||||
.fill(calibrationManager.isCollectingSamples ? Color.green : Color.blue)
|
.fill(calibrationManager.isCollectingSamples ? Color.green : Color.blue)
|
||||||
.frame(width: 60, height: 60)
|
.frame(width: 60, height: 60)
|
||||||
.animation(.easeInOut(duration: 0.3), value: calibrationManager.isCollectingSamples)
|
.animation(
|
||||||
|
.easeInOut(duration: 0.3), value: calibrationManager.isCollectingSamples)
|
||||||
|
|
||||||
// Countdown number or collecting indicator
|
// Countdown number or collecting indicator
|
||||||
if viewModel.isCountingDown && viewModel.countdownValue > 0 {
|
if viewModel.isCountingDown && viewModel.countdownValue > 0 {
|
||||||
Text("\(viewModel.countdownValue)")
|
Text("\(viewModel.countdownValue)")
|
||||||
@@ -220,7 +226,7 @@ struct CalibrationOverlayView: View {
|
|||||||
.foregroundStyle(.white)
|
.foregroundStyle(.white)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(instructionText(for: step))
|
Text(instructionText(for: step))
|
||||||
.font(.title2)
|
.font(.title2)
|
||||||
.foregroundStyle(.white)
|
.foregroundStyle(.white)
|
||||||
@@ -231,7 +237,7 @@ struct CalibrationOverlayView: View {
|
|||||||
}
|
}
|
||||||
.position(position)
|
.position(position)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func instructionText(for step: CalibrationStep) -> String {
|
private func instructionText(for step: CalibrationStep) -> String {
|
||||||
if viewModel.isCountingDown && viewModel.countdownValue > 0 {
|
if viewModel.isCountingDown && viewModel.countdownValue > 0 {
|
||||||
return "Get ready..."
|
return "Get ready..."
|
||||||
@@ -241,9 +247,9 @@ struct CalibrationOverlayView: View {
|
|||||||
return step.instructionText
|
return step.instructionText
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Buttons
|
// MARK: - Buttons
|
||||||
|
|
||||||
private var skipButton: some View {
|
private var skipButton: some View {
|
||||||
Button {
|
Button {
|
||||||
viewModel.skipCurrentStep(calibrationManager: calibrationManager)
|
viewModel.skipCurrentStep(calibrationManager: calibrationManager)
|
||||||
@@ -257,10 +263,11 @@ struct CalibrationOverlayView: View {
|
|||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var cancelButton: some View {
|
private var cancelButton: some View {
|
||||||
Button {
|
Button {
|
||||||
viewModel.cleanup(eyeTrackingService: eyeTrackingService, calibrationManager: calibrationManager)
|
viewModel.cleanup(
|
||||||
|
eyeTrackingService: eyeTrackingService, calibrationManager: calibrationManager)
|
||||||
onDismiss()
|
onDismiss()
|
||||||
} label: {
|
} label: {
|
||||||
HStack(spacing: 6) {
|
HStack(spacing: 6) {
|
||||||
@@ -276,24 +283,24 @@ struct CalibrationOverlayView: View {
|
|||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.keyboardShortcut(.escape, modifiers: [])
|
.keyboardShortcut(.escape, modifiers: [])
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Completion View
|
// MARK: - Completion View
|
||||||
|
|
||||||
private var completionView: some View {
|
private var completionView: some View {
|
||||||
VStack(spacing: 30) {
|
VStack(spacing: 30) {
|
||||||
Image(systemName: "checkmark.circle.fill")
|
Image(systemName: "checkmark.circle.fill")
|
||||||
.font(.system(size: 80))
|
.font(.system(size: 80))
|
||||||
.foregroundStyle(.green)
|
.foregroundStyle(.green)
|
||||||
|
|
||||||
Text("Calibration Complete!")
|
Text("Calibration Complete!")
|
||||||
.font(.largeTitle)
|
.font(.largeTitle)
|
||||||
.foregroundStyle(.white)
|
.foregroundStyle(.white)
|
||||||
.fontWeight(.bold)
|
.fontWeight(.bold)
|
||||||
|
|
||||||
Text("Your eye tracking has been calibrated successfully.")
|
Text("Your eye tracking has been calibrated successfully.")
|
||||||
.font(.title3)
|
.font(.title3)
|
||||||
.foregroundStyle(.gray)
|
.foregroundStyle(.gray)
|
||||||
|
|
||||||
Button("Done") {
|
Button("Done") {
|
||||||
onDismiss()
|
onDismiss()
|
||||||
}
|
}
|
||||||
@@ -307,18 +314,18 @@ struct CalibrationOverlayView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Helper Methods
|
// MARK: - Helper Methods
|
||||||
|
|
||||||
private func targetPosition(for step: CalibrationStep, screenSize: CGSize) -> CGPoint {
|
private func targetPosition(for step: CalibrationStep, screenSize: CGSize) -> CGPoint {
|
||||||
let width = screenSize.width
|
let width = screenSize.width
|
||||||
let height = screenSize.height
|
let height = screenSize.height
|
||||||
|
|
||||||
let centerX = width / 2
|
let centerX = width / 2
|
||||||
let centerY = height / 2
|
let centerY = height / 2
|
||||||
let marginX: CGFloat = 150
|
let marginX: CGFloat = 150
|
||||||
let marginY: CGFloat = 120
|
let marginY: CGFloat = 120
|
||||||
|
|
||||||
switch step {
|
switch step {
|
||||||
case .center:
|
case .center:
|
||||||
return CGPoint(x: centerX, y: centerY)
|
return CGPoint(x: centerX, y: centerY)
|
||||||
@@ -356,23 +363,24 @@ class CalibrationOverlayViewModel: ObservableObject {
|
|||||||
@Published var showError: String?
|
@Published var showError: String?
|
||||||
@Published var calibrationStarted = false
|
@Published var calibrationStarted = false
|
||||||
@Published var stableFaceDetected = false // Debounced face detection
|
@Published var stableFaceDetected = false // Debounced face detection
|
||||||
|
|
||||||
private var countdownTask: Task<Void, Never>?
|
private var countdownTask: Task<Void, Never>?
|
||||||
private var faceDetectionCancellable: AnyCancellable?
|
private var faceDetectionCancellable: AnyCancellable?
|
||||||
private var lastFaceDetectedTime: Date = .distantPast
|
private var lastFaceDetectedTime: Date = .distantPast
|
||||||
private let faceDetectionDebounce: TimeInterval = 0.5 // 500ms debounce
|
private let faceDetectionDebounce: TimeInterval = 0.5 // 500ms debounce
|
||||||
|
|
||||||
func startCamera(eyeTrackingService: EyeTrackingService, calibrationManager: CalibrationManager) async {
|
func startCamera(eyeTrackingService: EyeTrackingService, calibrationManager: CalibrationManager)
|
||||||
|
async
|
||||||
|
{
|
||||||
do {
|
do {
|
||||||
try await eyeTrackingService.startEyeTracking()
|
try await eyeTrackingService.startEyeTracking()
|
||||||
cameraStarted = true
|
cameraStarted = true
|
||||||
|
|
||||||
// Set up debounced face detection
|
// Set up debounced face detection
|
||||||
setupFaceDetectionObserver(eyeTrackingService: eyeTrackingService)
|
setupFaceDetectionObserver(eyeTrackingService: eyeTrackingService)
|
||||||
|
|
||||||
// Small delay to let camera stabilize
|
try? await Task.sleep(for: .seconds(0.5))
|
||||||
try? await Task.sleep(nanoseconds: 500_000_000)
|
|
||||||
|
|
||||||
// Reset any previous calibration data before starting fresh
|
// Reset any previous calibration data before starting fresh
|
||||||
calibrationManager.resetForNewCalibration()
|
calibrationManager.resetForNewCalibration()
|
||||||
calibrationManager.startCalibration()
|
calibrationManager.startCalibration()
|
||||||
@@ -382,13 +390,13 @@ class CalibrationOverlayViewModel: ObservableObject {
|
|||||||
showError = "Failed to start camera: \(error.localizedDescription)"
|
showError = "Failed to start camera: \(error.localizedDescription)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setupFaceDetectionObserver(eyeTrackingService: EyeTrackingService) {
|
private func setupFaceDetectionObserver(eyeTrackingService: EyeTrackingService) {
|
||||||
faceDetectionCancellable = eyeTrackingService.$faceDetected
|
faceDetectionCancellable = eyeTrackingService.$faceDetected
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { [weak self] detected in
|
.sink { [weak self] detected in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
|
||||||
if detected {
|
if detected {
|
||||||
// Face detected - update immediately
|
// Face detected - update immediately
|
||||||
self.lastFaceDetectedTime = Date()
|
self.lastFaceDetectedTime = Date()
|
||||||
@@ -402,39 +410,39 @@ class CalibrationOverlayViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func cleanup(eyeTrackingService: EyeTrackingService, calibrationManager: CalibrationManager) {
|
func cleanup(eyeTrackingService: EyeTrackingService, calibrationManager: CalibrationManager) {
|
||||||
countdownTask?.cancel()
|
countdownTask?.cancel()
|
||||||
countdownTask = nil
|
countdownTask = nil
|
||||||
faceDetectionCancellable?.cancel()
|
faceDetectionCancellable?.cancel()
|
||||||
faceDetectionCancellable = nil
|
faceDetectionCancellable = nil
|
||||||
isCountingDown = false
|
isCountingDown = false
|
||||||
|
|
||||||
if calibrationManager.isCalibrating {
|
if calibrationManager.isCalibrating {
|
||||||
calibrationManager.cancelCalibration()
|
calibrationManager.cancelCalibration()
|
||||||
}
|
}
|
||||||
|
|
||||||
eyeTrackingService.stopEyeTracking()
|
eyeTrackingService.stopEyeTracking()
|
||||||
}
|
}
|
||||||
|
|
||||||
func skipCurrentStep(calibrationManager: CalibrationManager) {
|
func skipCurrentStep(calibrationManager: CalibrationManager) {
|
||||||
countdownTask?.cancel()
|
countdownTask?.cancel()
|
||||||
countdownTask = nil
|
countdownTask = nil
|
||||||
isCountingDown = false
|
isCountingDown = false
|
||||||
calibrationManager.skipStep()
|
calibrationManager.skipStep()
|
||||||
}
|
}
|
||||||
|
|
||||||
func startStepCountdown(calibrationManager: CalibrationManager) {
|
func startStepCountdown(calibrationManager: CalibrationManager) {
|
||||||
countdownTask?.cancel()
|
countdownTask?.cancel()
|
||||||
countdownTask = nil
|
countdownTask = nil
|
||||||
countdownValue = 1
|
countdownValue = 1
|
||||||
isCountingDown = true
|
isCountingDown = true
|
||||||
|
|
||||||
countdownTask = Task { @MainActor in
|
countdownTask = Task { @MainActor in
|
||||||
// Just 1 second countdown
|
// Just 1 second countdown
|
||||||
try? await Task.sleep(for: .seconds(1))
|
try? await Task.sleep(for: .seconds(1))
|
||||||
if Task.isCancelled { return }
|
if Task.isCancelled { return }
|
||||||
|
|
||||||
// Done counting, start collecting
|
// Done counting, start collecting
|
||||||
isCountingDown = false
|
isCountingDown = false
|
||||||
countdownValue = 0
|
countdownValue = 0
|
||||||
@@ -446,4 +454,3 @@ class CalibrationOverlayViewModel: ObservableObject {
|
|||||||
#Preview {
|
#Preview {
|
||||||
CalibrationOverlayView(onDismiss: {})
|
CalibrationOverlayView(onDismiss: {})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ final class OnboardingWindowPresenter {
|
|||||||
windowController = nil
|
windowController = nil
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
NSApp.unhide(nil)
|
NSApp.unhide(nil)
|
||||||
NSApp.activate(ignoringOtherApps: true)
|
NSApp.activate(ignoringOtherApps: true)
|
||||||
@@ -83,7 +83,9 @@ final class OnboardingWindowPresenter {
|
|||||||
window.titlebarAppearsTransparent = true
|
window.titlebarAppearsTransparent = true
|
||||||
window.center()
|
window.center()
|
||||||
window.isReleasedWhenClosed = true
|
window.isReleasedWhenClosed = true
|
||||||
window.collectionBehavior = [.managed, .participatesInCycle, .moveToActiveSpace, .fullScreenAuxiliary]
|
window.collectionBehavior = [
|
||||||
|
.managed, .participatesInCycle, .moveToActiveSpace, .fullScreenAuxiliary,
|
||||||
|
]
|
||||||
|
|
||||||
window.contentView = NSHostingView(
|
window.contentView = NSHostingView(
|
||||||
rootView: OnboardingContainerView(settingsManager: settingsManager)
|
rootView: OnboardingContainerView(settingsManager: settingsManager)
|
||||||
|
|||||||
@@ -6,92 +6,89 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import XCTest
|
import XCTest
|
||||||
|
|
||||||
@testable import Gaze
|
@testable import Gaze
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
final class AppDelegateTestabilityTests: XCTestCase {
|
final class AppDelegateTestabilityTests: XCTestCase {
|
||||||
|
|
||||||
var testEnv: TestEnvironment!
|
var testEnv: TestEnvironment!
|
||||||
|
|
||||||
override func setUp() async throws {
|
override func setUp() async throws {
|
||||||
testEnv = TestEnvironment(settings: .onboardingCompleted)
|
testEnv = TestEnvironment(settings: .onboardingCompleted)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func tearDown() async throws {
|
override func tearDown() async throws {
|
||||||
testEnv = nil
|
testEnv = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func testAppDelegateCreationWithMocks() {
|
func testAppDelegateCreationWithMocks() {
|
||||||
let appDelegate = testEnv.createAppDelegate()
|
let appDelegate = testEnv.createAppDelegate()
|
||||||
|
|
||||||
XCTAssertNotNil(appDelegate)
|
XCTAssertNotNil(appDelegate)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testWindowManagerReceivesReminderEvents() async throws {
|
func testWindowManagerReceivesReminderEvents() async throws {
|
||||||
let appDelegate = testEnv.createAppDelegate()
|
let appDelegate = testEnv.createAppDelegate()
|
||||||
|
|
||||||
// Simulate app launch
|
|
||||||
let notification = Notification(name: NSApplication.didFinishLaunchingNotification)
|
let notification = Notification(name: NSApplication.didFinishLaunchingNotification)
|
||||||
appDelegate.applicationDidFinishLaunching(notification)
|
appDelegate.applicationDidFinishLaunching(notification)
|
||||||
|
|
||||||
// Give time for setup
|
try await Task.sleep(for: .milliseconds(100))
|
||||||
try await Task.sleep(nanoseconds: 100_000_000) // 100ms
|
|
||||||
|
|
||||||
// Trigger a reminder through timer engine
|
|
||||||
if let timerEngine = appDelegate.timerEngine {
|
if let timerEngine = appDelegate.timerEngine {
|
||||||
let timerId = TimerIdentifier.builtIn(.blink)
|
let timerId = TimerIdentifier.builtIn(.blink)
|
||||||
timerEngine.triggerReminder(for: timerId)
|
timerEngine.triggerReminder(for: timerId)
|
||||||
|
|
||||||
// Give time for reminder to propagate
|
try await Task.sleep(for: .milliseconds(100))
|
||||||
try await Task.sleep(nanoseconds: 100_000_000) // 100ms
|
|
||||||
|
|
||||||
// Verify window manager received the show command
|
// Verify window manager received the show command
|
||||||
XCTAssertTrue(testEnv.windowManager.didPerformOperation(.showSubtleReminder))
|
XCTAssertTrue(testEnv.windowManager.didPerformOperation(.showSubtleReminder))
|
||||||
} else {
|
} else {
|
||||||
XCTFail("TimerEngine not initialized")
|
XCTFail("TimerEngine not initialized")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func testSettingsChangesPropagate() async throws {
|
func testSettingsChangesPropagate() async throws {
|
||||||
let appDelegate = testEnv.createAppDelegate()
|
let appDelegate = testEnv.createAppDelegate()
|
||||||
|
|
||||||
// Change a setting
|
// Change a setting
|
||||||
testEnv.settingsManager.settings.lookAwayTimer.enabled = false
|
testEnv.settingsManager.settings.lookAwayTimer.enabled = false
|
||||||
|
|
||||||
// Give time for observation
|
try await Task.sleep(for: .milliseconds(50))
|
||||||
try await Task.sleep(nanoseconds: 50_000_000) // 50ms
|
|
||||||
|
|
||||||
// Verify the change propagated
|
// Verify the change propagated
|
||||||
XCTAssertFalse(testEnv.settingsManager.settings.lookAwayTimer.enabled)
|
XCTAssertFalse(testEnv.settingsManager.settings.lookAwayTimer.enabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testOpenSettingsUsesWindowManager() {
|
func testOpenSettingsUsesWindowManager() {
|
||||||
let appDelegate = testEnv.createAppDelegate()
|
let appDelegate = testEnv.createAppDelegate()
|
||||||
|
|
||||||
appDelegate.openSettings(tab: 2)
|
appDelegate.openSettings(tab: 2)
|
||||||
|
|
||||||
// Give time for async dispatch
|
// Give time for async dispatch
|
||||||
let expectation = XCTestExpectation(description: "Settings opened")
|
let expectation = XCTestExpectation(description: "Settings opened")
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
|
||||||
XCTAssertTrue(self.testEnv.windowManager.didPerformOperation(.showSettings(initialTab: 2)))
|
XCTAssertTrue(
|
||||||
|
self.testEnv.windowManager.didPerformOperation(.showSettings(initialTab: 2)))
|
||||||
expectation.fulfill()
|
expectation.fulfill()
|
||||||
}
|
}
|
||||||
|
|
||||||
wait(for: [expectation], timeout: 1.0)
|
wait(for: [expectation], timeout: 1.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testOpenOnboardingUsesWindowManager() {
|
func testOpenOnboardingUsesWindowManager() {
|
||||||
let appDelegate = testEnv.createAppDelegate()
|
let appDelegate = testEnv.createAppDelegate()
|
||||||
|
|
||||||
appDelegate.openOnboarding()
|
appDelegate.openOnboarding()
|
||||||
|
|
||||||
// Give time for async dispatch
|
// Give time for async dispatch
|
||||||
let expectation = XCTestExpectation(description: "Onboarding opened")
|
let expectation = XCTestExpectation(description: "Onboarding opened")
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
|
||||||
XCTAssertTrue(self.testEnv.windowManager.didPerformOperation(.showOnboarding))
|
XCTAssertTrue(self.testEnv.windowManager.didPerformOperation(.showOnboarding))
|
||||||
expectation.fulfill()
|
expectation.fulfill()
|
||||||
}
|
}
|
||||||
|
|
||||||
wait(for: [expectation], timeout: 1.0)
|
wait(for: [expectation], timeout: 1.0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,36 +7,37 @@
|
|||||||
|
|
||||||
import Combine
|
import Combine
|
||||||
import XCTest
|
import XCTest
|
||||||
|
|
||||||
@testable import Gaze
|
@testable import Gaze
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
final class TimerEngineTests: XCTestCase {
|
final class TimerEngineTests: XCTestCase {
|
||||||
|
|
||||||
var testEnv: TestEnvironment!
|
var testEnv: TestEnvironment!
|
||||||
var timerEngine: TimerEngine!
|
var timerEngine: TimerEngine!
|
||||||
var cancellables: Set<AnyCancellable>!
|
var cancellables: Set<AnyCancellable>!
|
||||||
|
|
||||||
override func setUp() async throws {
|
override func setUp() async throws {
|
||||||
testEnv = TestEnvironment(settings: .defaults)
|
testEnv = TestEnvironment(settings: .defaults)
|
||||||
timerEngine = testEnv.container.timerEngine
|
timerEngine = testEnv.container.timerEngine
|
||||||
cancellables = []
|
cancellables = []
|
||||||
}
|
}
|
||||||
|
|
||||||
override func tearDown() async throws {
|
override func tearDown() async throws {
|
||||||
timerEngine?.stop()
|
timerEngine?.stop()
|
||||||
cancellables = nil
|
cancellables = nil
|
||||||
timerEngine = nil
|
timerEngine = nil
|
||||||
testEnv = nil
|
testEnv = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Initialization Tests
|
// MARK: - Initialization Tests
|
||||||
|
|
||||||
func testTimerEngineInitialization() {
|
func testTimerEngineInitialization() {
|
||||||
XCTAssertNotNil(timerEngine)
|
XCTAssertNotNil(timerEngine)
|
||||||
XCTAssertEqual(timerEngine.timerStates.count, 0)
|
XCTAssertEqual(timerEngine.timerStates.count, 0)
|
||||||
XCTAssertNil(timerEngine.activeReminder)
|
XCTAssertNil(timerEngine.activeReminder)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testTimerEngineWithCustomTimeProvider() {
|
func testTimerEngineWithCustomTimeProvider() {
|
||||||
let timeProvider = MockTimeProvider()
|
let timeProvider = MockTimeProvider()
|
||||||
let engine = TimerEngine(
|
let engine = TimerEngine(
|
||||||
@@ -44,174 +45,173 @@ final class TimerEngineTests: XCTestCase {
|
|||||||
enforceModeService: nil,
|
enforceModeService: nil,
|
||||||
timeProvider: timeProvider
|
timeProvider: timeProvider
|
||||||
)
|
)
|
||||||
|
|
||||||
XCTAssertNotNil(engine)
|
XCTAssertNotNil(engine)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Start/Stop Tests
|
// MARK: - Start/Stop Tests
|
||||||
|
|
||||||
func testStartTimers() {
|
func testStartTimers() {
|
||||||
timerEngine.start()
|
timerEngine.start()
|
||||||
|
|
||||||
// Should create timer states for enabled timers
|
// Should create timer states for enabled timers
|
||||||
XCTAssertGreaterThan(timerEngine.timerStates.count, 0)
|
XCTAssertGreaterThan(timerEngine.timerStates.count, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testStopTimers() {
|
func testStopTimers() {
|
||||||
timerEngine.start()
|
timerEngine.start()
|
||||||
let initialCount = timerEngine.timerStates.count
|
let initialCount = timerEngine.timerStates.count
|
||||||
XCTAssertGreaterThan(initialCount, 0)
|
XCTAssertGreaterThan(initialCount, 0)
|
||||||
|
|
||||||
timerEngine.stop()
|
timerEngine.stop()
|
||||||
|
|
||||||
// Timers should be cleared
|
// Timers should be cleared
|
||||||
XCTAssertEqual(timerEngine.timerStates.count, 0)
|
XCTAssertEqual(timerEngine.timerStates.count, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testRestartTimers() {
|
func testRestartTimers() {
|
||||||
timerEngine.start()
|
timerEngine.start()
|
||||||
let firstCount = timerEngine.timerStates.count
|
let firstCount = timerEngine.timerStates.count
|
||||||
|
|
||||||
timerEngine.stop()
|
timerEngine.stop()
|
||||||
XCTAssertEqual(timerEngine.timerStates.count, 0)
|
XCTAssertEqual(timerEngine.timerStates.count, 0)
|
||||||
|
|
||||||
timerEngine.start()
|
timerEngine.start()
|
||||||
let secondCount = timerEngine.timerStates.count
|
let secondCount = timerEngine.timerStates.count
|
||||||
|
|
||||||
XCTAssertEqual(firstCount, secondCount)
|
XCTAssertEqual(firstCount, secondCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Pause/Resume Tests
|
// MARK: - Pause/Resume Tests
|
||||||
|
|
||||||
func testPauseAllTimers() {
|
func testPauseAllTimers() {
|
||||||
timerEngine.start()
|
timerEngine.start()
|
||||||
timerEngine.pause()
|
timerEngine.pause()
|
||||||
|
|
||||||
for (_, state) in timerEngine.timerStates {
|
for (_, state) in timerEngine.timerStates {
|
||||||
XCTAssertTrue(state.isPaused)
|
XCTAssertTrue(state.isPaused)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func testResumeAllTimers() {
|
func testResumeAllTimers() {
|
||||||
timerEngine.start()
|
timerEngine.start()
|
||||||
timerEngine.pause()
|
timerEngine.pause()
|
||||||
timerEngine.resume()
|
timerEngine.resume()
|
||||||
|
|
||||||
for (_, state) in timerEngine.timerStates {
|
for (_, state) in timerEngine.timerStates {
|
||||||
XCTAssertFalse(state.isPaused)
|
XCTAssertFalse(state.isPaused)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func testPauseSpecificTimer() {
|
func testPauseSpecificTimer() {
|
||||||
timerEngine.start()
|
timerEngine.start()
|
||||||
|
|
||||||
guard let firstTimer = timerEngine.timerStates.keys.first else {
|
guard let firstTimer = timerEngine.timerStates.keys.first else {
|
||||||
XCTFail("No timers available")
|
XCTFail("No timers available")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
timerEngine.pauseTimer(identifier: firstTimer)
|
timerEngine.pauseTimer(identifier: firstTimer)
|
||||||
|
|
||||||
let state = timerEngine.timerStates[firstTimer]
|
let state = timerEngine.timerStates[firstTimer]
|
||||||
XCTAssertTrue(state?.isPaused ?? false)
|
XCTAssertTrue(state?.isPaused ?? false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testResumeSpecificTimer() {
|
func testResumeSpecificTimer() {
|
||||||
timerEngine.start()
|
timerEngine.start()
|
||||||
|
|
||||||
guard let firstTimer = timerEngine.timerStates.keys.first else {
|
guard let firstTimer = timerEngine.timerStates.keys.first else {
|
||||||
XCTFail("No timers available")
|
XCTFail("No timers available")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
timerEngine.pauseTimer(identifier: firstTimer)
|
timerEngine.pauseTimer(identifier: firstTimer)
|
||||||
XCTAssertTrue(timerEngine.isTimerPaused(firstTimer))
|
XCTAssertTrue(timerEngine.isTimerPaused(firstTimer))
|
||||||
|
|
||||||
timerEngine.resumeTimer(identifier: firstTimer)
|
timerEngine.resumeTimer(identifier: firstTimer)
|
||||||
XCTAssertFalse(timerEngine.isTimerPaused(firstTimer))
|
XCTAssertFalse(timerEngine.isTimerPaused(firstTimer))
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Skip Tests
|
// MARK: - Skip Tests
|
||||||
|
|
||||||
func testSkipNext() {
|
func testSkipNext() {
|
||||||
timerEngine.start()
|
timerEngine.start()
|
||||||
|
|
||||||
guard let firstTimer = timerEngine.timerStates.keys.first else {
|
guard let firstTimer = timerEngine.timerStates.keys.first else {
|
||||||
XCTFail("No timers available")
|
XCTFail("No timers available")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
timerEngine.skipNext(identifier: firstTimer)
|
timerEngine.skipNext(identifier: firstTimer)
|
||||||
|
|
||||||
// Timer should be reset
|
// Timer should be reset
|
||||||
XCTAssertNotNil(timerEngine.timerStates[firstTimer])
|
XCTAssertNotNil(timerEngine.timerStates[firstTimer])
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Reminder Tests
|
// MARK: - Reminder Tests
|
||||||
|
|
||||||
func testTriggerReminder() async throws {
|
func testTriggerReminder() async throws {
|
||||||
timerEngine.start()
|
timerEngine.start()
|
||||||
|
|
||||||
guard let firstTimer = timerEngine.timerStates.keys.first else {
|
guard let firstTimer = timerEngine.timerStates.keys.first else {
|
||||||
XCTFail("No timers available")
|
XCTFail("No timers available")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
timerEngine.triggerReminder(for: firstTimer)
|
timerEngine.triggerReminder(for: firstTimer)
|
||||||
|
|
||||||
// Give time for async operations
|
try await Task.sleep(for: .milliseconds(50))
|
||||||
try await Task.sleep(nanoseconds: 50_000_000)
|
|
||||||
|
|
||||||
XCTAssertNotNil(timerEngine.activeReminder)
|
XCTAssertNotNil(timerEngine.activeReminder)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testDismissReminder() {
|
func testDismissReminder() {
|
||||||
timerEngine.start()
|
timerEngine.start()
|
||||||
|
|
||||||
guard let firstTimer = timerEngine.timerStates.keys.first else {
|
guard let firstTimer = timerEngine.timerStates.keys.first else {
|
||||||
XCTFail("No timers available")
|
XCTFail("No timers available")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
timerEngine.triggerReminder(for: firstTimer)
|
timerEngine.triggerReminder(for: firstTimer)
|
||||||
XCTAssertNotNil(timerEngine.activeReminder)
|
XCTAssertNotNil(timerEngine.activeReminder)
|
||||||
|
|
||||||
timerEngine.dismissReminder()
|
timerEngine.dismissReminder()
|
||||||
XCTAssertNil(timerEngine.activeReminder)
|
XCTAssertNil(timerEngine.activeReminder)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Time Remaining Tests
|
// MARK: - Time Remaining Tests
|
||||||
|
|
||||||
func testGetTimeRemaining() {
|
func testGetTimeRemaining() {
|
||||||
timerEngine.start()
|
timerEngine.start()
|
||||||
|
|
||||||
guard let firstTimer = timerEngine.timerStates.keys.first else {
|
guard let firstTimer = timerEngine.timerStates.keys.first else {
|
||||||
XCTFail("No timers available")
|
XCTFail("No timers available")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let remaining = timerEngine.getTimeRemaining(for: firstTimer)
|
let remaining = timerEngine.getTimeRemaining(for: firstTimer)
|
||||||
XCTAssertGreaterThan(remaining, 0)
|
XCTAssertGreaterThan(remaining, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testGetFormattedTimeRemaining() {
|
func testGetFormattedTimeRemaining() {
|
||||||
timerEngine.start()
|
timerEngine.start()
|
||||||
|
|
||||||
guard let firstTimer = timerEngine.timerStates.keys.first else {
|
guard let firstTimer = timerEngine.timerStates.keys.first else {
|
||||||
XCTFail("No timers available")
|
XCTFail("No timers available")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let formatted = timerEngine.getFormattedTimeRemaining(for: firstTimer)
|
let formatted = timerEngine.getFormattedTimeRemaining(for: firstTimer)
|
||||||
XCTAssertFalse(formatted.isEmpty)
|
XCTAssertFalse(formatted.isEmpty)
|
||||||
XCTAssertTrue(formatted.contains(":"))
|
XCTAssertTrue(formatted.contains(":"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Timer State Publisher Tests
|
// MARK: - Timer State Publisher Tests
|
||||||
|
|
||||||
func testTimerStatesPublisher() async throws {
|
func testTimerStatesPublisher() async throws {
|
||||||
let expectation = XCTestExpectation(description: "Timer states changed")
|
let expectation = XCTestExpectation(description: "Timer states changed")
|
||||||
|
|
||||||
timerEngine.$timerStates
|
timerEngine.$timerStates
|
||||||
.dropFirst()
|
.dropFirst()
|
||||||
.sink { states in
|
.sink { states in
|
||||||
@@ -220,15 +220,15 @@ final class TimerEngineTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
timerEngine.start()
|
timerEngine.start()
|
||||||
|
|
||||||
await fulfillment(of: [expectation], timeout: 1.0)
|
await fulfillment(of: [expectation], timeout: 1.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testActiveReminderPublisher() async throws {
|
func testActiveReminderPublisher() async throws {
|
||||||
let expectation = XCTestExpectation(description: "Active reminder changed")
|
let expectation = XCTestExpectation(description: "Active reminder changed")
|
||||||
|
|
||||||
timerEngine.$activeReminder
|
timerEngine.$activeReminder
|
||||||
.dropFirst()
|
.dropFirst()
|
||||||
.sink { reminder in
|
.sink { reminder in
|
||||||
@@ -237,67 +237,67 @@ final class TimerEngineTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
timerEngine.start()
|
timerEngine.start()
|
||||||
|
|
||||||
guard let firstTimer = timerEngine.timerStates.keys.first else {
|
guard let firstTimer = timerEngine.timerStates.keys.first else {
|
||||||
XCTFail("No timers available")
|
XCTFail("No timers available")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
timerEngine.triggerReminder(for: firstTimer)
|
timerEngine.triggerReminder(for: firstTimer)
|
||||||
|
|
||||||
await fulfillment(of: [expectation], timeout: 1.0)
|
await fulfillment(of: [expectation], timeout: 1.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - System Sleep/Wake Tests
|
// MARK: - System Sleep/Wake Tests
|
||||||
|
|
||||||
func testHandleSystemSleep() {
|
func testHandleSystemSleep() {
|
||||||
timerEngine.start()
|
timerEngine.start()
|
||||||
let statesBefore = timerEngine.timerStates.count
|
let statesBefore = timerEngine.timerStates.count
|
||||||
|
|
||||||
timerEngine.handleSystemSleep()
|
timerEngine.handleSystemSleep()
|
||||||
|
|
||||||
// States should still exist
|
// States should still exist
|
||||||
XCTAssertEqual(timerEngine.timerStates.count, statesBefore)
|
XCTAssertEqual(timerEngine.timerStates.count, statesBefore)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testHandleSystemWake() {
|
func testHandleSystemWake() {
|
||||||
timerEngine.start()
|
timerEngine.start()
|
||||||
timerEngine.handleSystemSleep()
|
timerEngine.handleSystemSleep()
|
||||||
timerEngine.handleSystemWake()
|
timerEngine.handleSystemWake()
|
||||||
|
|
||||||
// Should handle wake event without crashing
|
// Should handle wake event without crashing
|
||||||
XCTAssertGreaterThan(timerEngine.timerStates.count, 0)
|
XCTAssertGreaterThan(timerEngine.timerStates.count, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Disabled Timer Tests
|
// MARK: - Disabled Timer Tests
|
||||||
|
|
||||||
func testDisabledTimersNotInitialized() {
|
func testDisabledTimersNotInitialized() {
|
||||||
var settings = AppSettings.defaults
|
var settings = AppSettings.defaults
|
||||||
settings.lookAwayTimer.enabled = false
|
settings.lookAwayTimer.enabled = false
|
||||||
settings.blinkTimer.enabled = false
|
settings.blinkTimer.enabled = false
|
||||||
settings.postureTimer.enabled = false
|
settings.postureTimer.enabled = false
|
||||||
|
|
||||||
let settingsManager = EnhancedMockSettingsManager(settings: settings)
|
let settingsManager = EnhancedMockSettingsManager(settings: settings)
|
||||||
let engine = TimerEngine(settingsManager: settingsManager)
|
let engine = TimerEngine(settingsManager: settingsManager)
|
||||||
|
|
||||||
engine.start()
|
engine.start()
|
||||||
|
|
||||||
XCTAssertEqual(engine.timerStates.count, 0)
|
XCTAssertEqual(engine.timerStates.count, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testPartiallyEnabledTimers() {
|
func testPartiallyEnabledTimers() {
|
||||||
var settings = AppSettings.defaults
|
var settings = AppSettings.defaults
|
||||||
settings.lookAwayTimer.enabled = true
|
settings.lookAwayTimer.enabled = true
|
||||||
settings.blinkTimer.enabled = false
|
settings.blinkTimer.enabled = false
|
||||||
settings.postureTimer.enabled = false
|
settings.postureTimer.enabled = false
|
||||||
|
|
||||||
let settingsManager = EnhancedMockSettingsManager(settings: settings)
|
let settingsManager = EnhancedMockSettingsManager(settings: settings)
|
||||||
let engine = TimerEngine(settingsManager: settingsManager)
|
let engine = TimerEngine(settingsManager: settingsManager)
|
||||||
|
|
||||||
engine.start()
|
engine.start()
|
||||||
|
|
||||||
XCTAssertEqual(engine.timerStates.count, 1)
|
XCTAssertEqual(engine.timerStates.count, 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,11 @@
|
|||||||
// Test helpers and utilities for unit testing.
|
// Test helpers and utilities for unit testing.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
// MARK: - Import Statement for Combine
|
||||||
|
import Combine
|
||||||
import Foundation
|
import Foundation
|
||||||
import XCTest
|
import XCTest
|
||||||
|
|
||||||
@testable import Gaze
|
@testable import Gaze
|
||||||
|
|
||||||
// MARK: - Enhanced MockSettingsManager
|
// MARK: - Enhanced MockSettingsManager
|
||||||
@@ -16,21 +19,22 @@ import XCTest
|
|||||||
@Observable
|
@Observable
|
||||||
final class EnhancedMockSettingsManager: SettingsProviding {
|
final class EnhancedMockSettingsManager: SettingsProviding {
|
||||||
var settings: AppSettings
|
var settings: AppSettings
|
||||||
|
|
||||||
@ObservationIgnored
|
@ObservationIgnored
|
||||||
private let _settingsSubject: CurrentValueSubject<AppSettings, Never>
|
private let _settingsSubject: CurrentValueSubject<AppSettings, Never>
|
||||||
|
|
||||||
var settingsPublisher: AnyPublisher<AppSettings, Never> {
|
var settingsPublisher: AnyPublisher<AppSettings, Never> {
|
||||||
_settingsSubject.eraseToAnyPublisher()
|
_settingsSubject.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ObservationIgnored
|
@ObservationIgnored
|
||||||
private let timerConfigKeyPaths: [TimerType: WritableKeyPath<AppSettings, TimerConfiguration>] = [
|
private let timerConfigKeyPaths: [TimerType: WritableKeyPath<AppSettings, TimerConfiguration>] =
|
||||||
.lookAway: \.lookAwayTimer,
|
[
|
||||||
.blink: \.blinkTimer,
|
.lookAway: \.lookAwayTimer,
|
||||||
.posture: \.postureTimer,
|
.blink: \.blinkTimer,
|
||||||
]
|
.posture: \.postureTimer,
|
||||||
|
]
|
||||||
|
|
||||||
// Track method calls for verification
|
// Track method calls for verification
|
||||||
@ObservationIgnored
|
@ObservationIgnored
|
||||||
private(set) var saveCallCount = 0
|
private(set) var saveCallCount = 0
|
||||||
@@ -40,19 +44,19 @@ final class EnhancedMockSettingsManager: SettingsProviding {
|
|||||||
private(set) var loadCallCount = 0
|
private(set) var loadCallCount = 0
|
||||||
@ObservationIgnored
|
@ObservationIgnored
|
||||||
private(set) var resetToDefaultsCallCount = 0
|
private(set) var resetToDefaultsCallCount = 0
|
||||||
|
|
||||||
init(settings: AppSettings = .defaults) {
|
init(settings: AppSettings = .defaults) {
|
||||||
self.settings = settings
|
self.settings = settings
|
||||||
self._settingsSubject = CurrentValueSubject(settings)
|
self._settingsSubject = CurrentValueSubject(settings)
|
||||||
}
|
}
|
||||||
|
|
||||||
func timerConfiguration(for type: TimerType) -> TimerConfiguration {
|
func timerConfiguration(for type: TimerType) -> TimerConfiguration {
|
||||||
guard let keyPath = timerConfigKeyPaths[type] else {
|
guard let keyPath = timerConfigKeyPaths[type] else {
|
||||||
preconditionFailure("Unknown timer type: \(type)")
|
preconditionFailure("Unknown timer type: \(type)")
|
||||||
}
|
}
|
||||||
return settings[keyPath: keyPath]
|
return settings[keyPath: keyPath]
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateTimerConfiguration(for type: TimerType, configuration: TimerConfiguration) {
|
func updateTimerConfiguration(for type: TimerType, configuration: TimerConfiguration) {
|
||||||
guard let keyPath = timerConfigKeyPaths[type] else {
|
guard let keyPath = timerConfigKeyPaths[type] else {
|
||||||
preconditionFailure("Unknown timer type: \(type)")
|
preconditionFailure("Unknown timer type: \(type)")
|
||||||
@@ -60,7 +64,7 @@ final class EnhancedMockSettingsManager: SettingsProviding {
|
|||||||
settings[keyPath: keyPath] = configuration
|
settings[keyPath: keyPath] = configuration
|
||||||
_settingsSubject.send(settings)
|
_settingsSubject.send(settings)
|
||||||
}
|
}
|
||||||
|
|
||||||
func allTimerConfigurations() -> [TimerType: TimerConfiguration] {
|
func allTimerConfigurations() -> [TimerType: TimerConfiguration] {
|
||||||
var configs: [TimerType: TimerConfiguration] = [:]
|
var configs: [TimerType: TimerConfiguration] = [:]
|
||||||
for (type, keyPath) in timerConfigKeyPaths {
|
for (type, keyPath) in timerConfigKeyPaths {
|
||||||
@@ -68,27 +72,27 @@ final class EnhancedMockSettingsManager: SettingsProviding {
|
|||||||
}
|
}
|
||||||
return configs
|
return configs
|
||||||
}
|
}
|
||||||
|
|
||||||
func save() {
|
func save() {
|
||||||
saveCallCount += 1
|
saveCallCount += 1
|
||||||
_settingsSubject.send(settings)
|
_settingsSubject.send(settings)
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveImmediately() {
|
func saveImmediately() {
|
||||||
saveImmediatelyCallCount += 1
|
saveImmediatelyCallCount += 1
|
||||||
_settingsSubject.send(settings)
|
_settingsSubject.send(settings)
|
||||||
}
|
}
|
||||||
|
|
||||||
func load() {
|
func load() {
|
||||||
loadCallCount += 1
|
loadCallCount += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
func resetToDefaults() {
|
func resetToDefaults() {
|
||||||
resetToDefaultsCallCount += 1
|
resetToDefaultsCallCount += 1
|
||||||
settings = .defaults
|
settings = .defaults
|
||||||
_settingsSubject.send(settings)
|
_settingsSubject.send(settings)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test helpers
|
// Test helpers
|
||||||
func reset() {
|
func reset() {
|
||||||
saveCallCount = 0
|
saveCallCount = 0
|
||||||
@@ -105,17 +109,17 @@ final class EnhancedMockSettingsManager: SettingsProviding {
|
|||||||
@MainActor
|
@MainActor
|
||||||
final class MockFullscreenDetectionService: ObservableObject, FullscreenDetectionProviding {
|
final class MockFullscreenDetectionService: ObservableObject, FullscreenDetectionProviding {
|
||||||
@Published var isFullscreenActive: Bool = false
|
@Published var isFullscreenActive: Bool = false
|
||||||
|
|
||||||
var isFullscreenActivePublisher: Published<Bool>.Publisher {
|
var isFullscreenActivePublisher: Published<Bool>.Publisher {
|
||||||
$isFullscreenActive
|
$isFullscreenActive
|
||||||
}
|
}
|
||||||
|
|
||||||
private(set) var forceUpdateCallCount = 0
|
private(set) var forceUpdateCallCount = 0
|
||||||
|
|
||||||
func forceUpdate() {
|
func forceUpdate() {
|
||||||
forceUpdateCallCount += 1
|
forceUpdateCallCount += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
func simulateFullscreen(_ active: Bool) {
|
func simulateFullscreen(_ active: Bool) {
|
||||||
isFullscreenActive = active
|
isFullscreenActive = active
|
||||||
}
|
}
|
||||||
@@ -124,22 +128,22 @@ final class MockFullscreenDetectionService: ObservableObject, FullscreenDetectio
|
|||||||
@MainActor
|
@MainActor
|
||||||
final class MockIdleMonitoringService: ObservableObject, IdleMonitoringProviding {
|
final class MockIdleMonitoringService: ObservableObject, IdleMonitoringProviding {
|
||||||
@Published var isIdle: Bool = false
|
@Published var isIdle: Bool = false
|
||||||
|
|
||||||
var isIdlePublisher: Published<Bool>.Publisher {
|
var isIdlePublisher: Published<Bool>.Publisher {
|
||||||
$isIdle
|
$isIdle
|
||||||
}
|
}
|
||||||
|
|
||||||
private(set) var thresholdMinutes: Int = 5
|
private(set) var thresholdMinutes: Int = 5
|
||||||
private(set) var forceUpdateCallCount = 0
|
private(set) var forceUpdateCallCount = 0
|
||||||
|
|
||||||
func updateThreshold(minutes: Int) {
|
func updateThreshold(minutes: Int) {
|
||||||
thresholdMinutes = minutes
|
thresholdMinutes = minutes
|
||||||
}
|
}
|
||||||
|
|
||||||
func forceUpdate() {
|
func forceUpdate() {
|
||||||
forceUpdateCallCount += 1
|
forceUpdateCallCount += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
func simulateIdle(_ idle: Bool) {
|
func simulateIdle(_ idle: Bool) {
|
||||||
isIdle = idle
|
isIdle = idle
|
||||||
}
|
}
|
||||||
@@ -156,7 +160,7 @@ extension AppSettings {
|
|||||||
settings.postureTimer.enabled = false
|
settings.postureTimer.enabled = false
|
||||||
return settings
|
return settings
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Settings with only lookAway timer enabled
|
/// Settings with only lookAway timer enabled
|
||||||
static var onlyLookAwayEnabled: AppSettings {
|
static var onlyLookAwayEnabled: AppSettings {
|
||||||
var settings = AppSettings.defaults
|
var settings = AppSettings.defaults
|
||||||
@@ -165,7 +169,7 @@ extension AppSettings {
|
|||||||
settings.postureTimer.enabled = false
|
settings.postureTimer.enabled = false
|
||||||
return settings
|
return settings
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Settings with short intervals for testing
|
/// Settings with short intervals for testing
|
||||||
static var shortIntervals: AppSettings {
|
static var shortIntervals: AppSettings {
|
||||||
var settings = AppSettings.defaults
|
var settings = AppSettings.defaults
|
||||||
@@ -174,14 +178,14 @@ extension AppSettings {
|
|||||||
settings.postureTimer.intervalSeconds = 7
|
settings.postureTimer.intervalSeconds = 7
|
||||||
return settings
|
return settings
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Settings with onboarding completed
|
/// Settings with onboarding completed
|
||||||
static var onboardingCompleted: AppSettings {
|
static var onboardingCompleted: AppSettings {
|
||||||
var settings = AppSettings.defaults
|
var settings = AppSettings.defaults
|
||||||
settings.hasCompletedOnboarding = true
|
settings.hasCompletedOnboarding = true
|
||||||
return settings
|
return settings
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Settings with smart mode fully enabled
|
/// Settings with smart mode fully enabled
|
||||||
static var smartModeEnabled: AppSettings {
|
static var smartModeEnabled: AppSettings {
|
||||||
var settings = AppSettings.defaults
|
var settings = AppSettings.defaults
|
||||||
@@ -209,19 +213,19 @@ struct TestEnvironment {
|
|||||||
let windowManager: MockWindowManager
|
let windowManager: MockWindowManager
|
||||||
let settingsManager: EnhancedMockSettingsManager
|
let settingsManager: EnhancedMockSettingsManager
|
||||||
let timeProvider: MockTimeProvider
|
let timeProvider: MockTimeProvider
|
||||||
|
|
||||||
init(settings: AppSettings = .defaults) {
|
init(settings: AppSettings = .defaults) {
|
||||||
self.settingsManager = EnhancedMockSettingsManager(settings: settings)
|
self.settingsManager = EnhancedMockSettingsManager(settings: settings)
|
||||||
self.container = ServiceContainer(settingsManager: settingsManager)
|
self.container = ServiceContainer(settingsManager: settingsManager)
|
||||||
self.windowManager = MockWindowManager()
|
self.windowManager = MockWindowManager()
|
||||||
self.timeProvider = MockTimeProvider()
|
self.timeProvider = MockTimeProvider()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates an AppDelegate with all test dependencies
|
/// Creates an AppDelegate with all test dependencies
|
||||||
func createAppDelegate() -> AppDelegate {
|
func createAppDelegate() -> AppDelegate {
|
||||||
return AppDelegate(serviceContainer: container, windowManager: windowManager)
|
return AppDelegate(serviceContainer: container, windowManager: windowManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resets all mock state
|
/// Resets all mock state
|
||||||
func reset() {
|
func reset() {
|
||||||
windowManager.reset()
|
windowManager.reset()
|
||||||
@@ -245,10 +249,10 @@ extension XCTestCase {
|
|||||||
XCTFail(message)
|
XCTFail(message)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try await Task.sleep(nanoseconds: 10_000_000) // 10ms
|
try await Task.sleep(for: .milliseconds(100))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Waits for a published value to change
|
/// Waits for a published value to change
|
||||||
@MainActor
|
@MainActor
|
||||||
func waitForPublisher<T: Equatable>(
|
func waitForPublisher<T: Equatable>(
|
||||||
@@ -258,17 +262,14 @@ extension XCTestCase {
|
|||||||
) async throws {
|
) async throws {
|
||||||
let expectation = XCTestExpectation(description: "Publisher value changed")
|
let expectation = XCTestExpectation(description: "Publisher value changed")
|
||||||
var cancellable: AnyCancellable?
|
var cancellable: AnyCancellable?
|
||||||
|
|
||||||
cancellable = publisher.sink { value in
|
cancellable = publisher.sink { value in
|
||||||
if value == expectedValue {
|
if value == expectedValue {
|
||||||
expectation.fulfill()
|
expectation.fulfill()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await fulfillment(of: [expectation], timeout: timeout)
|
await fulfillment(of: [expectation], timeout: timeout)
|
||||||
cancellable?.cancel()
|
cancellable?.cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Import Statement for Combine
|
|
||||||
import Combine
|
|
||||||
|
|||||||
@@ -7,24 +7,25 @@
|
|||||||
|
|
||||||
import Combine
|
import Combine
|
||||||
import XCTest
|
import XCTest
|
||||||
|
|
||||||
@testable import Gaze
|
@testable import Gaze
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
final class TimerEngineTestabilityTests: XCTestCase {
|
final class TimerEngineTestabilityTests: XCTestCase {
|
||||||
|
|
||||||
var testEnv: TestEnvironment!
|
var testEnv: TestEnvironment!
|
||||||
var cancellables: Set<AnyCancellable>!
|
var cancellables: Set<AnyCancellable>!
|
||||||
|
|
||||||
override func setUp() async throws {
|
override func setUp() async throws {
|
||||||
testEnv = TestEnvironment(settings: .shortIntervals)
|
testEnv = TestEnvironment(settings: .shortIntervals)
|
||||||
cancellables = []
|
cancellables = []
|
||||||
}
|
}
|
||||||
|
|
||||||
override func tearDown() async throws {
|
override func tearDown() async throws {
|
||||||
cancellables = nil
|
cancellables = nil
|
||||||
testEnv = nil
|
testEnv = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func testTimerEngineCreationWithMocks() {
|
func testTimerEngineCreationWithMocks() {
|
||||||
let timeProvider = MockTimeProvider()
|
let timeProvider = MockTimeProvider()
|
||||||
let timerEngine = TimerEngine(
|
let timerEngine = TimerEngine(
|
||||||
@@ -32,30 +33,30 @@ final class TimerEngineTestabilityTests: XCTestCase {
|
|||||||
enforceModeService: nil,
|
enforceModeService: nil,
|
||||||
timeProvider: timeProvider
|
timeProvider: timeProvider
|
||||||
)
|
)
|
||||||
|
|
||||||
XCTAssertNotNil(timerEngine)
|
XCTAssertNotNil(timerEngine)
|
||||||
XCTAssertEqual(timerEngine.timerStates.count, 0)
|
XCTAssertEqual(timerEngine.timerStates.count, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testTimerEngineUsesInjectedSettings() {
|
func testTimerEngineUsesInjectedSettings() {
|
||||||
var settings = AppSettings.defaults
|
var settings = AppSettings.defaults
|
||||||
settings.lookAwayTimer.enabled = true
|
settings.lookAwayTimer.enabled = true
|
||||||
settings.blinkTimer.enabled = false
|
settings.blinkTimer.enabled = false
|
||||||
settings.postureTimer.enabled = false
|
settings.postureTimer.enabled = false
|
||||||
|
|
||||||
testEnv.settingsManager.settings = settings
|
testEnv.settingsManager.settings = settings
|
||||||
let timerEngine = testEnv.container.timerEngine
|
let timerEngine = testEnv.container.timerEngine
|
||||||
|
|
||||||
timerEngine.start()
|
timerEngine.start()
|
||||||
|
|
||||||
// Only lookAway should be active
|
// Only lookAway should be active
|
||||||
let lookAwayTimer = timerEngine.timerStates.first { $0.key == .builtIn(.lookAway) }
|
let lookAwayTimer = timerEngine.timerStates.first { $0.key == .builtIn(.lookAway) }
|
||||||
let blinkTimer = timerEngine.timerStates.first { $0.key == .builtIn(.blink) }
|
let blinkTimer = timerEngine.timerStates.first { $0.key == .builtIn(.blink) }
|
||||||
|
|
||||||
XCTAssertNotNil(lookAwayTimer)
|
XCTAssertNotNil(lookAwayTimer)
|
||||||
XCTAssertNil(blinkTimer)
|
XCTAssertNil(blinkTimer)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testTimerEngineWithMockTimeProvider() {
|
func testTimerEngineWithMockTimeProvider() {
|
||||||
let timeProvider = MockTimeProvider(startTime: Date())
|
let timeProvider = MockTimeProvider(startTime: Date())
|
||||||
let timerEngine = TimerEngine(
|
let timerEngine = TimerEngine(
|
||||||
@@ -63,52 +64,51 @@ final class TimerEngineTestabilityTests: XCTestCase {
|
|||||||
enforceModeService: nil,
|
enforceModeService: nil,
|
||||||
timeProvider: timeProvider
|
timeProvider: timeProvider
|
||||||
)
|
)
|
||||||
|
|
||||||
// Start timers
|
// Start timers
|
||||||
timerEngine.start()
|
timerEngine.start()
|
||||||
|
|
||||||
// Advance time
|
// Advance time
|
||||||
timeProvider.advance(by: 10)
|
timeProvider.advance(by: 10)
|
||||||
|
|
||||||
// Timer engine should use the mocked time
|
// Timer engine should use the mocked time
|
||||||
XCTAssertNotNil(timerEngine.timerStates)
|
XCTAssertNotNil(timerEngine.timerStates)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testPauseAndResumeWithMocks() {
|
func testPauseAndResumeWithMocks() {
|
||||||
let timerEngine = testEnv.container.timerEngine
|
let timerEngine = testEnv.container.timerEngine
|
||||||
timerEngine.start()
|
timerEngine.start()
|
||||||
|
|
||||||
timerEngine.pause()
|
timerEngine.pause()
|
||||||
|
|
||||||
// Verify all timers are paused
|
// Verify all timers are paused
|
||||||
for (_, state) in timerEngine.timerStates {
|
for (_, state) in timerEngine.timerStates {
|
||||||
XCTAssertTrue(state.isPaused)
|
XCTAssertTrue(state.isPaused)
|
||||||
}
|
}
|
||||||
|
|
||||||
timerEngine.resume()
|
timerEngine.resume()
|
||||||
|
|
||||||
// Verify all timers are resumed
|
// Verify all timers are resumed
|
||||||
for (_, state) in timerEngine.timerStates {
|
for (_, state) in timerEngine.timerStates {
|
||||||
XCTAssertFalse(state.isPaused)
|
XCTAssertFalse(state.isPaused)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func testReminderEventPublishing() async throws {
|
func testReminderEventPublishing() async throws {
|
||||||
let timerEngine = testEnv.container.timerEngine
|
let timerEngine = testEnv.container.timerEngine
|
||||||
|
|
||||||
var receivedReminder: ReminderEvent?
|
var receivedReminder: ReminderEvent?
|
||||||
timerEngine.$activeReminder
|
timerEngine.$activeReminder
|
||||||
.sink { reminder in
|
.sink { reminder in
|
||||||
receivedReminder = reminder
|
receivedReminder = reminder
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
let timerId = TimerIdentifier.builtIn(.lookAway)
|
let timerId = TimerIdentifier.builtIn(.lookAway)
|
||||||
timerEngine.triggerReminder(for: timerId)
|
timerEngine.triggerReminder(for: timerId)
|
||||||
|
|
||||||
// Give time for publisher to fire
|
try await Task.sleep(for: .milliseconds(10))
|
||||||
try await Task.sleep(nanoseconds: 10_000_000)
|
|
||||||
|
|
||||||
XCTAssertNotNil(receivedReminder)
|
XCTAssertNotNil(receivedReminder)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user