import { test } from 'node:test'; import assert from 'node:assert/strict'; import { Countdown, integer, splitSeconds } from './timer.mjs'; function fixture() { let now = 1000; return { timer: new Countdown(() => now), advance(ms) { now += ms; }, }; } test('starts idle at zero and accepts nonnegative integers only', () => { const { timer } = fixture(); assert.equal(timer.state, 'idle'); assert.deepEqual(timer.display(), { minutes: '0', seconds: '00' }); for (const invalid of ['-1', '1.5', '1e2', 'abc', '']) { assert.equal(integer(invalid), 0n); } assert.equal(integer('00090'), 90n); }); test('normalizes overflowing seconds and preserves arbitrarily large integers', () => { const { timer } = fixture(); timer.edit('0', '90'); timer.toggle(); assert.deepEqual(timer.display(), { minutes: '1', seconds: '30' }); const minutes = '123456789012345678901234567890'; timer.reset(); timer.edit(minutes, '120'); assert.deepEqual(timer.display(), { minutes: String(BigInt(minutes) + 2n), seconds: '00' }); assert.deepEqual(splitSeconds(61n), { minutes: '1', seconds: '01' }); }); test('uses elapsed time, rounding up, even when refreshes are delayed', () => { const { timer, advance } = fixture(); timer.edit('0', '90'); timer.toggle(); advance(999); timer.update(); assert.deepEqual(timer.display(), { minutes: '1', seconds: '30' }); advance(1); timer.update(); assert.deepEqual(timer.display(), { minutes: '1', seconds: '29' }); advance(75000); timer.update(); assert.deepEqual(timer.display(), { minutes: '0', seconds: '14' }); }); test('pause/resume preserves fractional seconds and excludes paused time', () => { const { timer, advance } = fixture(); timer.edit('0', '10'); timer.toggle(); advance(1250); timer.toggle(); assert.equal(timer.state, 'paused'); assert.equal(timer.remaining, 8750n); advance(100000); timer.update(); assert.equal(timer.remaining, 8750n); timer.toggle(); advance(749); timer.update(); assert.equal(timer.display().seconds, '09'); advance(1); timer.update(); assert.equal(timer.display().seconds, '08'); }); test('reset restores normalized last input while running or paused', () => { const { timer, advance } = fixture(); timer.edit('2', '90'); for (const pause of [false, true]) { timer.toggle(); advance(1500); if (pause) timer.toggle(); timer.reset(); assert.equal(timer.state, 'idle'); assert.deepEqual(timer.display(), { minutes: '3', seconds: '30' }); assert.equal(timer.finished, false); } }); test('editing a paused timer establishes a new countdown and reset value', () => { const { timer, advance } = fixture(); timer.edit('0', '10'); timer.toggle(); advance(2000); timer.toggle(); timer.edit('0', '90'); assert.equal(timer.state, 'idle'); timer.toggle(); advance(3000); timer.reset(); assert.deepEqual(timer.display(), { minutes: '1', seconds: '30' }); }); test('running countdown cannot be edited', () => { const { timer } = fixture(); timer.edit('0', '10'); timer.toggle(); timer.edit('5', '0'); assert.equal(timer.duration, 10000n); assert.equal(timer.state, 'running'); }); test('completion returns to idle and Start restarts the original duration', () => { const { timer, advance } = fixture(); timer.edit('0', '2'); timer.toggle(); advance(5000); timer.update(); assert.equal(timer.state, 'idle'); assert.equal(timer.finished, true); assert.equal(timer.display().seconds, '00'); timer.toggle(); assert.equal(timer.finished, false); assert.equal(timer.state, 'running'); assert.equal(timer.display().seconds, '02'); }); test('editing or resetting after completion clears the completion flag', () => { const { timer } = fixture(); timer.toggle(); assert.equal(timer.finished, true); timer.edit('0', '3'); assert.equal(timer.finished, false); timer.toggle(); timer.reset(); assert.equal(timer.finished, false); assert.equal(timer.display().seconds, '03'); }); test('stopping after the deadline completes instead of leaving a paused zero', () => { const { timer, advance } = fixture(); timer.edit('0', '1'); timer.toggle(); advance(1000); timer.toggle(); assert.equal(timer.state, 'idle'); assert.equal(timer.finished, true); });