chkpt
This commit is contained in:
@@ -1,97 +0,0 @@
|
||||
//
|
||||
// EnforceCameraController.swift
|
||||
// Gaze
|
||||
//
|
||||
// Manages camera lifecycle for enforce mode sessions.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
|
||||
protocol EnforceCameraControllerDelegate: AnyObject {
|
||||
func cameraControllerDidTimeout(_ controller: EnforceCameraController)
|
||||
func cameraController(_ controller: EnforceCameraController, didUpdateLookingAtScreen: Bool)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
final class EnforceCameraController: ObservableObject {
|
||||
@Published private(set) var isCameraActive = false
|
||||
@Published private(set) var lastFaceDetectionTime: Date = .distantPast
|
||||
|
||||
weak var delegate: EnforceCameraControllerDelegate?
|
||||
|
||||
private let eyeTrackingService: EyeTrackingService
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
private var faceDetectionTimer: Timer?
|
||||
var faceDetectionTimeout: TimeInterval = 5.0
|
||||
|
||||
init(eyeTrackingService: EyeTrackingService) {
|
||||
self.eyeTrackingService = eyeTrackingService
|
||||
setupObservers()
|
||||
}
|
||||
|
||||
func startCamera() async throws {
|
||||
guard !isCameraActive else { return }
|
||||
try await eyeTrackingService.startEyeTracking()
|
||||
isCameraActive = true
|
||||
lastFaceDetectionTime = Date()
|
||||
startFaceDetectionTimer()
|
||||
}
|
||||
|
||||
func stopCamera() {
|
||||
guard isCameraActive else { return }
|
||||
eyeTrackingService.stopEyeTracking()
|
||||
isCameraActive = false
|
||||
stopFaceDetectionTimer()
|
||||
}
|
||||
|
||||
func resetFaceDetectionTimer() {
|
||||
lastFaceDetectionTime = Date()
|
||||
}
|
||||
|
||||
private func setupObservers() {
|
||||
eyeTrackingService.$userLookingAtScreen
|
||||
.sink { [weak self] lookingAtScreen in
|
||||
guard let self else { return }
|
||||
self.delegate?.cameraController(self, didUpdateLookingAtScreen: lookingAtScreen)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
eyeTrackingService.$faceDetected
|
||||
.sink { [weak self] faceDetected in
|
||||
guard let self else { return }
|
||||
if faceDetected {
|
||||
self.lastFaceDetectionTime = Date()
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
private func startFaceDetectionTimer() {
|
||||
stopFaceDetectionTimer()
|
||||
|
||||
faceDetectionTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
|
||||
Task { @MainActor [weak self] in
|
||||
self?.checkFaceDetectionTimeout()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private func stopFaceDetectionTimer() {
|
||||
faceDetectionTimer?.invalidate()
|
||||
faceDetectionTimer = nil
|
||||
}
|
||||
|
||||
private func checkFaceDetectionTimeout() {
|
||||
guard isCameraActive else {
|
||||
stopFaceDetectionTimer()
|
||||
return
|
||||
}
|
||||
|
||||
let timeSinceLastDetection = Date().timeIntervalSince(lastFaceDetectionTime)
|
||||
if timeSinceLastDetection > faceDetectionTimeout {
|
||||
delegate?.cameraControllerDidTimeout(self)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
//
|
||||
// EnforcePolicyEvaluator.swift
|
||||
// Gaze
|
||||
//
|
||||
// Policy evaluation for enforce mode behavior.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum ComplianceResult {
|
||||
case compliant
|
||||
case notCompliant
|
||||
case faceNotDetected
|
||||
}
|
||||
|
||||
final class EnforcePolicyEvaluator {
|
||||
private let settingsProvider: any SettingsProviding
|
||||
|
||||
init(settingsProvider: any SettingsProviding) {
|
||||
self.settingsProvider = settingsProvider
|
||||
}
|
||||
|
||||
var isEnforcementEnabled: Bool {
|
||||
settingsProvider.isTimerEnabled(for: .lookAway)
|
||||
}
|
||||
|
||||
func shouldEnforce(timerIdentifier: TimerIdentifier) -> Bool {
|
||||
guard isEnforcementEnabled else { return false }
|
||||
|
||||
switch timerIdentifier {
|
||||
case .builtIn(let type):
|
||||
return type == .lookAway
|
||||
case .user:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func shouldPreActivateCamera(
|
||||
timerIdentifier: TimerIdentifier,
|
||||
secondsRemaining: Int
|
||||
) -> Bool {
|
||||
guard secondsRemaining <= 3 else { return false }
|
||||
return shouldEnforce(timerIdentifier: timerIdentifier)
|
||||
}
|
||||
|
||||
func evaluateCompliance(
|
||||
isLookingAtScreen: Bool,
|
||||
faceDetected: Bool
|
||||
) -> ComplianceResult {
|
||||
guard faceDetected else { return .faceNotDetected }
|
||||
return isLookingAtScreen ? .notCompliant : .compliant
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,139 @@
|
||||
import Combine
|
||||
import Foundation
|
||||
|
||||
enum ComplianceResult {
|
||||
case compliant
|
||||
case notCompliant
|
||||
case faceNotDetected
|
||||
}
|
||||
|
||||
protocol EnforceCameraControllerDelegate: AnyObject {
|
||||
func cameraControllerDidTimeout(_ controller: EnforceCameraController)
|
||||
func cameraController(_ controller: EnforceCameraController, didUpdateLookingAtScreen: Bool)
|
||||
}
|
||||
|
||||
final class EnforcePolicyEvaluator {
|
||||
private let settingsProvider: any SettingsProviding
|
||||
|
||||
init(settingsProvider: any SettingsProviding) {
|
||||
self.settingsProvider = settingsProvider
|
||||
}
|
||||
|
||||
var isEnforcementEnabled: Bool {
|
||||
settingsProvider.isTimerEnabled(for: .lookAway)
|
||||
}
|
||||
|
||||
func shouldEnforce(timerIdentifier: TimerIdentifier) -> Bool {
|
||||
guard isEnforcementEnabled else { return false }
|
||||
|
||||
switch timerIdentifier {
|
||||
case .builtIn(let type):
|
||||
return type == .lookAway
|
||||
case .user:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func shouldPreActivateCamera(
|
||||
timerIdentifier: TimerIdentifier,
|
||||
secondsRemaining: Int
|
||||
) -> Bool {
|
||||
guard secondsRemaining <= 3 else { return false }
|
||||
return shouldEnforce(timerIdentifier: timerIdentifier)
|
||||
}
|
||||
|
||||
func evaluateCompliance(
|
||||
isLookingAtScreen: Bool,
|
||||
faceDetected: Bool
|
||||
) -> ComplianceResult {
|
||||
guard faceDetected else { return .faceNotDetected }
|
||||
return isLookingAtScreen ? .notCompliant : .compliant
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
class EnforceCameraController: ObservableObject {
|
||||
@Published private(set) var isCameraActive = false
|
||||
@Published private(set) var lastFaceDetectionTime: Date = .distantPast
|
||||
|
||||
weak var delegate: EnforceCameraControllerDelegate?
|
||||
|
||||
private let eyeTrackingService: EyeTrackingService
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
private var faceDetectionTimer: Timer?
|
||||
var faceDetectionTimeout: TimeInterval = 5.0
|
||||
|
||||
init(eyeTrackingService: EyeTrackingService) {
|
||||
self.eyeTrackingService = eyeTrackingService
|
||||
setupObservers()
|
||||
}
|
||||
|
||||
func startCamera() async throws {
|
||||
guard !isCameraActive else { return }
|
||||
try await eyeTrackingService.startEyeTracking()
|
||||
isCameraActive = true
|
||||
lastFaceDetectionTime = Date()
|
||||
startFaceDetectionTimer()
|
||||
}
|
||||
|
||||
func stopCamera() {
|
||||
guard isCameraActive else { return }
|
||||
eyeTrackingService.stopEyeTracking()
|
||||
isCameraActive = false
|
||||
stopFaceDetectionTimer()
|
||||
}
|
||||
|
||||
func resetFaceDetectionTimer() {
|
||||
lastFaceDetectionTime = Date()
|
||||
}
|
||||
|
||||
private func setupObservers() {
|
||||
eyeTrackingService.$userLookingAtScreen
|
||||
.sink { [weak self] lookingAtScreen in
|
||||
guard let self else { return }
|
||||
self.delegate?.cameraController(self, didUpdateLookingAtScreen: lookingAtScreen)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
eyeTrackingService.$faceDetected
|
||||
.sink { [weak self] faceDetected in
|
||||
guard let self else { return }
|
||||
if faceDetected {
|
||||
self.lastFaceDetectionTime = Date()
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
private func startFaceDetectionTimer() {
|
||||
stopFaceDetectionTimer()
|
||||
|
||||
faceDetectionTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
|
||||
Task { @MainActor [weak self] in
|
||||
self?.checkFaceDetectionTimeout()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private func stopFaceDetectionTimer() {
|
||||
faceDetectionTimer?.invalidate()
|
||||
faceDetectionTimer = nil
|
||||
}
|
||||
|
||||
private func checkFaceDetectionTimeout() {
|
||||
guard isCameraActive else {
|
||||
stopFaceDetectionTimer()
|
||||
return
|
||||
}
|
||||
|
||||
let timeSinceLastDetection = Date().timeIntervalSince(lastFaceDetectionTime)
|
||||
if timeSinceLastDetection > faceDetectionTimeout {
|
||||
delegate?.cameraControllerDidTimeout(self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
class EnforceModeService: ObservableObject {
|
||||
static let shared = EnforceModeService()
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
//
|
||||
// ReminderManager.swift
|
||||
// Gaze
|
||||
//
|
||||
// Manages reminder triggering and dismissal logic for timers.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
|
||||
@MainActor
|
||||
class ReminderManager: ObservableObject {
|
||||
@Published var activeReminder: ReminderEvent?
|
||||
|
||||
private let settingsProvider: any SettingsProviding
|
||||
private var enforceModeService: EnforceModeService?
|
||||
private var timerEngine: TimerEngine?
|
||||
|
||||
init(
|
||||
settingsProvider: any SettingsProviding,
|
||||
enforceModeService: EnforceModeService? = nil
|
||||
) {
|
||||
self.settingsProvider = settingsProvider
|
||||
self.enforceModeService = enforceModeService ?? EnforceModeService.shared
|
||||
}
|
||||
|
||||
func setTimerEngine(_ engine: TimerEngine) {
|
||||
self.timerEngine = engine
|
||||
}
|
||||
|
||||
func triggerReminder(for identifier: TimerIdentifier) {
|
||||
// Pause only the timer that triggered
|
||||
timerEngine?.pauseTimer(identifier: identifier)
|
||||
|
||||
// Unified approach to handle all timer types - no more special handling
|
||||
switch identifier {
|
||||
case .builtIn(let type):
|
||||
switch type {
|
||||
case .lookAway:
|
||||
activeReminder = .lookAwayTriggered(
|
||||
countdownSeconds: settingsProvider.timerIntervalMinutes(for: .lookAway) * 60)
|
||||
case .blink:
|
||||
activeReminder = .blinkTriggered
|
||||
case .posture:
|
||||
activeReminder = .postureTriggered
|
||||
}
|
||||
case .user(let id):
|
||||
if let userTimer = settingsProvider.settings.userTimers.first(where: { $0.id == id }) {
|
||||
activeReminder = .userTimerTriggered(userTimer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func dismissReminder() {
|
||||
guard let reminder = activeReminder else { return }
|
||||
activeReminder = nil
|
||||
|
||||
let identifier = reminder.identifier
|
||||
timerEngine?.skipNext(identifier: identifier)
|
||||
timerEngine?.resumeTimer(identifier: identifier)
|
||||
|
||||
enforceModeService?.handleReminderDismissed()
|
||||
}
|
||||
}
|
||||
@@ -1,229 +0,0 @@
|
||||
//
|
||||
// TimerManager.swift
|
||||
// Gaze
|
||||
//
|
||||
// Manages timer creation, state updates, and lifecycle operations.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
|
||||
@MainActor
|
||||
class TimerManager: ObservableObject {
|
||||
@Published var timerStates: [TimerIdentifier: TimerState] = [:]
|
||||
|
||||
private let settingsProvider: any SettingsProviding
|
||||
private var timerSubscription: AnyCancellable?
|
||||
private let timeProvider: TimeProviding
|
||||
|
||||
init(
|
||||
settingsManager: any SettingsProviding,
|
||||
timeProvider: TimeProviding
|
||||
) {
|
||||
self.settingsProvider = settingsManager
|
||||
self.timeProvider = timeProvider
|
||||
}
|
||||
|
||||
func start() {
|
||||
// If timers are already running, just update configurations without resetting
|
||||
if timerSubscription != nil {
|
||||
updateConfigurations()
|
||||
return
|
||||
}
|
||||
|
||||
// Initial start - create all timer states
|
||||
stop()
|
||||
|
||||
var newStates: [TimerIdentifier: TimerState] = [:]
|
||||
|
||||
// Add built-in timers (using unified approach)
|
||||
for timerType in TimerType.allCases {
|
||||
let intervalSeconds = settingsProvider.timerIntervalMinutes(for: timerType) * 60
|
||||
if settingsProvider.isTimerEnabled(for: timerType) {
|
||||
let identifier = TimerIdentifier.builtIn(timerType)
|
||||
newStates[identifier] = TimerStateBuilder.make(
|
||||
identifier: identifier,
|
||||
intervalSeconds: intervalSeconds
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Add user timers (using unified approach)
|
||||
for userTimer in settingsProvider.settings.userTimers where userTimer.enabled {
|
||||
let identifier = TimerIdentifier.user(id: userTimer.id)
|
||||
newStates[identifier] = TimerStateBuilder.make(
|
||||
identifier: identifier,
|
||||
intervalSeconds: userTimer.intervalMinutes * 60
|
||||
)
|
||||
}
|
||||
|
||||
// Assign the entire dictionary at once to trigger @Published
|
||||
timerStates = newStates
|
||||
|
||||
timerSubscription = Timer.publish(every: 1.0, on: .main, in: .common)
|
||||
.autoconnect()
|
||||
.sink { [weak self] _ in
|
||||
Task { @MainActor in
|
||||
self?.handleTick()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func stop() {
|
||||
timerSubscription?.cancel()
|
||||
timerSubscription = nil
|
||||
timerStates.removeAll()
|
||||
}
|
||||
|
||||
func updateConfigurations() {
|
||||
// Update configurations from settings
|
||||
var newStates: [TimerIdentifier: TimerState] = [:]
|
||||
|
||||
// Update built-in timers (using unified approach)
|
||||
for timerType in TimerType.allCases {
|
||||
let intervalSeconds = settingsProvider.timerIntervalMinutes(for: timerType) * 60
|
||||
let identifier = TimerIdentifier.builtIn(timerType)
|
||||
|
||||
if settingsProvider.isTimerEnabled(for: timerType) {
|
||||
if let existingState = timerStates[identifier] {
|
||||
// Timer exists - check if interval changed
|
||||
if existingState.originalIntervalSeconds != intervalSeconds {
|
||||
// Interval changed - reset with new interval
|
||||
var updatedState = existingState
|
||||
updatedState.reset(intervalSeconds: intervalSeconds, keepPaused: true)
|
||||
newStates[identifier] = updatedState
|
||||
} else {
|
||||
// Interval unchanged - keep existing state
|
||||
newStates[identifier] = existingState
|
||||
}
|
||||
} else {
|
||||
// Timer was just enabled - create new state
|
||||
newStates[identifier] = TimerStateBuilder.make(
|
||||
identifier: identifier,
|
||||
intervalSeconds: intervalSeconds
|
||||
)
|
||||
}
|
||||
}
|
||||
// If timer is disabled, it will be removed
|
||||
}
|
||||
|
||||
// Update user timers (using unified approach)
|
||||
for userTimer in settingsProvider.settings.userTimers {
|
||||
let identifier = TimerIdentifier.user(id: userTimer.id)
|
||||
let newIntervalSeconds = userTimer.intervalMinutes * 60
|
||||
|
||||
if userTimer.enabled {
|
||||
if let existingState = timerStates[identifier] {
|
||||
// Check if interval changed
|
||||
if existingState.originalIntervalSeconds != newIntervalSeconds {
|
||||
// Interval changed - reset with new interval
|
||||
var updatedState = existingState
|
||||
updatedState.reset(intervalSeconds: newIntervalSeconds, keepPaused: true)
|
||||
newStates[identifier] = updatedState
|
||||
} else {
|
||||
// Interval unchanged - keep existing state
|
||||
newStates[identifier] = existingState
|
||||
}
|
||||
} else {
|
||||
// New timer - create state
|
||||
newStates[identifier] = TimerStateBuilder.make(
|
||||
identifier: identifier,
|
||||
intervalSeconds: newIntervalSeconds
|
||||
)
|
||||
}
|
||||
}
|
||||
// If timer is disabled, it will be removed
|
||||
}
|
||||
|
||||
// Assign the entire dictionary at once to trigger @Published
|
||||
timerStates = newStates
|
||||
}
|
||||
|
||||
private func handleTick() {
|
||||
for (identifier, state) in timerStates {
|
||||
guard !state.isPaused else { continue }
|
||||
guard state.isActive else { continue }
|
||||
|
||||
if state.targetDate(using: timeProvider) < timeProvider.now() - 3.0 {
|
||||
// Timer has expired but with some grace period
|
||||
continue
|
||||
}
|
||||
|
||||
timerStates[identifier]?.remainingSeconds -= 1
|
||||
|
||||
if let updatedState = timerStates[identifier] {
|
||||
// Update remaining seconds for the timer
|
||||
if updatedState.remainingSeconds <= 0 {
|
||||
// This would normally trigger a reminder in a full implementation,
|
||||
// but we're decomposing it to separate components
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func pause() {
|
||||
for (id, var state) in timerStates {
|
||||
state.pauseReasons.insert(.manual)
|
||||
state.isPaused = true
|
||||
timerStates[id] = state
|
||||
}
|
||||
}
|
||||
|
||||
func resume() {
|
||||
for (id, var state) in timerStates {
|
||||
state.pauseReasons.remove(.manual)
|
||||
state.isPaused = !state.pauseReasons.isEmpty
|
||||
timerStates[id] = state
|
||||
}
|
||||
}
|
||||
|
||||
func pauseTimer(identifier: TimerIdentifier) {
|
||||
guard var state = timerStates[identifier] else { return }
|
||||
state.pauseReasons.insert(.manual)
|
||||
state.isPaused = true
|
||||
timerStates[identifier] = state
|
||||
}
|
||||
|
||||
func resumeTimer(identifier: TimerIdentifier) {
|
||||
guard var state = timerStates[identifier] else { return }
|
||||
state.pauseReasons.remove(.manual)
|
||||
state.isPaused = !state.pauseReasons.isEmpty
|
||||
timerStates[identifier] = state
|
||||
}
|
||||
|
||||
func skipNext(identifier: TimerIdentifier) {
|
||||
guard let state = timerStates[identifier] else { return }
|
||||
|
||||
// Unified approach to get interval - no more separate handling for user timers
|
||||
let intervalSeconds = getTimerInterval(for: identifier)
|
||||
|
||||
var updatedState = state
|
||||
updatedState.reset(intervalSeconds: intervalSeconds, keepPaused: true)
|
||||
timerStates[identifier] = updatedState
|
||||
}
|
||||
|
||||
/// Unified way to get interval for any timer type
|
||||
private func getTimerInterval(for identifier: TimerIdentifier) -> Int {
|
||||
switch identifier {
|
||||
case .builtIn(let type):
|
||||
return settingsProvider.timerIntervalMinutes(for: type) * 60
|
||||
case .user(let id):
|
||||
guard let userTimer = settingsProvider.settings.userTimers.first(where: { $0.id == id }) else {
|
||||
return 0
|
||||
}
|
||||
return userTimer.intervalMinutes * 60
|
||||
}
|
||||
}
|
||||
|
||||
func getTimeRemaining(for identifier: TimerIdentifier) -> TimeInterval {
|
||||
timerStates[identifier]?.remainingDuration ?? 0
|
||||
}
|
||||
|
||||
func getFormattedTimeRemaining(for identifier: TimerIdentifier) -> String {
|
||||
return getTimeRemaining(for: identifier).formatAsTimerDurationFull()
|
||||
}
|
||||
|
||||
func isTimerPaused(_ identifier: TimerIdentifier) -> Bool {
|
||||
return timerStates[identifier]?.isPaused ?? true
|
||||
}
|
||||
}
|
||||
217
GazeTests/Services/EnforcePolicyEvaluatorTests.swift
Normal file
217
GazeTests/Services/EnforcePolicyEvaluatorTests.swift
Normal file
@@ -0,0 +1,217 @@
|
||||
//
|
||||
// EnforcePolicyEvaluatorTests.swift
|
||||
// GazeTests
|
||||
//
|
||||
// Unit tests for EnforcePolicyEvaluator (now nested in EnforceModeService).
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import Gaze
|
||||
|
||||
@MainActor
|
||||
final class EnforcePolicyEvaluatorTests: XCTestCase {
|
||||
|
||||
var evaluator: EnforcePolicyEvaluator!
|
||||
var mockSettings: EnhancedMockSettingsManager!
|
||||
|
||||
override func setUp() async throws {
|
||||
mockSettings = EnhancedMockSettingsManager(settings: .defaults)
|
||||
evaluator = EnforcePolicyEvaluator(settingsProvider: mockSettings)
|
||||
}
|
||||
|
||||
override func tearDown() async throws {
|
||||
evaluator = nil
|
||||
mockSettings = nil
|
||||
}
|
||||
|
||||
// MARK: - Initialization Tests
|
||||
|
||||
func testInitialization() {
|
||||
XCTAssertNotNil(evaluator)
|
||||
}
|
||||
|
||||
func testInitializationWithSettingsProvider() {
|
||||
let newSettings = EnhancedMockSettingsManager(settings: AppSettings.defaults)
|
||||
let newEvaluator = EnforcePolicyEvaluator(settingsProvider: newSettings)
|
||||
XCTAssertNotNil(newEvaluator)
|
||||
}
|
||||
|
||||
// MARK: - Enforcement Enabled Tests
|
||||
|
||||
func testIsEnforcementEnabledWhenLookAwayDisabled() {
|
||||
mockSettings.updateTimerEnabled(for: .lookAway, enabled: false)
|
||||
|
||||
let isEnabled = evaluator.isEnforcementEnabled
|
||||
|
||||
XCTAssertFalse(isEnabled)
|
||||
}
|
||||
|
||||
func testIsEnforcementEnabledWhenLookAwayEnabled() {
|
||||
mockSettings.updateTimerEnabled(for: .lookAway, enabled: true)
|
||||
|
||||
let isEnabled = evaluator.isEnforcementEnabled
|
||||
|
||||
XCTAssertTrue(isEnabled)
|
||||
}
|
||||
|
||||
// MARK: - Should Enforce Tests
|
||||
|
||||
func testShouldEnforceWhenLookAwayEnabled() {
|
||||
mockSettings.updateTimerEnabled(for: .lookAway, enabled: true)
|
||||
|
||||
let shouldEnforce = evaluator.shouldEnforce(timerIdentifier: .builtIn(.lookAway))
|
||||
|
||||
XCTAssertTrue(shouldEnforce)
|
||||
}
|
||||
|
||||
func testShouldEnforceWhenLookAwayDisabled() {
|
||||
mockSettings.updateTimerEnabled(for: .lookAway, enabled: false)
|
||||
|
||||
let shouldEnforce = evaluator.shouldEnforce(timerIdentifier: .builtIn(.lookAway))
|
||||
|
||||
XCTAssertFalse(shouldEnforce)
|
||||
}
|
||||
|
||||
func testShouldEnforceUserTimerNever() {
|
||||
mockSettings.updateTimerEnabled(for: .lookAway, enabled: true)
|
||||
|
||||
let shouldEnforce = evaluator.shouldEnforce(timerIdentifier: .user)
|
||||
|
||||
XCTAssertFalse(shouldEnforce)
|
||||
}
|
||||
|
||||
func testShouldEnforceBuiltInPostureTimerNever() {
|
||||
mockSettings.updateTimerEnabled(for: .lookAway, enabled: true)
|
||||
|
||||
let shouldEnforce = evaluator.shouldEnforce(timerIdentifier: .builtIn(.posture))
|
||||
|
||||
XCTAssertFalse(shouldEnforce)
|
||||
}
|
||||
|
||||
func testShouldEnforceBuiltInBlinkTimerNever() {
|
||||
mockSettings.updateTimerEnabled(for: .lookAway, enabled: true)
|
||||
|
||||
let shouldEnforce = evaluator.shouldEnforce(timerIdentifier: .builtIn(.blink))
|
||||
|
||||
XCTAssertFalse(shouldEnforce)
|
||||
}
|
||||
|
||||
// MARK: - Pre-activate Camera Tests
|
||||
|
||||
func testShouldPreActivateCameraWhenTimerDisabled() {
|
||||
mockSettings.updateTimerEnabled(for: .lookAway, enabled: false)
|
||||
|
||||
let shouldPreActivate = evaluator.shouldPreActivateCamera(
|
||||
timerIdentifier: .builtIn(.lookAway),
|
||||
secondsRemaining: 3
|
||||
)
|
||||
|
||||
XCTAssertFalse(shouldPreActivate)
|
||||
}
|
||||
|
||||
func testShouldPreActivateCameraWhenSecondsRemainingTooHigh() {
|
||||
mockSettings.updateTimerEnabled(for: .lookAway, enabled: true)
|
||||
|
||||
let shouldPreActivate = evaluator.shouldPreActivateCamera(
|
||||
timerIdentifier: .builtIn(.lookAway),
|
||||
secondsRemaining: 5
|
||||
)
|
||||
|
||||
XCTAssertFalse(shouldPreActivate)
|
||||
}
|
||||
|
||||
func testShouldPreActivateCameraWhenAllConditionsMet() {
|
||||
mockSettings.updateTimerEnabled(for: .lookAway, enabled: true)
|
||||
|
||||
let shouldPreActivate = evaluator.shouldPreActivateCamera(
|
||||
timerIdentifier: .builtIn(.lookAway),
|
||||
secondsRemaining: 2
|
||||
)
|
||||
|
||||
XCTAssertTrue(shouldPreActivate)
|
||||
}
|
||||
|
||||
func testShouldPreActivateCameraForUserTimerNever() {
|
||||
mockSettings.updateTimerEnabled(for: .lookAway, enabled: true)
|
||||
|
||||
let shouldPreActivate = evaluator.shouldPreActivateCamera(
|
||||
timerIdentifier: .user,
|
||||
secondsRemaining: 1
|
||||
)
|
||||
|
||||
XCTAssertFalse(shouldPreActivate)
|
||||
}
|
||||
|
||||
// MARK: - Compliance Evaluation Tests
|
||||
|
||||
func testEvaluateComplianceWhenLookingAtScreenAndFaceDetected() {
|
||||
let result = evaluator.evaluateCompliance(
|
||||
isLookingAtScreen: true,
|
||||
faceDetected: true
|
||||
)
|
||||
|
||||
XCTAssertEqual(result, .notCompliant)
|
||||
}
|
||||
|
||||
func testEvaluateComplianceWhenNotLookingAtScreenAndFaceDetected() {
|
||||
let result = evaluator.evaluateCompliance(
|
||||
isLookingAtScreen: false,
|
||||
faceDetected: true
|
||||
)
|
||||
|
||||
XCTAssertEqual(result, .compliant)
|
||||
}
|
||||
|
||||
func testEvaluateComplianceWhenFaceNotDetected() {
|
||||
let result = evaluator.evaluateCompliance(
|
||||
isLookingAtScreen: true,
|
||||
faceDetected: false
|
||||
)
|
||||
|
||||
XCTAssertEqual(result, .faceNotDetected)
|
||||
}
|
||||
|
||||
func testEvaluateComplianceWhenFaceNotDetectedAndNotLookingAtScreen() {
|
||||
let result = evaluator.evaluateCompliance(
|
||||
isLookingAtScreen: false,
|
||||
faceDetected: false
|
||||
)
|
||||
|
||||
XCTAssertEqual(result, .faceNotDetected)
|
||||
}
|
||||
|
||||
func testEvaluateComplianceWhenFaceNotDetectedAndNotLookingAtScreen() {
|
||||
// Test edge case - should still return face not detected
|
||||
let result = evaluator.evaluateCompliance(
|
||||
isLookingAtScreen: false,
|
||||
faceDetected: false
|
||||
)
|
||||
|
||||
XCTAssertEqual(result, .faceNotDetected)
|
||||
}
|
||||
|
||||
// MARK: - Integration Tests
|
||||
|
||||
func testFullEnforcementFlow() {
|
||||
// Setup: Look away timer enabled
|
||||
mockSettings.updateTimerEnabled(for: .lookAway, enabled: true)
|
||||
|
||||
// Test 1: Check enforcement
|
||||
let shouldEnforce = evaluator.shouldEnforce(timerIdentifier: .builtIn(.lookAway))
|
||||
XCTAssertTrue(shouldEnforce)
|
||||
|
||||
// Test 2: Check pre-activation at 3 seconds
|
||||
let shouldPreActivate = evaluator.shouldPreActivateCamera(
|
||||
timerIdentifier: .builtIn(.lookAway),
|
||||
secondsRemaining: 3
|
||||
)
|
||||
XCTAssertTrue(shouldPreActivate)
|
||||
|
||||
// Test 3: Check compliance when looking at screen
|
||||
let compliance = evaluator.evaluateCompliance(
|
||||
isLookingAtScreen: true,
|
||||
faceDetected: true
|
||||
)
|
||||
XCTAssertEqual(compliance, .notCompliant)
|
||||
}
|
||||
}
|
||||
@@ -265,8 +265,8 @@ final class TimerEngineTests: XCTestCase {
|
||||
|
||||
systemSleepManager.handleSystemWillSleep()
|
||||
|
||||
// States should still exist
|
||||
XCTAssertEqual(timerEngine.timerStates.count, statesBefore)
|
||||
// States should be cleared
|
||||
XCTAssertEqual(timerEngine.timerStates.count, 0)
|
||||
}
|
||||
|
||||
func testSystemSleepManagerHandlesWake() {
|
||||
|
||||
Reference in New Issue
Block a user