/** * Playwright smoke check for the /test/soma/* demo pages. * * What it verifies per route (beyond HTTP 200): * - No uncaught `pageerror` (thrown during component init / hydration). * - No `console.error` (runtime exceptions that didn't throw synchronously). * - No translation key missing warnings (`[lang] Translation key not found`). * - No soma context-not-found warnings (`Context "X" not found`). * * Usage: * node scripts/smoke-check.mjs [base-url] * npm run smoke * * Requires `npm run dev` running in another terminal (or passes an explicit * base URL). The script tries 5173-5180 if no URL is given. */ import { chromium } from 'playwright'; import { readdirSync, statSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { dirname, join } from 'node:path'; const __dirname = dirname(fileURLToPath(import.meta.url)); const ROUTES_DIR = join(__dirname, '..', 'src', 'routes', 'test', 'soma'); /** Find every `{route}/+page.svelte` under /test/soma recursively. */ function discoverRoutes(dir, prefix) { const routes = []; const entries = readdirSync(dir); for (const name of entries) { const full = join(dir, name); const st = statSync(full); if (st.isDirectory()) { routes.push(...discoverRoutes(full, `${prefix}/${name}`)); } else if (name === '+page.svelte') { routes.push(prefix || '/test/soma'); } } return routes; } async function probePort(startPort, endPort) { for (let port = startPort; port <= endPort; port++) { try { const res = await fetch(`http://localhost:${port}/`, { signal: AbortSignal.timeout(500) }); if (res.ok || res.status === 404 || res.status === 500) { return `http://localhost:${port}`; } } catch { // connection refused — try next } } return null; } const BASE = process.argv[2] ?? (await probePort(5173, 5180)); if (!BASE) { console.error('Could not find a running dev server on 5173-5180.'); console.error('Start it with `npm run dev` in another terminal.'); process.exit(2); } console.error(`Using dev server at ${BASE}`); const routes = discoverRoutes(ROUTES_DIR, '/test/soma').sort(); if (routes.length === 0) { console.error('No +page.svelte files found under /test/soma.'); process.exit(2); } console.error(`Checking ${routes.length} routes...`); const browser = await chromium.launch(); const ctx = await browser.newContext(); const failures = []; const MAX_PARALLEL = 4; async function checkRoute(route) { const page = await ctx.newPage(); const errors = []; const warnings = []; page.on('pageerror', (e) => errors.push(` pageerror: ${e.message}`)); page.on('console', (m) => { const t = m.text(); if (m.type() === 'error') { // Filter out some noisy devtools messages unrelated to correctness. if (t.includes('Download the React DevTools')) return; errors.push(` console.error: ${t}`); } if (m.type() === 'warning' || m.type() === 'warn') { if ( t.includes('Translation key not found') || t.includes('Context') || t.includes('[soma]') || t.includes('soma context') ) { warnings.push(` console.warning: ${t}`); } } }); try { const res = await page.goto(BASE + route, { waitUntil: 'networkidle', timeout: 20000 }); const status = res?.status() ?? 0; // Give reactive effects + lazy demos a moment to settle. await page.waitForTimeout(500); if (status !== 200 || errors.length > 0 || warnings.length > 0) { const lines = [`FAIL ${route} (HTTP ${status})`]; errors.forEach((e) => lines.push(e)); warnings.forEach((w) => lines.push(w)); return { ok: false, lines }; } return { ok: true, lines: [`PASS ${route}`] }; } catch (e) { return { ok: false, lines: [`FAIL ${route} (navigation error: ${e.message})`] }; } finally { await page.close(); } } // Run checks with bounded concurrency so the dev server doesn't get hammered. let cursor = 0; async function worker() { while (cursor < routes.length) { const idx = cursor++; const route = routes[idx]; const result = await checkRoute(route); result.lines.forEach((l) => console.log(l)); if (!result.ok) failures.push(route); } } await Promise.all(Array.from({ length: MAX_PARALLEL }, () => worker())); await browser.close(); console.log(''); console.log( failures.length === 0 ? `All ${routes.length} routes passed.` : `${failures.length}/${routes.length} routes failed: ${failures.join(', ')}` ); process.exit(failures.length === 0 ? 0 : 1);