import { test } from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync, readdirSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { dirname, join } from 'node:path'; // Guard against the route-CSS cascade-collision class of bug: every page stylesheet must keep all of // its rules scoped under that page's root class (e.g. `.sessions-page …`). Two pages defining the same // bare class (`.btn-action`) leak across each other depending on lazy-load/navigation order — the last // route visited wins the cascade. Scoping every rule under the page root makes leakage impossible. // This test fails the build if any page CSS rule is left unscoped, so the fix can never silently regress. const PAGES_DIR = join(dirname(fileURLToPath(import.meta.url)), 'pages'); function stripComments(css: string): string { return css.replace(/\/\*[\s\S]*?\*\//g, ''); } // Top-level (and @media-nested) selectors, excluding @keyframes/@font-face bodies. function selectors(css: string): string[] { const out: string[] = []; let i = 0; const n = css.length; while (i < n) { let j = i; while (j < n && css[j] !== '{' && css[j] !== '}') j++; if (j >= n) break; if (css[j] === '}') { i = j + 1; continue; } const header = css.slice(i, j).trim(); let depth = 1, k = j + 1; while (k < n && depth) { if (css[k] === '{') depth++; else if (css[k] === '}') depth--; k++; } const body = css.slice(j + 1, k - 1); if (/^@keyframes|^@font-face|^@page/.test(header)) { /* keyframe/font selectors are not class-scoped — skip */ } else if (/^@media|^@supports|^@container/.test(header)) { out.push(...selectors(body)); // recurse: inner rules must still be scoped } else if (header) { out.push(...header.split(',').map(s => s.trim()).filter(Boolean)); } i = k; } return out; } const files = readdirSync(PAGES_DIR).filter(f => f.endsWith('.css')); for (const file of files) { test(`${file}: every rule is scoped under its page root`, () => { const css = stripComments(readFileSync(join(PAGES_DIR, file), 'utf8')); const sels = selectors(css); assert.ok(sels.length > 0, `${file} produced no selectors (parse error?)`); // The page root is the first bare single-class selector (each page CSS opens with `.x-page { … }`). const root = sels.find(s => /^\.[a-zA-Z][\w-]*$/.test(s)); assert.ok(root, `${file}: could not find a root class rule`); const unscoped = sels.filter(s => !s.includes(root!)); assert.deepEqual( unscoped, [], `${file}: ${unscoped.length} selector(s) not scoped under "${root}" — move them under the page root ` + `(or into a global stylesheet) to avoid cross-page CSS collisions:\n ${unscoped.join('\n ')}`, ); }); }