HTML 77.2%
TypeScript 10.5%
Python 9.6%
JavaScript 2.5%
1/**2 * Shell flows (D0): command palette (⌘K → `m:claude` → select), More menu (keyboard), density toggle (persisted3 * `data-density`), evidence drawer on a model page (first `[data-evidence]`), watchlist add → /watchlist feed,4 * favicon render at 16 / 32 px, OG + icon routes. Screenshots → qa/screens/shell/.5 * Run: node qa/shell.mjs [BASE_URL] [API_URL]6 */7import { chromium } from '/Users/simon-pierreboucher/Desktop/uqo-eval/node_modules/playwright/index.mjs';8import { mkdirSync } from 'node:fs';910const BASE = process.argv[2] ?? 'http://localhost:8330';11const API = process.argv[3] ?? 'http://127.0.0.1:8331';12const OUT = new URL('./screens/shell/', import.meta.url).pathname;13mkdirSync(OUT, { recursive: true });14const MOD = process.platform === 'darwin' ? 'Meta' : 'Control';1516let failures = 0;17const check = (ok, label, extra = '') => {18 if (!ok) failures++;19 console.log(`${ok ? 'OK ' : 'FAIL'} ${label}${extra ? ' :: ' + extra : ''}`);20};2122const firstModel = await fetch(`${API}/api/v1/models?limit=1`)23 .then((r) => r.json())24 .then((j) => j.items?.[0]?.slug)25 .catch(() => null);26check(!!firstModel, `first model slug from API (${firstModel})`);2728const browser = await chromium.launch();29const errors = [];30const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 }, colorScheme: 'dark' });31await ctx.addInitScript(() => localStorage.setItem('aia-theme', 'dark'));32const page = await ctx.newPage();33page.on('pageerror', (e) => errors.push(String(e)));34// The 1.1 provenance endpoint may 404 until the API stream lands — the drawer falls back by design, so that resource error is expected.35page.on('console', (m) => m.type() === 'error' && !/favicon|Failed to load resource: the server responded with a status of 404/.test(m.text()) && errors.push(m.text()));3637// ---- 1. palette: ⌘K, type m:claude, arrow down, Enter → a model page38await page.goto(`${BASE}/`, { waitUntil: 'networkidle' });39await page.keyboard.press(`${MOD}+k`);40await page.waitForSelector('[data-palette-input]', { timeout: 5000 });41check(await page.isVisible('[data-palette]'), 'palette opens with ⌘K');42const cmdRows = await page.locator('[data-palette-row="command"]').count();43check(cmdRows >= 4, `palette shows commands when empty (${cmdRows})`);44await page.keyboard.type('m:claude');45await page.waitForSelector('[data-palette-row="entity"]', { timeout: 8000 }).catch(() => undefined);46const entRows = await page.locator('[data-palette-row="entity"]').count();47const prefixChip = await page.locator('[data-palette] .mono:has-text("Models")').count();48check(entRows > 0 && prefixChip > 0, `m: prefix filters to models (${entRows} rows, chip=${prefixChip})`);49const nonModel = await page.locator('[data-palette-row="entity"] span.uppercase').evaluateAll((els) => els.map((e) => e.textContent.trim()).filter((t) => t !== 'Model').length);50check(nonModel === 0, 'all palette rows are models');51await page.screenshot({ path: `${OUT}palette-m-claude.png` });52await page.keyboard.press('ArrowDown');53await page.keyboard.press('Enter');54await page.waitForURL(/\/models\//, { timeout: 15000 }).catch(() => undefined);55check(/\/models\//.test(page.url()), `Enter navigates to a model page (${new URL(page.url()).pathname})`);56const recent = await page.evaluate(() => JSON.parse(localStorage.getItem('aia-recent') || '[]').length);57check(recent >= 1, `recent entities stored (${recent})`);5859// commands mode: ">" then Toggle density60await page.waitForLoadState('networkidle');61await page.keyboard.press(`${MOD}+k`);62await page.waitForSelector('[data-palette-input]');63await page.focus('[data-palette-input]');64await page.keyboard.type('>dens');65await page.waitForSelector('[data-palette-row="command"][aria-selected="true"]:has-text("density")', { timeout: 5000 }).catch(() => undefined);66const cmdOnly = (await page.locator('[data-palette-row="entity"]').count()) === 0 && (await page.locator('[data-palette-row="command"]').count()) >= 1;67check(cmdOnly, '">" shows commands only');68await page.keyboard.press('Enter');69await page.waitForTimeout(200);70const dens1 = await page.evaluate(() => document.documentElement.getAttribute('data-density'));71check(dens1 === 'compact', `"Toggle density" command → data-density=${dens1}`);7273// ---- 2. More menu (mouse + keyboard)74await page.goto(`${BASE}/`, { waitUntil: 'networkidle' });75await page.click('[data-more-button]');76await page.waitForSelector('[data-more-menu]');77const items = await page.locator('[data-more-menu] [role="menuitem"]').count();78check(items >= 18, `More menu lists the full site (${items} items)`);79const menuBox = await page.locator('[data-more-menu]').boundingBox();80check(menuBox && menuBox.x >= 0 && menuBox.x + menuBox.width <= 1440, `More menu inside viewport (x=${Math.round(menuBox?.x)}, w=${Math.round(menuBox?.width)})`);81const focused = await page.evaluate(() => document.activeElement?.getAttribute('role'));82check(focused === 'menuitem', 'first menu item focused on open');83await page.keyboard.press('ArrowDown');84const second = await page.evaluate(() => document.activeElement?.textContent?.trim().split('\n')[0]);85check(!!second, `ArrowDown moves focus (${second})`);86await page.screenshot({ path: `${OUT}more-menu.png`, clip: { x: 0, y: 0, width: 1440, height: 460 } });87await page.keyboard.press('Escape');88check((await page.locator('[data-more-menu]').count()) === 0, 'Escape closes the More menu');8990// ---- 3. density toggle in header, persisted across reload91const before = await page.evaluate(() => document.documentElement.getAttribute('data-density'));92await page.click('[data-density-toggle]');93const after = await page.evaluate(() => document.documentElement.getAttribute('data-density'));94check(before !== after, `density toggle changes html[data-density] (${before} → ${after})`);95const padBefore = await page.evaluate(() => getComputedStyle(document.documentElement).getPropertyValue('--d-cell-y').trim());96await page.reload({ waitUntil: 'networkidle' });97const persisted = await page.evaluate(() => document.documentElement.getAttribute('data-density'));98check(persisted === after, `density persisted before paint after reload (${persisted})`);99await page.screenshot({ path: `${OUT}density-${after ?? 'comfortable'}.png` });100await page.evaluate(() => localStorage.removeItem('aia-density'));101check(!!padBefore, `density CSS variable readable (--d-cell-y=${padBefore})`);102103// ---- 4. evidence drawer on a model page104if (firstModel) {105 await page.goto(`${BASE}/models/${firstModel}`, { waitUntil: 'networkidle' });106 const triggers = await page.locator('[data-evidence]').count();107 check(triggers > 0, `model page has evidence triggers (${triggers})`);108 if (triggers) {109 await page.locator('[data-evidence]').first().click();110 await page.waitForSelector('[data-evidence-drawer]', { timeout: 5000 });111 await page.waitForTimeout(1200);112 const txt = await page.locator('[data-evidence-drawer]').innerText();113 check(/Source/.test(txt) && /Tier/.test(txt) && /Observed/.test(txt) && /Extractor/.test(txt), 'drawer shows Source · Tier · Observed · Extractor');114 check(/View history/.test(txt), 'drawer links to the property history');115 check(/#evidence=/.test(page.url()), `URL hash mirrors the drawer (${new URL(page.url()).hash.slice(0, 40)})`);116 await page.screenshot({ path: `${OUT}evidence-drawer.png` });117 await page.keyboard.press('Escape');118 await page.waitForTimeout(200);119 check((await page.locator('[data-evidence-drawer]').count()) === 0, 'Escape closes the drawer');120 }121122 // ---- 5. watchlist add + page123 await page.click('[data-watch-button]');124 await page.waitForTimeout(200);125 check((await page.getAttribute('[data-watch-button]', 'aria-pressed')) === 'true', 'Watch button toggles aria-pressed');126 const stored = await page.evaluate(() => JSON.parse(localStorage.getItem('aia-watchlist') || '[]'));127 check(stored.length === 1 && stored[0].slug === firstModel, `watchlist stored in localStorage (${stored.map((s) => s.slug).join(',')})`);128 await page.goto(`${BASE}/watchlist`, { waitUntil: 'networkidle' });129 await page.waitForSelector('[data-watchlist-items]', { timeout: 8000 });130 const listed = await page.locator('[data-watchlist-items] li').count();131 check(listed === 1, `/watchlist lists the watched entity (${listed})`);132 await page.waitForTimeout(1500);133 const feedRows = await page.locator('[data-watchlist-feed] li').count();134 const emptyMsg = await page.locator('text=No release, price, deprecation').count();135 check(feedRows > 0 || emptyMsg > 0, `/watchlist shows events or an honest empty state (rows=${feedRows}, empty=${emptyMsg})`);136 await page.screenshot({ path: `${OUT}watchlist.png` });137 await page.evaluate(() => localStorage.removeItem('aia-watchlist'));138}139140// ---- 6. favicon crispness at 16 / 32 px + brand routes141const brand = await ctx.newPage();142await brand.setViewportSize({ width: 360, height: 100 });143await brand.setContent(`<body style="margin:0;background:#e5e5e5;display:flex;gap:20px;align-items:center;padding:16px"><img src="${BASE}/icon.svg" width=16 height=16><img src="${BASE}/icon.svg" width=32 height=32><img src="${BASE}/apple-icon" width=60 height=60><img src="${BASE}/logo-lockup.svg" height=44></body>`);144await brand.waitForTimeout(600);145await brand.screenshot({ path: `${OUT}favicon-16-32.png` });146for (const p of ['/icon.svg', '/apple-icon', '/icon-192.png', '/icon-512.png', '/opengraph-image', '/manifest.webmanifest', '/logo.svg', '/logo-lockup.svg', '/logo-lockup-light.svg']) {147 const r = await fetch(BASE + p);148 check(r.ok, `${p} → ${r.status} ${r.headers.get('content-type')}`);149}150await brand.close();151152check(errors.length === 0, `no console/page errors during flows (${errors.length})`, errors[0]?.slice(0, 160));153await browser.close();154console.log(failures ? `\n${failures} failure(s)` : '\nall shell flows OK');155process.exit(failures ? 1 : 0);156