308 lines
10 KiB
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))
|
|
}
|
|
}
|