learning_ai_clock/ios/ChronoMindTests/TimerEngineTests.swift

308 lines
10 KiB
Swift

// Timer Engine Tests
// XCTest unit tests mirroring web Vitest tests
import XCTest
@testable import ChronoMind
final class TimerEngineTests: XCTestCase {
// MARK: - Create Alarm
func testCreateAlarm() {
let target = Date().addingTimeInterval(3600) // 1h from now
let timer = createAlarm(CreateAlarmParams(label: "Test Alarm", targetTime: target))
XCTAssertEqual(timer.type, .alarm)
XCTAssertEqual(timer.label, "Test Alarm")
XCTAssertEqual(timer.urgency, .standard)
XCTAssertEqual(timer.state, .active)
XCTAssertNotNil(timer.startedAt)
XCTAssertEqual(timer.snoozeCount, 0)
XCTAssertFalse(timer.id.isEmpty)
}
func testCreateAlarmWithUrgency() {
let target = Date().addingTimeInterval(7200)
let timer = createAlarm(CreateAlarmParams(
label: "Critical Meeting",
targetTime: target,
urgency: .critical,
cascade: CascadeConfig(preset: .aggressive, intervals: [])
))
XCTAssertEqual(timer.urgency, .critical)
XCTAssertEqual(timer.cascade.preset, .aggressive)
XCTAssertFalse(timer.warnings.isEmpty)
}
// MARK: - Create Countdown
func testCreateCountdown() {
let timer = createCountdown(CreateCountdownParams(
label: "Pasta",
durationSeconds: 600 // 10 min
))
XCTAssertEqual(timer.type, .countdown)
XCTAssertEqual(timer.label, "Pasta")
XCTAssertEqual(timer.duration, 600)
XCTAssertEqual(timer.state, .active)
XCTAssert(timer.targetTime > Date())
}
func testCreateCountdownWithCustomCascade() {
let timer = createCountdown(CreateCountdownParams(
label: "Test",
durationSeconds: 3600,
cascade: CascadeConfig(preset: .custom, intervals: [30, 15, 5])
))
// Custom intervals should produce warnings
let unfired = timer.warnings.filter { !$0.fired }
XCTAssertEqual(unfired.count, 3)
}
// MARK: - Create Pomodoro
func testCreatePomodoro() {
let timer = createPomodoro()
XCTAssertEqual(timer.type, .pomodoro)
XCTAssertEqual(timer.label, "Focus Session")
XCTAssertEqual(timer.state, .active)
XCTAssertEqual(timer.pomodoroConfig?.workMinutes, 25)
XCTAssertEqual(timer.pomodoroConfig?.breakMinutes, 5)
XCTAssertEqual(timer.pomodoroConfig?.rounds, 4)
XCTAssertEqual(timer.pomodoroState?.currentRound, 1)
XCTAssertEqual(timer.pomodoroState?.isBreak, false)
XCTAssertEqual(timer.pomodoroState?.completedRounds, 0)
}
func testCreatePomodoroCustomConfig() {
let config = PomodoroConfig(workMinutes: 50, breakMinutes: 10, longBreakMinutes: 30, rounds: 3)
let timer = createPomodoro(CreatePomodoroParams(
label: "Deep Work",
config: config
))
XCTAssertEqual(timer.label, "Deep Work")
XCTAssertEqual(timer.pomodoroConfig?.workMinutes, 50)
XCTAssertEqual(timer.pomodoroConfig?.rounds, 3)
XCTAssertEqual(timer.duration, 3000) // 50 * 60
}
// MARK: - State Transitions
func testPauseTimer() {
let timer = createCountdown(CreateCountdownParams(label: "Test", durationSeconds: 600))
let paused = pauseTimer(timer)
XCTAssertEqual(paused.state, .paused)
XCTAssertNotNil(paused.pausedAt)
}
func testPauseOnlyActiveOrWarning() {
var timer = createCountdown(CreateCountdownParams(label: "Test", durationSeconds: 600))
timer.state = .dismissed
let result = pauseTimer(timer)
XCTAssertEqual(result.state, .dismissed) // unchanged
}
func testResumeTimer() {
let timer = createCountdown(CreateCountdownParams(label: "Test", durationSeconds: 600))
let paused = pauseTimer(timer)
let resumed = resumeTimer(paused)
XCTAssertEqual(resumed.state, .active)
XCTAssertNil(resumed.pausedAt)
XCTAssertNotNil(resumed.startedAt)
XCTAssert(resumed.targetTime > Date())
}
func testResumeOnlyPaused() {
let timer = createCountdown(CreateCountdownParams(label: "Test", durationSeconds: 600))
let result = resumeTimer(timer) // already active, not paused
XCTAssertEqual(result.state, .active) // unchanged
}
func testFireTimer() {
let timer = createCountdown(CreateCountdownParams(label: "Test", durationSeconds: 600))
let fired = fireTimer(timer)
XCTAssertEqual(fired.state, .firing)
XCTAssertNotNil(fired.firedAt)
}
func testFireIgnoresDismissed() {
var timer = createCountdown(CreateCountdownParams(label: "Test", durationSeconds: 600))
timer = dismissTimer(timer)
let result = fireTimer(timer)
XCTAssertEqual(result.state, .dismissed)
}
func testSnoozeTimer() {
var timer = createCountdown(CreateCountdownParams(label: "Test", durationSeconds: 600))
timer = fireTimer(timer)
let snoozed = snoozeTimer(timer, snoozeMinutes: 5)
XCTAssertEqual(snoozed.state, .snoozed)
XCTAssertEqual(snoozed.snoozeCount, 1)
XCTAssertNotNil(snoozed.snoozedUntil)
// Snooze until should be ~5 minutes from now
let diff = snoozed.snoozedUntil!.timeIntervalSinceNow
XCTAssert(diff > 290 && diff < 310) // ~300 seconds with tolerance
}
func testSnoozeOnlyFiring() {
let timer = createCountdown(CreateCountdownParams(label: "Test", durationSeconds: 600))
let result = snoozeTimer(timer, snoozeMinutes: 5)
XCTAssertEqual(result.state, .active) // unchanged, not firing
}
func testDismissTimer() {
let timer = createCountdown(CreateCountdownParams(label: "Test", durationSeconds: 600))
let dismissed = dismissTimer(timer)
XCTAssertEqual(dismissed.state, .dismissed)
XCTAssertNotNil(dismissed.dismissedAt)
}
func testCompleteTimer() {
let timer = createCountdown(CreateCountdownParams(label: "Test", durationSeconds: 600))
let completed = completeTimer(timer)
XCTAssertEqual(completed.state, .completed)
XCTAssertNotNil(completed.completedAt)
}
// MARK: - Pomodoro Transitions
func testAdvancePomodoroWorkToBreak() {
let timer = createPomodoro()
guard let advanced = advancePomodoro(timer) else {
XCTFail("Should advance")
return
}
XCTAssertEqual(advanced.state, .active)
XCTAssertEqual(advanced.pomodoroState?.isBreak, true)
XCTAssertEqual(advanced.pomodoroState?.completedRounds, 1)
XCTAssertEqual(advanced.duration, 300) // 5 min break
}
func testAdvancePomodoroBreakToWork() {
var timer = createPomodoro()
// Advance to break
timer = advancePomodoro(timer)!
// Advance back to work
guard let next = advancePomodoro(timer) else {
XCTFail("Should advance")
return
}
XCTAssertEqual(next.pomodoroState?.isBreak, false)
XCTAssertEqual(next.pomodoroState?.currentRound, 2)
XCTAssertEqual(next.duration, 1500) // 25 min work
}
func testAdvancePomodoroToLongBreak() {
var timer = createPomodoro()
// Go through all 4 work rounds
for _ in 1...4 {
timer = advancePomodoro(timer)! // work break (or long break)
if timer.pomodoroState?.isLongBreak == true || timer.state == .completed {
break
}
if timer.pomodoroState?.isBreak == true {
timer = advancePomodoro(timer)! // break next work
}
}
// After 4 rounds, should be on long break
XCTAssertTrue(timer.pomodoroState?.isLongBreak == true || timer.state == .completed)
}
func testAdvancePomodoroCompletion() {
var timer = createPomodoro(CreatePomodoroParams(
config: PomodoroConfig(workMinutes: 1, breakMinutes: 1, longBreakMinutes: 1, rounds: 1)
))
// Work long break (only 1 round)
timer = advancePomodoro(timer)!
XCTAssertTrue(timer.pomodoroState?.isLongBreak == true)
// Long break complete
guard let completed = advancePomodoro(timer) else {
XCTFail("Should complete")
return
}
XCTAssertEqual(completed.state, .completed)
}
func testAdvancePomodoroReturnsNilForNonPomodoro() {
let timer = createCountdown(CreateCountdownParams(label: "Test", durationSeconds: 600))
XCTAssertNil(advancePomodoro(timer))
}
// MARK: - Utility Functions
func testGetRemainingSeconds() {
let timer = createCountdown(CreateCountdownParams(label: "Test", durationSeconds: 600))
let remaining = getRemainingSeconds(timer)
XCTAssert(remaining > 598 && remaining <= 600) // within 2 seconds
}
func testGetRemainingSecondsPaused() {
var timer = createCountdown(CreateCountdownParams(label: "Test", durationSeconds: 600))
// Simulate some elapsed time
timer.elapsedBeforePause = 100
timer.state = .paused
let remaining = getRemainingSeconds(timer)
XCTAssertEqual(remaining, 500) // 600 - 100
}
func testIsTimerActive() {
var timer = createCountdown(CreateCountdownParams(label: "Test", durationSeconds: 600))
XCTAssertTrue(isTimerActive(timer))
timer.state = .warning
XCTAssertTrue(isTimerActive(timer))
timer.state = .snoozed
XCTAssertTrue(isTimerActive(timer))
timer.state = .paused
XCTAssertFalse(isTimerActive(timer))
timer.state = .dismissed
XCTAssertFalse(isTimerActive(timer))
}
func testShouldTimerFire() {
var timer = createCountdown(CreateCountdownParams(label: "Test", durationSeconds: 600))
let now = Date()
// Not yet
XCTAssertFalse(shouldTimerFire(timer, now: now))
// Past target time
XCTAssertTrue(shouldTimerFire(timer, now: timer.targetTime.addingTimeInterval(1)))
// Snoozed and past snooze time
timer.state = .snoozed
timer.snoozedUntil = now.addingTimeInterval(-10)
XCTAssertTrue(shouldTimerFire(timer, now: now))
// Snoozed but not past snooze time
timer.snoozedUntil = now.addingTimeInterval(300)
XCTAssertFalse(shouldTimerFire(timer, now: now))
}
}