@bongos/core 1.19.448 → 1.19.450
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 +96 -36
- package/docs/copy-inventory.md +121 -114
- package/docs/copy-registry.json +377 -331
- package/docs/file-map.md +1 -1
- package/docs/module-api-changelog.md +4 -0
- package/modules/hall-ui/public/diagram-viewer.css +51 -34
- package/modules/hall-ui/public/diagram-viewer.js +4 -1
- package/modules/hall-ui/public/diagrams.css +45 -0
- package/modules/hall-ui/public/diagrams.html +12 -20
- package/modules/hall-ui/public/diagrams.states.json +28 -0
- package/modules/hall-ui/public/idea.css +138 -71
- package/modules/hall-ui/public/idea.html +13 -3
- package/modules/hall-ui/public/idea.js +17 -4
- package/modules/hall-ui/public/idea.states.json +14 -0
- package/modules/hall-ui/public/ideas.css +148 -107
- package/modules/hall-ui/public/ideas.html +15 -5
- package/modules/hall-ui/public/ideas.states.json +15 -0
- package/modules/hall-ui/public/primer.css +92 -400
- package/modules/hall-ui/public/primer.html +68 -26
- package/modules/hall-ui/public/primer.js +107 -308
- package/modules/hall-ui/public/primer.states.json +38 -0
- package/modules/hall-ui/public/ranks.html +9 -1
- package/modules/hall-ui/public/ranks.states.json +1 -1
- package/modules/hall-ui/public/record.css +324 -0
- package/modules/hall-ui/public/roadmap.css +152 -86
- package/modules/hall-ui/public/roadmap.html +6 -1
- package/modules/hall-ui/public/roadmap.states.json +31 -0
- package/modules/hall-ui/public/room.css +441 -0
- package/modules/hall-ui/public/style.css +269 -63
- package/modules/hall-ui/public/task.css +71 -0
- package/modules/hall-ui/public/task.html +15 -32
- package/modules/hall-ui/public/task.js +142 -39
- package/modules/hall-ui/public/task.states.json +14 -0
- package/modules/hall-ui/public/work.css +4 -15
- package/modules/ui-design/kit/serve.js +7 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/hall-preview/server.js +34 -0
- package/src/module-api.js +1 -1
- package/tests/hall_audit.mjs +18 -11
- package/tests/hall_reading_world.mjs +430 -0
- package/tests/hall_record_world.mjs +412 -0
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
// tests/hall_reading_world.mjs — the READING ROOMS in the chrome world: the
|
|
2
|
+
// primer, the diagrams index, the roadmap deck and the ranks ladder
|
|
3
|
+
// (task 1003306, goal 1000081).
|
|
4
|
+
//
|
|
5
|
+
// tests/hall_tokens.mjs pins the TIER these pages spend, hall_board_world.mjs
|
|
6
|
+
// pins how the board spends it and hall_record_world.mjs the record family;
|
|
7
|
+
// this is the same job for the reading rooms. Everything here is a fact that is
|
|
8
|
+
// SILENT in a browser: the page still renders, it just renders wrong, or it
|
|
9
|
+
// renders right here and fails a floor on the reviewer's machine.
|
|
10
|
+
//
|
|
11
|
+
// What it locks:
|
|
12
|
+
// * THE PAGE-LOCAL `:root` STAYS GONE. roadmap.css declared one — the only
|
|
13
|
+
// one in the hall, and forbidden both ways: the branding pack's :root is
|
|
14
|
+
// spliced at the END of <head> at (0,1,0), so a page-local one is outranked
|
|
15
|
+
// one way and defeats re-branding the other. Page tokens live on `body`.
|
|
16
|
+
// * ONE MARKDOWN PARSER. primer.js carried its own copy of md-reader.js's
|
|
17
|
+
// grammar minus blockquotes, so every `> aside` in the primer silently
|
|
18
|
+
// collapsed into a paragraph. The copy is gone; the pin is that it stays
|
|
19
|
+
// gone AND that the room has a blockquote rule to render what now arrives.
|
|
20
|
+
// * THE SOFT GATE IS aria-disabled. A real `disabled` button leaves the tab
|
|
21
|
+
// order, so a keyboard reader can never land on the acknowledge control and
|
|
22
|
+
// never hears the note saying why it is locked.
|
|
23
|
+
// * THE `ch` TRAP. primer.css capped the article at `72ch` under a comment
|
|
24
|
+
// claiming 60-72 characters; a `ch` is the font's ZERO glyph and Manrope's
|
|
25
|
+
// is wide, so it rendered about 90. The measure is --measure, and it is
|
|
26
|
+
// spent on PROSE only — on a display heading it is the inverse mistake
|
|
27
|
+
// hall_record_world §8 pins.
|
|
28
|
+
// * THE DRIFT FALLBACKS. diagram-viewer.css carried eleven
|
|
29
|
+
// `var(--x, LITERAL)` pairs whose tokens all exist, so the literal never
|
|
30
|
+
// rendered and only encoded drift (its --ink fallback said #2a2a2a where
|
|
31
|
+
// the sheet ships #17181a). Plus two raw shadows and a hand-written
|
|
32
|
+
// z-index: 9999.
|
|
33
|
+
// * THE SCROLL LOCK IS ON THE ROOT. The diagram's fullscreen locked `body`
|
|
34
|
+
// where the shell's own drawer locks `html`. Two overlays locking one page
|
|
35
|
+
// on two different elements is how one of them stops working.
|
|
36
|
+
// * THE ONE h1, ON EVERY ROOM. All four pages had none — the finding
|
|
37
|
+
// ranks.states.json had been carrying in the open since task 1003320.
|
|
38
|
+
// * THE COMPAT ALIASES. Each re-skinned sheet's removal list. The aliases
|
|
39
|
+
// still EXIST for the pages nobody has re-skinned, so spending one here is
|
|
40
|
+
// legal CSS that quietly opts the sheet back out of the world.
|
|
41
|
+
//
|
|
42
|
+
// Reproduce the renders these came from. The floors run against the kit's own
|
|
43
|
+
// stub; the POPULATED document needs the fixture harness, because neither the
|
|
44
|
+
// primer markdown nor the generated diagrams are the kit stub's to serve:
|
|
45
|
+
// node modules/ui-design/kit/render.js --states modules/hall-ui/public/primer.states.json
|
|
46
|
+
// node scripts/hall-preview/server.js --port 4612 --fixture-me
|
|
47
|
+
// node modules/ui-design/kit/render.js --states modules/hall-ui/public/primer.states.json \
|
|
48
|
+
// --base-in http://builders.localhost:4612 --base-out http://builders.localhost:4612
|
|
49
|
+
// (likewise diagrams.states.json, roadmap.states.json, ranks.states.json)
|
|
50
|
+
//
|
|
51
|
+
// §10 leaves static analysis behind and RUNS md-reader.js in a vm: the parser
|
|
52
|
+
// unification is a behaviour claim, and "primer.js no longer contains a
|
|
53
|
+
// parser" only proves half of it. The other half is that the parser it now
|
|
54
|
+
// calls emits what the primer's markdown needs.
|
|
55
|
+
|
|
56
|
+
import { test } from 'node:test';
|
|
57
|
+
import assert from 'node:assert/strict';
|
|
58
|
+
import fs from 'node:fs';
|
|
59
|
+
import path from 'node:path';
|
|
60
|
+
import vm from 'node:vm';
|
|
61
|
+
import { fileURLToPath } from 'node:url';
|
|
62
|
+
|
|
63
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
64
|
+
const HALL = path.join(ROOT, 'modules', 'hall-ui', 'public');
|
|
65
|
+
const read = (f) => fs.readFileSync(path.join(HALL, f), 'utf8');
|
|
66
|
+
|
|
67
|
+
const styleCss = read('style.css');
|
|
68
|
+
const roomCss = read('room.css');
|
|
69
|
+
const primerCss = read('primer.css');
|
|
70
|
+
const diagramsCss = read('diagrams.css');
|
|
71
|
+
const roadmapCss = read('roadmap.css');
|
|
72
|
+
const viewerCss = read('diagram-viewer.css');
|
|
73
|
+
const primerJs = read('primer.js');
|
|
74
|
+
const viewerJs = read('diagram-viewer.js');
|
|
75
|
+
const mdReaderJs = read('md-reader.js');
|
|
76
|
+
const primerHtml = read('primer.html');
|
|
77
|
+
const diagramsHtml = read('diagrams.html');
|
|
78
|
+
const roadmapHtml = read('roadmap.html');
|
|
79
|
+
const ranksHtml = read('ranks.html');
|
|
80
|
+
|
|
81
|
+
const decomment = (css) => css.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
82
|
+
// The body of the first rule matching `selector {`, comments stripped. Naive on
|
|
83
|
+
// purpose: these are flat declaration blocks, not nested at-rules. Takes a PLAIN
|
|
84
|
+
// selector and escapes it here — pass a pre-escaped one and it is escaped twice.
|
|
85
|
+
const rule = (css, selector) => {
|
|
86
|
+
const esc = selector.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
87
|
+
const m = decomment(css).match(new RegExp(`(^|[},])\\s*${esc}\\s*\\{([^}]*)\\}`, 'm'));
|
|
88
|
+
return m ? m[2] : null;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// The sheets this task re-skinned. A sheet joins this list when its page family
|
|
92
|
+
// lands, and never leaves it.
|
|
93
|
+
const RESKINNED = [
|
|
94
|
+
['room.css', roomCss],
|
|
95
|
+
['primer.css', primerCss],
|
|
96
|
+
['diagrams.css', diagramsCss],
|
|
97
|
+
['roadmap.css', roadmapCss],
|
|
98
|
+
['diagram-viewer.css', viewerCss],
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
// ---- 1. no page declares its own :root -------------------------------------
|
|
102
|
+
|
|
103
|
+
test('no hall page sheet declares a :root of its own', () => {
|
|
104
|
+
const offenders = [];
|
|
105
|
+
for (const f of fs.readdirSync(HALL).filter((n) => n.endsWith('.css') && n !== 'style.css')) {
|
|
106
|
+
if (/(^|[},])\s*:root\s*\{/.test(decomment(read(f)))) offenders.push(f);
|
|
107
|
+
}
|
|
108
|
+
assert.deepEqual(offenders, [],
|
|
109
|
+
'the pack :root is spliced at the end of <head> — a page-local one is outranked one way and defeats re-branding the other. Put page tokens on body.');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('roadmap.css puts its three helpers on the body, scoped to the page', () => {
|
|
113
|
+
const body = rule(roadmapCss, 'body[data-page="roadmap"]');
|
|
114
|
+
assert.ok(body, 'roadmap.css declares its tokens on body[data-page="roadmap"]');
|
|
115
|
+
for (const t of ['--rm-plan', '--rm-terra-weak']) {
|
|
116
|
+
assert.match(body, new RegExp(`${t}\\s*:`), `${t} survived the move to body`);
|
|
117
|
+
}
|
|
118
|
+
assert.doesNotMatch(decomment(roadmapCss), /--rm-shadow-soft/,
|
|
119
|
+
'v3 separates by TONE — a panel is never a drawn box and never a shadow');
|
|
120
|
+
assert.doesNotMatch(decomment(roadmapCss), /box-shadow\s*:\s*(?!inset)[^;]*(?:rgba|0 )/,
|
|
121
|
+
'no raw shadow came back with it');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('the :root guard CATCHES a reintroduction and tolerates a lookalike', () => {
|
|
125
|
+
assert.match(decomment(':root { --x: 1px; }'), /(^|[},])\s*:root\s*\{/);
|
|
126
|
+
assert.doesNotMatch('html:root-ish { }', /(^|[},])\s*:root\s*\{/);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// ---- 2. the compat aliases are retired from the re-skinned sheets ----------
|
|
130
|
+
|
|
131
|
+
const ALIASES = ['--surface', '--surface-2', '--border', '--border-strong', '--accent-weak', '--accent-text'];
|
|
132
|
+
|
|
133
|
+
test('no re-skinned reading-room sheet spends a compat alias', () => {
|
|
134
|
+
const found = [];
|
|
135
|
+
for (const [name, css] of RESKINNED) {
|
|
136
|
+
for (const alias of ALIASES) {
|
|
137
|
+
// \b would match --surface inside --surface-2, so require the var() close
|
|
138
|
+
if (new RegExp(`var\\(\\s*${alias}\\s*[,)]`).test(decomment(css))) found.push(`${name}: ${alias}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
assert.deepEqual(found, [],
|
|
142
|
+
'the aliases exist for pages nobody has re-skinned; spending one here quietly opts the sheet back out of the world');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('the ranks page section of style.css spends the real names too', () => {
|
|
146
|
+
const card = rule(styleCss, '.rank-card');
|
|
147
|
+
assert.ok(card, '.rank-card has a rule');
|
|
148
|
+
for (const alias of ALIASES) {
|
|
149
|
+
assert.doesNotMatch(card, new RegExp(`var\\(\\s*${alias}\\s*[,)]`), `.rank-card no longer spends ${alias}`);
|
|
150
|
+
}
|
|
151
|
+
assert.match(card, /border:\s*var\(--line\)\s+solid\s+transparent/,
|
|
152
|
+
'the edge is reserved as transparent so a modifier can draw one — collab.css\'s .btn--quiet is the precedent');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('the alias guard would CATCH --surface without matching --surface-2', () => {
|
|
156
|
+
const re = new RegExp('var\\(\\s*--surface\\s*[,)]');
|
|
157
|
+
assert.ok(re.test('background: var(--surface);'));
|
|
158
|
+
assert.ok(!re.test('background: var(--surface-2);'));
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// ---- 3. --measure is the BODY measure, and nothing else --------------------
|
|
162
|
+
|
|
163
|
+
test('the room measures PROSE, and lets a table, a figure and a code block run wider', () => {
|
|
164
|
+
const measured = decomment(roomCss).match(/\.primer-article\s*>\s*p,[\s\S]*?\{([^}]*)\}/);
|
|
165
|
+
assert.ok(measured, 'room.css caps the prose children explicitly, not the card');
|
|
166
|
+
assert.match(measured[1], /max-width:\s*var\(--measure\)/);
|
|
167
|
+
const article = rule(roomCss, '.primer-article');
|
|
168
|
+
assert.ok(article, '.primer-article has a rule');
|
|
169
|
+
assert.doesNotMatch(article, /max-width/,
|
|
170
|
+
'the CARD is not capped at the measure — a table and a diagram may take its whole width');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test('no reading sheet does fresh `ch` arithmetic in prose', () => {
|
|
174
|
+
for (const [name, css] of RESKINNED) {
|
|
175
|
+
// A `ch` is the ZERO glyph's width, not a character: primer.css's 72ch
|
|
176
|
+
// rendered ~90. --measure was measured; a fresh number is a guess.
|
|
177
|
+
const hits = [...decomment(css).matchAll(/max-width:\s*(\d+(?:\.\d+)?)ch/g)].map((m) => m[0]);
|
|
178
|
+
assert.deepEqual(hits, [], `${name} caps a measure in raw ch — spend var(--measure)`);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test('the room head takes the display voice, and NOT the body measure', () => {
|
|
183
|
+
const h = rule(styleCss, '.page-head--room .page-head__h');
|
|
184
|
+
assert.ok(h, '.page-head--room .page-head__h has a rule');
|
|
185
|
+
assert.match(h, /font-family:\s*var\(--font-display\)/);
|
|
186
|
+
assert.doesNotMatch(h, /var\(--measure\)/,
|
|
187
|
+
'--measure is the BODY measure; on a display line it caps near 950px and the title runs the column');
|
|
188
|
+
const sub = rule(styleCss, '.page-head--room .page-head__sub');
|
|
189
|
+
assert.match(sub, /max-width:\s*var\(--measure\)/, 'the sub IS body prose, so it takes the measure');
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// ---- 4. the room's one breakpoint is 760, and the script mirrors it --------
|
|
193
|
+
|
|
194
|
+
test('the room and primer.js agree on 760px, exactly', () => {
|
|
195
|
+
assert.match(decomment(roomCss), /@media\s*\(max-width:\s*760px\)/,
|
|
196
|
+
'the room keeps its own breakpoint — narrowing it to the shell\'s 640 reopens the 641-760 band');
|
|
197
|
+
assert.match(primerJs, /matchMedia\('\(max-width:\s*760px\)'\)/,
|
|
198
|
+
'primer.js closes the contents at the same width the sheet collapses them');
|
|
199
|
+
assert.doesNotMatch(decomment(roomCss), /@media\s*\(max-width:\s*640px\)/,
|
|
200
|
+
'a second, narrower breakpoint in the room is the drift hall_audit exists to catch');
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// ---- 5. one markdown parser -----------------------------------------------
|
|
204
|
+
|
|
205
|
+
test('primer.js carries no parser of its own any more', () => {
|
|
206
|
+
for (const gone of ['function parseMarkdown', 'function splitTableRow', 'function renderInline', 'function slugify', 'function escapeHtml']) {
|
|
207
|
+
assert.ok(!primerJs.includes(gone),
|
|
208
|
+
`primer.js still defines ${gone} — that copy is md-reader.js's, and the copy is what dropped blockquotes`);
|
|
209
|
+
}
|
|
210
|
+
assert.match(primerJs, /window\.OTBMdReader/, 'primer.js renders through the shared reader');
|
|
211
|
+
assert.match(primerJs, /MD\.parseMarkdown\(md,\s*resolveLinkHref\)/, 'and passes its own link resolver into it');
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test('primer.html loads the reader BEFORE the page script', () => {
|
|
215
|
+
// Positions of the SCRIPT TAGS, not of the filenames: the page's own comments
|
|
216
|
+
// name both files, and an indexOf over the raw HTML finds the prose first.
|
|
217
|
+
const srcs = [...primerHtml.matchAll(/<script[^>]+src="([^"?]+)/g)].map((m) => m[1]);
|
|
218
|
+
const reader = srcs.indexOf('/builders/md-reader.js');
|
|
219
|
+
const page = srcs.indexOf('/builders/primer.js');
|
|
220
|
+
assert.ok(reader > -1, 'primer.html loads md-reader.js');
|
|
221
|
+
assert.ok(reader < page, 'both are deferred, so document order is execution order — the reader has to be first');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test('the primer keeps the one rewrite that is its own', () => {
|
|
225
|
+
assert.match(primerJs, /drachmae-and-karma\.md.*\/builders\/drachmae/s,
|
|
226
|
+
'the economy doc has an in-hall reader page (task 1472); a link to it must stay in the hall, not bounce to GitHub');
|
|
227
|
+
assert.match(primerJs, /baseDir:\s*PRIMER_DIR/, 'and resolves its relative links against docs/onboarding/');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test('the room styles what the shared parser emits that the old copy never did', () => {
|
|
231
|
+
assert.match(mdReaderJs, /<blockquote>/, 'md-reader.js emits blockquotes');
|
|
232
|
+
const bq = rule(roomCss, '.primer-article blockquote');
|
|
233
|
+
assert.ok(bq, 'and the room has a rule for them — the pair IS the fix; either half alone is a silent no-op');
|
|
234
|
+
assert.match(bq, /var\(--glass\)/);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test('primer.js still injects the diagrams — the one parser-adjacent job that is the primer\'s', () => {
|
|
238
|
+
assert.match(primerJs, /function inlineDiagrams/);
|
|
239
|
+
assert.match(primerJs, /DIAGRAM_AT_HEADING/);
|
|
240
|
+
assert.match(primerJs, /fig\.className = 'primer-figure'/,
|
|
241
|
+
'the injected figure takes the class diagram-viewer.js upgrades and room.css frames');
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// ---- 6. the soft gate is reachable ----------------------------------------
|
|
245
|
+
|
|
246
|
+
test('the acknowledge gate is aria-disabled, never disabled', () => {
|
|
247
|
+
assert.match(primerHtml, /id="primer-ack-btn"[^>]*aria-disabled="true"/,
|
|
248
|
+
'a real disabled button leaves the tab order, so a keyboard reader never hears why it is locked');
|
|
249
|
+
assert.doesNotMatch(primerHtml, /id="primer-ack-btn"[^>]*\sdisabled(?:[=\s>])/, 'and carries no hard disabled attribute');
|
|
250
|
+
assert.doesNotMatch(primerJs, /btn\.disabled\s*=/, 'nor does the script set one');
|
|
251
|
+
assert.match(primerJs, /setAttribute\('aria-disabled'/, 'the lock is the aria state');
|
|
252
|
+
const locked = rule(primerCss, '.primer-ack__btn[aria-disabled="false"]');
|
|
253
|
+
assert.ok(locked, 'the sheet paints the OPEN state off the same attribute the script writes');
|
|
254
|
+
assert.match(locked, /background:\s*var\(--accent\)/, 'the accent lights only what is pressable');
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test('the locked note is described to the control while it is locked, and dropped when it opens', () => {
|
|
258
|
+
assert.match(primerHtml, /aria-describedby="primer-ack-note-locked"/);
|
|
259
|
+
assert.match(primerJs, /removeAttribute\('aria-describedby'\)/,
|
|
260
|
+
'a note explaining a lock that is gone describes nothing');
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// ---- 7. the diagram viewer -------------------------------------------------
|
|
264
|
+
|
|
265
|
+
test('diagram-viewer.css carries no fallback for a token that cannot be missing', () => {
|
|
266
|
+
const hits = [...decomment(viewerCss).matchAll(/var\(\s*--[a-z-]+\s*,[^)]+\)/g)].map((m) => m[0]);
|
|
267
|
+
assert.deepEqual(hits, [],
|
|
268
|
+
'every one of these tokens ships in style.css, so the literal never rendered — it only encoded drift (the --ink fallback said #2a2a2a where the sheet ships #17181a)');
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test('the viewer spends the named z rung, not a hand-written 9999', () => {
|
|
272
|
+
const full = rule(viewerCss, '.dgv.is-fullscreen');
|
|
273
|
+
assert.ok(full, '.dgv.is-fullscreen has a rule');
|
|
274
|
+
assert.match(full, /z-index:\s*var\(--z-diagram-full\)/);
|
|
275
|
+
assert.doesNotMatch(decomment(viewerCss), /z-index:\s*\d/, 'no bare number is left in the sheet');
|
|
276
|
+
assert.match(decomment(styleCss), /--z-diagram-full:\s*\d+/, 'and the rung is named in the shell ladder');
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test('the fullscreen scroll lock is on the root, where the shell already locks', () => {
|
|
280
|
+
assert.match(decomment(viewerCss), /html\.dgv-fullscreen-open\s*\{\s*overflow:\s*hidden/);
|
|
281
|
+
assert.doesNotMatch(decomment(viewerCss), /body\.dgv-fullscreen-open/);
|
|
282
|
+
assert.match(viewerJs, /document\.documentElement\.classList\.toggle\('dgv-fullscreen-open'/,
|
|
283
|
+
'the script writes the class where the sheet reads it — the shell\'s own drawer uses html.drawer-open');
|
|
284
|
+
assert.match(decomment(styleCss), /html\.drawer-open/, 'and that is the convention it is matching');
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test('the room frames BOTH figures the viewer upgrades', () => {
|
|
288
|
+
assert.match(viewerJs, /figure\.diagram img, figure\.primer-figure img/,
|
|
289
|
+
'one selector, two classes — the viewer has always treated them as one object');
|
|
290
|
+
const frame = decomment(roomCss).match(/\.primer-figure,\s*figure\.diagram\s*\{([^}]*)\}/);
|
|
291
|
+
assert.ok(frame, 'and the room frames them in one rule, so they cannot drift apart');
|
|
292
|
+
assert.match(frame[1], /var\(--glass\)/);
|
|
293
|
+
// the canvas is inserted INSIDE the figure, so exactly one of the two draws
|
|
294
|
+
// the frame or the diagram lands in nested boxes
|
|
295
|
+
const inner = decomment(roomCss).match(/\.primer-figure \.dgv,\s*figure\.diagram \.dgv\s*\{([^}]*)\}/);
|
|
296
|
+
assert.ok(inner, 'the inserted canvas gives up its own frame');
|
|
297
|
+
assert.match(inner[1], /border:\s*0/);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
// ---- 8. every room has exactly one h1 -------------------------------------
|
|
301
|
+
|
|
302
|
+
const ROOMS = [['primer.html', primerHtml], ['diagrams.html', diagramsHtml], ['roadmap.html', roadmapHtml], ['ranks.html', ranksHtml]];
|
|
303
|
+
|
|
304
|
+
// An HTML comment that mentions <h1> is prose, not markup — and these pages each
|
|
305
|
+
// carry one explaining why the heading is there at all.
|
|
306
|
+
const dehtmlcomment = (html) => html.replace(/<!--[\s\S]*?-->/g, '');
|
|
307
|
+
|
|
308
|
+
test('every reading room renders exactly one h1, and it is the page head\'s', () => {
|
|
309
|
+
for (const [name, html] of ROOMS.map(([n, h]) => [n, dehtmlcomment(h)])) {
|
|
310
|
+
const h1s = [...html.matchAll(/<h1\b/g)];
|
|
311
|
+
assert.equal(h1s.length, 1, `${name} has exactly one <h1> — one-header-main-h1 measures the rendered DOM, not the happy path`);
|
|
312
|
+
assert.match(html, /<h1 class="page-head__h">/, `${name}'s h1 is the page head's`);
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test('the three DOCS rooms take the room voice; the deck takes the board\'s', () => {
|
|
317
|
+
for (const [name, html] of [['primer.html', primerHtml], ['diagrams.html', diagramsHtml], ['ranks.html', ranksHtml]]) {
|
|
318
|
+
assert.match(html, /class="page-head page-head--room"/, `${name} is a reading room`);
|
|
319
|
+
assert.match(html, /class="page-head__eyebrow"/, `${name} carries the room's eyebrow`);
|
|
320
|
+
}
|
|
321
|
+
assert.match(roadmapHtml, /class="page-head page-head--board"/,
|
|
322
|
+
'the roadmap is a working surface whose figures are the thing to look at, so its own name stays quiet');
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test('the document\'s own h1 is suppressed, or the article would render a second one', () => {
|
|
326
|
+
const h1 = rule(roomCss, '.primer-article h1');
|
|
327
|
+
assert.ok(h1, 'room.css handles the markdown h1');
|
|
328
|
+
assert.match(h1, /display:\s*none/);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
// ---- 9. the archetype's shape ---------------------------------------------
|
|
332
|
+
|
|
333
|
+
test('the room opts in from its markup, and the archetype sheet is what carries it', () => {
|
|
334
|
+
for (const [name, html] of [['primer.html', primerHtml], ['diagrams.html', diagramsHtml]]) {
|
|
335
|
+
assert.match(html, /<body class="room[^"]*"/, `${name} opts into the room`);
|
|
336
|
+
assert.match(html, /href="\/builders\/room\.css"/, `${name} loads the archetype`);
|
|
337
|
+
}
|
|
338
|
+
const tokens = rule(roomCss, 'body.room');
|
|
339
|
+
assert.ok(tokens, 'the room\'s composition tokens are on the opt-in body class');
|
|
340
|
+
assert.doesNotMatch(tokens, /--measure\s*:/,
|
|
341
|
+
'the measure is NOT redeclared: a ch scales with the font size, so one measured token reads the same at 15px and 17px');
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
test('the room\'s panels are the shell\'s three card levels, not a fourth', () => {
|
|
345
|
+
for (const id of ['primer-article', 'primer-skeleton', 'primer-error', 'primer-ack']) {
|
|
346
|
+
assert.match(primerHtml, new RegExp(`class="[^"]*scroll"[^>]*id="${id}"|id="${id}"[^>]*class="[^"]*scroll"`),
|
|
347
|
+
`#${id} is the shell's .scroll — v3 has exactly three card levels and a fourth is a design decision, not a local override`);
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
test('no page sheet reaches into shell layout', () => {
|
|
352
|
+
for (const [name, css] of RESKINNED) {
|
|
353
|
+
assert.doesNotMatch(decomment(css), /\.app-content\s*>\s*main/,
|
|
354
|
+
`${name} overrides shell layout — the rule tests/hall_record_world.mjs pins`);
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
test('no reading room carries an inline <style>, and none needs an audit exception', () => {
|
|
359
|
+
const withStyle = ROOMS.filter(([, html]) => /<style[^>]*>[\s\S]*?\S[\s\S]*?<\/style>/.test(html)).map(([n]) => n);
|
|
360
|
+
assert.deepEqual(withStyle, [],
|
|
361
|
+
'diagrams.html held the hall\'s last SUBSTANTIVE one — 16 lines, one over the default budget, with its own hall_audit exception. The exception retired with it.');
|
|
362
|
+
const audit = fs.readFileSync(path.join(ROOT, 'tests', 'hall_audit.mjs'), 'utf8');
|
|
363
|
+
const budgets = audit.match(/const INLINE_STYLE_BUDGET = \{([\s\S]*?)\};/);
|
|
364
|
+
assert.ok(budgets, 'hall_audit.mjs still declares the ratchet');
|
|
365
|
+
assert.doesNotMatch(budgets[1].replace(/\/\/[^\n]*/g, ''), /['"][a-z-]+\.html['"]\s*:/,
|
|
366
|
+
'no page carries an inline-style exception any more');
|
|
367
|
+
// (scouting.html keeps a one-line block, comfortably inside the DEFAULT
|
|
368
|
+
// budget — an exception is what this asserts is gone, not every <style>.)
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
test('roadmap\'s two sub-24px controls clear the floor, on both axes', () => {
|
|
372
|
+
const t = rule(roadmapCss, '.rm-goal__t');
|
|
373
|
+
assert.ok(t, '.rm-goal__t has a rule');
|
|
374
|
+
assert.match(t, /min-height:\s*24px/, 'measured 20px: a 13px line box in a 7px row');
|
|
375
|
+
const more = rule(roadmapCss, '.rm-gmore');
|
|
376
|
+
assert.ok(more, '.rm-gmore has a rule');
|
|
377
|
+
assert.match(more, /min-height:\s*24px/, 'measured 19px');
|
|
378
|
+
// the WIDTH half is the one that gets missed: at 320 the four fixed items in
|
|
379
|
+
// the row ate it and `flex: 1; min-width: 0` resolved the title to 0x26
|
|
380
|
+
assert.match(decomment(roadmapCss), /@media\s*\(max-width:400px\)[\s\S]*?\.rm-goal__t\s*\{[^}]*flex:\s*1 0 100%/,
|
|
381
|
+
'and the row wraps before the title can resolve to zero width');
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// ---- 10. the parser, actually run -----------------------------------------
|
|
385
|
+
|
|
386
|
+
// md-reader.js is a browser IIFE that only touches `window` and `location`, so
|
|
387
|
+
// a two-property sandbox is the whole DOM it needs.
|
|
388
|
+
function loadReader() {
|
|
389
|
+
const sandbox = { window: {}, location: { hostname: 'builders.example.com', href: 'https://builders.example.com/primer' } };
|
|
390
|
+
vm.createContext(sandbox);
|
|
391
|
+
vm.runInContext(mdReaderJs, sandbox, { filename: 'md-reader.js' });
|
|
392
|
+
return sandbox.window.OTBMdReader;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
test('the shared parser renders the blockquote the primer\'s old copy dropped', () => {
|
|
396
|
+
const MD = loadReader();
|
|
397
|
+
const { html } = MD.parseMarkdown('> a note the primer used to lose\n', (h) => h);
|
|
398
|
+
assert.match(html, /<blockquote><p>a note the primer used to lose<\/p><\/blockquote>/);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
test('it slugs h2s the way the table of contents and the in-doc anchors expect', () => {
|
|
402
|
+
const MD = loadReader();
|
|
403
|
+
const { html, headings } = MD.parseMarkdown('## The claim → ship lifecycle\n', (h) => h);
|
|
404
|
+
assert.equal(headings.length, 1);
|
|
405
|
+
assert.equal(headings[0].level, 2);
|
|
406
|
+
assert.equal(headings[0].id, MD.slugify('The claim → ship lifecycle'));
|
|
407
|
+
assert.match(html, new RegExp(`<h2 id="${headings[0].id}">`),
|
|
408
|
+
'the anchor in the markup and the id the TOC links to come from one call, so they cannot drift');
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
test('the primer\'s link resolver sends a repo-relative doc to GitHub and leaves the rest alone', () => {
|
|
412
|
+
const MD = loadReader();
|
|
413
|
+
const resolve = MD.makeResolveLinkHref({
|
|
414
|
+
baseDir: 'docs/onboarding/',
|
|
415
|
+
getBlobBase: () => 'https://github.com/o/r/blob/main',
|
|
416
|
+
getTreeBase: () => 'https://github.com/o/r/tree/main',
|
|
417
|
+
});
|
|
418
|
+
assert.equal(resolve('../../CLAUDE.md'), 'https://github.com/o/r/blob/main/CLAUDE.md');
|
|
419
|
+
assert.equal(resolve('../recipes/'), 'https://github.com/o/r/tree/main/docs/recipes');
|
|
420
|
+
assert.equal(resolve('#a-section'), '#a-section', 'an in-page anchor is left alone');
|
|
421
|
+
assert.equal(resolve('https://example.com/x'), 'https://example.com/x', 'so is an absolute URL');
|
|
422
|
+
assert.equal(resolve('diagrams'), '/builders/diagrams', 'and the diagrams index stays in the hall');
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
test('with no repo bases the resolver degrades instead of crashing the page', () => {
|
|
426
|
+
const MD = loadReader();
|
|
427
|
+
const resolve = MD.makeResolveLinkHref({ baseDir: 'docs/onboarding/' });
|
|
428
|
+
assert.equal(resolve('../../CLAUDE.md'), '../../CLAUDE.md',
|
|
429
|
+
'the bases are fetched at boot; before they land a relative href passes through and 404s harmlessly');
|
|
430
|
+
});
|