// Opt-in, synthetic-only browser acceptance check. No app/backend/database start. // PLAYWRIGHT_MODULE points to an already installed Playwright package. import { build } from 'esbuild'; import { createServer } from 'node:http'; import { readFile } from 'node:fs/promises'; import { resolve, extname } from 'node:path'; import { pathToFileURL } from 'node:url'; import assert from 'node:assert/strict'; const { chromium } = await import(pathToFileURL(process.env.PLAYWRIGHT_MODULE).href); const output = resolve('outputs/client-info-design-check'); const fixture = resolve('tests/fixtures/client-info-design/preview.tsx'); await build({ entryPoints: [fixture], bundle: true, format: 'iife', jsx: 'automatic', outdir: output, entryNames: 'preview', loader: { '.woff2': 'file', '.woff': 'file', '.png': 'file' }, plugins: [{ name: 'synthetic-app-context', setup(builder) { builder.onResolve({ filter: /(?:^|\/)app\/AppProvider$/ }, () => ({ path: fixture })); }}] }); const server = createServer(async (request, response) => { const url = new URL(request.url, 'http://127.0.0.1'); if (url.pathname === '/') { response.setHeader('Content-Type', 'text/html'); response.end('Synthetic Client Info verification
'); return; } if (!/^\/[\w.-]+$/.test(url.pathname)) { response.writeHead(404).end(); return; } try { const extension = extname(url.pathname); response.setHeader('Content-Type', extension === '.js' ? 'text/javascript' : extension === '.css' ? 'text/css' : 'font/woff2'); response.end(await readFile(resolve(output, url.pathname.slice(1)))); } catch { response.writeHead(404).end(); } }); await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); const browser = await chromium.launch({ headless: true, channel: 'msedge' }); try { const page = await browser.newPage(); const errors = []; page.on('pageerror', error => errors.push(error.message)); for (const theme of ['light', 'dark']) { await page.setViewportSize({ width: 1600, height: 1100 }); await page.goto(`http://127.0.0.1:${server.address().port}/?theme=${theme}`); await page.locator('.client-domains li').first().waitFor(); await page.evaluate(() => document.fonts.ready); for (const width of [1600, 1100, 860, 390]) { await page.setViewportSize({ width, height: 1100 }); await page.waitForTimeout(150); const geometry = await page.evaluate(() => { const root = document.querySelector('.dashboard-page'); const svg = document.querySelector('.client-posture-connectors'); const ring = document.querySelector('.client-overall-ring').getBoundingClientRect(); const number = document.querySelector('.client-overall-ring-number strong').getBoundingClientRect(); const nodes = [...document.querySelectorAll('.client-domains .client-domain-score')]; const frame = svg.getBoundingClientRect(); return { overflow: document.documentElement.scrollWidth > innerWidth, background: getComputedStyle(root).backgroundColor, radius: getComputedStyle(document.querySelector('.client-overview-fact')).borderRadius, numberOffset: Math.abs(number.x + number.width / 2 - ring.x - ring.width / 2), buttonHeight: document.querySelector('.client-source-overview header button').getBoundingClientRect().height, connectors: getComputedStyle(svg).display === 'none' ? [] : [...svg.querySelectorAll('path')].map((path, index) => { const endpoint = path.getPointAtLength(path.getTotalLength()); const node = nodes[index].getBoundingClientRect(); return Math.hypot(endpoint.x + frame.x - node.x, endpoint.y + frame.y - node.y - node.height / 2); }) }; }); assert.equal(geometry.overflow, false, `${theme} ${width}: horizontal overflow`); assert.equal(geometry.radius, '0px'); assert(geometry.numberOffset < 1, 'Overall number must remain centered'); assert(geometry.buttonHeight < 50, 'Methodology action must not become a source tile'); if (width > 800) assert.equal(geometry.connectors.length, 8); assert(geometry.connectors.every(error => error < 1), 'Connector endpoints must follow domain nodes'); console.log(JSON.stringify({ theme, width, ...geometry })); if (width === 1600 || width === 390) await page.screenshot({ path: resolve(output, `${theme}-${width}.png`), fullPage: true }); } await page.setViewportSize({ width: 1600, height: 1100 }); const environment = page.locator('.client-environment-detail'); assert.equal(await environment.getAttribute('open'), null); await environment.locator('summary').focus(); await page.keyboard.press('Enter'); assert.notEqual(await environment.getAttribute('open'), null); await environment.locator('summary').press('Space'); assert.equal(await environment.getAttribute('open'), null); const group = page.locator('.risk-register-detail-group').first(); await group.locator('summary').focus(); await page.keyboard.press('Enter'); assert.notEqual(await group.getAttribute('open'), null); assert(await group.locator('dl').isVisible()); await page.locator('.client-risk-register-list button').last().click(); assert.equal(await group.getAttribute('open'), null); } assert.deepEqual(errors, []); } finally { await browser.close(); await new Promise(resolve => server.close(resolve)); }