#!/usr/bin/env node /** * Visual QA: screenshots at 1440×900 and 390×844 + console errors + horizontal overflow check. * node qa/screens.mjs [baseUrl] [pathFilter] (default http://localhost:8351) * Playwright is borrowed from ~/Desktop/uqo-eval/node_modules (not a dependency of this app). */ import { mkdirSync, existsSync } from 'node:fs'; import path from 'node:path'; const PW_ROOT = '/Users/simon-pierreboucher/Desktop/uqo-eval/node_modules/playwright/index.mjs'; if (!existsSync(PW_ROOT)) { console.error(`Playwright not found at ${PW_ROOT} — skipping visual QA.`); process.exit(0); } const { chromium } = await import(PW_ROOT); const BASE = process.argv[2] ?? 'http://localhost:8351'; const FILTER = process.argv[3] ?? ''; const OUT = path.resolve(import.meta.dirname, 'screens'); mkdirSync(OUT, { recursive: true }); const ALL = ['/', '/country/ca', '/asn/13335', '/service/cloudflare', '/routes', '/incidents', '/history', '/admin', '/internet/na-east', '/event/2026-09-12-north-america-east-latency-anomaly', '/bgp', '/probes', '/targets', '/methodology', '/api', '/services', '/asns', '/history/2026/9']; const PAGES = FILTER ? ALL.filter((p) => (FILTER === '/' ? p === '/' : p.startsWith(FILTER))) : ALL; const VIEWPORTS = [ { name: '1440', width: 1440, height: 900, isMobile: false, deviceScaleFactor: 1 }, { name: '390', width: 390, height: 844, isMobile: true, hasTouch: true, deviceScaleFactor: 2 }, ]; const browser = await chromium.launch(); let failures = 0; for (const vp of VIEWPORTS) { const ctx = await browser.newContext({ viewport: { width: vp.width, height: vp.height }, isMobile: vp.isMobile, hasTouch: vp.hasTouch ?? false, deviceScaleFactor: vp.deviceScaleFactor, colorScheme: 'dark' }); // admin token gate: pre-seed sessionStorage so /admin renders its sections await ctx.addInitScript(() => { try { window.sessionStorage.setItem('ip.admin-token', 'dev-admin-token'); } catch {} }); for (const p of PAGES) { const page = await ctx.newPage(); const errors = []; page.on('console', (m) => { if (m.type() === 'error') errors.push(m.text()); }); page.on('pageerror', (e) => errors.push(`pageerror: ${e.message}`)); const t0 = Date.now(); let status = 0; try { // `networkidle` never fires: the SSE stream (/api/v1/live) stays open by design. const res = await page.goto(BASE + p, { waitUntil: 'load', timeout: 90_000 }); status = res?.status() ?? 0; } catch (e) { errors.push(`goto: ${e.message}`); } await page.waitForTimeout(p === '/' || p.includes('/country/') || p.includes('/probes') || p.includes('/internet/') ? 4000 : 1500); // let the map tiles / charts settle const { sw, iw, h1, wide } = await page.evaluate(() => { // With mobile emulation the layout viewport grows to fit overflowing content: compare against the device width. const iw = Math.min(window.innerWidth, window.screen.width); const wide = []; for (const el of document.querySelectorAll('body *')) { const r = el.getBoundingClientRect(); if (r.right > iw + 1 && r.width > 40 && !el.closest('.maplibregl-map') && !el.closest('.scroll-x') && !el.closest('.snap-row')) wide.push(`${el.tagName.toLowerCase()}${el.className && typeof el.className === 'string' ? '.' + el.className.split(' ').slice(0, 3).join('.') : ''}@${Math.round(r.right)}`); } return { sw: document.documentElement.scrollWidth, iw, h1: document.querySelector('h1')?.textContent?.trim() ?? document.title, wide: wide.slice(0, 6) }; }); if (wide.length) console.log(` wide: ${wide.join(' | ')}`); const name = (p.replace(/^\//, '').replace(/[\/?=&]+/g, '_') || 'home').slice(0, 60); await page.screenshot({ path: `${OUT}/${name}-${vp.name}.png`, fullPage: true }); const realErrors = errors.filter((e) => !/openfreemap|tiles\.|Failed to load resource.*(png|pbf|json)|AbortError|net::ERR_/.test(e)); const overflow = sw > iw; const bad = overflow || realErrors.length || status !== 200; if (bad) failures++; console.log(`${bad ? 'FAIL' : 'ok '} ${vp.name}px ${p} status=${status} scrollWidth=${sw}/${iw} ${Date.now() - t0}ms "${(h1 ?? '').slice(0, 60)}"${realErrors.length ? '\n console: ' + realErrors.slice(0, 3).join(' | ').slice(0, 400) : ''}`); await page.close(); } await ctx.close(); } await browser.close(); console.log(`\nScreenshots in ${OUT}`); process.exit(failures ? 1 : 0);