spb/countryatlas
Public
TypeScript 57%
Python 38.6%
JavaScript 3.6%
CSS 0.6%
1/**2 * Core pages QA (real API): routes × widths → screenshots + checks (overflow, console errors, failed requests,3 * tap targets < 44 px on phones, "undefined/NaN/null" text). Output: qa/screens/core/<route>-<width>.png + report.json.4 *5 * node qa/core-qa.mjs [BASE_URL] [--quick] [--routes=/a,/b] [--widths=390,1440]6 */7import { chromium } from '/Users/simon-pierreboucher/Desktop/uqo-eval/node_modules/playwright/index.mjs';8import { mkdirSync, writeFileSync } from 'node:fs';910const args = process.argv.slice(2);11const quick = args.includes('--quick');12const BASE = args.find((a) => a.startsWith('http')) ?? process.env.BASE_URL ?? 'http://localhost:8290';13const routesArg = args.find((a) => a.startsWith('--routes='));14const widthsArg = args.find((a) => a.startsWith('--widths='));15const OUT = new URL('./screens/core/', import.meta.url).pathname;16mkdirSync(OUT, { recursive: true });1718const ROUTES = routesArg19 ? routesArg.slice('--routes='.length).split(',')20 : ['/', '/countries/canada', '/compare/canada/australia', '/compare/canada/united-states/france?tab=economy', '/rankings/gdp-per-capita', '/indicators/life-expectancy', '/regions/g7', '/regions/compare', '/peers', '/changes'];21const WIDTHS = widthsArg ? widthsArg.slice('--widths='.length).split(',').map(Number) : quick ? [390, 1440] : [320, 360, 375, 390, 430, 768, 1440, 1920];2223const slug = (p) => (p === '/' ? 'home' : p.slice(1).replace(/[/?=&]+/g, '_'));24const report = [];25const browser = await chromium.launch();26for (const width of WIDTHS) {27 const mobile = width < 768;28 const ctx = await browser.newContext({ viewport: { width, height: mobile ? 844 : 900 }, deviceScaleFactor: 1, isMobile: mobile, hasTouch: mobile });29 const page = await ctx.newPage();30 const errors = [];31 const failed = [];32 page.on('pageerror', (e) => errors.push(`pageerror: ${String(e).slice(0, 200)}`));33 page.on('console', (m) => {34 if (m.type() === 'error') errors.push(m.text().slice(0, 200));35 });36 page.on('response', (r) => {37 if (r.status() >= 400 && r.url().includes('/api/')) failed.push(`${r.status()} ${r.url().slice(0, 140)}`);38 });39 for (const path of ROUTES) {40 let status = 0;41 try {42 const resp = await page.goto(BASE + path, { waitUntil: 'networkidle', timeout: 90_000 });43 status = resp?.status() ?? 0;44 } catch (e) {45 report.push({ path, width, status: 'ERR', error: String(e).slice(0, 200) });46 continue;47 }48 await page.evaluate(() => document.fonts.ready);49 await page.evaluate(async () => {50 const h = document.documentElement.scrollHeight;51 for (let y = 0; y < h; y += 700) {52 window.scrollTo(0, y);53 await new Promise((r) => setTimeout(r, 50));54 }55 window.scrollTo(0, 0);56 });57 await page.waitForLoadState('networkidle').catch(() => {});58 await page.waitForTimeout(500);59 const m = await page.evaluate(60 ({ mobile }) => {61 const de = document.documentElement;62 const overflow = de.scrollWidth - de.clientWidth;63 const wide = [...document.querySelectorAll('body *')]64 .filter((el) => el.getBoundingClientRect().right > de.clientWidth + 1 && el.getBoundingClientRect().width > 0)65 .slice(0, 5)66 .map((el) => `${el.tagName.toLowerCase()}.${String(el.className).split(' ').slice(0, 3).join('.')} right=${Math.round(el.getBoundingClientRect().right)}`);67 const vis = (el) => {68 const r = el.getBoundingClientRect();69 if (!r.width || !r.height) return false;70 const cs = getComputedStyle(el);71 return cs.visibility !== 'hidden' && cs.display !== 'none';72 };73 const small = mobile74 ? [...document.querySelectorAll('a,button,[role=button],input,select,summary,[role=radio],[role=tab]')]75 .filter(vis)76 .filter((el) => !el.closest('svg') && el.getBoundingClientRect().height < 44 && el.getBoundingClientRect().width < 44)77 .map((el) => `${el.tagName.toLowerCase()} "${(el.getAttribute('aria-label') || el.textContent || '').trim().slice(0, 30)}" ${Math.round(el.getBoundingClientRect().width)}x${Math.round(el.getBoundingClientRect().height)}`)78 : [];79 const text = document.body.innerText || '';80 const bad = [];81 for (const re of [/\bundefined\b/g, /\bNaN\b/g, /(?<![\w"':])null(?![\w"':])/g]) {82 let mm;83 let n = 0;84 while ((mm = re.exec(text)) && n < 3) {85 bad.push(`${mm[0]} @ "${text.slice(Math.max(0, mm.index - 40), mm.index + 30).replace(/\s+/g, ' ')}"`);86 n++;87 }88 }89 const footer = document.querySelector('footer')?.innerText ?? '';90 const credits = /Simon-Pierre Boucher/.test(footer) && /contact@spboucher\.ai/.test(footer) && /MacLustr/.test(footer);91 return { overflow, wide, small: small.slice(0, 8), nSmall: small.length, bad, credits, title: document.title, docH: de.scrollHeight };92 },93 { mobile },94 );95 const file = `${slug(path)}-${width}.png`;96 await page.screenshot({ path: OUT + file, fullPage: true }).catch(() => {});97 report.push({ path, width, status, ...m, errors: errors.splice(0), failed: failed.splice(0), file });98 writeFileSync(OUT + 'report.json', JSON.stringify(report, null, 2));99 }100 await ctx.close();101}102await browser.close();103104let fails = 0;105for (const r of report) {106 const flags = [];107 if (r.status !== 200) flags.push(`HTTP ${r.status}`);108 if (r.overflow > 0) flags.push(`OVERFLOW +${r.overflow}px`);109 if (r.nSmall) flags.push(`${r.nSmall} small targets`);110 if (r.bad?.length) flags.push(`BAD TEXT ${r.bad.length}`);111 if (r.errors?.length) flags.push(`${r.errors.length} console errors`);112 if (r.failed?.length) flags.push(`${r.failed.length} failed API`);113 if (r.credits === false) flags.push('NO CREDITS');114 if (flags.length) fails++;115 console.log(`${String(r.width).padStart(4)} ${r.path.padEnd(52)} ${flags.join(' · ') || 'ok'} (h=${r.docH})`);116 if (r.wide?.length) console.log(' wide:', r.wide.join(' | '));117 if (r.small?.length) console.log(' small:', r.small.slice(0, 5).join(' | '));118 if (r.bad?.length) console.log(' bad:', r.bad.join(' | '));119 if (r.errors?.length) console.log(' errors:', r.errors.slice(0, 3).join(' | '));120 if (r.failed?.length) console.log(' failed:', r.failed.slice(0, 3).join(' | '));121}122console.log(`\n${report.length} renders, ${fails} with flags`);123