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;