174 lines
5.7 KiB
JavaScript
174 lines
5.7 KiB
JavaScript
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);
|
|
});
|