/** * Server-stamped phase timer math: every screen (admin, jury, big screen) * derives the same countdown from cursor timestamps — no client-local clocks. */ import { describe, it, expect } from 'vitest' import { elapsedMs, remainingSeconds, formatClock } from '@/lib/live-timer' const t0 = new Date('2026-06-11T10:00:00Z') const at = (s: number) => new Date(t0.getTime() + s * 1000) describe('live-timer', () => { it('elapsedMs counts from phaseStartedAt', () => { expect( elapsedMs( { phaseStartedAt: t0, phaseDurationSeconds: 300, phasePausedAt: null, phasePausedAccumMs: 0 }, at(90) ) ).toBe(90_000) }) it('elapsedMs freezes while paused and subtracts accumulated pause', () => { // paused at +60s, asked at +90s → frozen at 60s expect( elapsedMs( { phaseStartedAt: t0, phaseDurationSeconds: 300, phasePausedAt: at(60), phasePausedAccumMs: 0 }, at(90) ) ).toBe(60_000) // resumed with 30s pause accumulated, asked at +120s → 90s elapsed expect( elapsedMs( { phaseStartedAt: t0, phaseDurationSeconds: 300, phasePausedAt: null, phasePausedAccumMs: 30_000 }, at(120) ) ).toBe(90_000) }) it('elapsedMs accepts ISO strings (serialized cursor)', () => { expect( elapsedMs( { phaseStartedAt: t0.toISOString(), phaseDurationSeconds: 300, phasePausedAt: null, phasePausedAccumMs: 0, }, at(45) ) ).toBe(45_000) }) it('remainingSeconds goes negative on overtime', () => { expect( remainingSeconds( { phaseStartedAt: t0, phaseDurationSeconds: 60, phasePausedAt: null, phasePausedAccumMs: 0 }, at(75) ) ).toBe(-15) }) it('remainingSeconds is null without a running timer', () => { expect( remainingSeconds( { phaseStartedAt: null, phaseDurationSeconds: null, phasePausedAt: null, phasePausedAccumMs: 0 }, t0 ) ).toBeNull() }) it('formatClock renders m:ss and overtime as +m:ss', () => { expect(formatClock(305)).toBe('5:05') expect(formatClock(0)).toBe('0:00') expect(formatClock(-83)).toBe('+1:23') }) })