File size: 6,726 Bytes
0434220
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
/* test-v082.mjs: one assertion per change in 0.8.2.
 * Found while rebuilding the site on its own engine:
 *   1. "later wins" did not hold BETWEEN stylesheets. Each sheet numbers its
 *      rules from 0 and the cascade compared those numbers across sheets, so a
 *      later sheet lost to an earlier one that simply had more rules above the
 *      rule in question. Fixed by giving every rule one global position.
 *   2. a later tokens { } block could not override a token an earlier sheet
 *      set inside @media or @signal (same cause).
 *   3. fluid did not work on padding-top, margin-block and the other longhands.
 * The cascade tests use the engine's own sort (layer rank, then order) on rules
 * from real compiled sheets, so they check the thing that was wrong. */
import { assignGlobalOrder } from '../public/dssl/engine.js';
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 ? '; ' + extra : ''}`); }
};

const LAYER_RANK = { tokens: 0, base: 1, components: 2, utilities: 3, pin: 4 };
const cascade = (rules) => [...rules].sort((a, b) => (LAYER_RANK[a.layer] ?? 2) - (LAYER_RANK[b.layer] ?? 2) || a.order - b.order);
const bundleOf = (src) => { const { doc } = compile(src, 't.dssl'); return { rules: doc.rules, shadowRules: doc.shadowRules, raws: doc.raws || [] }; };
const winner = (rules, sel) => cascade(rules.filter((r) => r.sel === sel)).pop();
const val = (rule, prop) => rule.decls.filter((d) => d.prop === prop).pop()?.value;

/* ---- 1: later sheets win ---- */
{
  let filler = '';
  for (let i = 0; i < 30; i++) filler += `.zz${i} { color: green }\n`;
  const first = bundleOf(filler + '.x { color: red }');
  const second = bundleOf('.x { color: blue }');
  const before = winner([...first.rules, ...second.rules], '.x');
  ok('1a. the bug: without a global order the EARLIER sheet wins', val(before, 'color') === 'red', val(before, 'color'));

  const a = bundleOf(filler + '.x { color: red }');
  const b = bundleOf('.x { color: blue }');
  assignGlobalOrder([a, b]);
  const after = winner([...a.rules, ...b.rules], '.x');
  ok('1b. with a global order the LATER sheet wins', val(after, 'color') === 'blue', val(after, 'color'));

  const lastOfFirst = Math.max(...a.rules.map((r) => r.order));
  const firstOfSecond = Math.min(...b.rules.map((r) => r.order));
  ok('1c. every rule of a later sheet sorts after every rule of an earlier one', firstOfSecond > lastOfFirst, `${firstOfSecond} vs ${lastOfFirst}`);

  const snapshot = [...a.rules, ...b.rules].map((r) => r.order).join(',');
  assignGlobalOrder([a, b]);
  assignGlobalOrder([a, b]);
  ok('1d. it is idempotent: rebuilding does not make the numbers drift', [...a.rules, ...b.rules].map((r) => r.order).join(',') === snapshot);

  // replacing a sheet in place: new rules get numbered from the same slot
  const b2 = bundleOf('.x { color: purple }\n.y { color: teal }');
  assignGlobalOrder([a, b2]);
  ok('1e. a replaced sheet still sorts after the sheet before it', val(winner([...a.rules, ...b2.rules], '.x'), 'color') === 'purple');

  // three sheets in a row
  const c = bundleOf('.x { color: orange }');
  assignGlobalOrder([a, b2, c]);
  ok('1f. with three sheets the last one wins', val(winner([...a.rules, ...b2.rules, ...c.rules], '.x'), 'color') === 'orange');

  // within one sheet nothing changed: the later line still wins
  const one = bundleOf('.x { color: red }\n.x { color: green }');
  assignGlobalOrder([one]);
  ok('1g. inside one sheet the later rule still wins', val(winner(one.rules, '.x'), 'color') === 'green');

  // layers still outrank order: a utilities rule in an EARLY sheet beats a components rule in a late one
  const early = bundleOf('@layer utilities { .x { margin-top: 0 } }');
  const late = bundleOf('.x { margin-top: 30 }');
  assignGlobalOrder([early, late]);
  ok('1h. layers still outrank load order', val(winner([...early.rules, ...late.rules], '.x'), 'margin-top') === '0px');

  // shadow (pseudo-element) rules and raw blocks are renumbered with their sheet
  const sh1 = bundleOf('.a::before { content: "a" }');
  const sh2 = bundleOf('.a::before { content: "b" }');
  assignGlobalOrder([sh1, sh2]);
  ok('1i. pseudo-element rules follow the same global order', sh2.shadowRules[0].order > sh1.shadowRules[0].order);
}

/* ---- 2: a later tokens block beats an earlier conditional token ---- */
{
  const palettes = bundleOf(`tokens { color { accent: red } }
@media (prefers-color-scheme: dark) { tokens { color { accent: pink } } }`);
  const later = bundleOf('tokens { color { accent: blue } }');
  const tokenRules = (b) => b.rules.filter((r) => r.layer === 'tokens' && r.decls.some((d) => d.prop === '--dssl-color-accent'));
  const before = cascade([...tokenRules(palettes), ...tokenRules(later)]).pop();
  ok('2a. the bug: an earlier conditional token out-ranked a later tokens block', before.decls.some((d) => d.value === 'pink'));

  const p = bundleOf(`tokens { color { accent: red } }
@media (prefers-color-scheme: dark) { tokens { color { accent: pink } } }`);
  const l = bundleOf('tokens { color { accent: blue } }');
  assignGlobalOrder([p, l]);
  const after = cascade([...tokenRules(p), ...tokenRules(l)]).pop();
  ok('2b. now the later tokens block wins, even in dark mode', after.decls.some((d) => d.value === 'blue'));
}

/* ---- 3: fluid on the longhands ---- */
{
  const evalClamp = (css, vw) => {
    const m = /^clamp\(\s*(-?[\d.]+)px,\s*calc\(\s*(-?[\d.]+)px\s*\+\s*(-?[\d.]+)vw\s*\),\s*(-?[\d.]+)px\s*\)$/.exec(css);
    if (!m) return NaN;
    return Math.min(+m[4], Math.max(+m[1], +m[2] + (+m[3] * vw) / 100));
  };
  const near = (x, y) => Math.abs(x - y) < 0.01;
  for (const prop of ['padding-top', 'padding-bottom', 'padding-left', 'padding-right', 'padding-inline', 'padding-block', 'margin-top', 'margin-bottom', 'margin-left', 'margin-right', 'margin-inline', 'margin-block', 'letter-spacing', 'border-radius']) {
    const { doc, ok: good } = compile(`.a { ${prop}: fluid 8 24 }`);
    const v = doc.rules[0]?.decls.find((d) => d.prop === prop)?.value;
    ok(`3. ${prop}: fluid 8 24 is 8px at 360 and 24px at 1200`, good && near(evalClamp(v, 360), 8) && near(evalClamp(v, 1200), 24), v);
  }
  const still = compile('.a { padding: fluid 8 16 }');
  ok('3b. the shorthand still works', still.ok && /^clamp\(/.test(still.doc.rules[0].decls[0].value));
  const nope = compile('.a { opacity: fluid 0 1 }');
  ok('3c. fluid on a property that cannot take it is still an error, not silence', !nope.ok);
}

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