/** * QA for the satellite / launches pages: screenshots at 390 and 1440, console errors, horizontal overflow, * DOM-order = visual-order check on the satellite detail page. * node qa/satellite-qa.mjs [baseUrl] */ import { mkdirSync } from 'node:fs'; import path from 'node:path'; import { chromium } from '/Users/simon-pierreboucher/Desktop/uqo-eval/node_modules/playwright/index.mjs'; const BASE = process.argv[2] ?? 'http://localhost:8319'; const OUT = path.resolve(import.meta.dirname, 'screens/satellite'); mkdirSync(OUT, { recursive: true }); const PAGES = [ ['satellite-iss', '/satellite/iss-zarya-25544'], ['satellite-starlink', '/satellite/starlink-38436-100641'], ['satellite-sputnik', '/satellite/sputnik-1-2'], ['satellites', '/satellites'], ['satellites-starlink-active', '/satellites?constellation=starlink&status=ACTIVE'], ['satellites-empty', '/satellites?status=PLANNED&object_type=DEBRIS'], ['launches', '/launches'], ['launches-2026', '/launches?year=2026'], ['launch-1998-067', '/launch/1998-067'], ['launch-sites', '/launch-sites'], ['launch-site-baikonur', '/launch-sites/baikonur-cosmodrome-tyuratam'], ]; const VIEWPORTS = [ ['390', { width: 390, height: 844 }], ['1440', { width: 1440, height: 900 }], ]; const browser = await chromium.launch(); let failures = 0; for (const [vpName, viewport] of VIEWPORTS) { const ctx = await browser.newContext({ viewport, deviceScaleFactor: 1, colorScheme: 'dark' }); for (const [name, url] of PAGES) { const page = await ctx.newPage(); const errors = []; page.on('console', (m) => m.type() === 'error' && errors.push(m.text())); page.on('pageerror', (e) => errors.push(`pageerror: ${e.message}`)); const res = await page.goto(BASE + url, { waitUntil: 'networkidle', timeout: 90_000 }); await page.waitForTimeout(1200); const metrics = await page.evaluate(() => { const de = document.documentElement; const overflow = de.scrollWidth - de.clientWidth; const wide = [...document.querySelectorAll('body *')].filter((el) => el.getBoundingClientRect().right > de.clientWidth + 1).slice(0, 5).map((el) => `${el.tagName.toLowerCase()}.${String(el.className).split(' ').slice(0, 2).join('.')}`); // DOM order == visual order for the detail-page sections const ids = ['live', 'orbit', 'mission', 'ownership', 'launch', 'history', 'registration', 'sources', 'events', 'related', 'identifiers']; const tops = ids.map((id) => document.getElementById(id)?.getBoundingClientRect().top).filter((t) => t !== undefined); const ordered = tops.every((t, i) => i === 0 || t >= tops[i - 1]); const h1 = document.querySelector('h1'); const h1Top = h1 ? h1.getBoundingClientRect().top + window.scrollY : null; const firstSectionTop = tops[0] !== undefined ? tops[0] + window.scrollY : null; const usesOrder = [...document.querySelectorAll('main *')].some((el) => getComputedStyle(el).order !== '0'); return { overflow, wide, ordered, heroFirst: h1Top === null || firstSectionTop === null || h1Top < firstSectionTop, usesOrder, scrollY: window.scrollY, title: document.title }; }); const file = path.join(OUT, `${name}-${vpName}.png`); await page.screenshot({ path: file, fullPage: true }); const bad = (res?.status() ?? 0) >= 400 || errors.length || metrics.overflow > 0 || !metrics.ordered || !metrics.heroFirst || metrics.usesOrder || metrics.scrollY !== 0; if (bad) failures++; console.log(`${bad ? 'FAIL' : ' ok '} ${vpName.padEnd(4)} ${url.padEnd(52)} ${res?.status()} overflow=${metrics.overflow} order=${metrics.ordered ? 'ok' : 'BAD'} hero=${metrics.heroFirst ? 'first' : 'NOT-FIRST'} cssOrder=${metrics.usesOrder ? 'USED' : 'none'} errors=${errors.length}`); if (errors.length) console.log(' ', errors.slice(0, 3).join('\n ')); if (metrics.wide.length) console.log(' wide:', metrics.wide.join(', ')); await page.close(); } await ctx.close(); } await browser.close(); console.log(failures ? `\n${failures} page(s) failed` : '\nall pages passed'); process.exit(failures ? 1 : 0);