Files
sunrise/tests/time-controller.test.ts
horoli eb49ea4b84 Improve summit terrain, mobile HUD and multiplayer night equipment
Add the Blender flashlight asset with a 600 lm beam and reduced bloom, shared-mode self badge, and compact phone controls. Relocate the summit marker, blend terrain seams, widen walking routes, and align local server configuration on port 2027.
2026-10-31 23:14:44 +09:00

114 lines
5.8 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); }
getAttribute(name: string) { return this.attributes.get(name) ?? null; }
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', '#time-toggle', '#compact-time'].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일');
assert.equal(fields.get('#compact-time')!.textContent, '09:00');
fields.get('#time-toggle')!.dispatchEvent(new Event('click'));
assert.equal(fields.get('#time-toggle')!.getAttribute('aria-expanded'), 'true');
fields.get('#time-toggle')!.dispatchEvent(new Event('click'));
assert.equal(fields.get('#time-toggle')!.getAttribute('aria-expanded'), 'false');
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');
});