Claude Sonnet 5.5
v0.13.0: the compiled page is the page: no-JS parity, strict CSP, SSR, source maps, benchmark
444a402 Download scripts/test-guide-browser.mjs from EHS-Studio/dssl: direct link, hf CLI and curl.
- Browser
- Download file 8.25 kB
-
https://huggingface.co/EHS-Studio/dssl/resolve/main/scripts/test-guide-browser.mjs
- Command line
-
hf download hf://EHS-Studio/dssl/scripts/test-guide-browser.mjs
-
curl -L -o test-guide-browser.mjs https://huggingface.co/EHS-Studio/dssl/resolve/main/scripts/test-guide-browser.mjs
8.25 kB
| /* 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); | |