/** * QA for the Compare + Rankings pages: screenshots at 320/375/390/430/1280/1440 → qa/screens/compare-rankings/, * plus checks: horizontal overflow, tap targets < 44 px (mobile), console errors, sticky controls height, * URL state round-trip (reload keeps state), 404s. Run: node qa/compare-rankings.mjs [BASE_URL] */ import { chromium } from '/Users/simon-pierreboucher/Desktop/uqo-eval/node_modules/playwright/index.mjs'; import { mkdirSync, writeFileSync } from 'node:fs'; const BASE = process.argv[2] ?? 'http://localhost:8290'; const OUT = new URL('./screens/compare-rankings/', import.meta.url).pathname; mkdirSync(OUT, { recursive: true }); const PAGES = [ ['compare', '/compare'], ['cmp-snapshot', '/compare/canada/united-states/france'], ['cmp-economy', '/compare/canada/united-states/france?tab=economy'], ['cmp-hero', '/compare/canada/united-states/france?tab=economy&indicator=gdp-per-capita&from=1990&mode=index100'], ['rankings', '/rankings'], ['rk-gdppc', '/rankings/gdp-per-capita'], ['rk-life-oecd', '/rankings/life-expectancy?group=oecd'], ]; const WIDTHS = [320, 375, 390, 430, 1280, 1440]; const report = []; const browser = await chromium.launch(); for (const width of WIDTHS) { const mobile = width < 768; const ctx = await browser.newContext({ viewport: { width, height: mobile ? 800 : 900 }, deviceScaleFactor: 1, isMobile: mobile, hasTouch: mobile, colorScheme: 'light' }); const page = await ctx.newPage(); const errors = []; page.on('pageerror', (e) => errors.push(String(e))); page.on('console', (m) => { if (m.type() === 'error') errors.push(m.text()); }); for (const [name, path] of PAGES) { await page.goto(BASE + path, { waitUntil: 'networkidle' }); await page.evaluate(() => document.fonts.ready); await page.waitForTimeout(700); const m = await page.evaluate(() => { const de = document.documentElement; const overflow = de.scrollWidth - de.clientWidth; 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)}`); 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)}`); 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)}`); const sticky = [...document.querySelectorAll('.sticky')].filter((el) => el.getBoundingClientRect().height > 0).map((el) => Math.round(el.getBoundingClientRect().height)); return { overflow, wide, small: small.slice(0, 12), smallH: smallH.slice(0, 12), sticky, docH: de.scrollHeight }; }); await page.screenshot({ path: `${OUT}${name}-${width}.png`, fullPage: true }); report.push({ name, path, width, ...m, errors: errors.splice(0) }); } await ctx.close(); } // URL state round-trip + 404s (desktop) { const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } }); const page = await ctx.newPage(); const checks = []; await page.goto(`${BASE}/compare/canada/united-states/france?tab=economy`, { waitUntil: 'networkidle' }); await page.getByRole('radio', { name: 'Index = 100' }).click(); await page.waitForTimeout(1200); const url1 = page.url(); await page.reload({ waitUntil: 'networkidle' }); const modeChecked = await page.getByRole('radio', { name: 'Index = 100' }).getAttribute('aria-checked'); checks.push({ check: 'compare mode round-trip', url: url1, ok: url1.includes('mode=index100') && modeChecked === 'true' }); await page.getByRole('tab', { name: 'Population' }).click(); await page.waitForTimeout(1200); checks.push({ check: 'tab switch updates URL', url: page.url(), ok: page.url().includes('tab=population') }); await page.goto(`${BASE}/rankings/gdp-per-capita`, { waitUntil: 'networkidle' }); await page.locator('select[aria-label="Year"]:visible').first().selectOption('2015'); await page.waitForTimeout(1500); const url2 = page.url(); await page.reload({ waitUntil: 'networkidle' }); const yearVal = await page.locator('select[aria-label="Year"]:visible').first().inputValue(); checks.push({ check: 'ranking year round-trip', url: url2, ok: url2.includes('year=2015') && yearVal === '2015' }); for (const p of ['/compare/canada', '/rankings/not-a-thing', '/compare/canada/nowhere-land']) { const res = await page.goto(BASE + p, { waitUntil: 'domcontentloaded' }); checks.push({ check: `404 ${p}`, status: res?.status(), ok: res?.status() === 404 }); } report.push({ checks }); await ctx.close(); } await browser.close(); writeFileSync(`${OUT}report.json`, JSON.stringify(report, null, 2)); for (const r of report) { if (r.checks) { for (const c of r.checks) console.log(c.ok ? 'OK ' : 'FAIL', c.check, c.url ?? c.status ?? ''); continue; } 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); console.log(`${r.name}@${r.width} h=${r.docH} sticky=[${r.sticky.join(',')}] ${flags.join(' ; ')}`); }