Cold Fork is a freight yard: 29 events across two stages, a four-day cycle, and its own relationship and skill vocabulary. src/engine/ was not touched. Adding it needed three changes outside the engine: one save slot per pack, app.js holding the current pack, and turn.js no longer naming the mailroom (DECISIONS #34). build.js now handles imports wrapped across lines. A conformance suite runs over every registered pack. DECISIONS #35 records three calls on open questions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RxdJFbLq1rBKsUeticGV1g
64 lines
3.1 KiB
JavaScript
64 lines
3.1 KiB
JavaScript
// The single-file build.
|
|
//
|
|
// A build that produces a file proves nothing; this one runs the bundled
|
|
// script against the fake DOM and plays a turn through it. That is the closest
|
|
// thing to opening dist/theladder.html that is available without a browser —
|
|
// it catches a broken bundle, which is otherwise only discovered by whoever
|
|
// was handed the file.
|
|
|
|
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { execFileSync } from 'node:child_process';
|
|
import { readFileSync } from 'node:fs';
|
|
import { resolve, dirname } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { installFakeDom, byClass, firstByClass } from './fixtures/fake-dom.js';
|
|
|
|
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
|
|
test('the bundle builds, and the bundled game boots and plays', () => {
|
|
execFileSync('node', ['tools/build.js'], { cwd: root, stdio: 'pipe' });
|
|
const html = readFileSync(resolve(root, 'dist/theladder.html'), 'utf8');
|
|
|
|
assert.doesNotMatch(html, /<link\s+rel="stylesheet"/, 'the stylesheet is inlined');
|
|
assert.doesNotMatch(html, /type="module"/, 'no module scripts survive; file:// would block them');
|
|
assert.doesNotMatch(html, /^[ \t]*import\s/m, 'no bare import statements remain');
|
|
assert.match(html, /--paper:/, 'the CSS made it in');
|
|
// A distributed file has to be able to say which commit made it.
|
|
assert.match(html, /commit: '[^']+'/, 'the build stamp is present');
|
|
assert.doesNotMatch(html, /commit: 'dev'/, 'a built file is never stamped "dev"');
|
|
|
|
const script = html.match(/<script>([\s\S]*?)<\/script>/)?.[1];
|
|
assert.ok(script, 'the bundled script is present');
|
|
|
|
const dom = installFakeDom();
|
|
try {
|
|
// eslint-disable-next-line no-new-func -- running the build output is the point
|
|
new Function(script)();
|
|
|
|
// Nothing is saved and nothing is remembered, so a build shipping more
|
|
// than one setting opens on the picker. Click into the first one and carry
|
|
// on — this exercises the front door in the real bundle, which is the one
|
|
// place the pack registry, the picker screen and the save slots all meet.
|
|
assert.ok(firstByClass(dom.root, 'screen--packs'), 'the bundled game rendered the picker');
|
|
assert.match(dom.root.textContent, /Corporate Ladder/, 'the packs are named');
|
|
assert.match(dom.root.textContent, /The Frontier/);
|
|
|
|
const start = byClass(dom.root, 'button').find((b) => /Start here|Keep playing/.test(b.textContent));
|
|
assert.ok(start, 'the picker offers a way in');
|
|
start.click();
|
|
|
|
assert.ok(firstByClass(dom.root, 'screen--turn'), 'the bundled game rendered a turn');
|
|
assert.match(dom.root.textContent, /Day 1/);
|
|
assert.match(dom.root.textContent, /Marlene Hobb/);
|
|
assert.match(dom.root.textContent, /The Ladder/, 'the status bar is in the bundle');
|
|
|
|
const playable = byClass(dom.root, 'option').filter((b) => !b.disabled);
|
|
assert.ok(playable.length > 0, 'the bundled game offers choices');
|
|
playable[0].click();
|
|
assert.ok(firstByClass(dom.root, 'outcome'), 'a turn can be played in the bundle');
|
|
} finally {
|
|
dom.restore();
|
|
}
|
|
});
|