feat: <gm-shell> wiring, styles, and AFTERIMAGE boot

Adds the gm-shell root element that owns store/clock/stamps/scenario,
mounts director-rail and dice-tray, handles reached (stamp + persist)
and open-tool (toggle dice tray), and boots the AFTERIMAGE scenario in
main.js. Also adds tests/setup.js + a vite.config.js setupFiles entry:
Node's newer built-in global `localStorage` shadows jsdom's per-window
implementation in vitest's jsdom environment, leaving localStorage
undefined in tests until replaced with a real jsdom-backed instance.
This commit is contained in:
2026-07-20 14:15:50 +01:00
parent 549cb190d2
commit 1348048d50
7 changed files with 164 additions and 1 deletions
+38
View File
@@ -0,0 +1,38 @@
import { describe, it, expect, beforeEach } from 'vitest';
import '../../src/components/gm-shell.js';
import afterimage from '../../src/scenarios/afterimage.js';
describe('<gm-shell>', () => {
let el, t;
beforeEach(() => {
localStorage.clear();
document.body.innerHTML = '';
t = 0;
el = document.createElement('gm-shell');
el.now = () => t;
document.body.appendChild(el);
el.loadScenario(afterimage);
});
it('mounts a director-rail for the loaded scenario', () => {
expect(el.querySelector('director-rail')).not.toBe(null);
expect(el.querySelector('director-rail [data-role=here]').textContent).toContain('The pier');
});
it('stamps the next beat on "reached" and persists it', () => {
el.startSession(); // start at t=0
t = 6 * 60000; // 6 minutes later
el.querySelector('[data-role=reached]').dispatchEvent(new CustomEvent('reached', { bubbles: true }));
expect(el.stamps['a1-open']).toBeCloseTo(6, 5);
const persisted = JSON.parse(localStorage.getItem('gmd.afterimage.stamps'));
expect(persisted['a1-open']).toBeCloseTo(6, 5);
// rail now shows the reached beat as current
expect(el.querySelector('director-rail [data-role=here]').textContent).toContain('The pier');
});
it('reveals the dice tray on open-tool', () => {
expect(el.querySelector('dice-tray').hidden).toBe(true);
el.dispatchEvent(new CustomEvent('open-tool', { detail: { tool: 'dice' }, bubbles: true }));
expect(el.querySelector('dice-tray').hidden).toBe(false);
});
});
+14
View File
@@ -0,0 +1,14 @@
// Workaround: Node's own experimental global `localStorage` (added in recent
// Node versions) shadows jsdom's per-window localStorage before vitest's
// jsdom environment can install it, leaving `localStorage`/`window.localStorage`
// undefined in tests. Replace it with a real jsdom-backed Storage instance.
import { JSDOM } from 'jsdom';
if (typeof localStorage === 'undefined' || typeof localStorage.setItem !== 'function') {
const { window } = new JSDOM('', { url: 'http://localhost/' });
Object.defineProperty(globalThis, 'localStorage', {
value: window.localStorage,
configurable: true,
writable: true,
});
}