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.9 KB · 88 lines javascript
Raw Blame History
1/**2 * QA for the Compare + Rankings pages: screenshots at 320/375/390/430/1280/1440 → qa/screens/compare-rankings/,3 * plus checks: horizontal overflow, tap targets < 44 px (mobile), console errors, sticky controls height,4 * URL state round-trip (reload keeps state), 404s. Run: node qa/compare-rankings.mjs [BASE_URL]5 */6import { chromium } from '/Users/simon-pierreboucher/Desktop/uqo-eval/node_modules/playwright/index.mjs';7import { mkdirSync, writeFileSync } from 'node:fs';89const BASE = process.argv[2] ?? 'http://localhost:8290';10const OUT = new URL('./screens/compare-rankings/', import.meta.url).pathname;11mkdirSync(OUT, { recursive: true });1213const PAGES = [14  ['compare', '/compare'],15  ['cmp-snapshot', '/compare/canada/united-states/france'],16  ['cmp-economy', '/compare/canada/united-states/france?tab=economy'],17  ['cmp-hero', '/compare/canada/united-states/france?tab=economy&indicator=gdp-per-capita&from=1990&mode=index100'],18  ['rankings', '/rankings'],19  ['rk-gdppc', '/rankings/gdp-per-capita'],20  ['rk-life-oecd', '/rankings/life-expectancy?group=oecd'],21];22const WIDTHS = [320, 375, 390, 430, 1280, 1440];23const report = [];24const browser = await chromium.launch();2526for (const width of WIDTHS) {27  const mobile = width < 768;28  const ctx = await browser.newContext({ viewport: { width, height: mobile ? 800 : 900 }, deviceScaleFactor: 1, isMobile: mobile, hasTouch: mobile, colorScheme: 'light' });29  const page = await ctx.newPage();30  const errors = [];31  page.on('pageerror', (e) => errors.push(String(e)));32  page.on('console', (m) => { if (m.type() === 'error') errors.push(m.text()); });33  for (const [name, path] of PAGES) {34    await page.goto(BASE + path, { waitUntil: 'networkidle' });35    await page.evaluate(() => document.fonts.ready);36    await page.waitForTimeout(700);37    const m = await page.evaluate(() => {38      const de = document.documentElement;39      const overflow = de.scrollWidth - de.clientWidth;40      const wide = [...document.querySelectorAll('body *')].filter((el) => { const r = el.getBoundingClientRect(); return r.right > de.clientWidth + 1 && r.width > 0 && getComputedStyle(el).position !== 'fixed'; }).slice(0, 6).map((el) => `${el.tagName.toLowerCase()}.${String(el.className).split(' ').slice(0, 3).join('.')} right=${Math.round(el.getBoundingClientRect().right)}`);41      const small = [...document.querySelectorAll('a,button,[role=button],input,select,summary')].filter((el) => { const r = el.getBoundingClientRect(); if (r.width === 0 || r.height === 0) return false; const cs = getComputedStyle(el); if (cs.visibility === 'hidden') return false; 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)}`);42      const smallH = [...document.querySelectorAll('a,button,[role=button],input,select,summary')].filter((el) => { const r = el.getBoundingClientRect(); return r.width > 0 && r.height > 0 && r.height < 32; }).map((el) => `${el.tagName.toLowerCase()} "${(el.getAttribute('aria-label') || el.textContent || '').trim().slice(0, 30)}" h=${Math.round(el.getBoundingClientRect().height)}`);43      const sticky = [...document.querySelectorAll('.sticky')].filter((el) => el.getBoundingClientRect().height > 0).map((el) => Math.round(el.getBoundingClientRect().height));44      return { overflow, wide, small: small.slice(0, 12), smallH: smallH.slice(0, 12), sticky, docH: de.scrollHeight };45    });46    await page.screenshot({ path: `${OUT}${name}-${width}.png`, fullPage: true });47    report.push({ name, path, width, ...m, errors: errors.splice(0) });48  }49  await ctx.close();50}5152// URL state round-trip + 404s (desktop)53{54  const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } });55  const page = await ctx.newPage();56  const checks = [];57  await page.goto(`${BASE}/compare/canada/united-states/france?tab=economy`, { waitUntil: 'networkidle' });58  await page.getByRole('radio', { name: 'Index = 100' }).click();59  await page.waitForTimeout(1200);60  const url1 = page.url();61  await page.reload({ waitUntil: 'networkidle' });62  const modeChecked = await page.getByRole('radio', { name: 'Index = 100' }).getAttribute('aria-checked');63  checks.push({ check: 'compare mode round-trip', url: url1, ok: url1.includes('mode=index100') && modeChecked === 'true' });64  await page.getByRole('tab', { name: 'Population' }).click();65  await page.waitForTimeout(1200);66  checks.push({ check: 'tab switch updates URL', url: page.url(), ok: page.url().includes('tab=population') });67  await page.goto(`${BASE}/rankings/gdp-per-capita`, { waitUntil: 'networkidle' });68  await page.locator('select[aria-label="Year"]:visible').first().selectOption('2015');69  await page.waitForTimeout(1500);70  const url2 = page.url();71  await page.reload({ waitUntil: 'networkidle' });72  const yearVal = await page.locator('select[aria-label="Year"]:visible').first().inputValue();73  checks.push({ check: 'ranking year round-trip', url: url2, ok: url2.includes('year=2015') && yearVal === '2015' });74  for (const p of ['/compare/canada', '/rankings/not-a-thing', '/compare/canada/nowhere-land']) {75    const res = await page.goto(BASE + p, { waitUntil: 'domcontentloaded' });76    checks.push({ check: `404 ${p}`, status: res?.status(), ok: res?.status() === 404 });77  }78  report.push({ checks });79  await ctx.close();80}81await browser.close();82writeFileSync(`${OUT}report.json`, JSON.stringify(report, null, 2));83for (const r of report) {84  if (r.checks) { for (const c of r.checks) console.log(c.ok ? 'OK  ' : 'FAIL', c.check, c.url ?? c.status ?? ''); continue; }85  const flags = [r.overflow > 0 ? `OVERFLOW ${r.overflow}px ${r.wide.join(' | ')}` : '', r.width < 768 && r.small.length ? `small: ${r.small.join(' | ')}` : '', r.errors.length ? `errors: ${r.errors.join(' | ').slice(0, 300)}` : ''].filter(Boolean);86  console.log(`${r.name}@${r.width} h=${r.docH} sticky=[${r.sticky.join(',')}] ${flags.join(' ; ')}`);87}88