// Render fixtures to PNG + extract target bounding boxes from the DOM. // Node v22+, requires: npm install puppeteer-core // Connects to a pre-launched headless Chromium (mirrors the FreeBSD jail path // where puppeteer-core talks to a system-pkg Chromium over CDP). // // Usage: // 1) launch: chromium --headless=new --remote-debugging-port=9222 \ // --user-data-dir=/tmp/validation-profile about:blank & // 2) render: node render.mjs // // Produces: // screenshots/.png // results/.json { viewport, targets: [{label, bbox, center}] } // // Targets per fixture (DOM ids -> human labels): import { connect } from 'puppeteer-core'; import { readFileSync, writeFileSync, mkdirSync } from 'fs'; import path from 'path'; import { pathToFileURL } from 'url'; const FIXTURES = [ { name: '01-login', viewport: { width: 1024, height: 768 }, targets: [ { id: 'u', label: 'Email input field' }, { id: 'p', label: 'Password input field' }, { id: 'submit', label: 'Sign in button' }, { id: 'cancel', label: 'Cancel button' }, { id: 'forgot', label: 'Forgot password link' }, ], }, { name: '02-dashboard', viewport: { width: 1024, height: 768 }, targets: [ { id: 't-deploy', label: 'Deploy tile' }, { id: 't-stop', label: 'Stop service tile' }, { id: 't-health', label: 'Health tile' }, { id: 't-settings', label: 'Settings tile' }, { id: 'b-refresh', label: 'Refresh button' }, { id: 'b-export', label: 'Export CSV button' }, { id: 'b-new', label: 'New project button' }, ], }, { name: '03-modal', viewport: { width: 1024, height: 768 }, targets: [ { id: 'm-close', label: 'Modal close (X) button' }, { id: 'm-cancel', label: 'Cancel button in modal' }, { id: 'm-confirm', label: 'Delete tenant button' }, { id: 'm-confirm-input', label: 'Tenant name confirmation input' }, { id: 'bg-action-1', label: 'Pause tenant button (behind modal)' }, ], }, ]; const ROOT = path.resolve(import.meta.dirname); mkdirSync(path.join(ROOT, 'screenshots'), { recursive: true }); mkdirSync(path.join(ROOT, 'results'), { recursive: true }); const browser = await connect({ browserURL: 'http://127.0.0.1:9222' }); for (const f of FIXTURES) { const page = await browser.newPage(); await page.setViewport(f.viewport); const fileUrl = pathToFileURL(path.join(ROOT, 'fixtures', `${f.name}.html`)).href; await page.goto(fileUrl, { waitUntil: 'networkidle0' }); const targets = await page.evaluate((wanted) => { return wanted.map(({ id, label }) => { const el = document.getElementById(id); if (!el) return { id, label, missing: true }; const r = el.getBoundingClientRect(); const bbox = { x: Math.round(r.left), y: Math.round(r.top), w: Math.round(r.width), h: Math.round(r.height), }; const center = { x: Math.round(r.left + r.width / 2), y: Math.round(r.top + r.height / 2), }; return { id, label, bbox, center }; }); }, f.targets); const pngPath = path.join(ROOT, 'screenshots', `${f.name}.png`); await page.screenshot({ path: pngPath, fullPage: false }); const meta = { fixture: f.name, viewport: f.viewport, targets }; writeFileSync(path.join(ROOT, 'results', `${f.name}.json`), JSON.stringify(meta, null, 2)); console.log(`[${f.name}] ${pngPath} (${targets.length} targets)`); await page.close(); } await browser.disconnect(); console.log('done');