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.
147 lines
4.4 KiB
147 lines
4.4 KiB
|
6 months ago
|
/**
|
||
|
|
* 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);
|