/** #266: invariants of the styles split. */ import test from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { cardStyles, baseStyles, planStyles, devicesStyles, chromeStyles, dialogsStyles } from '../test-build/styles.js'; const FILES = ['base', 'plan', 'devices', 'chrome', 'dialogs']; const sourceOf = (name) => readFileSync(new URL(`../src/styles/${name}.styles.ts`, import.meta.url), 'utf8'); const selectorsOf = (tsSource) => { // Only the css`` payload: the TS import/export scaffolding is not CSS. const css = [...tsSource.matchAll(/css`([\s\S]*?)`/g)].map((m) => m[1]).join('\n'); const text = css.replace(/\/\*[\s\S]*?\*\//g, ''); const out = new Set(); const walk = (chunk, scope) => { let i = 0; while (i < chunk.length) { const open = chunk.indexOf('{', i); if (open === -1) break; const header = chunk.slice(i, open).replace(/\s+/g, ' ').trim(); let depth = 1, j = open + 1; while (j < chunk.length && depth > 0) { if (chunk[j] === '{') depth++; else if (chunk[j] === '}') depth--; j++; } if (header.startsWith('@media') || header.startsWith('@supports')) { walk(chunk.slice(open + 1, j - 1), `${scope}${header} :: `); } else if (!header.startsWith('@')) { // The key includes the media scope: the SAME selector inside and // outside a wrapper are different cascade entries by construction // (a multi-zone @media block lands in base as a whole). for (const sel of header.split(',')) out.add(`${scope}${sel.trim()}`); } i = j; } }; walk(text, ''); return out; }; test('issue 266 the aggregator is exactly the five surface files in the cascade order', () => { assert.deepEqual(cardStyles, [baseStyles, planStyles, devicesStyles, chromeStyles, dialogsStyles], 'the cascade order is a contract — the golden set was accepted against it'); }); test('issue 266 surface files do not share a single selector', () => { const sets = FILES.map((name) => ({ name, set: selectorsOf(sourceOf(name)) })); const clashes = []; for (let i = 0; i < sets.length; i++) { for (let j = i + 1; j < sets.length; j++) { for (const sel of sets[i].set) { if (sets[j].set.has(sel)) clashes.push(`${sel} (${sets[i].name} ∩ ${sets[j].name})`); } } } // Spec §1.3.2: every allowed exception is named with its reason. const allowed = [ // The `:host([data-pointer-hover]) .dev:hover, .dev:focus-visible` GROUP // is cross-surface (host gate + device selector) and therefore lives in // base per spec §1.1, while the plain `.dev:focus-visible` outline rule // stays with its surface. Their source order (group first, outline // second) is preserved by the [base, …, devices] assembly order — golden // holds the cascade. '.dev:focus-visible (base ∩ devices)', ]; assert.deepEqual(clashes, allowed); }); test('issue 266 the media wrappers survived the move', () => { const all = FILES.map((name) => sourceOf(name)).join('\n'); assert.equal((all.match(/@media \(forced-colors: active\)/g) || []).length, 2, 'both forced-colors blocks must survive — golden never emulates them'); assert.equal((all.match(/@media \(prefers-reduced-motion: reduce\)/g) || []).length, 10, 'all ten reduced-motion blocks must survive — golden always shoots reduced'); });