Files
TheLadder/test/build.test.js
T
JesseMarkowitzandClaude Opus 5 76072edf4e Show the build, play from the keyboard, and clean up the ledger
Five things from the second playtest.

A status line at the top of every screen: the game, the pack, your current
title, and the version plus the commit it came from. `tools/stamp.js` writes the
commit into src/build-info.js before serving and before building, so a
distributed dist/theladder.html states exactly which commit produced it. An
uncommitted working tree gets a `+` suffix — "4cab9fc+" is honest in a way a
bare SHA would not be.

The whole game is now playable on Enter. Every screen marks one control and the
app focuses it after each render: the first *available* option while choosing,
the next-turn button once the result is in, "keep going" on the retrospective.
Tab and Shift+Tab reach the other options. Locked options needed no special
handling — the browser's tab order already skips disabled controls, so they stay
visible without being in the way.

"What is notable? under What changed?" — two fixes. `notable` is the
retrospective's own list of moments; a display rule can now say
`inLedger: false` and it is left out, because "Notable — changed" tells nobody
anything. Flags are the opposite and stay, but as sentences the pack supplies
rather than as booleans: "The envelope is in your locker." instead of "the
envelope: now true".

Exported logs live in logs/, gitignored, and `npm run analyze` reads the newest
one there when given no argument. A log records what a real person did turn by
turn, including whatever they typed into the feedback box — committing one once
would put it in the history for good.

Version set to 0.3.0 on the reasoning that the engine was 0.1 and the interface
0.2; say if you want a different scheme and I will renumber.

119 tests. Reasoning in docs/DECISIONS.md §25-28.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VMSFHyVPitUoosW5wyEADj
2026-09-10 05:41:32 -04:00

52 lines
2.4 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)();
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();
}
});