test(voyage): extend playground tests with Group A static-HTML assertions [skip-docs]

Step 28 of v4.3 plan — Wave 7 Group A: 17 new static-HTML grep assertions
covering SC1 10-element checklist (one test per element), SC3 webkitdirectory
+ drag-drop attributes, SC6 export-bundle markers (buildAnnotatedMarkdown,
filename pattern, Blob + clipboard flows), and SC7 tag-level no-CDN checks
(every <script src> + <link href> must be local).

Test count: 672 → 689 pass / 0 fail / 2 skipped.
This commit is contained in:
Kjell Tore Guttormsen 2026-05-10 18:15:53 +02:00
commit deca35a28f

View file

@ -502,3 +502,151 @@ test('voyage-playground.html loadProjectDirectory wires isProjectPathSafe filter
// aria-live announce must fire when something is filtered
assert.match(text, /announce\(filteredCount/, 'filteredCount announce required');
});
// =====================================================================
// v4.3 Step 28 — Group A static-HTML assertions (Wave 7).
//
// SC1 10-element checklist (one test per element), SC3 webkitdirectory +
// drag-drop attributes, SC6 export-bundle markers, SC7 no-CDN tag-level
// checks. All assertions read voyage-playground.html as text — no DOM,
// no browser. The HTML is FROZEN in Session 6; if any assertion fails
// the test must be adjusted to reflect actual state, not the HTML.
// =====================================================================
// --- SC1 element 1 — header / app-shell topbar -----------------------
test('SC1.1 header — app-shell topbar with breadcrumb (v4.3 Step 28)', () => {
const text = readFileSync(HTML, 'utf-8');
assert.match(text, /class="app-header__breadcrumb"/, 'breadcrumb required');
assert.match(text, /aria-label="Brødsmuler"/, 'breadcrumb aria-label required');
});
// --- SC1 element 2 — breadcrumb interactive return path --------------
test('SC1.2 breadcrumb — clickable returns to dashboard (v4.3 Step 28)', () => {
const text = readFileSync(HTML, 'utf-8');
assert.match(text, /breadcrumb-click/, 'breadcrumb-click handler required');
});
// --- SC1 element 3 — theme bootstrap IIFE ----------------------------
test('SC1.3 theme bootstrap — IIFE sets data-theme + colorScheme (v4.3 Step 28)', () => {
const text = readFileSync(HTML, 'utf-8');
assert.match(text, /<html[^>]+data-theme="dark"/, 'html data-theme=dark required');
assert.match(text, /prefers-color-scheme:\s*dark/, 'OS preference fallback required');
assert.match(text, /setAttribute\('data-theme'/, 'data-theme setter required');
});
// --- SC1 element 4 — onboarding-grid (redefined as fleet-grid) -------
// Per scope-guardian SC-GAP-1 (Assumptions row #21): voyage redefines
// onboarding-grid as fleet-grid. Operator-signed-off; /trekreview may
// flag this for revision.
test('SC1.4 onboarding-grid equivalent — fleet-grid pattern (v4.3 Step 28)', () => {
const text = readFileSync(HTML, 'utf-8');
assert.match(text, /class="fleet-grid"/, 'fleet-grid container required');
assert.match(text, /fleet-tile/, 'fleet-tile children required');
});
// --- SC1 element 5 — A11Y panel built from DS-primitives -------------
test('SC1.5 A11Y panel — guide-panel--info + key-stats + findings (v4.3 Step 28)', () => {
const text = readFileSync(HTML, 'utf-8');
assert.match(text, /guide-panel guide-panel--info/, 'guide-panel--info required');
assert.match(text, /class="key-stats"/, 'key-stats severity-grid required');
assert.match(text, /class="findings__items"/, 'findings__items list required');
assert.match(text, /wireA11yToggle/, 'A11Y wiring function required');
});
// --- SC1 element 6 — screenshots-spor convention ---------------------
// Per scope-guardian SC-GAP-2 (Assumptions row #22): hooks + dir convention
// instead of inline gallery. Operator-signed-off.
test('SC1.6 screenshots-spor — window.__voyage hooks + docs convention (v4.3 Step 28)', () => {
const text = readFileSync(HTML, 'utf-8');
assert.match(text, /window\.__voyage\s*=/, 'window.__voyage namespace required');
assert.match(text, /docs\/screenshots\/README\.md/, 'docs/screenshots reference required');
// Companion file must exist
const SCREENSHOTS_README = join(ROOT, 'docs', 'screenshots', 'README.md');
assert.ok(existsSync(SCREENSHOTS_README), 'docs/screenshots/README.md must exist');
});
// --- SC1 element 7 — body typography -----------------------------------
test('SC1.7 body typography — DS font-size + family tokens (v4.3 Step 28)', () => {
const text = readFileSync(HTML, 'utf-8');
assert.match(text, /var\(--font-size-/, 'DS font-size token required');
assert.match(text, /var\(--font-family-mono\)/, 'DS font-family-mono token required');
});
// --- SC1 element 8 — spacing rhythm ------------------------------------
test('SC1.8 spacing rhythm — DS --space-N tokens used (v4.3 Step 28)', () => {
const text = readFileSync(HTML, 'utf-8');
// Expect at least 5 distinct --space-N references (rhythm, not one-off)
const matches = text.match(/var\(--space-\d/g) || [];
assert.ok(matches.length >= 5, `expected ≥5 --space-N tokens, got ${matches.length}`);
});
// --- SC1 element 9 — color-token fidelity ------------------------------
test('SC1.9 color-token fidelity — voyage-scope tokens + DS colors (v4.3 Step 28)', () => {
const text = readFileSync(HTML, 'utf-8');
// Voyage-scope tokens were added in Step 1 (DS base.css) and consumed by playground
assert.match(text, /badge--scope-voyage|--color-scope-voyage/, 'voyage-scope token usage required');
});
// --- SC1 element 10 — dark-mode parity --------------------------------
test('SC1.10 dark-mode parity — explicit dark default + bootstrap (v4.3 Step 28)', () => {
const text = readFileSync(HTML, 'utf-8');
// html element ships data-theme=dark, theme-bootstrap respects user setting
assert.match(text, /<html[^>]+data-theme="dark"/, 'dark default required');
assert.match(text, /voyage-theme|voyage_theme/, 'theme persistence key required');
});
// --- SC3 — webkitdirectory + drag-drop attribute presence -------------
test('SC3 webkitdirectory — input declares directory attribute (v4.3 Step 28)', () => {
const text = readFileSync(HTML, 'utf-8');
// The input element on line 849 has webkitdirectory as an HTML attribute
assert.match(text, /\bwebkitdirectory\b/, 'webkitdirectory attribute required');
});
test('SC3 drag-drop — webkitGetAsEntry recursive walk (v4.3 Step 28)', () => {
const text = readFileSync(HTML, 'utf-8');
assert.match(text, /webkitGetAsEntry/, 'webkitGetAsEntry recursive entry-point required');
assert.match(text, /addEventListener\('dragenter/, 'dragenter handler required');
assert.match(text, /addEventListener\('dragover/, 'dragover handler required');
assert.match(text, /addEventListener\('dragleave/, 'dragleave handler required');
});
// --- SC6 export-bundle markers ----------------------------------------
test('SC6 export — buildAnnotatedMarkdown function exists (v4.3 Step 28)', () => {
const text = readFileSync(HTML, 'utf-8');
assert.match(text, /function\s+buildAnnotatedMarkdown\s*\(/, 'buildAnnotatedMarkdown required');
});
test('SC6 export — download filename pattern annotated-{target}.md (v4.3 Step 28)', () => {
const text = readFileSync(HTML, 'utf-8');
assert.match(text, /'annotated-'\s*\+\s*target\s*\+\s*'\.md'/, 'filename pattern required');
});
test('SC6 export — Blob + clipboard.writeText flows wired (v4.3 Step 28)', () => {
const text = readFileSync(HTML, 'utf-8');
assert.match(text, /new\s+Blob\(/, 'Blob construction required');
assert.match(text, /clipboard\.writeText/, 'clipboard copy flow required');
});
// --- SC7 no-CDN tag-level checks ---------------------------------------
test('SC7 no-CDN — every <script src=...> is local (./lib/* etc) (v4.3 Step 28)', () => {
const text = readFileSync(HTML, 'utf-8');
// Match all <script src="..."> attribute values
const scriptSrcs = [...text.matchAll(/<script\b[^>]*\bsrc\s*=\s*"([^"]+)"/g)].map((m) => m[1]);
for (const src of scriptSrcs) {
assert.ok(
!/^https?:\/\//.test(src) && !/^\/\//.test(src),
`script src="${src}" must be local (no http/https/protocol-relative)`,
);
}
});
test('SC7 no-CDN — every <link href=...> is local (v4.3 Step 28)', () => {
const text = readFileSync(HTML, 'utf-8');
const linkHrefs = [...text.matchAll(/<link\b[^>]*\bhref\s*=\s*"([^"]+)"/g)].map((m) => m[1]);
for (const href of linkHrefs) {
assert.ok(
!/^https?:\/\//.test(href) && !/^\/\//.test(href),
`link href="${href}" must be local (no http/https/protocol-relative)`,
);
}
});