SPB Git forge

spb/countryatlas

Public
20commits 1branches 0releases
268.3 MBsize
maindefault branch
12 days agolast push
TypeScript 57% Python 38.6% JavaScript 3.6% CSS 0.6%
5.5 KB · 87 lines javascript
Raw Blame History
1/**2 * Flagship views QA: /explore, /trajectories, /scatter, /finder, /extremes × 320/375/390/430/768/1440/1920.3 * Checks: HTTP 200, no horizontal overflow, no console errors, no failed requests, tap targets ≥ 44 px on phones;4 * exercises the year slider (arrow keys + play 2 s) and a country tap on /explore at 390 and 1440.5 * Screenshots → qa/screens/flagship/<route>-<width>.png; report → qa/screens/flagship/report.json.6 *   node qa/flagship-qa.mjs [BASE_URL]7 */8import { chromium } from '/Users/simon-pierreboucher/Desktop/uqo-eval/node_modules/playwright/index.mjs';9import { mkdirSync, writeFileSync } from 'node:fs';1011const BASE = process.argv[2] ?? process.env.BASE_URL ?? 'http://localhost:8290';12const OUT = new URL('./screens/flagship/', import.meta.url).pathname;13mkdirSync(OUT, { recursive: true });14const ROUTES = ['/explore', '/explore?indicator=life-expectancy&year=1990&view=rank', '/trajectories', '/scatter', '/finder?f=gdp-per-capita:gt:40000&f=population:gt:10000000', '/extremes?window=10'];15const WIDTHS = [320, 375, 390, 430, 768, 1440, 1920];16const slug = (p) => p.slice(1).replace(/[/?=&:,]+/g, '_');17const report = [];18const browser = await chromium.launch();19for (const width of WIDTHS) {20  const mobile = width < 768;21  const ctx = await browser.newContext({ viewport: { width, height: mobile ? 844 : 1000 }, deviceScaleFactor: 1, isMobile: mobile, hasTouch: mobile });22  const page = await ctx.newPage();23  const errors = [];24  const failed = [];25  page.on('pageerror', (e) => errors.push(`pageerror: ${String(e).slice(0, 160)}`));26  page.on('console', (m) => { if (m.type() === 'error') errors.push(m.text().slice(0, 160)); });27  page.on('requestfailed', (r) => { if (!r.url().includes('_rsc')) failed.push(r.url().slice(BASE.length)); });28  for (const path of ROUTES) {29    let status = 0;30    try {31      const resp = await page.goto(BASE + path, { waitUntil: 'networkidle', timeout: 90_000 });32      status = resp?.status() ?? 0;33    } catch (e) {34      report.push({ path, width, status: 'ERR', error: String(e).slice(0, 160) });35      continue;36    }37    await page.evaluate(() => document.fonts.ready);38    await page.waitForTimeout(600);39    if (path.startsWith('/explore') && (width === 390 || width === 1440)) {40      const range = page.locator('input[type=range]').first();41      if (await range.count()) {42        await range.focus();43        for (let i = 0; i < 8; i++) await page.keyboard.press('ArrowLeft');44        await page.waitForTimeout(300);45        const play = page.locator('button[aria-label="Play"]').first();46        if (await play.count()) { await play.click(); await page.waitForTimeout(2000); await page.locator('button[aria-label="Pause"]').first().click().catch(() => {}); }47        const br = page.locator('path[aria-label^="Brazil"]').first();48        if (await br.count()) { await br.click({ force: true }).catch(() => {}); await page.waitForTimeout(600); }49      }50    }51    const m = await page.evaluate(({ mobile }) => {52      const de = document.documentElement;53      const overflow = de.scrollWidth - de.clientWidth;54      const vis = (el) => { const r = el.getBoundingClientRect(); if (!r.width || !r.height) return false; const cs = getComputedStyle(el); return cs.visibility !== 'hidden' && cs.display !== 'none'; };55      const targets = [...document.querySelectorAll('a,button,[role=button],input,select,[role=radio],[role=tab]')].filter(vis).filter((el) => !el.closest('svg'));56      const small = mobile ? targets.filter((el) => { const r = el.getBoundingClientRect(); return r.height < 44 && r.width < 44; }).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)}`) : [];57      const text = document.body.innerText || '';58      const bad = [];59      for (const re of [/\bundefined\b/g, /\bNaN\b/g]) { let mm; while ((mm = re.exec(text)) && bad.length < 3) bad.push(text.slice(Math.max(0, mm.index - 30), mm.index + 20)); }60      return { overflow, small: small.slice(0, 8), nSmall: small.length, bad, title: document.title, url: location.href };61    }, { mobile });62    const file = `${slug(path)}-${width}.png`;63    await page.screenshot({ path: OUT + file, fullPage: !path.startsWith('/explore') && !path.startsWith('/trajectories') }).catch(() => {});64    report.push({ path, width, status, ...m, errors: errors.splice(0), failed: failed.splice(0), file });65  }66  await ctx.close();67}68await browser.close();69writeFileSync(OUT + 'report.json', JSON.stringify(report, null, 2));70let fails = 0;71for (const r of report) {72  const flags = [];73  if (r.status !== 200) flags.push(`HTTP ${r.status}`);74  if (r.overflow > 0) flags.push(`OVERFLOW +${r.overflow}px`);75  if (r.nSmall) flags.push(`${r.nSmall} small targets`);76  if (r.bad?.length) flags.push(`BAD TEXT ${r.bad.length}`);77  if (r.errors?.length) flags.push(`${r.errors.length} console errors`);78  if (r.failed?.length) flags.push(`${r.failed.length} failed requests`);79  if (flags.length) fails++;80  console.log(`${String(r.width).padStart(4)} ${r.path.padEnd(60)} ${flags.join(' · ') || 'ok'}`);81  if (r.small?.length) console.log('       small:', r.small.slice(0, 4).join(' | '));82  if (r.errors?.length) console.log('       errors:', r.errors.slice(0, 2).join(' | '));83  if (r.failed?.length) console.log('       failed:', r.failed.slice(0, 3).join(' | '));84  if (r.bad?.length) console.log('       bad:', r.bad.join(' | '));85}86console.log(`\n${report.length} renders, ${fails} with flags`);87