import { test } from 'node:test'; import assert from 'node:assert/strict'; import { Countdown, integer, splitSeconds, adjustTime } from './timer.mjs'; for (const [minutes, seconds, delta, expectedMinutes, expectedSeconds] of [ ['1', '59', 1n, '2', '00'], ['2', '00', -1n, '1', '59'], ['2', '05', -10n, '1', '55'], ['1', '58', 10n, '2', '08'], ['2', '00', -10n, '1', '50'], ['0', '90', 1n, '1', '31'], ['0', '90', -10n, '1', '20'], ['0', '00', -1n, '0', '00'], ['0', '05', -10n, '0', '00'], ]) { test(`seconds adjustment ${minutes}:${seconds} ${delta} normalizes and rolls`, () => { assert.deepEqual(adjustTime(minutes, seconds, 'seconds', delta), { minutes: expectedMinutes, seconds: expectedSeconds, }); }); } test('minute adjustments normalize first and keep minutes nonnegative', () => { assert.deepEqual(adjustTime('0', '90', 'minutes', 1n), { minutes: '2', seconds: '30' }); assert.deepEqual(adjustTime('0', '90', 'minutes', -1n), { minutes: '0', seconds: '30' }); assert.deepEqual(adjustTime('1', '90', 'minutes', 10n), { minutes: '12', seconds: '30' }); assert.deepEqual(adjustTime('1', '90', 'minutes', -10n), { minutes: '0', seconds: '30' }); }); test('keyboard adjustments preserve large integer precision', () => { assert.deepEqual(adjustTime('12345678901234567890', '59', 'seconds', 1n), { minutes: '12345678901234567891', seconds: '00', }); }); 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); });