dssl / scripts /test-v0163.mjs
EHS-Studio's picture
Claude Sonnet 5.5
v0.16.3: native DSSL spec, and the seven bugs writing it found
9da4a9b
Raw History Blame Contribute Delete
10.6 kB
#!/usr/bin/env node
/* test-v0163.mjs: seven known divergences fixed (D-1 to D-5, D-7, D-8): unquoted url() with //, token prefix matching,
* unknown @layer, undefined radius word, escaped quotes in values, selector lists when nesting, HTML element names
* that look like state typos. Pure Node, no browser. */
import { compile } from '../public/dssl/index.js';
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}`);
const run = (src) => compile(src, 't.dssl');
const sels = (src) => run(src).doc.rules.filter((r) => r.layer !== 'tokens').map((r) => r.sel);
const shadow = (src) => run(src).doc.shadowRules.map((r) => `${r.sel}::${r.pseudo}`);
const decl = (src, prop) => { for (const r of run(src).doc.rules) for (const d of r.decls) if (d.prop === prop) return d.value; return undefined; };
const J = JSON.stringify;
section('D-1. // inside an unquoted url()');
{
const r = run('.a { background: url(http://x.com/a.png) }');
ok('unquoted url with // compiles clean', r.errors.length === 0 && r.ok, J(r.errors));
ok('and keeps the whole url', decl('.a { background: url(http://x.com/a.png) }', 'background') === 'url(http://x.com/a.png)');
ok('a declaration after it on the next line survives', decl('.a { background: url(http://x.com/a.png)\n color: red }', 'color') === 'red');
ok('upper case URL( with spaces', run('.a { background: URL( http://x.com/a.png ) }').errors.length === 0);
ok('inside raw { } the closing brace is kept', run('raw { .a { background: url(http://x.com/a.png) } }\n.b { color: red }').errors.length === 0);
ok('quoted url still fine', run('.a { background: url("http://x.com/a.png") }').errors.length === 0 && decl('.a { background: url("http://x.com/a.png") }', 'background') === 'url("http://x.com/a.png")');
ok('a real comment after the url still works', decl('.a { background: url(a.png) // note\n color: red }', 'color') === 'red' && run('.a { background: url(a.png) // note\n color: red }').errors.length === 0);
ok('a // comment elsewhere still strips', run('.a { color: red } // trailing\n.b { color: blue }').errors.length === 0);
const e = run('.a { background: url(http://x.com/a.png)\n color: nonsense }').errors;
ok('line numbers do not drift after the url', e.length === 1 && e[0].line === 2, J(e));
}
section('D-2. token lookup is exact');
{
const src = 'tokens { color { ink2: #111 } }\n.a { color: color.ink2.bogus }';
const r = run(src);
ok('color.ink2.bogus is an unknown value', r.errors.length === 1 && /unknown value "color\.ink2\.bogus"/.test(r.errors[0].message), J(r.errors));
ok('the error carries file, line, message, hint keys', r.errors[0] && ['file', 'line', 'message', 'hint'].every((k) => k in r.errors[0]));
ok('color.ink2 itself still resolves', decl('tokens { color { ink2: #111 } }\n.a { color: color.ink2 }', 'color') === 'var(--dssl-color-ink2)');
ok('alpha on an exact token still works', /color-mix/.test(decl('tokens { color { ink2: #111 } }\n.a { color: color.ink2/50 }', 'color')));
ok('built-in tokens still resolve', run('.a { color: color.ink }').errors.length === 0);
}
section('D-3. unknown @layer warns');
{
const r = run('@layer weird { .a { color: red } }');
ok('warns, never errors', r.errors.length === 0 && r.ok && r.warnings.length === 1, J(r.warnings));
ok('message and hint', r.warnings[0].message === 'unknown layer "weird", treated as "components"' && r.warnings[0].hint === 'the layers are tokens, base, components, utilities, pin', J(r.warnings));
ok('warning has the usual shape', ['file', 'line', 'message', 'hint'].every((k) => k in r.warnings[0]) && r.warnings[0].line === 1);
ok('still compiled as components', r.doc.rules.find((x) => x.sel === '.a').layer === 'components');
const t = run('@layer compnents { .a { color: red } }');
ok('a near miss gets a did-you-mean', t.warnings.length === 1 && t.warnings[0].hint === 'components', J(t.warnings));
ok('known layers are silent', ['tokens', 'base', 'components', 'utilities', 'pin'].every((l) => run(`@layer ${l} { .a { color: red } }`).warnings.length === 0));
ok('pin { } is silent', run('pin { .a { color: red } }').warnings.length === 0);
}
section('D-4. radius word with no token');
{
const r = run('.a { radius: xl }');
ok('radius: xl warns', r.errors.length === 0 && r.ok && r.warnings.some((w) => w.message === 'radius "xl" is not defined, so it will not apply' && w.hint === 'add it with tokens { radius { xl: 32 } }'), J(r.warnings));
ok('same declaration is still emitted', decl('.a { radius: xl }', 'border-radius') === 'var(--dssl-radius-xl)');
ok('no default token was added', !('radius.xl' in run('.a { radius: xl }').doc.tokens));
ok('defining it silences the warning', run('tokens { radius { xl: 32 } }\n.a { radius: xl }').warnings.length === 0);
ok('sm, md, lg, full are silent', ['sm', 'md', 'lg', 'full'].every((w) => run(`.a { radius: ${w} }`).warnings.length === 0));
ok('a number is silent', run('.a { radius: 8 }').warnings.length === 0);
ok('a custom radius token is silent', run('tokens { radius { pill: 99 } }\n.a { radius: pill }').warnings.length === 0);
}
section('D-5. a backslash escapes a quote in a value');
{
const r = run('.a { content: "a\\"b" }');
ok('content: "a\\"b" has no error', r.errors.length === 0, J(r.errors));
ok('and is passed through', decl('.a { content: "a\\"b" }', 'content') === '"a\\"b"');
ok('single quotes too', run(".a { content: 'it\\'s' }").errors.length === 0);
ok('inside a function', run('.a { content: attr(x) "q\\"r" }').errors.length === 0 && run('.a { background: image-set("a\\"b.png" 1x) }').errors.length === 0);
ok('a plain string is unchanged', decl('.a { content: "ab" }', 'content') === '"ab"');
ok('an unknown word after an escaped string is still an error', run('.a { content: "a\\"b" nonsense }').errors.length === 1);
}
section('D-7. nesting under selector lists');
{
ok('.a, .b { .c { } }', J(sels('.a, .b { .c { color: red } }')) === J(['.a .c, .b .c']), J(sels('.a, .b { .c { color: red } }')));
ok('.a, .b { hover { } }', J(sels('.a, .b { hover { color: red } }')) === J(['.a:hover, .b:hover']), J(sels('.a, .b { hover { color: red } }')));
ok('.a { .c, .d { } }', J(sels('.a { .c, .d { color: red } }')) === J(['.a .c, .a .d']), J(sels('.a { .c, .d { color: red } }')));
ok('.a, .b { .c, .d { } } is parent-major', J(sels('.a, .b { .c, .d { color: red } }')) === J(['.a .c, .a .d, .b .c, .b .d']), J(sels('.a, .b { .c, .d { color: red } }')));
ok('.a, .b { &.on { } }', J(sels('.a, .b { &.on { color: red } }')) === J(['.a.on, .b.on']), J(sels('.a, .b { &.on { color: red } }')));
ok('.a, .b { & > .c { } }', J(sels('.a, .b { & > .c { color: red } }')) === J(['.a > .c, .b > .c']));
ok('.a { .x &, & + & { } } substitutes every &', J(sels('.a { .x &, & + & { color: red } }')) === J(['.x .a, .a + .a']), J(sels('.a { .x &, & + & { color: red } }')));
ok('state inside a nested list child', J(sels('.a, .b { .c { hover { color: red } } }')) === J(['.a .c:hover, .b .c:hover']));
ok('a comma inside :is( ) is NOT split (parent)', J(sels('.a:is(.x, .y) { .c { color: red } }')) === J(['.a:is(.x, .y) .c']));
ok('a comma inside :is( ) is NOT split (child)', J(sels('.a { .c:is(.x, .y) { color: red } }')) === J(['.a .c:is(.x, .y)']));
ok('a comma inside [ ] is NOT split', J(sels('.a { [data-x="1,2"] { color: red } }')) === J(['.a [data-x="1,2"]']));
ok('a state on :is( ) parent', J(sels('.a:is(.x, .y) { hover { color: red } }')) === J(['.a:is(.x, .y):hover']));
ok('plain .a { .b { } } is unchanged', J(sels('.a { .b { color: red } }')) === J(['.a .b']));
ok('plain & is unchanged', J(sels('.a { &.on { color: red } }')) === J(['.a.on']));
ok('plain state is unchanged', J(sels('.a { hover { color: red } }')) === J(['.a:hover']));
ok('deep plain nesting is unchanged', J(sels('.a { .b { .c { color: red } } }')) === J(['.a .b .c']));
ok('no errors for any of it', run('.a, .b { .c, .d { hover { color: red } } }').errors.length === 0);
const sh = shadow('.a, .b { ::before { content: ""\n color: red } }');
ok('.a, .b { ::before { } } gives one shadow rule per part', J(sh) === J(['.a::before', '.b::before']), J(sh));
const sh2 = shadow('.a, .b { ::before { content: ""\n hover { color: red } } }');
ok('and a state inside it follows each part', J(sh2) === J(['.a::before', '.a:hover::before', '.b::before', '.b:hover::before']), J(sh2));
ok('plain .a { ::before { } } is unchanged', J(shadow('.a { ::before { content: "" } }')) === J(['.a::before']));
ok('the shadow rules do not share declaration objects', (() => { const r = run('.a, .b { ::before { content: "" } }').doc.shadowRules; return r.length === 2 && r[0].decls !== r[1].decls && r[0].id !== r[1].id; })());
ok('a top-level list of pseudo-elements is unchanged', J(shadow('.a::before, .b::before { content: "" }')) === J(['.a::before', '.b::before']));
}
section('D-8. HTML element names are not state typos');
{
const w = (src) => run(src).warnings.filter((x) => /compiled as a descendant selector/.test(x.message));
ok('li under a rule does not warn', w('.menu { li { color: red } }').length === 0);
const names = 'a abbr address article aside audio b blockquote body br button canvas caption code col details dialog div em fieldset figure footer form h1 h2 h3 h4 h5 h6 header hr html i iframe img input label legend li main mark menu nav ol optgroup option p pre section select small span strong sub summary sup table tbody td textarea tfoot th thead time tr u ul video'.split(' ');
const noisy = names.filter((n) => w(`.x { ${n} { color: red } }`).length);
ok(`none of ${names.length} element names warn`, noisy.length === 0, noisy.join(','));
ok('and they still compile as descendants', J(sels('.menu { li { color: red } }')) === J(['.menu li']));
ok('hove still warns', w('.a { hove { color: red } }').length === 1 && /"hover"/.test(w('.a { hove { color: red } }')[0].hint));
ok('focs still warns', w('.a { focs { color: red } }').length === 1 && /"focus"/.test(w('.a { focs { color: red } }')[0].hint));
ok('warnings never become errors', run('.a { hove { color: red } }').errors.length === 0 && run('.a { hove { color: red } }').ok);
ok('a custom word far from any state is silent', w('.a { widget { color: red } }').length === 0);
}
console.log(`\n${pass} passed, ${fail} failed`);
process.exit(fail ? 1 : 0);