File size: 7,784 Bytes
0ade7b1 794b22f 0ade7b1 794b22f 0ade7b1 794b22f 0ade7b1 794b22f 0ade7b1 794b22f 0ade7b1 794b22f 0ade7b1 794b22f 0ade7b1 794b22f 0ade7b1 794b22f 0ade7b1 794b22f 0ade7b1 794b22f 0ade7b1 794b22f 0ade7b1 794b22f 0ade7b1 794b22f 0ade7b1 794b22f 0ade7b1 794b22f 0ade7b1 794b22f 0ade7b1 794b22f c3fdf10 0ade7b1 | 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 | #!/usr/bin/env node
// v0.7.2 - regression suite for the external-review findings.
// Every assertion maps to a reported bug or a fixed behavior.
import { parse, STATE_WORDS } from '../public/dssl/parser.js';
import { resolve } from '../public/dssl/resolver.js';
import { VERSION } from '../public/dssl/index.js';
import { stateReach, gatePass } from '../public/dssl/engine.js';
let pass = 0, fail = 0;
const ok = (cond, msg) => { if (cond) { pass++; } else { fail++; console.error('FAIL:', msg); } };
function compile(src) {
const { ast, errors } = parse(src, 't.dssl');
const doc = resolve(ast, 't.dssl', {});
return { doc, parseErrors: errors };
}
const declsOf = (doc, sel) => doc.rules.filter(r => r.sel === sel).flatMap(r => r.decls);
/* 1 - THE headline: nested state must sort AFTER the base rule */
{
const { doc } = compile('.card {\n background: #26201a\n hover { background: #c0392b }\n}');
const base = doc.rules.find(r => r.sel === '.card');
const hov = doc.rules.find(r => r.sel === '.card:hover');
ok(base && hov, 'nested hover: both rules exist');
ok(hov.order > base.order, 'nested hover sorts after base (cascade fix)');
ok(declsOf(doc, '.card').some(d => d.value === '#26201a'), 'base declaration survives');
ok(declsOf(doc, '.card:hover').some(d => d.value === '#c0392b'), 'hover declaration survives');
}
/* 1b - nested media keeps its condition AND sorts after base */
{
const { doc } = compile('.card {\n color: red\n @media dark { color: blue }\n}');
const dark = doc.rules.find(r => r.sel === '.card' && r.media.length);
ok(dark && dark.media.length === 1, 'nested @media keeps its condition');
const base = doc.rules.find(r => r.sel === '.card' && !r.media.length);
ok(dark && base && dark.order > base.order, 'nested @media sorts after base');
}
/* 1c - nested signal keeps its condition */
{
const { doc } = compile('.card {\n color: red\n @signal on(true) { color: blue }\n}');
const sig = doc.rules.find(r => r.sel === '.card' && r.signals.length);
ok(sig && sig.signals.length === 1, 'nested @signal keeps its condition');
}
/* 1d - nested layer keeps its layer */
{
const { doc } = compile('.card {\n color: red\n @layer utilities { color: blue }\n}');
const lay = doc.rules.find(r => r.sel === '.card' && r.layer === 'utilities');
ok(lay, 'nested @layer keeps its layer');
}
/* 1e - decls AFTER a nested block become a later fragment (later wins) */
{
const { doc } = compile('.card {\n color: red\n hover { color: green }\n color: blue\n}');
ok(declsOf(doc, '.card').some(d => d.value === 'red'), 'pre-block decl kept');
ok(declsOf(doc, '.card').some(d => d.value === 'blue'), 'post-block decl kept');
ok(declsOf(doc, '.card:hover').some(d => d.value === 'green'), 'nested decl kept');
}
/* 2 - raw hex alpha: #ff0000/50 is color-mix, never /50px */
{
const { doc } = compile('.card { background: #ff0000/50 }');
const v = declsOf(doc, '.card')[0]?.value;
ok(v === 'color-mix(in oklab, #ff0000 50%, transparent)', 'raw hex /50 becomes color-mix, got: ' + v);
}
/* 3 - time properties: bare numbers are ms */
{
const { doc } = compile('.card { transition-delay: 200; animation-duration: 0.5 }');
const d = declsOf(doc, '.card');
ok(d.some(x => x.prop === 'transition-delay' && x.value === '200ms'), 'transition-delay: 200 -> 200ms');
ok(d.some(x => x.prop === 'animation-duration' && x.value === '0.5ms' || x.prop === 'animation-duration' && x.value === '500ms'), 'animation-duration bare -> ms, got: ' + JSON.stringify(d));
}
/* 4 - grid shorthand: gap is a first-class word */
{
const { doc } = compile('.grid { grid: 3 gap 12 }');
const d = declsOf(doc, '.grid');
ok(d.some(x => x.prop === 'grid-template-columns' && x.value === 'repeat(3, 1fr)'), 'grid: 3 -> repeat(3, 1fr)');
ok(d.some(x => x.prop === 'gap' && x.value === '12px'), 'grid gap 12 -> gap: 12px');
const { doc: d2 } = compile('.grid { grid: 2 rows 3 gap 8 }');
const d2l = declsOf(d2, '.grid');
ok(d2l.some(x => x.prop === 'grid-template-rows' && x.value === 'repeat(3, 1fr)'), 'grid rows 3 -> template rows');
}
/* 5 - seen { } with arrow is a LOUD error pointing at the real patterns */
{
const { doc } = compile('.card { seen { opacity: 0 -> 1 } }');
ok(doc.errors.some(e => e.message.includes('->') && e.hint.includes('animate')), 'seen arrow -> loud error with animate hint');
const { doc: d2 } = compile('motion rise { from { opacity: 0 } }\n.card { seen { animate: rise 400 } }');
ok(d2.errors.length === 0, 'seen { animate } stays legal');
}
/* 6 - unknown nested state word warns instead of silently becoming a descendant */
{
const { doc } = compile('.card { hovr { background: red } }');
ok(doc.warnings.some(w => w.message.includes('hovr') && w.hint.includes('hover')), 'hovr typo warns with did-you-mean');
const { doc: d2 } = compile('.card { title { color: red } }');
ok(!d2.warnings.some(w => w.message.includes('title')), 'legit descendant word does not warn');
}
/* 7 - SVG + modern properties pass through */
{
const { doc } = compile('.card { fill: red; stroke: blue; stroke-width: 2; text-wrap: balance; content-visibility: auto }');
const d = declsOf(doc, '.card');
for (const [p, v] of [['fill', 'red'], ['stroke', 'blue'], ['stroke-width', '2px'], ['text-wrap', 'balance'], ['content-visibility', 'auto']]) {
ok(d.some(x => x.prop === p && x.value === v), `property ${p}: ${v} resolves`);
}
}
/* 8 - ::part() rides the shadow pipeline, ::slotted() is a loud error */
{
const { doc } = compile('my-card::part(header) { background: red }');
ok(doc.errors.length === 0, '::part() compiles');
ok(doc.shadowRules.some(r => r.sel === 'my-card' && r.pseudo.includes('part(header)')), '::part() rides shadowRules');
const { doc: d2 } = compile('my-card::slotted(p) { background: red }');
ok(d2.errors.some(e => e.message.includes('shadow tree')), '::slotted() gets the shadow-tree explanation');
}
/* 9 - one-line decls + nested block on the same line */
{
const { doc, parseErrors } = compile('.card { background: red; hover { background: blue } }');
ok(parseErrors.length === 0, 'one-line mix parses');
ok(declsOf(doc, '.card').some(d => d.value === 'red'), 'one-line: base decl survives');
ok(declsOf(doc, '.card:hover').some(d => d.value === 'blue'), 'one-line: hover decl survives');
}
/* 10 - stateReach: the hover scoping math */
{
ok(stateReach('.card:hover', 'hover') === null, 'subject-state reach = null (chain only)');
const d = stateReach('.card:hover .title', 'hover');
ok(d && d.prefix === '.card' && d.rest === '.title', 'descendant reach: prefix .card rest .title');
const s = stateReach('.tab:hover + .panel', 'hover');
ok(s && s.sib === '+' && s.firstSeg === '.panel', 'sibling reach: + .panel');
const f = stateReach('.card:has(:hover) .x', 'hover');
ok(f && f.fallback === true, ':has() wrapper falls back to page-wide superset');
const deep = stateReach('.list .card:hover .title', 'hover');
ok(deep && deep.prefix === '.list .card' && deep.rest === '.title', 'deep ancestor prefix');
}
/* 11 - gatePass still duck-types on fake elements (unchanged contract) */
{
ok(gatePass({ tagName: 'div', classList: { contains: () => true } }, null) === true, 'gatePass null gate = true');
}
/* 12 - version */
{
// v0.8.0 - pin loosened the same way v070's was: the suite must survive
// later releases, but catch an accidental DOWNGRADE below 0.7.2.
const [maj, min, pat] = String(VERSION).split('.').map(Number);
ok(maj === 0 && (min > 7 || (min === 7 && pat >= 2)), 'engine VERSION is >= 0.7.2 (got ' + VERSION + ')');
ok(STATE_WORDS.has('hover') && STATE_WORDS.has('seen'), 'STATE_WORDS exported');
}
console.log(`${pass} passed, ${fail} failed`);
process.exitCode = fail ? 1 : 0;
|