#!/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); })();