#!/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 * `).join('\n')} `; 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);