/** * Smoke test — boots the built app and asserts: * * 1. `/api/health` returns 200 JSON with `ok: true`. * 2. `/` redirects to `//home`. * 3. Each public page (`/en/home`, `/en/about-us`, * `/en/contact-us`, `/en/privacy-policy`, `/en/terms-of-service`) * returns 200 with an HTML body containing the SPA root element. * * Run with `bun run e2e/smoke.test.ts`. Requires `bun run build` * to have produced `dist/client/index.html` first. */ import { spawn } from "node:child_process"; import { setTimeout as delay } from "node:timers/promises"; const PORT = 3399; const BASE = `http://localhost:${PORT}`; async function waitForReady(): Promise { for (let i = 0; i < 30; i++) { try { const res = await fetch(`${BASE}/api/health`); if (res.ok) return; } catch { // not up yet } await delay(500); } throw new Error("server failed to start within 15s"); } function assert(condition: unknown, message: string): asserts condition { if (!condition) throw new Error(`SMOKE FAIL: ${message}`); } async function main(): Promise { const child = spawn("bun", ["run", "src/server/index.ts"], { env: { ...process.env, PORT: String(PORT), NODE_ENV: "test" }, stdio: ["ignore", "pipe", "pipe"], }); try { await waitForReady(); const health = await (await fetch(`${BASE}/api/health`)).json(); // `@suaveplan/api`'s `registerHealthRoutes` emits // `{ status, version, state, timestamp }` — accept either the new // shape (`status: "healthy"`) or the legacy `{ ok: true }` shape so // the smoke remains forward-compatible if the route is rewritten. const healthy = health.status === "healthy" || health.ok === true; assert(healthy, `health probe not healthy: ${JSON.stringify(health)}`); const root = await fetch(`${BASE}/`, { redirect: "manual" }); // Server sends the SPA shell, client-side router handles the redirect. // We accept either a 200 (shell) or a 3xx redirect. assert(root.status === 200 || (root.status >= 300 && root.status < 400), `/ expected 200 or 3xx, got ${root.status}`); const paths = [ "/en/home", "/en/about-us", "/en/contact-us", "/en/privacy-policy", "/en/terms-of-service", ]; for (const path of paths) { const res = await fetch(`${BASE}${path}`); assert(res.ok, `${path} returned ${res.status}`); const body = await res.text(); assert(body.includes('id="root"'), `${path} body missing #root`); } // Auth endpoint reachable. const session = await fetch(`${BASE}/api/auth/get-session`); assert(session.status === 200, `/api/auth/get-session status ${session.status}`); console.log("SMOKE OK"); } finally { child.kill("SIGTERM"); } } main().catch((err) => { console.error(err); process.exit(1); });