diff --git a/ios/ChronoMindTests/CascadeTests.swift b/ios/ChronoMindTests/CascadeTests.swift index fea9714..2e36f0f 100644 --- a/ios/ChronoMindTests/CascadeTests.swift +++ b/ios/ChronoMindTests/CascadeTests.swift @@ -52,7 +52,7 @@ final class CascadeTests: XCTestCase { func testWarningsInPastAreFired() { let now = Date() - let targetTime = now.addingTimeInterval(600) // 10 min from now + let targetTime = now.addingTimeInterval(1800) // 30 min from now let intervals = [120, 60, 15, 5] // 2h and 1h are in the past let warnings = calculateCascadeWarnings(targetTime: targetTime, intervals: intervals, now: now) @@ -60,8 +60,8 @@ final class CascadeTests: XCTestCase { // 120min and 60min warnings should be marked as fired (they're in the past) let fired = warnings.filter(\.fired) XCTAssertEqual(fired.count, 2) - XCTAssertTrue(warnings[0].fired) // 120m - XCTAssertTrue(warnings[1].fired) // 60m + XCTAssertTrue(warnings[0].fired) // 120m — in the past + XCTAssertTrue(warnings[1].fired) // 60m — in the past XCTAssertFalse(warnings[2].fired) // 15m — still in the future XCTAssertFalse(warnings[3].fired) // 5m — still in the future } @@ -96,7 +96,7 @@ final class CascadeTests: XCTestCase { func testGetNextWarning() { let now = Date() - let targetTime = now.addingTimeInterval(3600) + let targetTime = now.addingTimeInterval(7200) // 2h from now let warnings = calculateCascadeWarnings( targetTime: targetTime, intervals: [60, 30, 15, 5], @@ -105,6 +105,7 @@ final class CascadeTests: XCTestCase { let next = getNextWarning(warnings) XCTAssertNotNil(next) + // All warnings are in the future (earliest at targetTime - 60m = 1h from now) XCTAssertEqual(next?.minutesBefore, 60) } @@ -127,19 +128,24 @@ final class CascadeTests: XCTestCase { func testCheckWarnings() { let now = Date() - let targetTime = now.addingTimeInterval(3600) + let targetTime = now.addingTimeInterval(7200) // 2h from now var warnings = calculateCascadeWarnings( targetTime: targetTime, intervals: [60, 30, 15, 5], now: now ) - // Check at a time when 60m warning should fire (targetTime - 60min = now) - let checkTime = targetTime.addingTimeInterval(-3600) // exactly at 60m warning - let fired = checkWarnings(&warnings, now: checkTime.addingTimeInterval(1)) // 1 second after + // All warnings should be unfired initially + XCTAssertTrue(warnings.allSatisfy { !$0.fired }) + + // Check at a time when 60m warning should fire (targetTime - 60min) + let checkTime = targetTime.addingTimeInterval(-3599) // 1 second after 60m mark + let fired = checkWarnings(&warnings, now: checkTime) // The 60m warning should fire - XCTAssertTrue(fired.count >= 1) + XCTAssertEqual(fired.count, 1) + XCTAssertTrue(warnings[0].fired) // 60m warning fired + XCTAssertFalse(warnings[1].fired) // 30m warning not yet } func testCheckWarningsNoneFire() { diff --git a/web/src/components/PomodoroView.tsx b/web/src/components/PomodoroView.tsx index 7916bec..4c4ed4a 100644 --- a/web/src/components/PomodoroView.tsx +++ b/web/src/components/PomodoroView.tsx @@ -5,7 +5,8 @@ import { getRemainingMs } from '@/lib/timer-engine'; import type { Timer } from '@/lib/timer-engine'; import { CountdownRing } from './CountdownRing'; import { formatDuration } from '@/lib/format'; -import { Coffee, Pause, Play, X, SkipForward } from 'lucide-react'; +import { Coffee, Pause, Play, X, SkipForward, Trophy } from 'lucide-react'; +import { showToast } from './Toast'; interface PomodoroViewProps { timer: Timer; @@ -26,12 +27,62 @@ export function PomodoroView({ timer }: PomodoroViewProps) { const isBreak = pomState.isBreak || pomState.isLongBreak; const isPaused = timer.state === 'paused'; const isFiring = timer.state === 'firing'; + const isCompleted = timer.state === 'completed'; const roundLabel = isBreak ? pomState.isLongBreak ? 'Long Break' : 'Break' : `Round ${pomState.currentRound} of ${config.rounds}`; const ringColor = isBreak ? 'var(--cm-accent-secondary)' : 'var(--cm-accent)'; + // Session complete celebration + if (isCompleted) { + const totalMinutes = config.workMinutes * config.rounds + config.breakMinutes * (config.rounds - 1) + config.longBreakMinutes; + return ( +
+
+
+ +
+
+

+ Session Complete! +

+

+ {timer.label} +

+

+ {config.rounds} rounds · ~{totalMinutes} minutes of focused work +

+
+ {Array.from({ length: config.rounds }).map((_, i) => ( +
+ ))} +
+ +
+ ); + } + return (