// ============ flow.mjs — simulate New Game click flow headlessly ============ let fails = 0; const ok = (cond, msg) => { console.log((cond ? '✔ ' : '✘ ') + msg); if (!cond) fails++; }; const makeCtx = () => new Proxy(function () {}, { get(t, p) { if (!(p in t)) t[p] = (...a) => makeCtx(); return typeof t[p] === 'function' ? t[p] : t[p]; }, set() { return true; }, apply() { return makeCtx(); }, }); const listenersOf = el => (el._listeners ??= {}); const fakeEl = (id = '') => { const el = { id, style: {}, dataset: {}, children: [], files: [], classList: { _set: new Set(['hidden']), add(c) { this._set.add(c); }, remove(c) { this._set.delete(c); }, toggle(c) { this._set.has(c) ? this._set.delete(c) : this._set.add(c); }, contains(c) { return this._set.has(c); }, }, getBoundingClientRect: () => ({ left: 0, top: 0, width: 100, height: 100 }), getContext: () => makeCtx(), appendChild(c) { el.children.push(c); c.parentNode = el; }, remove() { const p = el.parentNode; if (p) p.children = p.children.filter(x => x !== el); }, querySelectorAll: () => [], querySelector: () => null, addEventListener(ev, fn) { (listenersOf(el)[ev] ??= []).push(fn); }, dispatch(ev, arg = {}) { for (const f of listenersOf(el)[ev] || []) f(arg); }, setAttribute() {}, focus() {}, textContent: '', }; Object.defineProperty(el, 'innerHTML', { get() { return ''; }, set(v) { if (!v) el.children = []; }, }); let w = 300, h = 150; Object.defineProperty(el, 'width', { get: () => w, set: v => { w = v; } }); Object.defineProperty(el, 'height', { get: () => h, set: v => { h = v; } }); return el; }; globalThis.window = globalThis; globalThis.innerWidth = 1280; globalThis.innerHeight = 800; globalThis.requestAnimationFrame = () => 0; globalThis.cancelAnimationFrame = () => {}; globalThis.addEventListener = () => {}; globalThis.removeEventListener = () => {}; globalThis.localStorage = { _m: {}, getItem(k) { return this._m[k] ?? null; }, setItem(k, v) { this._m[k] = String(v); }, removeItem(k) { delete this._m[k]; } }; globalThis.document = { els: {}, getElementById(id) { return this.els[id] ?? (this.els[id] = fakeEl(id)); }, createElement: tag => fakeEl(tag), createTextNode: t => ({ nodeType: 3, textContent: String(t) }), querySelectorAll: () => [], querySelector: () => null, addEventListener() {}, body: { appendChild(c) { document.bodyChildren ??= []; document.bodyChildren.push(c); } }, }; const main = await import('../js/main.js'); ok(true, 'main.js evaluated (full graph)'); const dialogs = await import('../js/ui/dialogs.js'); const stateM = await import('../js/game/state.js'); // 1. click New Game const mmNew = document.getElementById('mm-new'); mmNew.dispatch('click'); ok(document.getElementById('modal-root').classList.contains('hidden') === false, 'scenario picker modal visible after click'); ok(document.getElementById('modal-root').children.length > 0, 'picker content rendered'); // find scenario cards among descendants const findCards = n => { let out = []; for (const c of n.children || []) { if ((c.className || '').includes && String(c.className).includes('scen-card')) out.push(c); out = out.concat(findCards(c)); } return out; }; const cards = findCards(document.getElementById('modal-root')); ok(cards.length === 4, `4 scenario cards rendered (${cards.length})`); // 2. pick first scenario let picked = null; cards[0].dispatch('click'); // handler calls onPick(scenarioId) → main.startNewGame ok(stateM.getState()?.map != null, `game state created (${stateM.getState()?.scenario})`); ok(document.getElementById('main-menu').classList.contains('hidden'), 'main menu hidden after start'); ok(document.getElementById('topbar').classList.contains('hidden') === false, 'HUD shown after start'); console.log(fails ? `\n${fails} FLOW FAILURES` : '\nNEW GAME FLOW OK'); process.exit(fails ? 1 : 0);