HTML 77.2%
TypeScript 10.5%
Python 9.6%
JavaScript 2.5%
1/**2 * D2 QA sweep — intelligence surfaces (frontier · prices · calculator · run-locally · find-a-model · open · pulse · providers · hardware ·3 * agents · tools · datasets · frameworks) at 320 · 360 · 375 · 390 · 430 · 768 · 1366 · 1440 · 1920, dark + light.4 * Asserts: HTTP status (404 for unknown slugs), zero console errors, no horizontal overflow, an `Estimated` label on every fit page,5 * the price-index chart or its honest note on /prices, then a few flows (offer history expansion, calculator result + context tab,6 * provider history step chart, mobile filter sheet). Slugs are discovered live from the API.7 * Run: node qa/d2.mjs [BASE_URL] [API_URL] (defaults http://localhost:8342, http://127.0.0.1:8332)8 */9import { chromium } from '/Users/simon-pierreboucher/Desktop/uqo-eval/node_modules/playwright/index.mjs';10import { mkdirSync } from 'node:fs';1112const BASE = process.argv[2] ?? 'http://localhost:8342';13const API = process.argv[3] ?? 'http://127.0.0.1:8332';14const OUT = new URL('./screens/d2/', import.meta.url).pathname;15mkdirSync(OUT, { recursive: true });16const QUICK = process.env.QUICK === '1';17const WIDTHS = process.env.WIDTHS ? process.env.WIDTHS.split(',').map(Number) : QUICK ? [390, 1440] : [320, 360, 375, 390, 430, 768, 1366, 1440, 1920];18const THEMES = ['dark', 'light'];1920const j = async (p) => (await fetch(`${API}/api/v1${p}`)).json();21const providers = await j('/providers').catch(() => ({ items: [] }));22const topProvider = (providers.items ?? []).slice().sort((a, b) => Number(b.model_count) - Number(a.model_count))[0]?.slug ?? 'openrouter-api';23const hw = await j('/hardware?limit=50&sort=memory').catch(() => ({ items: [] }));24const hwSlug = (hw.items ?? []).find((h) => h.slug === 'apple-m4-max')?.slug ?? hw.items?.[0]?.slug ?? 'apple-m4-max';25const sug = await j('/search/suggest?q=claude').catch(() => ({ items: [] }));26const model = (sug.items ?? []).find((s) => s.entity_type === 'model')?.slug ?? 'claude-opus-5';27console.log(`slugs: provider=${topProvider} hardware=${hwSlug} model=${model}`);2829/** [path, expectedStatus, checks[]] — checks: 'estimated' (an Estimated label exists), 'index' (price chart or honest note). */30const PAGES = [31 ['/frontier', 200, []],32 ['/prices', 200, ['index']],33 ['/prices?sort=cheapest_frontier&days=30', 200, ['index']],34 ['/calculator', 200, []],35 [`/calculator?model=${model}&input_tokens=2000&output_tokens=800&requests_per_day=500&cached=20`, 200, []],36 [`/calculator?model=${model}&tab=context&tokens=200000`, 200, []],37 ['/run-locally', 200, ['estimated']],38 ['/run-locally?memory_gb=64&quant=4bit', 200, ['estimated']],39 ['/run-locally?memory_gb=24&gpu_count=2&quant=8bit&platform=nvidia&use_case=coding', 200, ['estimated']],40 [`/run-locally?hardware=${hwSlug}`, 200, ['estimated']],41 ['/find-a-model', 200, []],42 ['/find-a-model?use_case=coding&deployment=local&memory_gb=64', 200, ['estimated']],43 ['/find-a-model?use_case=chat&deployment=api&max_output_price=5', 200, []],44 ['/open', 200, ['estimated']],45 ['/open?license=Apache-2.0&min_params=7B&sort=params', 200, ['estimated']],46 ['/pulse', 200, []],47 ['/pulse?days=30', 200, []],48 ['/providers', 200, []],49 ['/providers?feature=batch&sort=median_input', 200, []],50 [`/providers/${topProvider}`, 200, []],51 ['/providers/does-not-exist', 404, []],52 ['/hardware', 200, []],53 ['/hardware?kind=gpu&min_memory=64', 200, []],54 ['/hardware/frontier', 200, ['estimated']],55 [`/hardware/${hwSlug}`, 200, ['estimated']],56 [`/hardware/${hwSlug}?quant=8bit&context=32768`, 200, ['estimated']],57 ['/hardware/does-not-exist', 404, []],58 ['/agents', 200, []],59 ['/tools', 200, []],60 ['/datasets', 200, []],61 ['/frameworks', 200, []],62 ['/frameworks?sort=stars', 200, []],63];64const OG = ['/frontier/opengraph-image', '/prices/opengraph-image', '/open/opengraph-image', '/pulse/opengraph-image', `/providers/${topProvider}/opengraph-image`, `/hardware/${hwSlug}/opengraph-image`];6566// 404 favicon noise and 429 on the fire-and-forget view beacon (the API allows 1 view/s per IP; the sweep is much faster) are not page errors67const filterErrors = (errors, expected) => errors.filter((e) => !/favicon|Failed to load resource: the server responded with a status of (404|429)/.test(e)).filter((e) => !(expected === 404 && /Encountered a script tag while rendering React component/.test(e)));68const smallTargets = () => {69 const root = document.querySelector('[role="dialog"]') ?? document;70 return [...root.querySelectorAll('button, [role="button"], input:not([type=hidden]):not([type=radio]):not([type=checkbox]), select, a[href]')]71 .filter((el) => {72 const r = el.getBoundingClientRect();73 if (r.width === 0 || r.height === 0) return false;74 const cs = getComputedStyle(el);75 if (cs.visibility === 'hidden' || cs.display === 'none') return false;76 if (el.closest('.sr-only, p, dd, .prose-atlas, td, th, .kv, [data-ticker], .data-table, footer, header, [data-section-nav], .hint, li')) return false;77 if (el.tagName === 'A') {78 const inNav = !!el.closest('nav, [role="menu"], [role="listbox"]');79 if (!inNav && el.textContent.trim().length > 0) return false;80 }81 return Math.min(r.width, r.height) < 44;82 })83 .map((el) => `${el.tagName.toLowerCase()}.${[...el.classList].slice(0, 2).join('.')} "${(el.getAttribute('aria-label') || el.textContent || el.getAttribute('name') || '').trim().slice(0, 20)}" ${Math.round(el.getBoundingClientRect().width)}×${Math.round(el.getBoundingClientRect().height)}`);84};85const slug = (p) => p.replace(/[^a-z0-9]+/gi, '_').replace(/^_|_$/g, '').slice(0, 80) || 'home';8687const browser = await chromium.launch();88let failures = 0;89let smallTotal = 0;9091for (const theme of THEMES) {92 for (const width of WIDTHS) {93 const mobile = width < 768;94 const ctx = await browser.newContext({ viewport: { width, height: mobile ? 844 : 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 [path, expected, checks] of PAGES) {98 // full matrix in dark; light at the four reference widths to keep the sweep under control99 if (theme === 'light' && ![390, 430, 768, 1440].includes(width) && !QUICK && !process.env.WIDTHS) continue;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 t0 = Date.now();106 const res = await page.goto(BASE + path, { waitUntil: 'networkidle', timeout: 90000 }).catch((e) => ({ status: () => `ERR ${e.message.slice(0, 40)}` }));107 if (/calculator\?model/.test(path)) await page.waitForSelector('[data-cost-row], [data-context-row], [data-calculator] p', { timeout: 15000 }).catch(() => undefined);108 await page.waitForTimeout(500);109 const overflow = await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth).catch(() => -1);110 const applied = await page.evaluate(() => document.documentElement.getAttribute('data-theme')).catch(() => null);111 const est = checks.includes('estimated') ? await page.evaluate(() => !![...document.querySelectorAll('a,span')].find((el) => /^estimated$/i.test(el.textContent.trim()))).catch(() => false) : true;112 const idx = checks.includes('index') ? await page.evaluate(() => !!document.querySelector('[data-price-index-chart], [data-price-index-note]')).catch(() => false) : true;113 const small = mobile ? await page.evaluate(smallTargets).catch(() => []) : [];114 smallTotal += small.length;115 const status = res.status();116 const filtered = filterErrors(errors, expected);117 const ok = status === expected && overflow <= 0 && filtered.length === 0 && applied === theme && est && idx;118 if (!ok) failures++;119 console.log(`${ok ? 'OK ' : 'FAIL'} ${theme.padEnd(5)} ${String(width).padStart(4)} ${status} ${String(Date.now() - t0).padStart(5)}ms overflow=${overflow} errors=${filtered.length}${checks.includes('estimated') ? ` estimated=${est}` : ''}${checks.includes('index') ? ` index=${idx}` : ''}${small.length ? ` small=${small.length}` : ''} ${path}${filtered.length ? ' :: ' + filtered[0].slice(0, 160) : ''}${small.length ? ' :: ' + small.slice(0, 2).join(' | ') : ''}`);120 if (theme === 'dark' || width === 390 || width === 1440) await page.screenshot({ path: `${OUT}${theme}-${width}-${slug(path)}.png`, fullPage: false }).catch(() => undefined);121 page.off('pageerror', onErr);122 page.off('console', onCon);123 }124 await ctx.close();125 }126}127128// ---------------------------------------------------------------------------------------------------------------- flows129console.log('\n— flows —');130{131 const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 }, colorScheme: 'dark' });132 await ctx.addInitScript(() => localStorage.setItem('aia-theme', 'dark'));133 const page = await ctx.newPage();134 const errors = [];135 page.on('pageerror', (e) => errors.push(String(e)));136 page.on('console', (m) => { if (m.type() === 'error') errors.push(m.text()); });137 const step = (ok, label) => { if (!ok) failures++; console.log(`${ok ? 'OK ' : 'FAIL'} flow ${label}`); };138139 // prices: expand the first offer row → history row appears (sparklines or the honest "no change" note)140 await page.goto(`${BASE}/prices`, { waitUntil: 'networkidle' });141 await page.locator('[data-expand-history]').first().click();142 const hist = await page.waitForSelector('[data-offer-history]', { timeout: 15000 }).catch(() => null);143 await page.waitForTimeout(1500);144 const histText = hist ? await page.locator('[data-offer-history]').first().innerText() : '';145 step(!!hist && !/Loading/.test(histText), `prices: offer row expands to history (${histText.replace(/\s+/g, ' ').slice(0, 60)}…)`);146 await page.screenshot({ path: `${OUT}flow-prices-history.png` });147148 // prices mobile: filter sheet opens with the rail form149 await page.setViewportSize({ width: 390, height: 844 });150 await page.goto(`${BASE}/prices`, { waitUntil: 'networkidle' });151 await page.locator('[data-open-filters]').click();152 const sheetForm = await page.waitForSelector('[role="dialog"] [data-price-filters]', { timeout: 8000 }).catch(() => null);153 step(!!sheetForm, 'prices mobile: filter sheet shows the rail form');154 await page.screenshot({ path: `${OUT}flow-prices-filter-sheet-390.png` });155 await page.keyboard.press('Escape');156 await page.setViewportSize({ width: 1440, height: 900 });157158 // calculator: URL-seeded model → cost rows; switch to Context tab → context rows; URL mirrors the tab159 await page.goto(`${BASE}/calculator?model=${model}&input_tokens=2000&output_tokens=800`, { waitUntil: 'networkidle' });160 const costRows = await page.waitForSelector('[data-cost-row]', { timeout: 20000 }).catch(() => null);161 step(!!costRows, 'calculator: cost rows render for the URL model');162 await page.screenshot({ path: `${OUT}flow-calculator-workload.png` });163 await page.locator('[data-calc-tab="context"]').click();164 const ctxRows = await page.waitForSelector('[data-context-row]', { timeout: 20000 }).catch(() => null);165 await page.waitForURL(/tab=context/, { timeout: 10000 }).catch(() => undefined);166 step(!!ctxRows && /tab=context/.test(page.url()), `calculator: context tab renders rows and URL carries tab=context (${page.url().split('?')[1]})`);167 await page.screenshot({ path: `${OUT}flow-calculator-context.png` });168169 // provider page: expand a deployment → step chart or honest note170 await page.goto(`${BASE}/providers/${topProvider}`, { waitUntil: 'networkidle' });171 await page.locator('[data-expand-history]').first().click();172 const ph = await page.waitForSelector('[data-offer-history]', { timeout: 15000 }).catch(() => null);173 await page.waitForTimeout(1500);174 step(!!ph, 'provider: deployment row expands to price history');175 await page.screenshot({ path: `${OUT}flow-provider-history.png` });176177 // run-locally: form submit round-trips through the URL and shows the Estimated banner178 await page.goto(`${BASE}/run-locally`, { waitUntil: 'networkidle' });179 await page.selectOption('[data-run-locally-form] select[name="memory_gb"]', '128');180 await page.selectOption('[data-run-locally-form] select[name="quant"]', '8bit');181 await page.locator('[data-run-locally-form] button[type="submit"]').click();182 await page.waitForLoadState('networkidle');183 const banner = await page.$('[data-estimate-banner]');184 step(!!banner && /memory_gb=128/.test(page.url()) && /quant=8bit/.test(page.url()), `run-locally: submit → ${page.url().split('?')[1]} with estimate banner`);185 await page.screenshot({ path: `${OUT}flow-run-locally-128-8bit.png` });186187 // frontier: section nav present and the efficiency scatter renders points188 await page.goto(`${BASE}/frontier`, { waitUntil: 'networkidle' });189 const nav = await page.$('[data-section-nav]');190 const scatter = await page.$('[data-scatter] circle');191 step(!!nav && !!scatter, 'frontier: section nav + efficiency scatter points');192193 const filtered = filterErrors(errors, 200);194 step(filtered.length === 0, `flows: zero console errors${filtered.length ? ' :: ' + filtered[0].slice(0, 160) : ''}`);195 await ctx.close();196}197198// ---------------------------------------------------------------------------------------------------------------- OG images199for (const p of OG) {200 const r = await fetch(BASE + p).catch(() => null);201 const ok = !!r && r.status === 200 && /image\/png/.test(r.headers.get('content-type') ?? '');202 if (!ok) failures++;203 console.log(`${ok ? 'OK ' : 'FAIL'} og ${r?.status ?? 'ERR'} ${r?.headers.get('content-type') ?? ''} ${p}`);204}205206await browser.close();207console.log(`\nsmall touch targets seen on mobile widths (informational): ${smallTotal}`);208console.log(failures ? `\n${failures} failure(s)` : '\nall checks OK');209process.exit(failures ? 1 : 0);210