test(qa): production-build smoke (B15) — validates split chunks at runtime
- scripts/prod-smoke.js + prod-smoke.sh: build, boot `node server.js` serving dist/ against a scratch DB, and drive the real artifact with Playwright (login + lazy routes) to confirm the vendor-chunk split loads in production - npm run smoke:prod; passes green - docs: B15 harness command + status Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
819cfdfa9f
commit
3c1d000bab
|
|
@ -102,7 +102,7 @@ before cross-cutting; regression last). Update **Status** and **Findings** every
|
||||||
| B12 | Settings, Profile & global UI | `/settings`, `/profile`, static pages, command palette, sidebar/nav | any | ⬜ | 0 / 0 |
|
| B12 | Settings, Profile & global UI | `/settings`, `/profile`, static pages, command palette, sidebar/nav | any | ⬜ | 0 / 0 |
|
||||||
| B13 | API / backend direct | all `/api/*`: auth, CSRF, validation, rate limits, error shape, IDOR, cents | via HTTP client | 🔄 | 0 / 1 |
|
| B13 | API / backend direct | all `/api/*`: auth, CSRF, validation, rate limits, error shape, IDOR, cents | via HTTP client | 🔄 | 0 / 1 |
|
||||||
| B14 | Non-functional | a11y, performance, PWA/offline, XSS/secrets, timezone/DST | large + adversarial | 🔄 | 0 / 3 |
|
| B14 | Non-functional | a11y, performance, PWA/offline, XSS/secrets, timezone/DST | large + adversarial | 🔄 | 0 / 3 |
|
||||||
| B15 | Regression & sign-off | full smoke on **production build**, exit criteria | seeded | ⬜ | 0 / 0 |
|
| B15 | Regression & sign-off | full smoke on **production build**, exit criteria | seeded | 🔄 | 0 / 0 |
|
||||||
|
|
||||||
> After B15, if any batch is 🔁 or has open S1/S2, loop back. Then start a new
|
> After B15, if any batch is 🔁 or has open S1/S2, loop back. Then start a new
|
||||||
> cycle from B0 against the next build/version.
|
> cycle from B0 against the next build/version.
|
||||||
|
|
@ -231,6 +231,7 @@ Manual passes prove a button works **once**; they don't stop it regressing next
|
||||||
| `npm run test:e2e` | run the E2E suite headless (boots the app via `webServer`) |
|
| `npm run test:e2e` | run the E2E suite headless (boots the app via `webServer`) |
|
||||||
| `npm run test:e2e:ui` | Playwright UI mode — watch/debug interactively |
|
| `npm run test:e2e:ui` | Playwright UI mode — watch/debug interactively |
|
||||||
| `npm run test:e2e:update` | re-baseline visual-regression screenshots (review the diff before committing) |
|
| `npm run test:e2e:update` | re-baseline visual-regression screenshots (review the diff before committing) |
|
||||||
|
| `npm run smoke:prod` | **B15 production-build smoke** — builds, boots `node server.js` (dist/), drives the real artifact so the split vendor chunks are validated at runtime |
|
||||||
|
|
||||||
- **Setup (one-time):** `npm install` then `npx playwright install chromium`. Config: `playwright.config.js`; specs in `e2e/`.
|
- **Setup (one-time):** `npm install` then `npx playwright install chromium`. Config: `playwright.config.js`; specs in `e2e/`.
|
||||||
- **Scope:** the suite is a **thin critical-path smoke**, not a replacement for the manual playbooks — it locks the happy paths (login → pay bill → skip → note → reconcile), the primitive state matrix, per-page axe scans, and page screenshots. Grow it whenever a manual pass finds a UI regression that a click-test could have caught.
|
- **Scope:** the suite is a **thin critical-path smoke**, not a replacement for the manual playbooks — it locks the happy paths (login → pay bill → skip → note → reconcile), the primitive state matrix, per-page axe scans, and page screenshots. Grow it whenever a manual pass finds a UI regression that a click-test could have caught.
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
"test:e2e:ui": "node e2e/setup/prepare-db.js && playwright test --ui",
|
"test:e2e:ui": "node e2e/setup/prepare-db.js && playwright test --ui",
|
||||||
"test:e2e:update": "node e2e/setup/prepare-db.js && playwright test --project=chromium-desktop --project=chromium-mobile --update-snapshots",
|
"test:e2e:update": "node e2e/setup/prepare-db.js && playwright test --project=chromium-desktop --project=chromium-mobile --update-snapshots",
|
||||||
"test:e2e:probe": "node e2e/setup/prepare-db.js && playwright test --project=probe",
|
"test:e2e:probe": "node e2e/setup/prepare-db.js && playwright test --project=probe",
|
||||||
|
"smoke:prod": "bash scripts/prod-smoke.sh",
|
||||||
"ci": "npm run check:server && npm run test:all && npm run build",
|
"ci": "npm run check:server && npm run test:all && npm run build",
|
||||||
"start": "node server.js"
|
"start": "node server.js"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
#!/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);
|
||||||
|
})();
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Production-build smoke (QA_PLAN B15): builds the app, boots `node server.js`
|
||||||
|
# serving dist/ against a scratch DB, and drives the real artifact with Playwright
|
||||||
|
# to confirm the split vendor chunks load and the app works in production.
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
PORT="${PROD_SMOKE_PORT:-3098}"
|
||||||
|
export PROD_SMOKE_URL="http://localhost:${PORT}"
|
||||||
|
|
||||||
|
echo "[prod-smoke] building…"
|
||||||
|
npm run build >/dev/null
|
||||||
|
|
||||||
|
echo "[prod-smoke] preparing scratch DB…"
|
||||||
|
node e2e/setup/prepare-db.js >/dev/null 2>&1
|
||||||
|
|
||||||
|
echo "[prod-smoke] starting production server on :${PORT}…"
|
||||||
|
DB_PATH="db/e2e.db" PORT="${PORT}" BIND_HOST=127.0.0.1 node server.js >/tmp/prod-smoke-server.log 2>&1 &
|
||||||
|
SRV=$!
|
||||||
|
trap 'kill "${SRV}" 2>/dev/null || true' EXIT
|
||||||
|
|
||||||
|
for _ in $(seq 1 40); do
|
||||||
|
curl -sf "http://localhost:${PORT}/api/version" >/dev/null 2>&1 && break
|
||||||
|
sleep 0.5
|
||||||
|
done
|
||||||
|
|
||||||
|
node scripts/prod-smoke.js
|
||||||
Loading…
Reference in New Issue