@bongos/core 1.24.34 → 1.24.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +102 -147
- package/.claude/skills/merge-mode/SKILL.md +1 -1
- package/docs/adr/0228-the-reading-room-owes-a-reference-content-rules-not-a-second-archetype.md +1 -1
- package/docs/copy-inventory.md +136 -192
- package/docs/copy-registry.json +186 -752
- package/docs/module-api-changelog.md +4 -0
- package/docs/onboarding/diagrams/README.md +1 -2
- package/docs/onboarding/primer.md +66 -22
- package/docs/page-inventory.json +7 -101
- package/docs/page-readings.json +947 -1413
- package/docs/recipes/ui-look-before-you-ship.md +2 -2
- package/modules/design-styles/skills/style/SKILL.md +2 -2
- package/modules/hall-ui/public/ai-studio.html +2 -2
- package/modules/hall-ui/public/atlas.html +2 -2
- package/modules/hall-ui/public/blockers.html +2 -2
- package/modules/hall-ui/public/board-room.html +2 -2
- package/modules/hall-ui/public/builders.js +8 -6
- package/modules/hall-ui/public/city.html +2 -2
- package/modules/hall-ui/public/collab.html +2 -2
- package/modules/hall-ui/public/commands.html +2 -2
- package/modules/hall-ui/public/copy-desk.html +2 -2
- package/modules/hall-ui/public/craft-mode.js +1 -1
- package/modules/hall-ui/public/deploy.html +2 -2
- package/modules/hall-ui/public/diagram-viewer.css +1 -1
- package/modules/hall-ui/public/diagram-viewer.js +5 -5
- package/modules/hall-ui/public/fleet.html +2 -2
- package/modules/hall-ui/public/founding-mark.js +3 -2
- package/modules/hall-ui/public/gate.html +2 -2
- package/modules/hall-ui/public/goal-map.html +2 -2
- package/modules/hall-ui/public/goals.html +2 -2
- package/modules/hall-ui/public/government.html +2 -2
- package/modules/hall-ui/public/hall-render.js +62 -1
- package/modules/hall-ui/public/hall-widgets.js +11 -1
- package/modules/hall-ui/public/idea.html +2 -2
- package/modules/hall-ui/public/ideas.html +2 -2
- package/modules/hall-ui/public/index.html +4 -4
- package/modules/hall-ui/public/market-watch-item.html +2 -2
- package/modules/hall-ui/public/market-watch-rules.html +2 -2
- package/modules/hall-ui/public/market-watch.html +2 -2
- package/modules/hall-ui/public/modules.html +2 -2
- package/modules/hall-ui/public/my-agents.html +2 -2
- package/modules/hall-ui/public/palette.js +15 -0
- package/modules/hall-ui/public/primer-live.js +188 -0
- package/modules/hall-ui/public/primer.html +11 -6
- package/modules/hall-ui/public/primer.js +101 -22
- package/modules/hall-ui/public/primer.states.json +54 -7
- package/modules/hall-ui/public/profile.html +2 -2
- package/modules/hall-ui/public/project-settings.html +2 -2
- package/modules/hall-ui/public/roadmap.html +2 -2
- package/modules/hall-ui/public/room.css +23 -121
- package/modules/hall-ui/public/roster.html +2 -2
- package/modules/hall-ui/public/sessions.html +2 -2
- package/modules/hall-ui/public/settings.html +2 -2
- package/modules/hall-ui/public/shell.js +14 -21
- package/modules/hall-ui/public/street.html +2 -2
- package/modules/hall-ui/public/studio.html +2 -2
- package/modules/hall-ui/public/style.css +8 -55
- package/modules/hall-ui/public/task.html +2 -2
- package/modules/hall-ui/public/thinking.html +2 -2
- package/modules/hall-ui/public/tweak-editor.html +2 -2
- package/modules/hall-ui/public/watch.html +2 -2
- package/modules/hall-ui/public/work.html +2 -2
- package/modules/hall-ui/records/founding-mark.md +4 -4
- package/modules/hall-ui/records/reading-rooms.md +2 -0
- package/modules/hall-ui/records/reference-room.md +2 -0
- package/modules/onboarding/founding-mode.js +6 -2
- package/modules/ui-design/kit/render.js +1 -1
- package/modules/ui-design/styles/README.md +2 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/bongos/gen-hall-sitemap.js +15 -2
- package/scripts/hall-preview/server.js +6 -11
- package/src/bongos/serve-internal.js +28 -37
- package/src/module-api.js +1 -1
- package/tests/craft_mode_home.mjs +6 -2
- package/tests/founding_builders.mjs +6 -5
- package/tests/founding_mode.mjs +7 -4
- package/tests/hall_board_world.mjs +44 -1
- package/tests/hall_fullscreen.mjs +1 -1
- package/tests/hall_nav.mjs +24 -7
- package/tests/hall_page_gate_map.mjs +14 -5
- package/tests/hall_reading_room.mjs +426 -0
- package/tests/hall_reading_world.mjs +36 -30
- package/tests/hall_sitemap.mjs +25 -0
- package/tests/nav_permission_atoms.mjs +3 -2
- package/tests/rank_tier_single_source.mjs +0 -1
- package/tests/ui_design_kit.mjs +1 -1
- package/modules/hall-ui/public/diagrams.css +0 -45
- package/modules/hall-ui/public/diagrams.html +0 -106
- package/modules/hall-ui/public/diagrams.states.json +0 -28
- package/modules/hall-ui/public/drachmae.html +0 -110
- package/modules/hall-ui/public/drachmae.js +0 -431
- package/modules/hall-ui/public/drachmae.states.json +0 -58
- package/modules/hall-ui/public/ranks-founding.states.json +0 -12
- package/modules/hall-ui/public/ranks.html +0 -83
- package/modules/hall-ui/public/ranks.js +0 -229
- package/modules/hall-ui/public/ranks.states.json +0 -15
- package/tests/hall_reference_world.mjs +0 -352
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
// tests/hall_reading_room.mjs — ONE reading room (task 1005046, owner decision
|
|
2
|
+
// 2026-11-03, idea 1001311).
|
|
3
|
+
//
|
|
4
|
+
// Four Docs pages explained the same three topics — the task lifecycle, the rank
|
|
5
|
+
// ladder, and credits and karma — so their copies drifted. Ranks, Economy
|
|
6
|
+
// (/drachmae) and Diagrams became chapters of the Primer, each with its diagram
|
|
7
|
+
// beside it. This file holds what makes that true and keeps it true:
|
|
8
|
+
//
|
|
9
|
+
// * EVERY OLD URL LANDS ON ITS CHAPTER. /ranks, /drachmae and /diagrams 302 to
|
|
10
|
+
// the Primer chapter that replaced each — run against the real middleware,
|
|
11
|
+
// on both host shapes — and the diagram IMAGES under /builders/diagrams/*
|
|
12
|
+
// still serve (behind sign-in) rather than being swallowed by the redirect.
|
|
13
|
+
// * THE CHAPTERS EXIST, AND EACH CARRIES ITS DIAGRAM. Every id primer.js
|
|
14
|
+
// inlines a diagram at, every redirect target and every palette jump is a
|
|
15
|
+
// real h2/h3 slug of docs/onboarding/primer.md, computed by the SAME slugify
|
|
16
|
+
// the page runs — so retitling a chapter fails here, not silently in a
|
|
17
|
+
// browser (the credits diagram was lost that way once).
|
|
18
|
+
// * THE ECONOMY PAGE'S LIVE SECTIONS MOVED WHOLE. The treasury and the bounty
|
|
19
|
+
// rate card (primer-live.js, from drachmae.js, tasks 1003446 and 2174) are
|
|
20
|
+
// RUN in a vm against the checked-in fixtures: a Loading… line first, the
|
|
21
|
+
// real numbers after, and NOTHING — not an empty panel — when a feed fails.
|
|
22
|
+
// * THE SHARED READER the economy doc leaned on (h4, linked headings, nested
|
|
23
|
+
// emphasis) stays pinned: drachmae-and-karma.md is still the full reference.
|
|
24
|
+
//
|
|
25
|
+
// Moved here from tests/hall_reference_world.mjs, whose reference-room rules
|
|
26
|
+
// (room--reference, the h3 outline, the heading anchors) went with the page.
|
|
27
|
+
|
|
28
|
+
import { test } from 'node:test';
|
|
29
|
+
import assert from 'node:assert/strict';
|
|
30
|
+
import fs from 'node:fs';
|
|
31
|
+
import http from 'node:http';
|
|
32
|
+
import path from 'node:path';
|
|
33
|
+
import vm from 'node:vm';
|
|
34
|
+
import { createRequire } from 'node:module';
|
|
35
|
+
import { fileURLToPath } from 'node:url';
|
|
36
|
+
import { realOTB } from './helpers.mjs';
|
|
37
|
+
|
|
38
|
+
const require = createRequire(import.meta.url);
|
|
39
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
40
|
+
const HALL = path.join(ROOT, 'modules', 'hall-ui', 'public');
|
|
41
|
+
const read = (f) => fs.readFileSync(path.join(HALL, f), 'utf8');
|
|
42
|
+
|
|
43
|
+
const roomCss = read('room.css');
|
|
44
|
+
const primerHtml = read('primer.html');
|
|
45
|
+
const primerJs = read('primer.js');
|
|
46
|
+
const liveJs = read('primer-live.js');
|
|
47
|
+
const PRIMER_MD = fs.readFileSync(path.join(ROOT, 'docs', 'onboarding', 'primer.md'), 'utf8');
|
|
48
|
+
|
|
49
|
+
const decomment = (css) => css.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
50
|
+
const rule = (css, selector) => {
|
|
51
|
+
const esc = selector.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
52
|
+
const m = decomment(css).match(new RegExp(`(^|[},])\\s*${esc}\\s*\\{([^}]*)\\}`, 'm'));
|
|
53
|
+
return m ? m[2] : null;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
function loadReader() {
|
|
57
|
+
const sandbox = { window: { OTB: realOTB() }, location: { hostname: 'builders.example.com', href: 'https://builders.example.com/primer' } };
|
|
58
|
+
vm.createContext(sandbox);
|
|
59
|
+
vm.runInContext(read('md-reader.js'), sandbox, { filename: 'md-reader.js' });
|
|
60
|
+
return sandbox.window.OTBMdReader;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// The primer's headings exactly as the page renders them: parsed by the shared
|
|
64
|
+
// reader, ids from its slugify.
|
|
65
|
+
function primerHeadings() {
|
|
66
|
+
return loadReader().parseMarkdown(PRIMER_MD, (h) => h).headings;
|
|
67
|
+
}
|
|
68
|
+
const h2Ids = () => primerHeadings().filter((h) => h.level === 2).map((h) => h.id);
|
|
69
|
+
|
|
70
|
+
// ---- 1. every old URL lands on its chapter ---------------------------------
|
|
71
|
+
|
|
72
|
+
// The real middleware, mounted on a bare app. Nothing here needs a database:
|
|
73
|
+
// the redirect runs before any gate, and the gate it hands an image to only has
|
|
74
|
+
// to redirect an anonymous request into sign-in.
|
|
75
|
+
async function withHall(fn) {
|
|
76
|
+
const express = require('express');
|
|
77
|
+
const { mountInternalSurfaces } = require('../src/bongos/serve-internal.js');
|
|
78
|
+
const app = express();
|
|
79
|
+
mountInternalSurfaces(app);
|
|
80
|
+
const server = app.listen(0, '127.0.0.1');
|
|
81
|
+
await new Promise((r) => server.once('listening', r));
|
|
82
|
+
const { port } = server.address();
|
|
83
|
+
const get = (p, host) => new Promise((resolve, reject) => {
|
|
84
|
+
http.get({ host: '127.0.0.1', port, path: p, headers: { host } }, (res) => {
|
|
85
|
+
res.resume();
|
|
86
|
+
res.on('end', () => resolve({ status: res.statusCode, location: res.headers.location || '' }));
|
|
87
|
+
}).on('error', reject);
|
|
88
|
+
});
|
|
89
|
+
try { await fn(get); } finally { server.close(); }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const CHAPTER_OF = {
|
|
93
|
+
ranks: 'the-rank-ladder',
|
|
94
|
+
drachmae: 'credits-and-karma',
|
|
95
|
+
diagrams: 'the-claim-ship-lifecycle',
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
test('RUN: /ranks, /drachmae and /diagrams each 302 to the Primer chapter that replaced them', async () => {
|
|
99
|
+
await withHall(async (get) => {
|
|
100
|
+
for (const [page, chapter] of Object.entries(CHAPTER_OF)) {
|
|
101
|
+
for (const form of [`/${page}`, `/${page}/`, `/${page}.html`]) {
|
|
102
|
+
const r = await get(form, 'builders.127.0.0.1');
|
|
103
|
+
assert.equal(r.status, 302, `${form} is a temporary redirect, so a later re-point is never cached past`);
|
|
104
|
+
assert.equal(r.location, `/primer#${chapter}`, `${form} lands on #${chapter}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// A query string rides along; the chapter is the fragment after it.
|
|
108
|
+
const q = await get('/drachmae?from=mail', 'builders.127.0.0.1');
|
|
109
|
+
assert.equal(q.location, '/primer?from=mail#credits-and-karma');
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('RUN: the diagram images are NOT swallowed by the redirect, and stay behind sign-in', async () => {
|
|
114
|
+
await withHall(async (get) => {
|
|
115
|
+
const img = await get('/diagrams/01-task-lifecycle.png', 'builders.127.0.0.1');
|
|
116
|
+
assert.equal(img.status, 302);
|
|
117
|
+
assert.match(img.location, /\/auth\/web\/start\?return=/, 'an anonymous image request goes to sign-in, not to the Primer');
|
|
118
|
+
const primer = await get('/primer', 'builders.127.0.0.1');
|
|
119
|
+
assert.match(primer.location, /\/auth\/web\/start\?return=/, 'and the Primer itself keeps its sign-in gate');
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('every redirect target is a real chapter of the Primer', () => {
|
|
124
|
+
const ids = h2Ids();
|
|
125
|
+
for (const chapter of Object.values(CHAPTER_OF)) assert.ok(ids.includes(chapter), `primer.md has an h2 slugged ${chapter}`);
|
|
126
|
+
const src = fs.readFileSync(path.join(ROOT, 'src', 'bongos', 'serve-internal.js'), 'utf8');
|
|
127
|
+
const map = src.slice(src.indexOf('const RETIRED_DOC_CHAPTER'), src.indexOf('const RETIRED_DOC_PAGE_RE'));
|
|
128
|
+
for (const [page, chapter] of Object.entries(CHAPTER_OF)) {
|
|
129
|
+
assert.match(map, new RegExp(`${page}: '${chapter}'`), `serve-internal.js sends /${page} to #${chapter}`);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// ---- 2. the chapters, each with its diagram --------------------------------
|
|
134
|
+
|
|
135
|
+
const diagramTable = () => [...primerJs.matchAll(/\{ id: '([a-z0-9-]+)',\s*src: '\/builders\/diagrams\/([\w.-]+)'/g)]
|
|
136
|
+
.map((m) => ({ id: m[1], file: m[2] }));
|
|
137
|
+
|
|
138
|
+
test('all five diagrams are inlined, each at a chapter that exists', () => {
|
|
139
|
+
const table = diagramTable();
|
|
140
|
+
assert.deepEqual(table.map((d) => d.file.replace(/\.png$/, '')).sort(),
|
|
141
|
+
fs.readdirSync(path.join(ROOT, 'docs', 'onboarding', 'diagrams')).filter((f) => f.endsWith('.mmd')).map((f) => f.replace(/\.mmd$/, '')).sort(),
|
|
142
|
+
'one figure per diagram source, none left without a chapter');
|
|
143
|
+
const ids = h2Ids();
|
|
144
|
+
for (const d of table) assert.ok(ids.includes(d.id), `${d.file} is inlined at #${d.id}, which primer.md must have`);
|
|
145
|
+
assert.equal(new Set(table.map((d) => d.id)).size, table.length, 'and no chapter carries two');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('the lifecycle, the rank ladder and credits and karma are each explained ONCE', () => {
|
|
149
|
+
const ids = h2Ids();
|
|
150
|
+
for (const chapter of ['the-claim-ship-lifecycle', 'the-rank-ladder', 'credits-and-karma']) {
|
|
151
|
+
assert.equal(ids.filter((id) => id === chapter).length, 1, `one #${chapter} chapter`);
|
|
152
|
+
}
|
|
153
|
+
// The ladder chapter carries what the Ranks page said: every live rank, and
|
|
154
|
+
// how each promotion happens.
|
|
155
|
+
const ladder = PRIMER_MD.slice(PRIMER_MD.indexOf('## The rank ladder'), PRIMER_MD.indexOf('## Credits and karma'));
|
|
156
|
+
for (const rank of ['Xenos', 'Thetes', 'Metic', 'Archon']) assert.match(ladder, new RegExp(`\\*\\*${rank}\\*\\*`));
|
|
157
|
+
assert.match(ladder, /### How you advance/);
|
|
158
|
+
assert.match(ladder, /\/builders#accomplishments-progress/, 'and it points the reader at their own standing');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('the two stale primer lines are gone', () => {
|
|
162
|
+
assert.doesNotMatch(PRIMER_MD, /touches the game/, 'the core has no game');
|
|
163
|
+
assert.doesNotMatch(PRIMER_MD, /Entry to the project is \*\*approval-gated\*\*/);
|
|
164
|
+
assert.match(PRIMER_MD, /\*\*depends on the project\*\*/, 'entry depends on the project');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test('the primer keeps a hall address in the hall, and sends repo paths to GitHub', () => {
|
|
168
|
+
const sandbox = { window: { OTB: realOTB() }, document: { readyState: 'complete', querySelector: () => null } };
|
|
169
|
+
vm.createContext(sandbox);
|
|
170
|
+
vm.runInContext(read('md-reader.js'), sandbox, { filename: 'md-reader.js' });
|
|
171
|
+
// Lift the resolver out of primer.js without booting the page.
|
|
172
|
+
const src = primerJs.slice(primerJs.indexOf(' const resolveLinkHref = (() => {'), primerJs.indexOf(' function toast('));
|
|
173
|
+
const fn = vm.runInContext(`(() => { const MD = window.OTBMdReader; const PRIMER_DIR = 'docs/onboarding/'; let repoBlobBase = 'https://gh/blob/main'; let repoTreeBase = 'https://gh/tree/main';\n${src}\nreturn resolveLinkHref; })()`, sandbox);
|
|
174
|
+
assert.equal(fn('/builders#accomplishments-progress'), '/builders#accomplishments-progress');
|
|
175
|
+
assert.equal(fn('drachmae-and-karma.md'), 'https://gh/blob/main/docs/onboarding/drachmae-and-karma.md',
|
|
176
|
+
'the full reference is a repo doc now that its hall page is a chapter');
|
|
177
|
+
assert.equal(fn('../adr/'), 'https://gh/tree/main/docs/adr');
|
|
178
|
+
assert.equal(fn('/buildersx/'), 'https://gh/tree/main/buildersx', 'only a real /builders address is kept');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// Lift insertLiveSections out of primer.js (with the two consts it reads) and run
|
|
182
|
+
// it against a fake article, a fake OTBPrimerLive and a controllable observer.
|
|
183
|
+
function liftInsertLive({ hash = '', withObserver = true }) {
|
|
184
|
+
const src = primerJs.slice(primerJs.indexOf(' const LIVE_HASHES'), primerJs.indexOf(' // A deep link — /primer#the-rank-ladder'));
|
|
185
|
+
const fills = [];
|
|
186
|
+
const observers = [];
|
|
187
|
+
const window = {
|
|
188
|
+
location: { hash },
|
|
189
|
+
OTBPrimerLive: {
|
|
190
|
+
placeholder: (kind) => ({ kind }),
|
|
191
|
+
fillTreasury: async (s) => { fills.push(s.kind); return true; },
|
|
192
|
+
fillBounty: async (s) => { fills.push(s.kind); return true; },
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
if (withObserver) {
|
|
196
|
+
window.IntersectionObserver = class {
|
|
197
|
+
constructor(cb) { this.cb = cb; this.targets = []; observers.push(this); }
|
|
198
|
+
observe(t) { this.targets.push(t); }
|
|
199
|
+
disconnect() { this.gone = true; }
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
const next = { tagName: 'H2' };
|
|
203
|
+
const h = { tagName: 'H2', nextElementSibling: { tagName: 'P', nextElementSibling: next } };
|
|
204
|
+
const placed = [];
|
|
205
|
+
const article = {
|
|
206
|
+
querySelector: (sel) => (sel === 'h2[id="credits-and-karma"]' ? h : null),
|
|
207
|
+
insertBefore: (el, ref) => placed.push([el.kind, ref === next]),
|
|
208
|
+
appendChild: (el) => placed.push([el.kind, false]),
|
|
209
|
+
};
|
|
210
|
+
const ctx = { window, Promise };
|
|
211
|
+
vm.createContext(ctx);
|
|
212
|
+
const fn = vm.runInContext(`(() => { const LIVE_CHAPTER_ID = 'credits-and-karma';\n${src}\nreturn insertLiveSections; })()`, ctx);
|
|
213
|
+
return { run: () => fn(article), fills, observers, placed };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
test('RUN: the live reads wait until the reader comes near, so an ordinary Primer visit costs nothing', async () => {
|
|
217
|
+
const t = liftInsertLive({ hash: '#the-rank-ladder' });
|
|
218
|
+
await t.run();
|
|
219
|
+
assert.deepEqual(t.placed, [['treasury', true], ['bounty', true]], 'both placed at the end of the credits chapter, ahead of the next one');
|
|
220
|
+
assert.deepEqual(t.fills, [], 'no fetch on load');
|
|
221
|
+
assert.equal(t.observers.length, 1);
|
|
222
|
+
t.observers[0].cb([{ isIntersecting: false }]);
|
|
223
|
+
assert.deepEqual(t.fills, [], 'nor while they are still far away');
|
|
224
|
+
t.observers[0].cb([{ isIntersecting: true }]);
|
|
225
|
+
t.observers[0].cb([{ isIntersecting: true }]);
|
|
226
|
+
await Promise.resolve();
|
|
227
|
+
assert.deepEqual(t.fills, ['treasury', 'bounty'], 'one read each, once, when the reader approaches');
|
|
228
|
+
assert.equal(t.observers[0].gone, true, 'and the watch is dropped');
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test('RUN: an old /drachmae link (or a section address) fills at once, so the landing is on the final layout', async () => {
|
|
232
|
+
for (const hash of ['#credits-and-karma', '#the-treasury', '#security-bounty-rates']) {
|
|
233
|
+
const t = liftInsertLive({ hash });
|
|
234
|
+
await t.run();
|
|
235
|
+
assert.deepEqual(t.fills, ['treasury', 'bounty'], `${hash} reads both before landing`);
|
|
236
|
+
assert.equal(t.observers.length, 0);
|
|
237
|
+
}
|
|
238
|
+
const old = liftInsertLive({ withObserver: false });
|
|
239
|
+
await old.run();
|
|
240
|
+
assert.deepEqual(old.fills, ['treasury', 'bounty'], 'a browser with no observer still gets the sections');
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test('a deep link lands after the article, its live sections and the fonts', () => {
|
|
244
|
+
const boot = primerJs.slice(primerJs.indexOf('async function boot()'));
|
|
245
|
+
assert.match(boot, /const live = insertLiveSections\(article\);/);
|
|
246
|
+
assert.match(boot, /live\.then\(\(\) => \(document\.fonts \? document\.fonts\.ready : null\)\)\.then\(landOnHash\);/);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test('the states file shoots a redirected deep link, not just the top of the page', () => {
|
|
250
|
+
const states = JSON.parse(read('primer.states.json'));
|
|
251
|
+
assert.match(states.states.jump.url, /#credits-and-karma$/);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// ---- 3. the live sections, RUN ---------------------------------------------
|
|
255
|
+
|
|
256
|
+
const costSummaryFixture = JSON.parse(
|
|
257
|
+
fs.readFileSync(path.join(ROOT, 'modules', 'ui-design', 'kit', 'fixtures', 'cost-summary.json'), 'utf8'),
|
|
258
|
+
);
|
|
259
|
+
const bountyFixture = JSON.parse(
|
|
260
|
+
fs.readFileSync(path.join(ROOT, 'scripts', 'hall-preview', 'fixtures', 'public__bounty-table.json'), 'utf8'),
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
// Just enough of an element for primer-live.js: it only ever writes a section's
|
|
264
|
+
// innerHTML, its data-live and aria-busy, and removes it.
|
|
265
|
+
function fakeSection() {
|
|
266
|
+
return {
|
|
267
|
+
className: '', dataset: {}, attrs: {}, innerHTML: '', removed: false,
|
|
268
|
+
setAttribute(k, v) { this.attrs[k] = String(v); },
|
|
269
|
+
removeAttribute(k) { delete this.attrs[k]; },
|
|
270
|
+
remove() { this.removed = true; },
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function loadLive({ responses, branding = { currency: { label: 'drachmae' } }, kit = true }) {
|
|
275
|
+
const warnings = [];
|
|
276
|
+
const sandbox = {
|
|
277
|
+
window: { OTB: realOTB(), __BRANDING__: branding },
|
|
278
|
+
document: { createElement: () => fakeSection() },
|
|
279
|
+
console: { warn: (...a) => warnings.push(a.join(' ')) },
|
|
280
|
+
fetch: async (url) => {
|
|
281
|
+
const r = responses[url];
|
|
282
|
+
if (r instanceof Error) throw r;
|
|
283
|
+
if (!r) return { ok: false, status: 404, json: async () => ({}) };
|
|
284
|
+
return { ok: true, status: 200, json: async () => r };
|
|
285
|
+
},
|
|
286
|
+
};
|
|
287
|
+
if (kit) {
|
|
288
|
+
const kitPath = path.join(HALL, 'hall-kit.js');
|
|
289
|
+
sandbox.window.OTBKit = await import(`file://${kitPath.replace(/\\/g, '/')}`).then((m) => m.default);
|
|
290
|
+
}
|
|
291
|
+
vm.createContext(sandbox);
|
|
292
|
+
vm.runInContext(liveJs, sandbox, { filename: 'primer-live.js' });
|
|
293
|
+
return { live: sandbox.window.OTBPrimerLive, warnings };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const TREASURY = '/api/bongos/public/cost-summary';
|
|
297
|
+
const BOUNTY = '/api/bongos/public/bounty-table';
|
|
298
|
+
|
|
299
|
+
test('RUN: a section says Loading… the moment it is placed, under the heading a deep link needs', async () => {
|
|
300
|
+
const { live } = await loadLive({ responses: {} });
|
|
301
|
+
for (const [kind, id] of [['treasury', 'the-treasury'], ['bounty', 'security-bounty-rates']]) {
|
|
302
|
+
const s = live.placeholder(kind);
|
|
303
|
+
assert.match(s.innerHTML, new RegExp(`^<h3 id="${id}">`), `${kind} carries its address from the start`);
|
|
304
|
+
assert.match(s.innerHTML, /<p class="hall-loading" role="status">Loading…<\/p>/);
|
|
305
|
+
assert.equal(s.attrs['aria-busy'], 'true');
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test('RUN: the treasury fills from the payload, and its line ends on the total the prose states', async () => {
|
|
310
|
+
const { live } = await loadLive({ responses: { [TREASURY]: costSummaryFixture } });
|
|
311
|
+
const s = live.placeholder('treasury');
|
|
312
|
+
assert.equal(await live.fillTreasury(s), true);
|
|
313
|
+
assert.equal(s.removed, false);
|
|
314
|
+
assert.equal(s.attrs['aria-busy'], undefined, 'no longer busy');
|
|
315
|
+
assert.doesNotMatch(s.innerHTML, /Loading…/);
|
|
316
|
+
assert.match(s.innerHTML, /^<h3 id="the-treasury">The treasury, so far<\/h3>/);
|
|
317
|
+
const usd = `$${Number(costSummaryFixture.total_usd).toLocaleString(undefined, { maximumFractionDigits: 0 })}`;
|
|
318
|
+
assert.ok(s.innerHTML.includes(`<strong>${usd}</strong>`), 'the spend total is stated');
|
|
319
|
+
assert.match(s.innerHTML, /id="treasury-spend"/);
|
|
320
|
+
assert.match(s.innerHTML, /id="treasury-earned"/);
|
|
321
|
+
assert.match(s.innerHTML, /Drachmae awarded, cumulative/, 'the currency word is the branding pack\'s');
|
|
322
|
+
assert.doesNotMatch(s.innerHTML, /NaN|Infinity/, 'a NaN in a path renders as an invisible chart with no error anywhere');
|
|
323
|
+
assert.doesNotMatch(s.innerHTML, /tabindex="0"/, 'no focus stop: nothing here wires a readout');
|
|
324
|
+
assert.doesNotMatch(s.innerHTML, /&amp;|&#/, 'nothing is escaped twice');
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
test('RUN: a treasury that cannot be read, or holds nothing yet, leaves NO section behind', async () => {
|
|
328
|
+
for (const responses of [{}, { [TREASURY]: new Error('down') }, { [TREASURY]: { daily: [], daily_credits: [] } }]) {
|
|
329
|
+
const { live } = await loadLive({ responses });
|
|
330
|
+
const s = live.placeholder('treasury');
|
|
331
|
+
assert.equal(await live.fillTreasury(s), false);
|
|
332
|
+
assert.equal(s.removed, true, 'the Loading… line goes with it — never an empty panel');
|
|
333
|
+
}
|
|
334
|
+
const noKit = await loadLive({ responses: { [TREASURY]: costSummaryFixture }, kit: false });
|
|
335
|
+
const s = noKit.live.placeholder('treasury');
|
|
336
|
+
assert.equal(await noKit.live.fillTreasury(s), false, 'without the chart kit there is nothing honest to draw');
|
|
337
|
+
assert.equal(s.removed, true);
|
|
338
|
+
assert.match(noKit.warnings.join('\n'), /OTBKit missing/);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test('RUN: the bounty rate card renders the live table, tuned tiers marked', async () => {
|
|
342
|
+
const tuned = { ...bountyFixture, table: bountyFixture.table.map((r, i) => (i === 0 ? { ...r, overridden: true } : r)) };
|
|
343
|
+
const { live } = await loadLive({ responses: { [BOUNTY]: tuned }, branding: {} });
|
|
344
|
+
const s = live.placeholder('bounty');
|
|
345
|
+
assert.equal(await live.fillBounty(s), true);
|
|
346
|
+
assert.match(s.innerHTML, /^<h3 id="security-bounty-rates">Security bounty rates<\/h3>/);
|
|
347
|
+
assert.equal((s.innerHTML.match(/<tr><td><strong>/g) || []).length, bountyFixture.table.length, 'one row per severity');
|
|
348
|
+
assert.match(s.innerHTML, /<em>\(tuned\)<\/em>/);
|
|
349
|
+
assert.match(s.innerHTML, /<th>Credits<\/th>/, 'the neutral currency word when the pack names none');
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test('RUN: a bounty feed that fails leaves no section', async () => {
|
|
353
|
+
for (const responses of [{}, { [BOUNTY]: { table: [] } }]) {
|
|
354
|
+
const { live } = await loadLive({ responses });
|
|
355
|
+
const s = live.placeholder('bounty');
|
|
356
|
+
assert.equal(await live.fillBounty(s), false);
|
|
357
|
+
assert.equal(s.removed, true);
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
test('the live sections read endpoints that already existed, and draw only through the kit', () => {
|
|
362
|
+
assert.match(liveJs, /TREASURY_URL = '\/api\/bongos\/public\/cost-summary'/);
|
|
363
|
+
assert.match(liveJs, /BOUNTY_URL = '\/api\/bongos\/public\/bounty-table'/);
|
|
364
|
+
for (const fn of ['cumulateByDay', 'timeStepSeriesHtml', 'plotSvgHtml', 'plotValueLabelHtml', 'barSeriesHtml']) {
|
|
365
|
+
assert.match(liveJs, new RegExp(`kit\\.${fn}\\(`), `the treasury draws through ${fn}`);
|
|
366
|
+
}
|
|
367
|
+
assert.doesNotMatch(liveJs, /<(?:path|polyline|rect)\b[^>]*\b(?:d|points)="\$\{/, 'no hand-built SVG geometry');
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
test('the page loads the kit, then the live module, then the page script', () => {
|
|
371
|
+
const loaded = [...primerHtml.matchAll(/<script[^>]+src="([^"?]+)/g)].map((m) => m[1].split('/').pop());
|
|
372
|
+
const at = (f) => loaded.indexOf(f);
|
|
373
|
+
assert.ok(at('hall-kit.js') > -1 && at('primer-live.js') > -1 && at('primer.js') > -1);
|
|
374
|
+
assert.ok(at('hall-kit.js') < at('primer-live.js') && at('primer-live.js') < at('primer.js'));
|
|
375
|
+
// The figure chrome is shared (style.css), so the Primer needs no profile sheet.
|
|
376
|
+
assert.match(primerHtml, /style\.css/);
|
|
377
|
+
assert.doesNotMatch(primerHtml, /profile\.css/);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
// ---- 4. the shared reader, actually run -------------------------------------
|
|
381
|
+
|
|
382
|
+
test('the shared parser reaches h4 — the reference doc\'s earning sources are ####', () => {
|
|
383
|
+
const MD = loadReader();
|
|
384
|
+
const { html, headings } = MD.parseMarkdown('#### 4. Achievements\n\nUnlocking one pays its bonus.\n', (h) => h);
|
|
385
|
+
assert.equal(headings.length, 1);
|
|
386
|
+
assert.equal(headings[0].level, 4);
|
|
387
|
+
assert.match(html, /<h4 id="4-achievements">4\. Achievements<\/h4>/);
|
|
388
|
+
assert.doesNotMatch(html, /####/);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
test('a paragraph breaks before an h4 the way it breaks before an h3', () => {
|
|
392
|
+
const MD = loadReader();
|
|
393
|
+
const { html } = MD.parseMarkdown('a line\n#### A head\n', (h) => h);
|
|
394
|
+
assert.match(html, /<p>a line<\/p>\s*<h4/);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
test('a heading that carries a link slugs by its text, so the copied address stays readable', () => {
|
|
398
|
+
const MD = loadReader();
|
|
399
|
+
const withLink = '2. Per-task credits, adjusted by *kind* ([ADR 0023](../adr/0023-kind-multipliers-and-idea-bonus.md))';
|
|
400
|
+
assert.equal(MD.slugify(withLink), MD.slugify('2. Per-task credits, adjusted by *kind* (ADR 0023)'));
|
|
401
|
+
assert.doesNotMatch(MD.slugify(withLink), /adr0023|md$/, 'the target path is not part of the address');
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
test('emphasis inside a bold run renders — the API list-price line showed its asterisks', () => {
|
|
405
|
+
const MD = loadReader();
|
|
406
|
+
const { html } = MD.parseMarkdown('- **It is the API *list* price, deliberately** and not the rest\n', (h) => h);
|
|
407
|
+
assert.match(html, /<strong>It is the API <em>list<\/em> price, deliberately<\/strong> and not the rest/);
|
|
408
|
+
const two = MD.parseMarkdown('**a** and **b**\n', (h) => h).html;
|
|
409
|
+
assert.match(two, /<strong>a<\/strong> and <strong>b<\/strong>/, 'and two bold runs on one line stay two');
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
test('the room gives the fourth level a rule, the measure and the anchor offset', () => {
|
|
413
|
+
const h4 = rule(roomCss, '.primer-article h4');
|
|
414
|
+
assert.ok(h4, 'room.css styles h4 (the treasury\'s "Where it went" is one)');
|
|
415
|
+
assert.match(h4, /scroll-margin-top/, 'a deep link to an h4 clears the top bar');
|
|
416
|
+
const measured = decomment(roomCss).match(/\.primer-article\s*>\s*p,[\s\S]*?\{([^}]*)\}/);
|
|
417
|
+
assert.ok(measured, 'the measured-children list exists');
|
|
418
|
+
assert.match(measured[0], /\.primer-article\s*>\s*h4/, 'and the h4 is measured with the prose');
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
test('the reference rules left with the reference page', () => {
|
|
422
|
+
assert.doesNotMatch(decomment(roomCss), /room--reference/, 'no rule keyed on a body class no page wears');
|
|
423
|
+
for (const f of fs.readdirSync(HALL).filter((n) => n.endsWith('.html'))) {
|
|
424
|
+
assert.doesNotMatch(read(f), /room--reference/, `${f} wears no reference modifier`);
|
|
425
|
+
}
|
|
426
|
+
});
|
|
@@ -68,16 +68,13 @@ const read = (f) => fs.readFileSync(path.join(HALL, f), 'utf8');
|
|
|
68
68
|
const styleCss = read('style.css');
|
|
69
69
|
const roomCss = read('room.css');
|
|
70
70
|
const primerCss = read('primer.css');
|
|
71
|
-
const diagramsCss = read('diagrams.css');
|
|
72
71
|
const roadmapCss = read('roadmap.css');
|
|
73
72
|
const viewerCss = read('diagram-viewer.css');
|
|
74
73
|
const primerJs = read('primer.js');
|
|
75
74
|
const viewerJs = read('diagram-viewer.js');
|
|
76
75
|
const mdReaderJs = read('md-reader.js');
|
|
77
76
|
const primerHtml = read('primer.html');
|
|
78
|
-
const diagramsHtml = read('diagrams.html');
|
|
79
77
|
const roadmapHtml = read('roadmap.html');
|
|
80
|
-
const ranksHtml = read('ranks.html');
|
|
81
78
|
|
|
82
79
|
const decomment = (css) => css.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
83
80
|
// The body of the first rule matching `selector {`, comments stripped. Naive on
|
|
@@ -94,7 +91,6 @@ const rule = (css, selector) => {
|
|
|
94
91
|
const RESKINNED = [
|
|
95
92
|
['room.css', roomCss],
|
|
96
93
|
['primer.css', primerCss],
|
|
97
|
-
['diagrams.css', diagramsCss],
|
|
98
94
|
['roadmap.css', roadmapCss],
|
|
99
95
|
['diagram-viewer.css', viewerCss],
|
|
100
96
|
];
|
|
@@ -143,14 +139,18 @@ test('no re-skinned reading-room sheet spends a compat alias', () => {
|
|
|
143
139
|
'the aliases exist for pages nobody has re-skinned; spending one here quietly opts the sheet back out of the world');
|
|
144
140
|
});
|
|
145
141
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
142
|
+
// task 1005046: the Ranks page folded into the Primer; its ladder cards went with
|
|
143
|
+
// it and "Where you stand" (Accomplishments) kept the standing rules, which hold
|
|
144
|
+
// the same line.
|
|
145
|
+
test('the "Where you stand" section of style.css spends the real names too', () => {
|
|
146
|
+
for (const sel of ['.ranks-standing', '.ranks-standing__blurb', '.ranks-standing__next', '.ranks-rise__body']) {
|
|
147
|
+
const body = rule(styleCss, sel);
|
|
148
|
+
assert.ok(body, `${sel} has a rule`);
|
|
149
|
+
for (const alias of ALIASES) {
|
|
150
|
+
assert.doesNotMatch(body, new RegExp(`var\\(\\s*${alias}\\s*[,)]`), `${sel} spends no ${alias}`);
|
|
151
|
+
}
|
|
151
152
|
}
|
|
152
|
-
assert.
|
|
153
|
-
'the edge is reserved as transparent so a modifier can draw one — collab.css\'s .btn--quiet is the precedent');
|
|
153
|
+
assert.equal(rule(styleCss, '.rank-card'), null, 'the ladder cards left with the page');
|
|
154
154
|
});
|
|
155
155
|
|
|
156
156
|
test('the alias guard would CATCH --surface without matching --surface-2', () => {
|
|
@@ -223,8 +223,11 @@ test('primer.html loads the reader BEFORE the page script', () => {
|
|
|
223
223
|
});
|
|
224
224
|
|
|
225
225
|
test('the primer keeps the one rewrite that is its own', () => {
|
|
226
|
-
|
|
227
|
-
|
|
226
|
+
// task 1005046: the economy doc's in-hall page became a Primer chapter, so the
|
|
227
|
+
// rewrite that kept a link to it in the hall is gone; what is the primer's own
|
|
228
|
+
// now is a /builders address (run in tests/hall_reading_room.mjs).
|
|
229
|
+
assert.doesNotMatch(primerJs, /'\/builders\/drachmae'/, 'no link is sent to the retired Economy page');
|
|
230
|
+
assert.match(primerJs, /\/\^\\\/builders/, 'a hall address stays in the hall');
|
|
228
231
|
assert.match(primerJs, /baseDir:\s*PRIMER_DIR/, 'and resolves its relative links against docs/onboarding/');
|
|
229
232
|
});
|
|
230
233
|
|
|
@@ -237,7 +240,7 @@ test('the room styles what the shared parser emits that the old copy never did',
|
|
|
237
240
|
|
|
238
241
|
test('primer.js still injects the diagrams — the one parser-adjacent job that is the primer\'s', () => {
|
|
239
242
|
assert.match(primerJs, /function inlineDiagrams/);
|
|
240
|
-
assert.match(primerJs, /
|
|
243
|
+
assert.match(primerJs, /DIAGRAM_AT_CHAPTER/, 'keyed by chapter id (tests/hall_reading_room.mjs checks each id exists)');
|
|
241
244
|
assert.match(primerJs, /fig\.className = 'primer-figure'/,
|
|
242
245
|
'the injected figure takes the class diagram-viewer.js upgrades and room.css frames');
|
|
243
246
|
});
|
|
@@ -285,17 +288,19 @@ test('the fullscreen scroll lock is on the root, where the shell already locks',
|
|
|
285
288
|
assert.match(decomment(styleCss), /html\.drawer-open/, 'and that is the convention it is matching');
|
|
286
289
|
});
|
|
287
290
|
|
|
288
|
-
test('the room frames
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
assert.
|
|
293
|
-
|
|
291
|
+
test('the room frames the figure the viewer upgrades', () => {
|
|
292
|
+
// task 1005046: the diagrams index's `figure.diagram` was the second class; with
|
|
293
|
+
// that page folded into the primer, the primer's figure is the only one.
|
|
294
|
+
assert.match(viewerJs, /SELECTOR = 'figure\.primer-figure img'/, 'the viewer upgrades the primer\'s figures');
|
|
295
|
+
assert.doesNotMatch(decomment(roomCss), /figure\.diagram/, 'and the room keeps no rule for a class no page draws');
|
|
296
|
+
const frame = rule(roomCss, '.primer-figure');
|
|
297
|
+
assert.ok(frame, 'the room frames the figure');
|
|
298
|
+
assert.match(frame, /var\(--glass\)/);
|
|
294
299
|
// the canvas is inserted INSIDE the figure, so exactly one of the two draws
|
|
295
300
|
// the frame or the diagram lands in nested boxes
|
|
296
|
-
const inner =
|
|
301
|
+
const inner = rule(roomCss, '.primer-figure .dgv');
|
|
297
302
|
assert.ok(inner, 'the inserted canvas gives up its own frame');
|
|
298
|
-
assert.match(inner
|
|
303
|
+
assert.match(inner, /border:\s*0/);
|
|
299
304
|
});
|
|
300
305
|
|
|
301
306
|
// ---- 7b. the figure follows the theme (task 1003639) ----------------------
|
|
@@ -326,8 +331,8 @@ test('dark turns the canvas figure, in both twins, byte-identical', () => {
|
|
|
326
331
|
assert.doesNotMatch(rule(viewerCss, '.dgv__pan svg'), /filter/, 'light keeps the render exactly as mermaid drew it');
|
|
327
332
|
});
|
|
328
333
|
|
|
329
|
-
test('the fallback PNG turns with it, on
|
|
330
|
-
const both = (pre) => `${pre} .primer-figure img
|
|
334
|
+
test('the fallback PNG turns with it, on the figure the viewer upgrades', () => {
|
|
335
|
+
const both = (pre) => `${pre} .primer-figure img`;
|
|
331
336
|
const sw = rule(roomCss, both('html[data-theme="dark"]'));
|
|
332
337
|
const media = mediaRule(roomCss, both('html:not([data-theme])'));
|
|
333
338
|
assert.ok(sw && media, 'the PNG shows before the SVG arrives and whenever it cannot — a dark reader must not get a white flash');
|
|
@@ -344,7 +349,8 @@ test('the viewer drops the white ground mermaid bakes into the SVG', () => {
|
|
|
344
349
|
|
|
345
350
|
// ---- 8. every room has exactly one h1 -------------------------------------
|
|
346
351
|
|
|
347
|
-
|
|
352
|
+
// task 1005046: diagrams.html and ranks.html folded into the primer's chapters.
|
|
353
|
+
const ROOMS = [['primer.html', primerHtml], ['roadmap.html', roadmapHtml]];
|
|
348
354
|
|
|
349
355
|
// An HTML comment that mentions <h1> is prose, not markup — and these pages each
|
|
350
356
|
// carry one explaining why the heading is there at all.
|
|
@@ -358,8 +364,8 @@ test('every reading room renders exactly one h1, and it is the page head\'s', ()
|
|
|
358
364
|
}
|
|
359
365
|
});
|
|
360
366
|
|
|
361
|
-
test('the
|
|
362
|
-
for (const [name, html] of [['primer.html', primerHtml]
|
|
367
|
+
test('the DOCS room takes the room voice; the deck takes the board\'s', () => {
|
|
368
|
+
for (const [name, html] of [['primer.html', primerHtml]]) {
|
|
363
369
|
assert.match(html, /class="page-head page-head--room"/, `${name} is a reading room`);
|
|
364
370
|
}
|
|
365
371
|
assert.match(roadmapHtml, /class="page-head page-head--board"/,
|
|
@@ -375,7 +381,7 @@ test('the document\'s own h1 is suppressed, or the article would render a second
|
|
|
375
381
|
// ---- 9. the archetype's shape ---------------------------------------------
|
|
376
382
|
|
|
377
383
|
test('the room opts in from its markup, and the archetype sheet is what carries it', () => {
|
|
378
|
-
for (const [name, html] of [['primer.html', primerHtml]
|
|
384
|
+
for (const [name, html] of [['primer.html', primerHtml]]) {
|
|
379
385
|
assert.match(html, /<body class="room[^"]*"/, `${name} opts into the room`);
|
|
380
386
|
assert.match(html, /href="\/builders\/room\.css"/, `${name} loads the archetype`);
|
|
381
387
|
}
|
|
@@ -724,13 +730,13 @@ test('the timeline module is a pure builder, so it is testable and cannot boot i
|
|
|
724
730
|
// reader sees, and a script writing into one is a message nobody gets.
|
|
725
731
|
test('every Docs page head holds only its h1, and no script writes into it', () => {
|
|
726
732
|
const pub = path.join(ROOT, 'modules/hall-ui/public');
|
|
727
|
-
for (const page of ['primer', '
|
|
733
|
+
for (const page of ['primer', 'commands', 'atlas', 'modules']) {
|
|
728
734
|
const html = dehtmlcomment(fs.readFileSync(path.join(pub, `${page}.html`), 'utf8'));
|
|
729
735
|
const head = html.match(/<div class="page-head[^"]*">([\s\S]*?)<\/div>/);
|
|
730
736
|
assert.ok(head, `${page}.html keeps its head for the h1`);
|
|
731
737
|
assert.doesNotMatch(head[1], /page-head__eyebrow|page-head__sub/, `${page}.html's hidden head holds no eyebrow or sub-line`);
|
|
732
738
|
}
|
|
733
|
-
for (const [file, id] of [['
|
|
739
|
+
for (const [file, id] of [['modules.js', 'modules-sub']]) {
|
|
734
740
|
assert.ok(!fs.readFileSync(path.join(pub, file), 'utf8').includes(id), `${file} no longer writes into #${id}`);
|
|
735
741
|
}
|
|
736
742
|
});
|
package/tests/hall_sitemap.mjs
CHANGED
|
@@ -61,6 +61,31 @@ test('a link is read the ways the hall writes one, and an unknown one is broken'
|
|
|
61
61
|
assert.equal(gen.classify('/builders/style.css?v=1', urls), null, 'an asset is not a page link');
|
|
62
62
|
});
|
|
63
63
|
|
|
64
|
+
test('the Primer links to Repo Atlas and Commands, and in the hall they open those pages (task 1005069)', () => {
|
|
65
|
+
const primer = model.pages.find((p) => p.url === '/primer');
|
|
66
|
+
for (const u of ['/atlas', '/commands']) {
|
|
67
|
+
assert.ok(primer.out.includes(u), `/primer links to ${u}`);
|
|
68
|
+
assert.ok(model.pages.find((p) => p.url === u).in.includes('/primer'), `${u} is linked from /primer`);
|
|
69
|
+
}
|
|
70
|
+
// primer.js's own rewrite, run on the primer's own links: each opens its hall page
|
|
71
|
+
const src = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/primer.js'), 'utf8');
|
|
72
|
+
const literal = /const HALL_PAGE = (\/.+\/);/.exec(src);
|
|
73
|
+
assert.ok(literal, 'primer.js declares the hall-page rewrite');
|
|
74
|
+
const HALL_PAGE = new RegExp(literal[1].slice(1, -1));
|
|
75
|
+
const md = fs.readFileSync(path.join(ROOT, gen.CONTENT['/primer']), 'utf8');
|
|
76
|
+
const hrefs = [...md.matchAll(/\]\(((?:\.\.\/)+modules\/hall-ui\/public\/[a-z0-9-]+\.html)\)/g)].map((m) => m[1]);
|
|
77
|
+
assert.ok(hrefs.length >= 2);
|
|
78
|
+
const urls = new Set(model.pages.map((p) => p.url));
|
|
79
|
+
for (const h of hrefs) {
|
|
80
|
+
const m = HALL_PAGE.exec(h);
|
|
81
|
+
assert.ok(m, `${h} is rewritten by primer.js`);
|
|
82
|
+
assert.ok(urls.has('/' + m[1]), `${h} names a real hall page`);
|
|
83
|
+
}
|
|
84
|
+
// a primer link to a page that does not exist is a broken link, not a silent miss
|
|
85
|
+
assert.deepEqual(gen.mdHallLinks('[x](../../modules/hall-ui/public/no-such-page.html)'), ['/no-such-page']);
|
|
86
|
+
assert.equal(gen.classify('/no-such-page', urls), false);
|
|
87
|
+
});
|
|
88
|
+
|
|
64
89
|
test('the street is Governing\'s home: no nav item, reached by clicking from the city', () => {
|
|
65
90
|
const street = model.pages.find((p) => p.url === '/street');
|
|
66
91
|
assert.ok(street, 'the street is a hall page');
|
|
@@ -94,8 +94,9 @@ test('the hall-preview fixture carries every atom /me resolves', () => {
|
|
|
94
94
|
// "Upgraded to core X" notice after a move; task 1004826: Standing is Accomplishments;
|
|
95
95
|
// task 1004846: the new logo (its bump landed late, in task 1004852); task 1004938: the
|
|
96
96
|
// Market Watch settings nav item and its fuel glyph; task 1005041: the Update pill replaced the
|
|
97
|
-
// core-update banner; task 1005045: the Market Watch settings item left (one sidebar item)
|
|
98
|
-
|
|
97
|
+
// core-update banner; task 1005045: the Market Watch settings item left (one sidebar item);
|
|
98
|
+
// task 1005046: Ranks, Economy and Diagrams left Docs (one reading room, the Primer).
|
|
99
|
+
const SHELL_SHA = '36c64a1cf84e9db1';
|
|
99
100
|
|
|
100
101
|
test('shell.js has not changed without its cache-bust being bumped', () => {
|
|
101
102
|
const actual = crypto.createHash('sha256').update(SHELL).digest('hex').slice(0, 16);
|
|
@@ -92,7 +92,6 @@ const EXACT_LADDER_RE = /\[\s*'xenos',\s*'thetes',\s*'metic',\s*'archon'\s*\]/;
|
|
|
92
92
|
const STATIC_LADDER_FILES = [
|
|
93
93
|
'modules/discord/discord-channels.js',
|
|
94
94
|
'modules/memory/search.js',
|
|
95
|
-
'modules/hall-ui/public/ranks.js',
|
|
96
95
|
'modules/hall-ui/public/profile.js',
|
|
97
96
|
'modules/hall-ui/public/watch.js',
|
|
98
97
|
// task 1003094: the Constitution panel and the Board Room's sitting card both
|