BillTracker/scripts/prod-smoke.js

69 lines
3.3 KiB
JavaScript

#!/usr/bin/env node
/**
* Production-build smoke (QA_PLAN B15). Drives the REAL built artifact — `node
* server.js` serving dist/ — not the Vite dev server, so it validates that the
* split vendor chunks (QA-B0-01) actually load and the app boots in production.
*
* Usage: node scripts/prod-smoke.js (expects `npm run build` already run and a
* server already listening on PROD_SMOKE_URL — the shell wrapper handles both).
*/
const { chromium } = require('@playwright/test');
const URL = process.env.PROD_SMOKE_URL || 'http://localhost:3098';
const USER = process.env.E2E_USER || 'e2e_user';
const PASS = process.env.E2E_PASS || 'e2e_pass_1234';
(async () => {
const browser = await chromium.launch();
const page = await browser.newPage();
const errors = [];
const failed = [];
page.on('console', (m) => { if (m.type() === 'error') errors.push(m.text()); });
page.on('pageerror', (e) => errors.push(String(e)));
page.on('requestfailed', (r) => failed.push(`${r.url()} ${r.failure()?.errorText || ''}`));
let ok = true;
const fail = (msg) => { ok = false; console.error(' ✖', msg); };
try {
// 1. App shell + split chunks load; login page renders.
await page.goto(URL + '/login', { waitUntil: 'networkidle' });
const heading = page.getByRole('heading', { name: /sign in/i });
if (await heading.isVisible().catch(() => false)) console.log(' ✔ login page renders (vendor chunks loaded)');
else fail('login page did not render — a chunk may have failed to load');
// 2. Log in and reach the authenticated app (lazy route chunks load).
await page.locator('#username').fill(USER);
await page.locator('#password').fill(PASS);
await page.getByRole('button', { name: /sign in/i }).click();
await page.waitForURL((u) => !u.pathname.startsWith('/login'), { timeout: 15000 });
const gotIt = page.getByRole('button', { name: 'Got it' });
await gotIt.click({ timeout: 8000 }).catch(() => {});
if (await page.getByRole('button', { name: 'Add Bill' }).isVisible().catch(() => false)) {
console.log(' ✔ authenticated Tracker renders on the production build');
} else fail('authenticated Tracker did not render');
// 3. A couple of lazy-loaded routes render (their chunks resolve).
for (const path of ['/bills', '/analytics', '/spending']) {
await page.goto(URL + path, { waitUntil: 'networkidle' });
const empty = await page.locator('body').innerText().catch(() => '');
if (empty && empty.trim().length > 0) console.log(`${path} rendered`);
else fail(`${path} rendered blank (lazy chunk failure?)`);
}
} catch (e) {
fail('exception: ' + e.message);
}
const chunkErrors = failed.filter((f) => /\.js|\.css|assets/.test(f));
if (chunkErrors.length) fail('failed asset requests:\n ' + chunkErrors.join('\n '));
// The pre-login page probes /api/auth/session and gets 401 by design (then shows
// login) — that's expected, not a defect. Everything else is a real error.
const benign = /session check failed|Not authenticated|auth\/session|status of 401/i;
const realErrors = errors.filter((e) => !benign.test(e));
if (realErrors.length) fail('console/page errors:\n ' + realErrors.slice(0, 5).join('\n '));
await browser.close();
console.log(ok ? '\nPROD SMOKE: PASS' : '\nPROD SMOKE: FAIL');
process.exit(ok ? 0 : 1);
})();