108 lines
5.3 KiB
TypeScript
108 lines
5.3 KiB
TypeScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { TimeController } from '../src/ui/timeController.ts';
|
|
|
|
class Field extends EventTarget {
|
|
value = '';
|
|
max = '';
|
|
step = '';
|
|
disabled = false;
|
|
validity = { valid: true };
|
|
attributes = new Map<string, string>();
|
|
textContent = '';
|
|
setAttribute(name: string, value: string) { this.attributes.set(name, value); }
|
|
matches() { return this.disabled; }
|
|
closest() { return this; }
|
|
}
|
|
|
|
test('live ticks preserve date edits; custom freezes; live and visibility resync wall clock', (t) => {
|
|
const fields = new Map(['#date', '#date-display', '#time-slider', '#time-output', '#live-button'].map(id => [id, new Field()]));
|
|
let dialogOpen = false;
|
|
const doc = Object.assign(new EventTarget(), {
|
|
activeElement: null as Field | null,
|
|
querySelector: (id: string) => id === 'dialog[open]' ? (dialogOpen ? {} : null) : fields.get(id),
|
|
});
|
|
const originalDocument = Object.getOwnPropertyDescriptor(globalThis, 'document');
|
|
const originalWindow = Object.getOwnPropertyDescriptor(globalThis, 'window');
|
|
Object.defineProperty(globalThis, 'document', { configurable: true, value: doc });
|
|
Object.defineProperty(globalThis, 'window', { configurable: true, value: new EventTarget() });
|
|
t.mock.timers.enable({ apis: ['Date', 'setInterval'], now: Date.parse('2026-10-29T00:00:00Z') });
|
|
const calls: { date: Date; mode: string }[] = [];
|
|
const controller = new TimeController((date, mode) => calls.push({ date, mode }));
|
|
assert.equal(fields.get('#live-button')!.attributes.get('aria-pressed'), 'true');
|
|
assert.equal(fields.get('#date-display')!.textContent, '2026년 10월 29일');
|
|
const arrow = (key: string) => {
|
|
const event = Object.assign(new Event('keydown', { cancelable: true }), { key });
|
|
window.dispatchEvent(event);
|
|
return event;
|
|
};
|
|
assert.equal(arrow('ArrowRight').defaultPrevented, true);
|
|
assert.equal(fields.get('#time-output')!.value, '09:00:10');
|
|
assert.equal(controller.mode, 'CUSTOM');
|
|
assert.equal(fields.get('#live-button')!.attributes.get('aria-pressed'), 'false');
|
|
arrow('ArrowLeft');
|
|
assert.equal(fields.get('#time-output')!.value, '09:00:00');
|
|
dialogOpen = true;
|
|
assert.equal(arrow('ArrowRight').defaultPrevented, false);
|
|
assert.equal(fields.get('#time-output')!.value, '09:00:00');
|
|
dialogOpen = false;
|
|
doc.activeElement = fields.get('#time-slider')!;
|
|
assert.equal(arrow('ArrowRight').defaultPrevented, false, 'focused slider keeps its native keyboard handling');
|
|
doc.activeElement = null;
|
|
fields.get('#live-button')!.dispatchEvent(new Event('click'));
|
|
t.after(() => {
|
|
controller.dispose();
|
|
for (const [key, descriptor] of [['document', originalDocument], ['window', originalWindow]] as const) {
|
|
if (descriptor) Object.defineProperty(globalThis, key, descriptor);
|
|
else Reflect.deleteProperty(globalThis, key);
|
|
}
|
|
});
|
|
const date = fields.get('#date')!;
|
|
doc.activeElement = date;
|
|
assert.equal(arrow('ArrowRight').defaultPrevented, false, 'date editing keeps its native keyboard handling');
|
|
date.value = '';
|
|
t.mock.timers.tick(1000);
|
|
assert.equal(date.value, '', 'live refresh must not overwrite an unfinished date edit');
|
|
date.value = '2026-12-21';
|
|
fields.get('#time-slider')!.value = '28810';
|
|
date.dispatchEvent(new Event('change'));
|
|
assert.equal(calls.at(-1)!.mode, 'CUSTOM');
|
|
assert.equal(calls.at(-1)!.date.toISOString(), '2026-12-20T23:00:10.000Z');
|
|
assert.equal(fields.get('#time-output')!.value, '08:00:10');
|
|
assert.equal(fields.get('#date-display')!.textContent, '2026년 12월 21일');
|
|
assert.equal(fields.get('#live-button')!.attributes.get('aria-pressed'), 'false');
|
|
assert.equal(fields.get('#time-slider')!.step, '10');
|
|
fields.get('#time-slider')!.value = '86390';
|
|
fields.get('#time-slider')!.dispatchEvent(new Event('input'));
|
|
assert.equal(calls.at(-1)!.date.toISOString(), '2026-12-21T14:59:50.000Z');
|
|
assert.equal(fields.get('#time-output')!.value, '23:59:50');
|
|
assert.equal(fields.get('#live-button')!.attributes.get('aria-pressed'), 'false');
|
|
const count = calls.length;
|
|
t.mock.timers.tick(3000);
|
|
doc.dispatchEvent(new Event('visibilitychange'));
|
|
assert.equal(calls.length, count);
|
|
doc.activeElement = null;
|
|
fields.get('#live-button')!.dispatchEvent(new Event('click'));
|
|
assert.equal(calls.at(-1)!.mode, 'LIVE');
|
|
assert.equal(fields.get('#live-button')!.attributes.get('aria-pressed'), 'true');
|
|
assert.equal(calls.at(-1)!.date.getTime(), Date.now());
|
|
t.mock.timers.setTime(Date.parse('2026-10-30T00:00:00Z'));
|
|
doc.dispatchEvent(new Event('visibilitychange'));
|
|
assert.equal(date.value, '2026-10-30');
|
|
const serverDate = new Date('2026-11-01T21:00:00Z');
|
|
controller.setSharedClock(() => serverDate);
|
|
assert.equal(fields.get('#date')!.disabled, true);
|
|
assert.equal(calls.at(-1)!.date.getTime(), +serverDate);
|
|
doc.activeElement = null;
|
|
assert.equal(arrow('ArrowRight').defaultPrevented, false);
|
|
assert.equal(calls.at(-1)!.date.getTime(), +serverDate, 'arrows cannot change shared time');
|
|
date.value = '2027-01-01';
|
|
date.dispatchEvent(new Event('change'));
|
|
assert.equal(controller.mode, 'LIVE', 'programmatic events cannot change shared time');
|
|
controller.setSharedClock(null);
|
|
assert.equal(fields.get('#date')!.disabled, false);
|
|
date.value = '2027-01-01';
|
|
date.dispatchEvent(new Event('change'));
|
|
assert.equal(controller.mode, 'CUSTOM');
|
|
});
|