// Visual check of the Theme Builder, font roles, the Google Fonts browser and Appearance's account / // pane themes through the panel harness (/?panel, mocked native commands; the harness fetches Google // Fonts itself, like native code would). Needs a Vite dev server (default 127.0.0.1:1437). // // node scripts/theme-builder-shots.mjs [outDir] [port] import { mkdirSync } from 'node:fs'; import { chromium } from 'playwright'; const out = process.argv[2] ?? '../../verification/theme-builder'; const port = process.argv[3] ?? '1437'; mkdirSync(out, { recursive: true }); const browser = await chromium.launch(); const results = []; async function shot(name, { width = 1440, height = 900, query = '', action } = {}) { const page = await browser.newPage({ viewport: { width, height }, deviceScaleFactor: 2, colorScheme: 'dark' }); const errors = []; page.on('pageerror', error => errors.push(error.message)); page.on('console', message => { if (message.type() === 'error') errors.push(message.text()); }); await page.goto(`http://127.0.0.1:${port}/?panel${query ? `&${query}` : ''}`); await page.waitForTimeout(1000); let note = ''; if (action) note = (await action(page)) ?? ''; await page.waitForTimeout(500); await page.screenshot({ path: `${out}/${name}.png` }); results.push(`${name}: ${errors.length ? `ERRORS ${errors.join(' | ')}` : 'ok'}${note ? ` (${note})` : ''}`); await page.close(); } // Builder at desktop and split-pane widths. await shot('builder-wide', { query: 'section=builder' }); await shot('builder-narrow', { width: 520, height: 1000, query: 'section=builder' }); // A light base: palette, contrast report with failures, then auto-fix. await shot('builder-contrast', { query: 'section=builder', action: async page => { await page.getByLabel('Background color hex').fill('#fbf8f1'); await page.getByLabel('Background color hex').press('Enter'); await page.getByLabel('Text color hex').fill('#8a8a8a'); await page.getByLabel('Text color hex').press('Enter'); await page.getByLabel('Accent color hex').fill('#ffd400'); await page.getByLabel('Accent color hex').press('Enter'); await page.locator('.wb-card[aria-label="Contrast"]').scrollIntoViewIfNeeded(); const failing = await page.locator('.wb-contrast li.is-fail').count(); return `${failing} failing pairs before auto-fix`; }, }); await shot('builder-autofix', { query: 'section=builder', action: async page => { await page.getByLabel('Text color hex').fill('#8a8a8a'); await page.getByLabel('Text color hex').press('Enter'); await page.getByRole('button', { name: /Auto-fix/ }).click(); await page.locator('.wb-card[aria-label="Contrast"]').scrollIntoViewIfNeeded(); const failing = await page.locator('.wb-contrast li.is-fail').count(); // Undo brings the failing palette back, redo the fix. await page.keyboard.press('Control+z'); const undone = await page.locator('.wb-contrast li.is-fail').count(); await page.keyboard.press('Control+Shift+z'); const redone = await page.locator('.wb-contrast li.is-fail').count(); return `after fix ${failing} failing; undo ${undone}; redo ${redone}`; }, }); await shot('builder-fonts', { query: 'section=builder', action: async page => { await page.locator('.wb-card[aria-label="Type"]').scrollIntoViewIfNeeded(); await page.getByRole('button', { name: 'Fine-tune Numbers' }).click(); }, }); // The font browser: live previews, search, detail with sample text and weights. await shot('font-browser', { query: 'section=builder', action: async page => { await page.locator('.wb-card[aria-label="Type"]').scrollIntoViewIfNeeded(); await page.getByRole('button', { name: /^Post text font/ }).click(); await page.waitForTimeout(2500); }, }); await shot('font-browser-detail', { query: 'section=builder', action: async page => { await page.locator('.wb-card[aria-label="Type"]').scrollIntoViewIfNeeded(); await page.getByRole('button', { name: /^Post text font/ }).click(); await page.getByLabel('Search Google Fonts').fill('fraunces'); await page.waitForTimeout(600); await page.getByRole('option', { name: /Fraunces/ }).first().click(); await page.waitForTimeout(2500); }, }); await shot('font-browser-installed', { query: 'section=builder', action: async page => { await page.locator('.wb-card[aria-label="Type"]').scrollIntoViewIfNeeded(); await page.getByRole('button', { name: /^Names and headings font/ }).click(); await page.getByLabel('Search Google Fonts').fill('space grotesk'); await page.waitForTimeout(500); await page.getByRole('option', { name: /Space Grotesk/ }).first().click(); await page.getByRole('button', { name: /Download and use/ }).click(); await page.waitForSelector('.wb-sheet', { state: 'detached', timeout: 20000 }); await page.waitForTimeout(1200); return (await page.locator('.wb-role[aria-label="Names and headings"] .wb-role-name').innerText()).replace(/\s+/g, ' '); }, }); await shot('font-browser-narrow', { width: 560, height: 980, query: 'section=builder', action: async page => { await page.locator('.wb-card[aria-label="Type"]').scrollIntoViewIfNeeded(); await page.getByRole('button', { name: /^Post text font/ }).click(); await page.getByRole('radio', { name: 'Serif' }).click(); await page.waitForTimeout(2500); }, }); await shot('appearance-fonts', { query: 'section=themes', action: async page => { await page.locator('[aria-label="Interface"]').first().scrollIntoViewIfNeeded(); } }); await shot('appearance-scopes', { query: 'section=themes', action: async page => { await page.locator('#theme-scopes-h').scrollIntoViewIfNeeded(); } }); await shot('appearance-light-chrome', { query: 'section=themes&theme=paper' }); await browser.close(); console.log(results.join('\n'));