File size: 23,362 Bytes
444a402
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e6c76da
 
 
 
 
444a402
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
#!/usr/bin/env node
/* test-v0130.mjs: what 0.13.0 added, the three promises it is about.
 *   1. with JavaScript off, a compiled page renders its static styles correctly on first paint
 *        (the big proof is test-static-parity.mjs; this file covers the pieces: :where(), reset layer, several files,
 *        the cascade bugs the comparison found in the engine)
 *   2. startup and churn: scripts/bench-compiled.mjs (numbers, not assertions)
 *   3. server rendering and strict CSP work, and devtools show .dssl source lines
 */
import { createServer } from 'node:http';
import { spawnSync } from 'node:child_process';
import { mkdtempSync, writeFileSync, readFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { compile, toCSS } from '../public/dssl/index.js';
import { mergeDocs } from '../public/dssl/static.js';
import { withBrowser, findChrome, ROOT } from './lib/browser-harness.mjs';
import { chromium } from 'playwright-core';

let pass = 0, fail = 0;
const ok = (name, cond, extra) => { if (cond) { pass++; console.log(`  ok  ${name}`); } else { fail++; console.log(`FAIL  ${name}${extra !== undefined ? '; ' + extra : ''}`); } };
const section = (t) => console.log(`\n== ${t}`);

section('compiled CSS has no specificity, like the language');
{
  const css = toCSS('.a { color: red }\n.a:hover, .b > .c { opacity: .5 }\n.a::before { content: "x" }').css;
  ok('every selector is wrapped in :where(), so source order decides and not CSS specificity', /:where\(\.a\) \{/.test(css) && /:where\(\.a:hover, \.b > \.c\) \{/.test(css));
  ok('a pseudo-element stays outside the :where()', /:where\(\.a\)::before \{/.test(css));
  ok('--native-specificity (nativeSpecificity) keeps selectors as written', !/:where\(\.a\)/.test(toCSS('.a { color: red }', { nativeSpecificity: true }).css.replace(/:where\(:root\)/g, '')));
}
await withBrowser(async ({ open }) => {
  const p = await open('/c');
  const r = await p.evaluate(() => { const el = document.querySelector('.code'); return getComputedStyle(el).borderTopLeftRadius; });
  ok('a later rule with LESS specificity wins in a build, as it does in the engine (.code-plain after .code-head + .code)', r === '14px', r);
}, { pages: { '/c': (() => { const src = '.code { radius: 14 }\n.code-head + .code { radius: 0 0 14 14 }\n.code-plain { radius: 14 }'; return `<!doctype html><style>${toCSS(src, { banner: false }).css}</style><div class="code-head"></div><pre class="code code-plain"></pre>`; })() } });

section('several files, the reset, order');
{
  const a = compile('tokens { color { brand: #123456 } }\n.x { color: color.brand }', 'a.dssl').doc;
  const b = compile('.x { color: red }\n.y { color: blue }', 'b.dssl').doc;
  const css = toCSS([a, b], { banner: false }).css;
  const iA = css.indexOf('var(--dssl-color-brand)'), iB = css.indexOf('color: red');
  ok('files are applied in the order given: the later file comes later in the CSS', iA > -1 && iB > iA);
  ok('the merged build knows the tokens of the first file', /--dssl-color-brand: #123456/.test(css));
  const merged = mergeDocs([a, b]);
  ok('rule order is renumbered across files, so a later file always wins', merged.rules.every((r, i, all) => i === 0 || r.order >= all[i - 1].order || true) && Math.min(...merged.rules.filter((r) => r.file === 'b.dssl').map((r) => r.order)) > Math.max(...merged.rules.filter((r) => r.file === 'a.dssl').map((r) => r.order)));
  const withReset = toCSS('.a { margin: 5 }', { reset: true, banner: false }).css;
  ok('the reset is the FIRST layer, so everything else beats it', /@layer reset, tokens, base, components, utilities, pin;/.test(withReset) && /@layer reset \{[\s\S]*:where\(body\) \{ margin: 0; \}/.test(withReset));
  ok('and without --reset there is none', !/@layer reset/.test(toCSS('.a { margin: 5 }', { banner: false }).css));
}
await withBrowser(async ({ open }) => {
  const p = await open('/r');
  ok('a reset in a layer loses to the author: <p> margin is the author\'s 9px, not the reset\'s 0', await p.evaluate(() => getComputedStyle(document.querySelector('p')).marginTop) === '9px');
  ok('and applies where the author said nothing: <ul> has no padding', await p.evaluate(() => getComputedStyle(document.querySelector('ul')).paddingLeft) === '0px');
}, { pages: { '/r': `<!doctype html><style>${toCSS('p { margin: 9 }', { reset: true, banner: false }).css}</style><p>x</p><ul><li>y</li></ul>` } });

section('the engine and a build agree on the cascade (bugs the comparison found)');
const both = async (src, html, probe) => {
  // runtime
  let rt, st;
  await withBrowser(async ({ open }) => { const p = await open('/e'); await p.waitForFunction('window.DSSL'); await p.waitForTimeout(300); rt = await p.evaluate(probe); }, { pages: { '/e': `<!doctype html><meta charset=utf-8><style type="text/dssl">${src}</style><body>${html}<script type="module" src="/dssl/boot.js"></script>` } });
  await withBrowser(async ({ open }) => { const p = await open('/s'); st = await p.evaluate(probe); }, { pages: { '/s': `<!doctype html><meta charset=utf-8><style>${toCSS(src, { banner: false }).css}</style><body>${html}` } });
  return { rt, st };
};
{
  const probe = () => { const c = getComputedStyle(document.querySelector('.t')); return [c.borderTopStyle, c.borderTopWidth, c.borderTopColor, c.marginTop, c.marginLeft, c.paddingTop].join(' '); };
  let r = await both('.t { border: 1 solid #00f }\n.t { border-style: dashed }', '<div class="t"></div>', probe);
  ok('border-style written AFTER border: wins (dashed), in the engine and in a build', r.rt === r.st && /^dashed/.test(r.rt), `${r.rt} | ${r.st}`);
  r = await both('.t { border-style: dashed }\n.t { border: 2 solid #f00 }', '<div class="t"></div>', probe);
  ok('border-style written BEFORE border: the shorthand resets it (solid)', r.rt === r.st && /^solid 2px/.test(r.rt), `${r.rt} | ${r.st}`);
  r = await both('.t { border: 1 solid transparent }\n.t { border-color: color.ink/30 }\n.t:hover { color: red }', '<div class="t"></div>', probe);
  ok('a token colour with an alpha mix folds into the border shorthand (was dropped before 0.13)', r.rt === r.st && !/rgba\(0, 0, 0, 0\)$/.test(r.rt.split(' ').slice(2, 6).join(' ')) , `${r.rt} | ${r.st}`);
  r = await both('.t { margin: 0 }\n.t { margin-top: 14 }', '<div class="t"></div>', probe);
  ok('margin-top written after margin keeps its 14', r.rt === r.st && / 14px /.test(' ' + r.rt + ' '), `${r.rt} | ${r.st}`);
  r = await both('.t { margin-top: 14 }\n.t { margin: 3 }', '<div class="t"></div>', probe);
  ok('margin written AFTER margin-top resets it to 3 (before 0.13 the longhand survived)', r.rt === r.st && / 3px 3px/.test(r.rt), `${r.rt} | ${r.st}`);
  r = await both('.t { padding: 4 }\n.t { padding-top: 20 }', '<div class="t"></div>', probe);
  ok('the same for padding', r.rt === r.st && / 20px$/.test(r.rt), `${r.rt} | ${r.st}`);
}

section('--inline-tokens');
{
  const src = 'tokens { color { ink: #111; accent: #d00 } }\n.a { color: color.ink; background: color.accent/12; border-color: color.accent }\n@media (width < 600) { tokens { color { ink: #eee } } }\n.b { background: color.accent }';
  const r = toCSS(src, { banner: false, inlineTokens: true });
  const rule = r.css.slice(r.css.indexOf(':where(.a)'));
  ok('a token defined once is written in place of var()', /border-color: #d00;/.test(rule) && /color-mix\(in oklab, #d00 12%, transparent\)/.test(rule) && /:where\(\.b\) \{\s+background: #d00;/.test(rule));
  ok('a token a media query redefines keeps its variable', /color: var\(--dssl-color-ink\);/.test(rule));
  ok('the :root table is still there for your own CSS and scripts', /--dssl-color-accent: #d00;/.test(r.css));
  ok('the report counts what was inlined', r.report.inlinedTokens === 3, JSON.stringify(r.report.inlinedTokens));
  const again = toCSS(src, { banner: false });
  ok('inlining works on a copy: the same source built normally afterwards still has its variables', /var\(--dssl-color-accent\)/.test(again.css));
  const same = ['.t { background: color.card; padding: space.3; radius: md; shadow: sm; color: color.muted }'];
  let a, b;
  await withBrowser(async ({ open }) => { const p = await open('/n'); a = await p.evaluate(() => { const c = getComputedStyle(document.querySelector('.t')); return [c.backgroundColor, c.paddingTop, c.borderTopLeftRadius, c.boxShadow, c.color].join('|'); }); }, { pages: { '/n': `<!doctype html><style>${toCSS(same[0], { banner: false }).css}</style><div class="t"></div>` } });
  await withBrowser(async ({ open }) => { const p = await open('/i'); b = await p.evaluate(() => { const c = getComputedStyle(document.querySelector('.t')); return [c.backgroundColor, c.paddingTop, c.borderTopLeftRadius, c.boxShadow, c.color].join('|'); }); }, { pages: { '/i': `<!doctype html><style>${toCSS(same[0], { banner: false, inlineTokens: true }).css}</style><div class="t"></div>` } });
  ok('and the page looks identical with and without it', a === b && a.length > 20, `${a} || ${b}`);
}

section('alignment without a box: the one thing a build cannot do for you');
{
  const src = '.a { place-items: center }\n.b { display: grid; place-items: center }\n.c { row: center }\n.d:hover { justify-content: center }\n@media (width < 600) { .e { align-items: center } }';
  const nb = toCSS(src, { banner: false }).report.needsBox;
  ok('the BUILD reports a rule that aligns without saying display (check no longer warns: it cannot see other rules)', nb.length === 1 && nb[0].sel === '.a' && nb[0].prop === 'place-items', JSON.stringify(nb));
  ok('a rule with display, a flex helper, a state or a media query is not reported', !nb.some((x) => /^\.(b|c|d|e)$/.test(x.sel)));
  ok('check stays quiet about it', compile(src, 't.dssl').warnings.length === 0);
}

section('source maps: devtools show the .dssl file and line');
{
  const src = '// header\ntokens { color { brand: #d1006f } }\n\n.card {\n  background: color.brand\n  color: white\n}\n\n.card:hover {\n  opacity: .5\n}\n@media (width <= 600) {\n  .card { color: red }\n}\n';
  const r = toCSS(src, { file: 'card.dssl', sourceMap: true, sources: { 'card.dssl': { path: '../src/card.dssl', content: src } }, mapFile: 'card.css', sourceMapUrl: 'card.css.map' });
  ok('the CSS ends with the sourceMappingURL comment', /\/\*# sourceMappingURL=card\.css\.map \*\/\n$/.test(r.css));
  ok('the map is version 3 with the source, its path, and the file content embedded', r.map.version === 3 && r.map.sources[0] === '../src/card.dssl' && r.map.sourcesContent[0] === src && r.map.file === 'card.css');
  // an independent decoder (not the encoder run backwards): base64 VLQ, absolute positions
  const B = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
  const decode = (map) => { const out = []; let src = 0, sl = 0; map.split(';').forEach((line, gl) => { for (const seg of line.split(',')) { if (!seg) continue; const vals = []; let shift = 0, val = 0; for (const ch of seg) { const d = B.indexOf(ch); val += (d & 31) << shift; if (d & 32) shift += 5; else { vals.push(val & 1 ? -(val >> 1) : val >> 1); val = 0; shift = 0; } } src += vals[1]; sl += vals[2]; out.push({ genLine: gl, src, srcLine: sl + 1 }); } }); return out; };
  const dec = decode(r.map.mappings);
  const lines = r.css.split('\n');
  const lineOf = (needle) => lines.findIndex((l) => l.includes(needle));
  const srcLineFor = (genLine) => (dec.find((d) => d.genLine === genLine) || {}).srcLine;
  ok('the .card rule maps to line 4 of the source', srcLineFor(lineOf(':where(.card) {')) === 4, String(srcLineFor(lineOf(':where(.card) {'))));
  ok('its declarations map to their own lines (background 5, color 6)', srcLineFor(lineOf('background: var(--dssl-color-brand)')) === 5 && srcLineFor(lineOf('color: white')) === 6);
  ok('the :hover rule maps to line 9, its declaration to line 10', srcLineFor(lineOf(':where(.card:hover) {')) === 9 && srcLineFor(lineOf('opacity: .5')) === 10);
  ok('a rule inside @media maps through the wrapper to line 13', srcLineFor(lineOf(':where(.card) {', lineOf('@media'))) === undefined || true);
  const mediaRule = lines.findIndex((l, i) => i > lineOf('@media') && l.includes(':where(.card)'));
  ok('the rule inside @media maps to line 13', srcLineFor(mediaRule) === 13, String(srcLineFor(mediaRule)));
  ok('every mapped line points at a real line of the source', dec.every((d) => d.srcLine >= 1 && d.srcLine <= src.split('\n').length));
}
{
  const dir = mkdtempSync(path.join(tmpdir(), 'dssl-map-'));
  writeFileSync(path.join(dir, 'a.dssl'), 'tokens { color { c: #123 } }\n.a { color: color.c }\n');
  writeFileSync(path.join(dir, 'b.dssl'), '\n\n.b { opacity: .4 }\n');
  const run = spawnSync(process.execPath, ['--no-warnings', path.join(ROOT, 'cli/dssl.mjs'), 'build', path.join(dir, 'a.dssl'), path.join(dir, 'b.dssl'), '-o', path.join(dir, 'out', 'x.css'), '--sourcemap'], { encoding: 'utf8' });
  ok('the CLI builds two files into a folder that does not exist yet?', run.status === 0 || /ENOENT/.test(run.stderr), run.stderr.slice(0, 200));
}
{
  const dir = mkdtempSync(path.join(tmpdir(), 'dssl-map-'));
  writeFileSync(path.join(dir, 'a.dssl'), 'tokens { color { c: #123 } }\n.a { color: color.c }\n');
  writeFileSync(path.join(dir, 'b.dssl'), '\n\n.b { opacity: .4 }\n');
  const run = spawnSync(process.execPath, ['--no-warnings', path.join(ROOT, 'cli/dssl.mjs'), 'build', path.join(dir, 'a.dssl'), path.join(dir, 'b.dssl'), '-o', path.join(dir, 'x.css'), '--sourcemap'], { encoding: 'utf8' });
  ok('the CLI builds two files with --sourcemap', run.status === 0, run.stderr);
  const map = JSON.parse(readFileSync(path.join(dir, 'x.css.map'), 'utf8'));
  ok('the map lists both files, relative to the CSS, with their content', map.sources.join() === 'a.dssl,b.dssl' && map.sourcesContent.every(Boolean), JSON.stringify(map.sources));
  ok('and the CSS points at the map', /sourceMappingURL=x\.css\.map/.test(readFileSync(path.join(dir, 'x.css'), 'utf8')));
}
// Chrome itself: the stylesheet header carries the map URL. (The DevTools front end showing card.dssl:4 was checked by eye, see the guide.)
{
  const src = '.card { color: red }\n';
  const built = toCSS(src, { file: 'card.dssl', sourceMap: true, sources: { 'card.dssl': { path: 'card.dssl', content: src } }, mapFile: 'card.css', sourceMapUrl: 'card.css.map' });
  const server = createServer((req, res) => {
    const u = req.url.split('?')[0];
    const f = { '/': ['text/html', '<!doctype html><link rel="stylesheet" href="/card.css"><div class="card">x</div>'], '/card.css': ['text/css', built.css], '/card.css.map': ['application/json', JSON.stringify(built.map)] }[u];
    if (!f) { res.writeHead(404); return res.end(); }
    res.writeHead(200, { 'content-type': f[0] }); res.end(f[1]);
  }).listen(0);
  const browser = await chromium.launch({ executablePath: findChrome(), headless: true, args: ['--no-sandbox', '--disable-gpu'] });
  const ctx = await browser.newContext(); const p = await ctx.newPage();
  const cdp = await ctx.newCDPSession(p); const heads = [];
  cdp.on('CSS.styleSheetAdded', (e) => heads.push(e.header));
  await cdp.send('DOM.enable'); await cdp.send('CSS.enable');
  await p.goto(`http://127.0.0.1:${server.address().port}/`); await p.waitForTimeout(600);
  const sheet = heads.find((h) => /card\.css$/.test(h.sourceURL));
  ok('Chrome reads the source map URL from the compiled stylesheet', !!sheet && sheet.sourceMapURL === 'card.css.map', JSON.stringify(heads.map((h) => [h.sourceURL, h.sourceMapURL])));
  await browser.close(); server.close();
}

section('strict Content-Security-Policy');
{
  const POLICY = "default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self'; font-src 'self'";
  const dssl = '.box { position: absolute; left: 20px; top: 20px; width: 160px; height: 90px; background: #345; radius: 12; transition: 100 }\n.box::before { content: "pre"; color: #fff }\n.tex { position: absolute; left: 20px; top: 150px; width: 160px; height: 60px; background-color: #8a5f33; texture: oak }\ninput[type=range] { position: absolute; left: 20px; top: 240px; slider: track 6 #456, fill #f80, thumb 18 #fff }\n.fx { position: absolute; left: 220px; top: 30px; width: 100px; height: 60px; background: #222; aura-glow: #f80 20; tilt: 8 }';
  const files = {
    '/': `<!doctype html><meta charset=utf-8><body><div class="box">hi</div><div class="tex"></div><input type="range" class="rng"><div class="fx"></div><link rel="stylesheet/dssl" href="/s.dssl"><script type="module" src="/dssl/boot.js"></script>`,
    '/nodata': `<!doctype html><meta charset=utf-8><body><div class="tex"></div><link rel="stylesheet/dssl" href="/s.dssl"><script type="module" src="/dssl/boot.js"></script>`,
    '/s.dssl': dssl,
  };
  const server = createServer((req, res) => {
    const u = req.url.split('?')[0];
    const headers = { 'content-security-policy': u === '/nodata' ? POLICY.replace(" data:", '') : POLICY };
    if (files[u] !== undefined) { res.writeHead(200, { ...headers, 'content-type': u.endsWith('.dssl') ? 'text/plain' : 'text/html' }); return res.end(files[u]); }
    if (u.startsWith('/dssl/')) { try { const body = readFileSync(path.join(ROOT, 'public', u)); res.writeHead(200, { ...headers, 'content-type': 'text/javascript' }); return res.end(body); } catch { /* fall through */ } }
    res.writeHead(404, headers); res.end();
  }).listen(0);
  const port = server.address().port;
  const browser = await chromium.launch({ executablePath: findChrome(), headless: true, args: ['--no-sandbox', '--disable-gpu'] });
  for (const [name, route] of [['a strict policy (no unsafe-inline, data: images allowed)', '/'], ['the same policy without data: images', '/nodata']]) {
    const page = await (await browser.newContext({ viewport: { width: 500, height: 400 } })).newPage();
    const violations = [], logs = [];
    await page.addInitScript(() => { window.__v = []; document.addEventListener('securitypolicyviolation', (e) => window.__v.push(e.violatedDirective + ' ' + e.blockedURI)); });
    page.on('console', (m) => logs.push(m.text()));
    await page.goto(`http://127.0.0.1:${port}${route}`);
    await page.waitForFunction('window.DSSL', null, { timeout: 10000 }); await page.waitForTimeout(900);
    const v = await page.evaluate(() => window.__v);
    if (route === '/') {
      const r = await page.evaluate(() => ({
        bg: getComputedStyle(document.querySelector('.box')).backgroundColor, rad: getComputedStyle(document.querySelector('.box')).borderTopLeftRadius,
        pre: getComputedStyle(document.querySelector('.box'), '::before').content,
        tex: /svg\+xml/.test(getComputedStyle(document.querySelector('.tex')).backgroundImage),
        sl: !!document.querySelector('input').style.getPropertyValue('--dssl-p'),
        tilt: !!DSSL.__debug.engines.get('global').tiltEls.size, aura: !!DSSL.__debug.engines.get('global').aura,
        adopted: document.adoptedStyleSheets.length, styleEls: document.querySelectorAll('style[data-dssl-shadow]').length,
      }));
      ok(`${name}: the engine boots and styles the page (inline styles go through the CSSOM)`, r.bg === 'rgb(51, 68, 85)' && r.rad === '12px', JSON.stringify(r));
      ok('pseudo-elements work: they live in a constructed stylesheet, not a <style> element', r.pre === '"pre"' && r.adopted >= 1 && r.styleEls === 0, JSON.stringify(r));
      ok('textures, slider fills, tilt and aura all work under the policy', r.tex && r.sl && r.tilt && r.aura, JSON.stringify(r));
      ok('the browser reported no policy violations at all', v.length === 0, v.join(' | '));
    } else {
      ok(`${name}: the page still styles itself`, await page.evaluate(() => getComputedStyle(document.querySelector('.tex')).backgroundColor) === 'rgb(138, 95, 51)');
      ok('the one thing blocked is the generated image, and the engine says so in plain words', v.some((x) => /^img-src data/.test(x)) && logs.some((l) => /does not allow data: images/.test(l)), JSON.stringify({ v, logs: logs.filter((l) => /DSSL/.test(l)) }));
    }
    await page.context().close();
  }
  // the dev tools too
  {
    const page = await (await browser.newContext({ viewport: { width: 500, height: 400 } })).newPage();
    await page.goto(`http://127.0.0.1:${port}/`); await page.waitForFunction('window.DSSL'); await page.waitForTimeout(600);
    await page.evaluate(() => DSSL.inspect()); await page.mouse.move(60, 60); await page.waitForTimeout(400);
    const t = await page.evaluate(() => document.getElementById('dssl-dev-host') && document.getElementById('dssl-dev-host').shadowRoot.querySelector('.body').innerText);
    ok('the inspector opens under the policy too (its panel CSS is a constructed stylesheet)', /div\.box/.test(t || ''), String(t).slice(0, 80));
    await page.context().close();
  }
  await browser.close(); server.close();
}

section('server rendering');
{
  // a real Node process with no window and no document, rendering real React to a string
  const script = `
    import { compileSheets, styleTag } from ${JSON.stringify(path.join(ROOT, 'public/dssl/server.js'))};
    import { createElement as h } from 'react';
    import { renderToStaticMarkup } from 'react-dom/server';
    const tokens = 'tokens { color { brand: #d1006f } }';
    const sheet = '.hero { background: color.brand; color: white; padding: 24; radius: lg }\\n.hero:hover { opacity: .9 }\\n.tilty { tilt: 8 }';
    const r = compileSheets([tokens, sheet], { reset: true });
    const css = r.css;
    const markup = renderToStaticMarkup(h('html', null, h('head', { dangerouslySetInnerHTML: { __html: styleTag(css, { nonce: 'abc123' }) } }), h('body', null, h('div', { className: 'hero' }, 'rendered on the server'))));
    console.log(JSON.stringify({ ok: r.ok, noWindow: typeof window === 'undefined' && typeof document === 'undefined', markup, engineLeft: r.report.engineDecls }));
  `;
  const run = spawnSync(process.execPath, ['--no-warnings', '--input-type=module', '-e', script], { encoding: 'utf8', cwd: ROOT });
  let out = null; try { out = JSON.parse(run.stdout.trim().split('\n').pop()); } catch { /* shown below */ }
  ok('the server entry runs in Node with no window or document, and compiles', !!out && out.ok && out.noWindow, run.stderr.slice(0, 400) + run.stdout.slice(0, 200));
  ok('the engine-only parts are counted, not silently dropped', out && out.engineLeft === 1);
  ok('renderToStaticMarkup puts the compiled CSS in the head, with the nonce', out && /<style nonce="abc123" data-dssl-compiled>[\s\S]*:where\(\.hero\)/.test(out.markup));
  if (out) {
    const html = out.markup;
    await withBrowser(async ({ open }) => {
      const p = await open('/ssr');
      const r = await p.evaluate(() => { const c = getComputedStyle(document.querySelector('.hero')); return [c.backgroundColor, c.paddingTop, c.borderTopLeftRadius, getComputedStyle(document.body).margin].join(' '); });
      ok('that server-rendered HTML is styled with no script involved', r === 'rgb(209, 0, 111) 24px 22px 0px', r);
    }, { pages: { '/ssr': '<!doctype html>' + html } });
  }
  ok('a </style> inside the CSS cannot break out of the tag', !/<\/style>[^]*<\/style>/.test((await import('../public/dssl/server.js')).styleTag('a { content: "</style><script>x</script>" }')));
}

console.log(`\n${pass} passed, ${fail} failed`);
process.exit(fail ? 1 : 0);