Claude Sonnet 5.5
Site and docs for 0.9.0; drop template leftovers; scripts without hard-coded paths
794b22f Download scripts/test-v072.mjs from EHS-Studio/dssl: direct link, hf CLI and curl.
- Browser
- Download file 7.78 kB
-
https://huggingface.co/EHS-Studio/dssl/resolve/main/scripts/test-v072.mjs
- Command line
-
hf download hf://EHS-Studio/dssl/scripts/test-v072.mjs
-
curl -L -o test-v072.mjs https://huggingface.co/EHS-Studio/dssl/resolve/main/scripts/test-v072.mjs
7.78 kB
| // 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; | |