diff options
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/gallery-probes/audit-extraction.mjs | 77 | ||||
| -rw-r--r-- | tests/gallery-probes/probe.mjs | 19 |
2 files changed, 96 insertions, 0 deletions
diff --git a/tests/gallery-probes/audit-extraction.mjs b/tests/gallery-probes/audit-extraction.mjs new file mode 100644 index 0000000..3a69f2b --- /dev/null +++ b/tests/gallery-probes/audit-extraction.mjs @@ -0,0 +1,77 @@ +// audit-extraction.mjs — report-only grader for the extraction-readiness bar. +// +// Every DUPRE builder should be liftable out of the gallery without archaeology: +// a contract comment documenting opts + handle, its CSS in DUPRE_CSS rather than +// the page stylesheet, and no page code reaching past a handle into instrument +// DOM. This audit is static (no browser): it reads the sources and reports a +// worklist. It gates nothing — the gate flips on only when the sweep reaches +// 100% (the VSTATUS/policy ratchet pattern). Exit 0 always, unless the audit +// itself cannot parse the sources. +// +// Checks: +// C1 contract comment — the block comment directly above DUPRE.<name> = +// function mentions both opts and the handle (the split-flap shape). +// C2 page-styled internals — class selectors in the page <style> that also +// appear in DUPRE_CSS (the page overriding component internals). +// C3 page reach-ins — page script querySelectors descending into a card's +// stage (past the handle) instead of using the .dupre surface. +// +// Usage: node audit-extraction.mjs [--verbose] + +import { readFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = join(dirname(fileURLToPath(import.meta.url)), '..', '..'); +const kitSrc = readFileSync(join(root, 'docs/prototypes/widgets.js'), 'utf8'); +const page = readFileSync(join(root, 'docs/prototypes/panel-widget-gallery.html'), 'utf8'); +const verbose = process.argv.includes('--verbose'); + +// ---- C1: contract comments ------------------------------------------------- +// Builders are `DUPRE.<name> = function`; the comment block that ends directly +// above (allowing blank lines) is its documentation. +const builders = []; +const re = /DUPRE\.(\w+)\s*=\s*function/g; +for (let m; (m = re.exec(kitSrc)); ) { + const name = m[1]; + const head = kitSrc.slice(0, m.index); + // Only the LAST comment block directly above counts — a lazy match from the + // file start would hand every builder the whole file's text and grade + // everything compliant (it did, on the audit's first run). + const at = head.lastIndexOf('/*'); + const tail = at >= 0 ? head.slice(at) : ''; + const comment = /^\/\*[\s\S]*\*\/\s*$/.test(tail) ? tail : ''; + const contract = /\bopts\b/i.test(comment) && /\bhandle\b/i.test(comment); + builders.push({ name, contract, commentLen: comment.length }); +} + +// ---- C2: page-styled instrument internals ------------------------------------- +const classSels = src => new Set([...src.matchAll(/\.([a-zA-Z][\w-]*)/g)].map(x => x[1])); +const cssBlock = kitSrc.match(/DUPRE_CSS\s*=\s*\[([\s\S]*?)\]\.join|DUPRE_CSS\s*=\s*`([\s\S]*?)`/); +const dupreCss = cssBlock ? (cssBlock[1] || cssBlock[2]) : ''; +if (!dupreCss) { console.error('AUDIT ERROR: DUPRE_CSS not found'); process.exit(1); } +const pageStyle = [...page.matchAll(/<style>([\s\S]*?)<\/style>/g)].map(x => x[1]).join('\n'); +const dupreClasses = classSels(dupreCss); +// Rig and page chrome legitimately live in the page; only instrument-internal +// classes (defined in DUPRE_CSS) styled by the page are violations. +const pageOverrides = [...classSels(pageStyle)].filter(c => dupreClasses.has(c)); + +// ---- C3: page reach-ins ---------------------------------------------------- +// Page script (inline <script> bodies only; widgets.js is external). +const pageScript = [...page.matchAll(/<script>([\s\S]*?)<\/script>/g)].map(x => x[1]).join('\n'); +const RIG = /\.(famchips|fc|fgroup|lab|opts|wrd|stagew|winfo|wnote|wname|no)\b/; +const reachIns = [...pageScript.matchAll(/querySelector(?:All)?\(\s*['"](#card-[\w-]+\s+[^'"]+)['"]/g)] + .map(x => x[1]).filter(sel => !RIG.test(sel)); + +// ---- report ---------------------------------------------------------------- +const good = builders.filter(b => b.contract); +console.log(`extraction audit — ${builders.length} builders`); +console.log(`C1 contract comments: ${good.length}/${builders.length} compliant`); +if (verbose || true) { + const bad = builders.filter(b => !b.contract).map(b => b.name); + if (bad.length) console.log(` missing (${bad.length}): ${bad.join(' ')}`); +} +console.log(`C2 page-styled instrument internals: ${pageOverrides.length}${pageOverrides.length ? ' — ' + [...new Set(pageOverrides)].join(' ') : ' (clean)'}`); +console.log(`C3 page reach-ins past handles: ${reachIns.length}${reachIns.length ? '' : ' (clean)'}`); +reachIns.forEach(r => console.log(' ' + r)); +process.exit(0); diff --git a/tests/gallery-probes/probe.mjs b/tests/gallery-probes/probe.mjs index d9ba22f..4892188 100644 --- a/tests/gallery-probes/probe.mjs +++ b/tests/gallery-probes/probe.mjs @@ -1109,6 +1109,25 @@ try { })()`); ok('N20 chips are miniature flap cells: board colour, ink letter, real face', n20g === 'ok', n20g); + // N24 jewels — the handle reads and drives lamp state. Waybar-bound: a + // consumer must be able to set a jewel without synthesizing clicks. + const jw = await evl(`(() => { + const h = document.getElementById('card-N24').dupre; + if (!h.get || !h.set) return 'no get/set on handle'; + const init = h.get().join(','); + if (init !== '0,1,2,-1') return 'initial ' + init; + h.set(3, 1); + if (h.get()[3] !== 1) return 'set(3,1) not reflected: ' + h.get().join(','); + const el = document.querySelectorAll('#card-N24 .dupre-jewel')[3]; + if (!el) return 'no .dupre-jewel elements'; + if (el.classList.contains('dupre-dim')) return 'jewel 3 still dark after set(3,1)'; + h.set(3, -1); + if (!el.classList.contains('dupre-dim')) return 'set(3,-1) did not dark'; + if (h.get()[3] !== -1) return 'get after dark: ' + h.get().join(','); + return 'ok'; + })()`); + ok('N24 jewels handle exposes get/set', jw === 'ok', jw); + // Under prefers-reduced-motion the board paints once and RESTS — the dwell // chain must stay gated off, or the panel self-advances every 2s forever. await send('Emulation.setEmulatedMedia', { features: [{ name: 'prefers-reduced-motion', value: 'reduce' }] }); |
