// Gates over the CSS artifact itself, on a deliberately hostile synthetic page. // No server needed — these are properties of the bundle, not of the deployment. // // A CONTAINMENT every computed style OUTSIDE the wrapper is unchanged by the bundle // B FIDELITY styles INSIDE it DO change (a bundle that styles nothing passes A) // C NO INBOUND LEAK the site's element rules do not reach inside // D GLOSSARY .gloss-term still decorates deck text — the point of the projection // // Usage: node gates/artifact.mjs import { readFile } from 'node:fs/promises' import { join } from 'node:path' import { chromium, publicDir } from '../lib/paths.mjs' const id = process.argv[2] if (!id) { console.error('uso: node gates/artifact.mjs '); process.exit(2) } const bundle = await readFile(join(publicDir(id), 'deck.css'), 'utf8') // Bare element selectors with the kind of declarations a real content site ships. If any // of these reach inside the deck, C fails. const SITE_CSS = ` body { margin: 2rem; font-family: Georgia, serif; font-size: 18px; line-height: 1.6; color: #222; background: #fff; } h1 { font-size: 2rem; font-weight: 700; } h2 { font-size: 1.5rem; } p { margin: 1rem 0; } ul { list-style: disc; padding-left: 2rem; } code { font-family: monospace; background: #eee; padding: 2px 4px; border-radius: 3px; } pre { background: #eee; padding: 1rem; } a { color: #0645ad; text-decoration: underline; } td, th { border: 1px solid #999; padding: .4rem; } ` // Exactly as glossary-tooltip.js injects it. const TOOLTIP_CSS = '.gloss-term{background:none;border:0;padding:0;font:inherit;color:inherit;' + 'cursor:help;border-bottom:1px dotted currentColor;opacity:.95}' const DECK = `
` const page = (withDeck) => ` ${withDeck ? `` : ''}

Título del sitio

Texto con NCL y enlace.

${DECK}

Después del deck.

` const PROBES = ['p-h1', 'p-p', 'p-code', 'p-a', 'p-ul', 'p-li', 'p-after'] const PROPS = ['font-family', 'font-size', 'font-weight', 'line-height', 'color', 'background-color', 'margin-top', 'margin-bottom', 'padding-left', 'display', 'list-style-type', 'text-decoration-line', 'box-sizing', 'border-top-width'] const snap = (p) => p.evaluate(({ PROBES, PROPS }) => { const out = {} const grab = (el, key) => { if (!el) return const cs = getComputedStyle(el) out[key] = Object.fromEntries(PROPS.map(x => [x, cs.getPropertyValue(x)])) } PROBES.forEach(id => grab(document.getElementById(id), id)) grab(document.body, 'body') grab(document.documentElement, 'html') const d = document.querySelector('.deck-read') grab(d?.querySelector('h1'), 'DECK:h1') grab(d?.querySelector('p'), 'DECK:p') grab(d?.querySelector('code'), 'DECK:code') grab(d?.querySelector('pre'), 'DECK:pre') grab(d?.querySelector('li'), 'DECK:li') grab(d?.querySelector('td'), 'DECK:td') return out }, { PROBES, PROPS }) const browser = await chromium.launch() const p = await browser.newPage({ viewport: { width: 1200, height: 900 } }) await p.setContent(page(false), { waitUntil: 'load' }) const before = await snap(p) await p.setContent(page(true), { waitUntil: 'load' }) await p.waitForTimeout(500) const after = await snap(p) const outside = [], inside = [] for (const k of Object.keys(before)) { for (const prop of PROPS) { if (before[k]?.[prop] !== after[k]?.[prop]) { ;(k.startsWith('DECK:') ? inside : outside).push({ el: k, prop, de: before[k]?.[prop], a: after[k]?.[prop] }) } } } // Fingerprints of the site's own rules that must NOT survive inside the deck. const LEAKS = [ { el: 'DECK:code', prop: 'background-color', forbidden: 'rgb(238, 238, 238)', from: 'code{background:#eee}' }, { el: 'DECK:pre', prop: 'background-color', forbidden: 'rgb(238, 238, 238)', from: 'pre{background:#eee}' }, { el: 'DECK:li', prop: 'list-style-type', forbidden: 'disc', from: 'ul{list-style:disc}' }, { el: 'DECK:p', prop: 'font-family', forbidden: 'Georgia, serif', from: 'body{font-family:Georgia}' }, { el: 'DECK:td', prop: 'border-top-width', forbidden: '1px', from: 'td{border:1px}' }, ].filter(f => after[f.el] && after[f.el][f.prop] === f.forbidden) const gloss = await p.evaluate(() => { const g = id => { const cs = getComputedStyle(document.getElementById(id)) return cs.borderBottomStyle + '/' + cs.borderBottomWidth + '/' + cs.cursor } return { dentro: g('in'), fuera: g('out') } }) await browser.close() const A = outside.length === 0 const B = inside.length > 0 const C = LEAKS.length === 0 const D = gloss.dentro === gloss.fuera && gloss.dentro.startsWith('dotted') const ok = A && B && C && D console.log(JSON.stringify({ A_contencion: A ? 'PASS — 0 cambios fuera del wrapper' : `FAIL — ${outside.length} cambios fuera`, B_fidelidad: B ? `PASS — ${inside.length} props estiladas dentro` : 'FAIL — el deck no está estilado', C_sin_fuga_de_entrada: C ? 'PASS — ninguna regla del sitio alcanza el interior' : `FAIL — ${LEAKS.length} fugas`, D_glosario: D ? `PASS — decora dentro igual que fuera (${gloss.dentro})` : `FAIL — dentro ${gloss.dentro}, fuera ${gloss.fuera}`, ...(A ? {} : { cambiosFuera: outside.slice(0, 10) }), ...(C ? {} : { fugas: LEAKS }), }, null, 2)) process.exit(ok ? 0 : 1)