File size: 8,249 Bytes
c3fdf10 794b22f c3fdf10 794b22f c3fdf10 794b22f c3fdf10 794b22f c3fdf10 794b22f c3fdf10 794b22f c3fdf10 794b22f c3fdf10 794b22f c3fdf10 794b22f c3fdf10 794b22f c3fdf10 794b22f c3fdf10 794b22f c3fdf10 794b22f c3fdf10 794b22f c3fdf10 444a402 c3fdf10 794b22f c3fdf10 | 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 | #!/usr/bin/env node
/* test-guide-browser.mjs: DOM-level tests for the docs.
*
* The between-center bug compiled clean and laid out wrong, which is exactly
* the gap "it compiles" cannot cover. This boots the REAL engine in headless
* Chrome, applies every GUIDE.md example through the documented
* <script type="text/dssl"> path, and asserts the computed styles the guide
* promises. It uses scripts/lib/browser-harness.mjs, so any installed Chrome
* or Chromium will do. */
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { withBrowser, ROOT } from './lib/browser-harness.mjs';
// the GUIDE.md blocks, verbatim, applied through the auto-boot path
const guide = readFileSync(join(ROOT, 'GUIDE.md'), 'utf8');
// The 0.9.0 chapters (texture, aura, slider) have their own suites, test-browser-v090.mjs and the
// designer tests. Their examples reuse names like .card on purpose, and later wins, so they would
// override the original chapters this file asserts on. Keep this file on the original language.
const blocks = [...guide.matchAll(/```dssl\n([\s\S]*?)```/g)].map(m => m[1]).filter((b) => !/\b(texture|aura|aura-glow|aura-particle|glow-particle|slider)\s*:|behavior (fire|ki|embers|electric|snow|old-leather)\b/.test(b));
if (blocks.length < 5) { console.error('expected at least 5 guide blocks, found ' + blocks.length); process.exit(1); }
const html = `<!doctype html><html><head><meta charset="utf8">
<style>body { margin: 0 } .board { width: 600px; container-type: inline-size } .gallery { width: 400px }</style>
</head><body>
<div class="card">card</div>
<div class="toolbar"><span>a</span><span>b</span></div>
<div class="sidebar"><span>a</span><span>b</span></div>
<div class="board"><span>1</span><span>2</span><span>3</span><span>4</span></div>
<div class="gallery"><span>1</span><span>2</span><span>3</span></div>
<div class="modal"><span>mid</span></div>
<div class="hero"><span>hero</span></div>
${blocks.map(b => `<script type="text/dssl">\n${b}\n</script>`).join('\n')}
<script type="module">
import '/dssl/index.js';
</script>
</body></html>`;
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 : ''}`); }
};
await withBrowser(async ({ open }) => {
const page = await open('/guide', { wait: 'domcontentloaded' });
await page.setViewportSize({ width: 1200, height: 900 });
const consoleErrors = [];
page.on('console', m => { if (m.type() === 'error' || m.type() === 'warning') consoleErrors.push(m.type() + ': ' + m.text()); });
page.on('pageerror', e => consoleErrors.push('pageerror: ' + e.message));
await page.reload({ waitUntil: 'domcontentloaded' });
await page.waitForFunction(() => window.DSSL && window.__dsslBooted, { timeout: 8000 }).catch(() => {});
await page.waitForTimeout(600); // IntersectionObserver + animations settle
const cs = (sel, prop, pseudo) => page.evaluate(([sel, prop, pseudo]) => {
const el = document.querySelector(sel);
if (!el) return null;
return getComputedStyle(el, pseudo || null).getPropertyValue(prop).trim();
}, [sel, prop, pseudo]);
/* block 1: tokens + the card base. The guide COMPOSES: block 3 (the cascade example) sets
* .card { background: #26201a; shadow: sm } LATER, and block 4 adds @media 768 { padding: 24 }.
* Later wins across stylesheets (0.8.2 fixed that; before it, an earlier block could win just
* by being longer), so those later values are the ones a visitor sees at 1200px wide. */
ok('B1 the later cascade block sets the card background (#26201a)', (await cs('.card', 'background-color')) === 'rgb(38, 32, 26)', await cs('.card', 'background-color'));
ok('B1 card padding at 1200px is block 4\'s @media 768 value (24px)', (await cs('.card', 'padding')) === '24px', await cs('.card', 'padding'));
ok('B1 card radius is radius.md (14px), from block 1', (await cs('.card', 'border-radius')) === '14px', await cs('.card', 'border-radius'));
ok('B1 the later block\'s shadow: sm wins over block 1\'s md', (await cs('.card', 'box-shadow')).includes('0px 1px 2px'), await cs('.card', 'box-shadow'));
/* block 2 - the layout words (the between-center bug lives here) */
ok('B2 toolbar justify-content space-between (the review bug)', (await cs('.toolbar', 'justify-content')) === 'space-between', await cs('.toolbar', 'justify-content'));
ok('B2 toolbar align-items center survives the same line', (await cs('.toolbar', 'align-items')) === 'center', await cs('.toolbar', 'align-items'));
ok('B2 toolbar is flex row with 8px gap', (await cs('.toolbar', 'display')) === 'flex' && (await cs('.toolbar', 'gap')) === '8px');
ok('B2 sidebar is a column with 16px gap', (await cs('.sidebar', 'flex-direction')) === 'column' && (await cs('.sidebar', 'gap')) === '16px');
const boardCols = await cs('.board', 'grid-template-columns');
ok('B2 board is a grid (auto-fill 220 form wins as the later rule)', (await cs('.board', 'display')) === 'grid' && boardCols.split(' ').length >= 2, boardCols);
ok('B2 gallery grid: 2 cols, rows 3, gap 8', (await cs('.gallery', 'grid-template-columns')).split(' ').length === 2 && (await cs('.gallery', 'gap')) === '8px', `${await cs('.gallery', 'grid-template-columns')} / ${await cs('.gallery', 'gap')}`);
const modalAI = await cs('.modal', 'align-items'), modalJI = await cs('.modal', 'justify-items');
ok('B2 modal stack centers both axes', modalAI === 'center' && modalJI === 'center', `${modalAI} / ${modalJI}`);
const heroPlace = await cs('.hero', 'place-items');
ok('B2 hero center: both centers everything', (heroPlace === 'center center') || (await cs('.hero', 'display')) === 'grid', heroPlace);
/* block 3 - hover beats base, by order (the 0.7.2 headline fix) */
const before = await cs('.card', 'background-color');
await page.hover('.card');
await page.waitForTimeout(120);
const after = await cs('.card', 'background-color');
ok('B3 hover changes the card background (real mouse)', before !== after && after === 'rgb(58, 47, 38)', `${before} -> ${after}`);
await page.mouse.move(0, 0);
await page.waitForTimeout(120);
ok('B3 un-hoving restores the base', (await cs('.card', 'background-color')) === before);
/* block 4 - media conditions */
await page.emulateMedia({ colorScheme: 'dark' });
await page.waitForTimeout(150);
ok('B4 dark mode applies (#181310)', (await cs('.card', 'background-color')) === 'rgb(24, 19, 16)', await cs('.card', 'background-color'));
await page.setViewportSize({ width: 700, height: 900 });
await page.waitForTimeout(150);
ok('B4 below 768 the wide padding is gone', (await cs('.card', 'padding')) === '16px', await cs('.card', 'padding'));
await page.setViewportSize({ width: 900, height: 900 });
await page.waitForTimeout(150);
ok('B4 at 900px padding: 24 applies', (await cs('.card', 'padding')) === '24px', await cs('.card', 'padding'));
await page.emulateMedia({ colorScheme: 'light' });
await page.setViewportSize({ width: 1200, height: 900 });
await page.waitForTimeout(150);
/* block 5 - seen + hover animations (WAAPI) */
const animCount = await page.evaluate(() => {
const el = document.querySelector('.card');
el.scrollIntoView();
return new Promise(res => setTimeout(() => res(el.getAnimations().length), 400));
});
ok('B5 seen fires a real animation on the card', animCount > 0, 'animations: ' + animCount);
await page.hover('.card');
await page.waitForTimeout(120);
const liftAnims = await page.evaluate(() => document.querySelector('.card').getAnimations().length);
ok('B5 hover plays the lift motion', liftAnims > 0, 'animations on hover: ' + liftAnims);
// B2 above deliberately has a rule that aligns with no display, to prove the engine gives it a box. Since 0.13.0 the linter says
// so (a build would not), and that one warning is expected here.
const unexpected = consoleErrors.filter((m) => !/"place-items" on "\.hero" only works on a flex or grid box/.test(m));
ok('no console errors or warnings during the whole run (but the one alignment warning B2 asks for)', unexpected.length === 0, unexpected.slice(0, 3).join(' | '));
}, { pages: { '/guide': html }, viewport: { width: 1200, height: 900 } });
console.log(`\n${pass} passed, ${fail} failed`);
process.exit(fail ? 1 : 0);
|