SPB Git forge
15commits 1branches 0releases
29.7 MBsize
maindefault branch
10 days agolast push
TypeScript 36.3% Python 31.8% Go 18% JavaScript 9.8% Shell 1.9% SQL 1.4% CSS 0.5%
4.4 KB · 81 lines javascript
Raw Blame History
1#!/usr/bin/env node2/**3 * Visual QA: screenshots at 1440×900 and 390×844 + console errors + horizontal overflow check.4 *   node qa/screens.mjs [baseUrl] [pathFilter]      (default http://localhost:8351)5 * Playwright is borrowed from ~/Desktop/uqo-eval/node_modules (not a dependency of this app).6 */7import { mkdirSync, existsSync } from 'node:fs';8import path from 'node:path';910const PW_ROOT = '/Users/simon-pierreboucher/Desktop/uqo-eval/node_modules/playwright/index.mjs';11if (!existsSync(PW_ROOT)) {12  console.error(`Playwright not found at ${PW_ROOT} — skipping visual QA.`);13  process.exit(0);14}15const { chromium } = await import(PW_ROOT);1617const BASE = process.argv[2] ?? 'http://localhost:8351';18const FILTER = process.argv[3] ?? '';19const OUT = path.resolve(import.meta.dirname, 'screens');20mkdirSync(OUT, { recursive: true });2122const 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'];23const PAGES = FILTER ? ALL.filter((p) => (FILTER === '/' ? p === '/' : p.startsWith(FILTER))) : ALL;24const VIEWPORTS = [25  { name: '1440', width: 1440, height: 900, isMobile: false, deviceScaleFactor: 1 },26  { name: '390', width: 390, height: 844, isMobile: true, hasTouch: true, deviceScaleFactor: 2 },27];2829const browser = await chromium.launch();30let failures = 0;31for (const vp of VIEWPORTS) {32  const ctx = await browser.newContext({ viewport: { width: vp.width, height: vp.height }, isMobile: vp.isMobile, hasTouch: vp.hasTouch ?? false, deviceScaleFactor: vp.deviceScaleFactor, colorScheme: 'dark' });33  // admin token gate: pre-seed sessionStorage so /admin renders its sections34  await ctx.addInitScript(() => {35    try {36      window.sessionStorage.setItem('ip.admin-token', 'dev-admin-token');37    } catch {}38  });39  for (const p of PAGES) {40    const page = await ctx.newPage();41    const errors = [];42    page.on('console', (m) => {43      if (m.type() === 'error') errors.push(m.text());44    });45    page.on('pageerror', (e) => errors.push(`pageerror: ${e.message}`));46    const t0 = Date.now();47    let status = 0;48    try {49      // `networkidle` never fires: the SSE stream (/api/v1/live) stays open by design.50      const res = await page.goto(BASE + p, { waitUntil: 'load', timeout: 90_000 });51      status = res?.status() ?? 0;52    } catch (e) {53      errors.push(`goto: ${e.message}`);54    }55    await page.waitForTimeout(p === '/' || p.includes('/country/') || p.includes('/probes') || p.includes('/internet/') ? 4000 : 1500); // let the map tiles / charts settle56    const { sw, iw, h1, wide } = await page.evaluate(() => {57      // With mobile emulation the layout viewport grows to fit overflowing content: compare against the device width.58      const iw = Math.min(window.innerWidth, window.screen.width);59      const wide = [];60      for (const el of document.querySelectorAll('body *')) {61        const r = el.getBoundingClientRect();62        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)}`);63      }64      return { sw: document.documentElement.scrollWidth, iw, h1: document.querySelector('h1')?.textContent?.trim() ?? document.title, wide: wide.slice(0, 6) };65    });66    if (wide.length) console.log(`     wide: ${wide.join(' | ')}`);67    const name = (p.replace(/^\//, '').replace(/[\/?=&]+/g, '_') || 'home').slice(0, 60);68    await page.screenshot({ path: `${OUT}/${name}-${vp.name}.png`, fullPage: true });69    const realErrors = errors.filter((e) => !/openfreemap|tiles\.|Failed to load resource.*(png|pbf|json)|AbortError|net::ERR_/.test(e));70    const overflow = sw > iw;71    const bad = overflow || realErrors.length || status !== 200;72    if (bad) failures++;73    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) : ''}`);74    await page.close();75  }76  await ctx.close();77}78await browser.close();79console.log(`\nScreenshots in ${OUT}`);80process.exit(failures ? 1 : 0);81