59 lines
5.3 KiB
JavaScript
59 lines
5.3 KiB
JavaScript
// node --test tools/scene/legend.test.mjs (key-22, the project lead's screenshot of the app's chain key against /live, 7 October 2026)
|
||
// The key is ONE export: IgneumDag.legend({ mine }) in scene/live-dag.js (entries, order, token names, shapes); the app's
|
||
// chain card and the site's /live key render from it, so they cannot drift. This test fails when either key's entries,
|
||
// order or tokens differ from the export, and when a colour the scene draws on blocks has no key entry.
|
||
import { test } from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import { readFileSync } from 'node:fs';
|
||
import { fileURLToPath } from 'node:url';
|
||
import { dirname, join } from 'node:path';
|
||
import { execSync } from 'node:child_process';
|
||
const here = dirname(fileURLToPath(import.meta.url)), root = join(here, '../..');
|
||
const src = readFileSync(join(root, 'scene/live-dag.js'), 'utf8');
|
||
const win = { document: { hidden: false, addEventListener() {}, createElement: () => ({ style: {} }) }, addEventListener() {}, matchMedia: () => ({ matches: false, addEventListener() {} }), getComputedStyle: () => ({ getPropertyValue: () => '' }), devicePixelRatio: 1, innerWidth: 1280 };
|
||
new Function('window', 'document', src)(win, win.document);
|
||
const Dag = win.IgneumDag;
|
||
const SITE_ORDER = ['Pending', 'Included', 'Excluded', 'Selected chain', 'Proven', 'Locked checkpoint'];
|
||
// the entries a page's markup carries today, from its literal spans (a hard-coded key) or none (a rendered key)
|
||
const literal = (html, tag) => [...html.matchAll(new RegExp('<span><i class="' + tag + '[^>]*></i>([^<]+)</span>', 'g'))].map((m) => m[1].trim());
|
||
|
||
test('the export: Pending first, Included, Excluded, Selected chain, Proven, Locked checkpoint; the app adds Your blocks first; every entry names its token and shape (known-failed on 0.3.22)', () => {
|
||
assert.ok(Dag && typeof Dag.legend === 'function', 'IgneumDag.legend exists');
|
||
const site = Dag.legend({ mine: false }), app = Dag.legend({ mine: true });
|
||
assert.deepEqual(site.map((e) => e.label), SITE_ORDER);
|
||
assert.deepEqual(app.map((e) => e.label), ['Your blocks'].concat(SITE_ORDER));
|
||
for (const e of app) { assert.match(e.token, /^--[a-z-]+$/, e.label + ' token'); assert.ok(['square', 'circle', 'tick', 'ringed'].includes(e.shape), e.label + ' shape'); assert.ok(e.id); }
|
||
assert.equal(app[0].token, '--molten'); assert.equal(app[0].shape, 'ringed');
|
||
assert.equal(site.find((e) => e.label === 'Included').token, '--included', 'the key draws Included in the colour the scene draws it');
|
||
assert.equal(site.find((e) => e.label === 'Excluded').token, '--excluded');
|
||
assert.equal(site.find((e) => e.label === 'Locked checkpoint').shape, 'circle');
|
||
assert.equal(site.find((e) => e.label === 'Proven').shape, 'tick');
|
||
assert.equal(typeof Dag.renderLegend, 'function', 'one renderer for both pages');
|
||
});
|
||
|
||
test('every colour the scene draws on a block or a band has a key entry', () => {
|
||
const tokens = new Set(Dag.legend({ mine: true }).map((e) => e.token));
|
||
// the renderer's palette read: col={ember:c('--ember',…), …, blue:c('--included',…), red:c('--excluded',…)}
|
||
const read = [...src.matchAll(/c\('(--[a-z0-9-]+)'/g)].map((m) => m[1]);
|
||
const drawn = ['--included', '--excluded', '--ember', '--molten', '--bone', '--ash'];
|
||
for (const t of drawn) { assert.ok(read.includes(t), 'the scene reads ' + t); assert.ok(tokens.has(t), 'the key has an entry in ' + t); }
|
||
});
|
||
|
||
// the site tree on a release-* branch is frozen and deploys from master alone (tools/scene/sync.mjs's scope rule), so the
|
||
// site half of the check binds on master (or with SCENE_SYNC_SCOPE=site) and is a diagnostic elsewhere
|
||
const branch = (() => { try { return execSync('git rev-parse --abbrev-ref HEAD', { cwd: root, stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim(); } catch (e) { return ''; } })();
|
||
const siteBinds = process.env.SCENE_SYNC_SCOPE === 'site' || branch === 'master';
|
||
|
||
test('the app key and the site key are the export, not a hand-written list: no literal entries left, and any literal list that remains equals the export in labels and order', (t) => {
|
||
const app = readFileSync(join(root, 'app/igneum-app/ui/index.html'), 'utf8'), site = readFileSync(join(root, 'site/live.html'), 'utf8');
|
||
const appLit = literal(app, 'lg'), siteLit = literal(site, 'lgi');
|
||
assert.ok(/data-legend="mine"/.test(app), 'the app legend renders from IgneumDag.legend({ mine: true })');
|
||
assert.deepEqual(appLit, [], 'hand-written entries in the app key: ' + appLit.join(', '));
|
||
const siteOk = siteLit.length ? JSON.stringify(siteLit) === JSON.stringify(SITE_ORDER) : /data-legend/.test(site);
|
||
if (siteBinds) assert.ok(siteOk, 'the site key differs from the export: ' + (siteLit.length ? siteLit.join(', ') : 'no data-legend hook'));
|
||
else if (!siteOk) t.diagnostic('site/live.html still carries a hand-written key (' + siteLit.join(', ') + '); it binds on master once the site lane renders it from IgneumDag.renderLegend');
|
||
// the app's source line stays
|
||
assert.match(readFileSync(join(root, 'app/igneum-app/ui/app.js'), 'utf8'), /From Igneum’s observer\. Your blocks are ringed\./);
|
||
// the copies are the source (the sync gate says the same; this fails first when a legend edit misses the sync)
|
||
assert.equal(readFileSync(join(root, 'app/igneum-app/ui/live-dag.js'), 'utf8'), src, 'the app copy of live-dag.js is the scene source');
|
||
});
|