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%
7.0 KB · 121 lines javascript
Raw Blame History
1/**2 * QA for the explore-agent routes (indicators / regions / explore / changes / data / sources / methodology / api /3 * admin): screenshots at 320/375/390/430/1280/1440 → qa/screens/explore/, plus automated checks per render:4 *  - horizontal overflow (scrollWidth > clientWidth) + the offending elements5 *  - tap targets < 44 px (both dimensions) on phone widths6 *  - console errors / page errors7 *  - 404 status for unknown slugs8 *  - the indicator map year slider reacts to touch (390 px, hasTouch) and a country tap shows the label9 * Run: node qa/shots-explore.mjs [BASE_URL]   (Playwright from ~/Desktop/uqo-eval via absolute import)10 */11import { chromium } from '/Users/simon-pierreboucher/Desktop/uqo-eval/node_modules/playwright/index.mjs';12import { mkdirSync, writeFileSync } from 'node:fs';1314const BASE = process.argv[2] ?? process.env.BASE_URL ?? 'http://localhost:8290';15const OUT = new URL('./screens/explore/', import.meta.url).pathname;16mkdirSync(OUT, { recursive: true });1718const PAGES = ['/indicators', '/indicators?topic=health', '/indicators/life-expectancy', '/indicators/gdp-per-capita', '/regions', '/regions/oecd', '/explore', '/changes', '/data', '/sources', '/sources/worldbank', '/methodology', '/api', '/admin/login'];19const NOT_FOUND = ['/indicators/nope', '/regions/nope', '/sources/nope'];20const WIDTHS = (process.env.WIDTHS ?? '320,375,390,430,1280,1440').split(',').map(Number);21const report = [];2223const browser = await chromium.launch();2425// --- 404s26for (const p of NOT_FOUND) {27  const ctx = await browser.newContext({ viewport: { width: 390, height: 844 } });28  const page = await ctx.newPage();29  const res = await page.goto(BASE + p, { waitUntil: 'domcontentloaded' });30  report.push({ path: p, width: 390, status: res?.status(), check: '404' });31  console.log(`${String(res?.status()).padStart(4)} ${p}`);32  await ctx.close();33}3435// --- screenshots + checks36for (const width of WIDTHS) {37  const mobile = width < 768;38  const ctx = await browser.newContext({ viewport: { width, height: mobile ? 844 : 900 }, deviceScaleFactor: 1, isMobile: mobile, hasTouch: mobile, colorScheme: 'light' });39  const page = await ctx.newPage();40  const errors = [];41  page.on('pageerror', (e) => errors.push(String(e)));42  page.on('console', (m) => {43    if (m.type() === 'error') errors.push(m.text());44  });45  for (const path of PAGES) {46    const res = await page.goto(BASE + path, { waitUntil: 'networkidle', timeout: 180_000 });47    await page.evaluate(() => document.fonts.ready);48    await page.waitForTimeout(500);49    const metrics = await page.evaluate(() => {50      const de = document.documentElement;51      const overflow = de.scrollWidth - de.clientWidth;52      const wide = [...document.querySelectorAll('body *')]53        .filter((el) => {54          const r = el.getBoundingClientRect();55          return r.right > de.clientWidth + 1 && r.width > 0 && getComputedStyle(el).position !== 'fixed';56        })57        .slice(0, 6)58        .map((el) => `${el.tagName.toLowerCase()}.${String(el.className).split(' ').slice(0, 3).join('.')} right=${Math.round(el.getBoundingClientRect().right)}`);59      const small = [...document.querySelectorAll('a,button,[role=button],input,select,summary')]60        .filter((el) => {61          const r = el.getBoundingClientRect();62          if (r.width === 0 || r.height === 0) return false;63          const cs = getComputedStyle(el);64          if (cs.visibility === 'hidden') return false;65          if (el.closest('.sr-only') || el.classList.contains('sr-only')) return false;66          return r.height < 44 && r.width < 44;67        })68        .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)}`);69      return { overflow, wide, small, docH: de.scrollHeight };70    });71    const name = `${path.slice(1).replace(/[/?=]/g, '_')}-${width}.png`;72    await page.screenshot({ path: OUT + name, fullPage: true });73    const r = { path, width, status: res?.status(), overflow: metrics.overflow, wide: metrics.wide, small: mobile ? metrics.small.slice(0, 10) : [], docH: metrics.docH, errors: errors.splice(0), file: name };74    report.push(r);75    const flags = [r.overflow > 0 ? `OVERFLOW +${r.overflow}px` : 'ok', r.small.length ? `${r.small.length} small targets` : '', r.errors.length ? `${r.errors.length} console errors` : ''].filter(Boolean).join(' · ');76    console.log(`${String(width).padStart(4)} ${path.padEnd(34)} ${flags}`);77    if (r.overflow > 0) console.log('     wide:', r.wide.join(' | '));78    if (r.small.length) console.log('     small:', r.small.slice(0, 6).join(' | '));79    if (r.errors.length) console.log('     errors:', r.errors.slice(0, 3).join(' | '));80  }81  await ctx.close();82}8384// --- touch interaction: year slider + country tap on the indicator map (390 px)85{86  const ctx = await browser.newContext({ viewport: { width: 390, height: 844 }, isMobile: true, hasTouch: true });87  const page = await ctx.newPage();88  await page.goto(BASE + '/indicators/life-expectancy', { waitUntil: 'networkidle', timeout: 180_000 });89  const slider = page.locator('input[type=range]').first();90  const before = await slider.evaluate((el) => el.getAttribute('aria-valuetext'));91  const box = await slider.boundingBox();92  await page.touchscreen.tap(box.x + box.width * 0.35, box.y + box.height / 2);93  await page.waitForTimeout(900);94  const after = await slider.evaluate((el) => el.getAttribute('aria-valuetext'));95  const legendTitle = await page.locator('#map figure title').first().textContent();96  const mapOk = before !== after && legendTitle?.includes(after ?? '');97  console.log(`slider touch: ${before} → ${after} · legend "${legendTitle}" · ${mapOk ? 'OK' : 'FAIL'}`);98  // country tap → sticky label with "Open" link99  const can = page.locator('#map path[aria-label^="Canada"]').first();100  const cb = await can.boundingBox();101  await page.touchscreen.tap(cb.x + cb.width / 2, cb.y + cb.height / 2);102  await page.waitForTimeout(400);103  const label = await page.locator('#map .pointer-events-none').first().textContent().catch(() => null);104  console.log(`country tap label: ${label ? label.slice(0, 60) : 'none'} · ${label?.includes('Canada') ? 'OK' : 'FAIL'}`);105  await page.screenshot({ path: OUT + 'indicators_life-expectancy-390-touch.png', fullPage: false });106  report.push({ path: '/indicators/life-expectancy', width: 390, check: 'touch', sliderBefore: before, sliderAfter: after, legendTitle, label });107  await ctx.close();108}109110// --- dark mode sample111{112  const ctx = await browser.newContext({ viewport: { width: 390, height: 844 }, colorScheme: 'dark', isMobile: true, hasTouch: true });113  const page = await ctx.newPage();114  await page.goto(BASE + '/indicators/gdp-per-capita', { waitUntil: 'networkidle', timeout: 180_000 });115  await page.screenshot({ path: OUT + 'indicators_gdp-per-capita-390-dark.png', fullPage: true });116  await ctx.close();117}118119await browser.close();120writeFileSync(OUT + 'report.json', JSON.stringify(report, null, 2));121