HTML 77.2%
TypeScript 10.5%
Python 9.6%
JavaScript 2.5%
1/**2 * QA sweep: key routes at 390 and 1440 px, dark and light — HTTP status, console errors, horizontal overflow, screenshot.3 * The shell pages (`/`, `/about`, `/watchlist`, and `/` with the command palette open) are also swept at4 * 320 · 360 · 390 · 430 · 768 · 1366 · 1440 · 1920 in both themes, with a touch-target check (≥ 44 px) on interactive elements.5 * Also asserts that the homepage counters match GET /api/v1/stats.6 * Run: node qa/screens.mjs [BASE_URL] [API_URL] (defaults http://localhost:8330, http://127.0.0.1:8331)7 */8import { chromium } from '/Users/simon-pierreboucher/Desktop/uqo-eval/node_modules/playwright/index.mjs';9import { mkdirSync } from 'node:fs';1011const BASE = process.argv[2] ?? 'http://localhost:8330';12const API = process.argv[3] ?? 'http://127.0.0.1:8331';13const OUT = new URL('./screens/', import.meta.url).pathname;14mkdirSync(OUT, { recursive: true });1516const PAGES = ['/', '/search?q=claude', '/models', '/models?openness=open-weights&sort=params', '/models/claude-opus-5', '/companies', '/companies/anthropic', '/providers', '/providers/anthropic-2', '/benchmarks', '/benchmarks/tau-bench', '/hardware/nvidia-dgx-b200', '/hardware', '/papers', '/frameworks', '/datasets', '/tools', '/changes', `/changes/${new Date().toISOString().slice(0, 10)}`, '/timeline', '/compare', '/explore', '/methodology', '/sources', '/about', '/watchlist', '/developers', '/bot', '/models/does-not-exist', '/does-not-exist'];17const WIDTHS = [390, 1440];18const THEMES = ['dark', 'light'];19/** Shell sweep (D0): every width in the spec, both themes, palette open as a fourth "page". */20const SHELL_PAGES = ['/', '/about', '/watchlist', '/#palette'];21const SHELL_WIDTHS = [320, 360, 390, 430, 768, 1366, 1440, 1920];22const fmt0 = new Intl.NumberFormat('en-US', { maximumFractionDigits: 0 });2324/** `SHELL_ONLY=1 node qa/screens.mjs` skips the route sweep and runs the shell sweep + counters check only (~2 min). */25const SHELL_ONLY = process.env.SHELL_ONLY === '1';2627const browser = await chromium.launch();28let failures = 0;2930const filterErrors = (errors, expected) => errors.filter((e) => !/favicon|Failed to load resource: the server responded with a status of 404/.test(e)).filter((e) => !(expected === 404 && /Encountered a script tag while rendering React component/.test(e)));3132/**33 * Interactive *controls* smaller than 44 × 44 CSS px: buttons, inputs, selects, icon links and nav links. Inline text links inside34 * content (feed rows, prose, tables, key–value, footers) are exempt — they are text, not controls. When a modal (`[data-palette]`,35 * `[role="dialog"]`) is open only its own controls are measured, since the page behind it is inert.36 */37const smallTargets = () => {38 const root = document.querySelector('[data-palette], [role="dialog"]') ?? document;39 return [...root.querySelectorAll('button, [role="button"], input, select, a[href]')]40 .filter((el) => {41 const r = el.getBoundingClientRect();42 if (r.width === 0 || r.height === 0) return false;43 const cs = getComputedStyle(el);44 if (cs.visibility === 'hidden' || cs.display === 'none') return false;45 if (el.closest('.sr-only, p, dd, .prose-atlas, td, .kv, [data-ticker], .data-table, footer')) return false;46 if (el.tagName === 'A') {47 // Text links outside navigation (feed rows, list items, headings) are content, not controls; icon-only links are controls.48 const inNav = !!el.closest('nav, [role="menu"], [role="listbox"]');49 const textLink = el.textContent.trim().length > 0;50 if (!inNav && textLink) return false;51 }52 return Math.min(r.width, r.height) < 44;53 })54 .map((el) => `${el.tagName.toLowerCase()}${el.id ? '#' + el.id : ''}.${[...el.classList].slice(0, 2).join('.')} "${(el.getAttribute('aria-label') || el.textContent || '').trim().slice(0, 24)}" ${Math.round(el.getBoundingClientRect().width)}×${Math.round(el.getBoundingClientRect().height)}`);55};5657for (const theme of SHELL_ONLY ? [] : THEMES) {58 for (const width of WIDTHS) {59 const mobile = width < 768;60 const ctx = await browser.newContext({ viewport: { width, height: mobile ? 844 : 900 }, deviceScaleFactor: 1, isMobile: mobile, hasTouch: mobile, colorScheme: theme });61 await ctx.addInitScript((t) => localStorage.setItem('aia-theme', t), theme);62 const page = await ctx.newPage();63 for (const path of PAGES) {64 if (theme === 'light' && !['/', '/models', '/models/claude-opus-5', '/changes', '/search?q=claude', '/companies/anthropic', '/about', '/watchlist'].includes(path)) continue;65 const errors = [];66 const onErr = (e) => errors.push(String(e));67 const onCon = (m) => { if (m.type() === 'error') errors.push(m.text()); };68 page.on('pageerror', onErr);69 page.on('console', onCon);70 const t0 = Date.now();71 const res = await page.goto(BASE + path, { waitUntil: 'networkidle', timeout: 60000 }).catch((e) => ({ status: () => `ERR ${e.message.slice(0, 40)}` }));72 await page.waitForTimeout(600);73 const overflow = await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth).catch(() => -1);74 const applied = await page.evaluate(() => document.documentElement.getAttribute('data-theme')).catch(() => null);75 const expected = /does-not-exist/.test(path) ? 404 : 200;76 const status = res.status();77 const filtered = filterErrors(errors, expected);78 const ok = status === expected && overflow <= 0 && filtered.length === 0 && applied === theme;79 if (!ok) failures++;80 console.log(`${ok ? 'OK ' : 'FAIL'} ${theme.padEnd(5)} ${width} ${status} ${String(Date.now() - t0).padStart(5)}ms overflow=${overflow} errors=${filtered.length} theme=${applied} ${path}${filtered.length ? ' :: ' + filtered[0].slice(0, 140) : ''}`);81 await page.screenshot({ path: `${OUT}${theme}-${width}-${path.replace(/[^a-z0-9]+/gi, '_').replace(/^_|_$/g, '') || 'home'}.png`, fullPage: false }).catch(() => undefined);82 page.off('pageerror', onErr);83 page.off('console', onCon);84 }85 await ctx.close();86 }87}8889// ---------------------------------------------------------------------------------------------------------- shell sweep90console.log('\n— shell sweep (320 … 1920, dark + light, palette open) —');91for (const theme of THEMES) {92 for (const width of SHELL_WIDTHS) {93 const mobile = width < 768;94 const ctx = await browser.newContext({ viewport: { width, height: mobile ? 800 : 900 }, deviceScaleFactor: 1, isMobile: mobile, hasTouch: mobile, colorScheme: theme });95 await ctx.addInitScript((t) => localStorage.setItem('aia-theme', t), theme);96 const page = await ctx.newPage();97 for (const p of SHELL_PAGES) {98 const palette = p === '/#palette';99 const path = palette ? '/' : p;100 const errors = [];101 const onErr = (e) => errors.push(String(e));102 const onCon = (m) => { if (m.type() === 'error') errors.push(m.text()); };103 page.on('pageerror', onErr);104 page.on('console', onCon);105 const res = await page.goto(BASE + path, { waitUntil: 'networkidle', timeout: 60000 }).catch((e) => ({ status: () => `ERR ${e.message.slice(0, 40)}` }));106 if (palette) {107 await page.keyboard.press(process.platform === 'darwin' ? 'Meta+k' : 'Control+k');108 await page.waitForSelector('[data-palette-input]', { timeout: 5000 }).catch(() => undefined);109 await page.waitForTimeout(300);110 } else await page.waitForTimeout(400);111 const overflow = await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth).catch(() => -1);112 const small = mobile ? await page.evaluate(smallTargets).catch(() => []) : [];113 const filtered = filterErrors(errors, 200);114 const ok = res.status() === 200 && overflow <= 0 && filtered.length === 0 && small.length === 0;115 if (!ok) failures++;116 console.log(`${ok ? 'OK ' : 'FAIL'} ${theme.padEnd(5)} ${String(width).padStart(4)} ${res.status()} overflow=${overflow} errors=${filtered.length} small-targets=${small.length} ${p}${small.length ? ' :: ' + small.slice(0, 3).join(' | ') : ''}${filtered.length ? ' :: ' + filtered[0].slice(0, 120) : ''}`);117 await page.screenshot({ path: `${OUT}shell-${theme}-${width}-${p.replace(/[^a-z0-9]+/gi, '_').replace(/^_|_$/g, '') || 'home'}.png`, fullPage: false }).catch(() => undefined);118 if (palette) await page.keyboard.press('Escape');119 page.off('pageerror', onErr);120 page.off('console', onCon);121 }122 await ctx.close();123 }124}125126// Homepage counters must come from /api/v1/stats (never hardcoded). The homepage is ISR-cached for 60 s while the API may be127// ingesting, so a counter is accepted when it lies between (stats before − growth over 60 s) and (stats after the page load):128// the tolerance is derived from the observed ingestion rate and is zero when the database is static.129try {130 const pick = (s) => ({ models: s.entities?.model, 'benchmark results': s.benchmark_results, documents: s.documents, papers: s.entities?.paper, providers: s.entities?.provider, datasets: s.entities?.dataset, 'current prices': s.prices_current });131 const t0 = Date.now();132 const before = pick(await (await fetch(`${API}/api/v1/stats`)).json());133 const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });134 const page = await ctx.newPage();135 await page.goto(BASE + '/', { waitUntil: 'networkidle' });136 const text = await page.evaluate(() => document.body.innerText);137 await ctx.close();138 const after = pick(await (await fetch(`${API}/api/v1/stats`)).json());139 const elapsed = Math.max(1, (Date.now() - t0) / 1000);140 const nums = [...text.matchAll(/\d{1,3}(?:,\d{3})*/g)].map((m) => Number(m[0].replace(/,/g, '')));141 for (const label of Object.keys(before)) {142 const a = Number(before[label]);143 const b = Number(after[label]);144 if (!Number.isFinite(a) || !Number.isFinite(b)) continue;145 const rate = Math.max(0, b - a) / elapsed;146 const lo = a - Math.ceil(rate * 60);147 const hit = nums.some((n) => n >= lo && n <= b);148 if (!hit) failures++;149 console.log(`${hit ? 'OK ' : 'FAIL'} counter ${label}: page shows a value in [${fmt0.format(lo)}, ${fmt0.format(b)}] (stats ${fmt0.format(a)} → ${fmt0.format(b)} in ${elapsed.toFixed(0)} s)${hit ? '' : ' — NOT found'}`);150 }151} catch (e) {152 console.log(`SKIP counters check: API unreachable (${e.message})`);153}154155await browser.close();156console.log(failures ? `\n${failures} failure(s)` : '\nall checks OK');157process.exit(failures ? 1 : 0);158