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.8 KB · 82 lines javascript
Raw Blame History
1/**2 * Mobile-first QA: screenshots of the key pages at phone + desktop widths, plus automated checks:3 *  - no horizontal overflow (scrollWidth <= innerWidth)4 *  - interactive elements >= 44 px tall (buttons/links in header, tab bar, chips, metrics)5 *  - fixed bottom tab bar does not overlap the footer (body padding-bottom)6 *  - layout shift after fonts/charts settle (compare heights before/after)7 * Run: NODE_PATH=/Users/simon-pierreboucher/Desktop/uqo-eval/node_modules node qa/screens.mjs [BASE_URL]8 */9import { chromium } from 'playwright';10import { mkdirSync, writeFileSync } from 'node:fs';11import { createRequire } from 'node:module';1213const BASE = process.argv[2] ?? process.env.BASE_URL ?? 'http://localhost:8290';14const OUT = new URL('./screens/', import.meta.url).pathname;15mkdirSync(OUT, { recursive: true });1617const PAGES = ['/', '/countries', '/countries/canada', '/countries/canada/economy'];18const WIDTHS = [320, 360, 375, 390, 414, 430, 1280, 1440];19const report = [];2021const browser = await chromium.launch();22for (const width of WIDTHS) {23  const mobile = width < 768;24  const ctx = await browser.newContext({ viewport: { width, height: mobile ? 800 : 900 }, deviceScaleFactor: 1, isMobile: mobile, hasTouch: mobile, colorScheme: 'light' });25  const page = await ctx.newPage();26  const errors = [];27  page.on('pageerror', (e) => errors.push(String(e)));28  page.on('console', (m) => { if (m.type() === 'error') errors.push(m.text()); });29  for (const path of PAGES) {30    await page.goto(BASE + path, { waitUntil: 'networkidle' });31    await page.evaluate(() => document.fonts.ready);32    const h1 = await page.evaluate(() => document.documentElement.scrollHeight);33    await page.waitForTimeout(600);34    const metrics = await page.evaluate(() => {35      const de = document.documentElement;36      const overflow = de.scrollWidth - de.clientWidth;37      // elements wider than the viewport38      const wide = [...document.querySelectorAll('body *')].filter((el) => { const r = el.getBoundingClientRect(); return r.right > de.clientWidth + 1 && r.width > 0; }).slice(0, 8).map((el) => `${el.tagName.toLowerCase()}.${String(el.className).split(' ').slice(0, 3).join('.')} right=${Math.round(el.getBoundingClientRect().right)}`);39      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)}`);40      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)}`);41      const tab = document.querySelector('nav.fixed');42      const bodyPad = parseFloat(getComputedStyle(document.body).paddingBottom);43      const footer = document.querySelector('footer');44      return { overflow, wide, small, smallH: smallH.slice(0, 12), tabBar: tab ? tab.getBoundingClientRect().height : 0, bodyPad, footerBottom: footer ? footer.getBoundingClientRect().bottom + window.scrollY : 0, docH: de.scrollHeight };45    });46    const h2 = metrics.docH;47    const name = `${path === '/' ? 'home' : path.slice(1).replace(/\//g, '_')}-${width}.png`;48    await page.screenshot({ path: OUT + name, fullPage: true });49    report.push({ path, width, overflow: metrics.overflow, wide: metrics.wide, small: metrics.small.slice(0, 10), smallH: metrics.smallH, tabBar: metrics.tabBar, bodyPad: metrics.bodyPad, cls: h2 - h1, errors: errors.splice(0), file: name });50  }51  await ctx.close();52}53// Dark-mode sample + favicon rasterisation54{55  const ctx = await browser.newContext({ viewport: { width: 390, height: 800 }, colorScheme: 'dark', isMobile: true, hasTouch: true });56  const page = await ctx.newPage();57  await page.goto(BASE + '/countries/canada', { waitUntil: 'networkidle' });58  await page.screenshot({ path: OUT + 'countries_canada-390-dark.png', fullPage: true });59  await ctx.close();60}61{62  const require = createRequire(import.meta.url);63  const { readFileSync } = require('node:fs');64  const svg = readFileSync(new URL('../src/app/icon.svg', import.meta.url), 'utf8');65  for (const [size, file] of [[512, '../src/app/icon.png'], [180, '../src/app/apple-icon.png']]) {66    const ctx = await browser.newContext({ viewport: { width: size, height: size }, deviceScaleFactor: 1 });67    const page = await ctx.newPage();68    await page.setContent(`<html><body style="margin:0;background:transparent">${svg.replace(/width="\d+" height="\d+"/, `width="${size}" height="${size}"`)}</body></html>`);69    await page.screenshot({ path: new URL(file, import.meta.url).pathname, omitBackground: true, clip: { x: 0, y: 0, width: size, height: size } });70    await ctx.close();71  }72}73await browser.close();74writeFileSync(OUT + 'report.json', JSON.stringify(report, null, 2));75for (const r of report) {76  const flags = [r.overflow > 0 ? `OVERFLOW +${r.overflow}px` : 'ok', r.small.length ? `${r.small.length} small targets` : '', r.cls ? `Δh ${r.cls}` : '', r.errors.length ? `${r.errors.length} console errors` : ''].filter(Boolean).join(' · ');77  console.log(`${r.width.toString().padStart(4)} ${r.path.padEnd(28)} ${flags}`);78  if (r.overflow > 0) console.log('     wide:', r.wide.join(' | '));79  if (r.small.length) console.log('     small:', r.small.slice(0, 6).join(' | '));80  if (r.errors.length) console.log('     errors:', r.errors.slice(0, 3).join(' | '));81}82