You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/scripts/smoke-check.mjs

206 lines
6.5 KiB

/**
* Playwright smoke check for the SvelteKit demo/docs pages under web/routes.
*
* 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 (`Translation key not found`).
* - No soma context-not-found warnings (`Context "X" not found`).
* - No rendered `__uix_lang_missing__` fallback marker.
*
* Usage:
* node scripts/smoke-check.mjs [base-url]
* SMOKE_SCOPE=/uix 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 { existsSync, readdirSync } 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, '..', 'web', 'routes');
const scope = normaliseScope(process.env.SMOKE_SCOPE);
function normaliseScope(value) {
if (!value) return null;
const trimmed = value.trim();
if (!trimmed || trimmed === '/') return null;
return trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
}
function routeFromSegments(segments) {
const routeSegments = [];
for (const segment of segments) {
if (segment.startsWith('(') && segment.endsWith(')')) continue;
if (segment.startsWith('@')) continue;
if (segment.includes('[')) return null;
routeSegments.push(segment);
}
return `/${routeSegments.join('/')}`.replace(/\/$/, '') || '/';
}
/** Find every concrete `{route}/+page.svelte` under web/routes recursively. */
function discoverRoutes(dir, segments = []) {
const routes = [];
const entries = readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const name = entry.name;
const full = join(dir, name);
if (entry.isDirectory()) {
routes.push(...discoverRoutes(full, [...segments, name]));
} else if (name === '+page.svelte') {
const route = routeFromSegments(segments);
if (route && (!scope || route === scope || route.startsWith(`${scope}/`))) {
routes.push(route);
}
}
}
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 baseOrigin = new URL(BASE).origin;
if (!existsSync(ROUTES_DIR)) {
console.error(`Routes directory not found: ${ROUTES_DIR}`);
process.exit(2);
}
const routes = discoverRoutes(ROUTES_DIR).sort();
if (routes.length === 0) {
console.error(
scope
? `No concrete +page.svelte files found under web/routes for scope ${scope}.`
: 'No concrete +page.svelte files found under web/routes.'
);
process.exit(2);
}
console.error(`Checking ${routes.length} routes${scope ? ` under ${scope}` : ''}...`);
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('requestfailed', (request) => {
const url = request.url();
if (url.startsWith('data:') || url.startsWith('blob:') || url.includes('/@vite/')) return;
const origin = new URL(url).origin;
if (origin !== baseOrigin) return;
errors.push(` requestfailed: ${url} (${request.failure()?.errorText ?? 'unknown error'})`);
});
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;
// Resource request failures are handled by the `requestfailed` hook above,
// where we can distinguish local app regressions from optional external assets.
if (t.startsWith('Failed to load resource:')) return;
errors.push(` console.error: ${t}`);
}
if (m.type() === 'warning' || m.type() === 'warn') {
if (
t.includes('Translation key not found') ||
t.includes('__uix_lang_missing__') ||
t.includes('Context') ||
t.includes('[soma]') ||
t.includes('soma context')
) {
warnings.push(` console.warning: ${t}`);
}
}
});
try {
const res = await page.goto(BASE + route, {
waitUntil: 'domcontentloaded',
timeout: 20000
});
const status = res?.status() ?? 0;
// Give reactive effects + lazy demos a moment to settle.
await page.waitForTimeout(500);
const bodyText = await page
.locator('body')
.innerText({ timeout: 2000 })
.catch(() => '');
if (bodyText.includes('__uix_lang_missing__')) {
errors.push(' body: rendered __uix_lang_missing__ fallback marker');
}
if (
bodyText.includes('Internal Error') ||
bodyText.includes('undefined is not a function') ||
bodyText.includes('Cannot read properties')
) {
errors.push(' body: rendered runtime error text');
}
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);

Powered by TurnKey Linux.