/** * Full-site integration test — runs against http://localhost:8008 * Tests: routes, auth flows, API endpoints, link integrity, images, spacing. * * Run: npx tsx scripts/full-site-test.ts */ const BASE = "http://localhost:8008"; interface Result { name: string; pass: boolean; detail: string; } const results: Result[] = []; let cookies = ""; // session cookies accumulated per test user function pass(name: string, detail = "") { results.push({ name, pass: true, detail }); console.log(` ✅ ${name}${detail ? " → " + detail : ""}`); } function fail(name: string, detail = "") { results.push({ name, pass: false, detail }); console.error(` ❌ ${name}${detail ? " → " + detail : ""}`); } async function get(path: string, opts: RequestInit = {}) { const res = await fetch(`${BASE}${path}`, { redirect: "follow", headers: { cookie: cookies, "user-agent": "SiteTestBot/1.0", ...(opts.headers as Record ?? {}) }, ...opts, }); return res; } async function postJson(path: string, body: unknown, extraHeaders: Record = {}) { const res = await fetch(`${BASE}${path}`, { method: "POST", headers: { "Content-Type": "application/json", cookie: cookies, "user-agent": "SiteTestBot/1.0", ...extraHeaders, }, body: JSON.stringify(body), redirect: "follow", }); // persist Set-Cookie const sc = res.headers.get("set-cookie"); if (sc) cookies += "; " + sc.split(";")[0]; return res; } function extractLinks(html: string): string[] { const hrefs: string[] = []; const re = /href="([^"#][^"]*)"/g; let m; while ((m = re.exec(html)) !== null) { hrefs.push(m[1]); } return hrefs; } function extractImgSrcs(html: string): string[] { const srcs: string[] = []; const re = /(?:src|srcset)="([^"]+)"/g; let m; while ((m = re.exec(html)) !== null) { srcs.push(m[1].split(" ")[0]); } return srcs; } function detectSpacingIssues(html: string, context: string): void { // Strip tags to check text nodes const text = html.replace(/]*>[\s\S]*?<\/style>/gi, "") .replace(/]*>[\s\S]*?<\/script>/gi, "") .replace(/<[^>]+>/g, " ") .replace(/&/g, "&") .replace(/ /g, " ") .replace(/\s+/g, " "); // Common double-space, missing space before/after common words const issues: string[] = []; if (/\w{2,}\s{2,}\w/.test(text)) issues.push("double-space detected"); if (/[a-z][A-Z][a-z]/.test(text.replace(/\s/g, ""))) { // camelCase bleed into content is already stripped by tag removal above — skip } if (issues.length > 0) fail(`spacing: ${context}`, issues.join(", ")); else pass(`spacing: ${context}`); } // ───────────────────────────────────────── // 1. PUBLIC ROUTES // ───────────────────────────────────────── async function testPublicRoutes() { console.log("\n── 1. PUBLIC ROUTES ──────────────────────────────"); const routes: Array<{ path: string; minLen?: number }> = [ { path: "/", minLen: 2000 }, { path: "/raised", minLen: 500 }, { path: "/register", minLen: 500 }, { path: "/login", minLen: 300 }, { path: "/forgot-password", minLen: 200 }, { path: "/privacy-policy", minLen: 500 }, { path: "/datadeletion", minLen: 500 }, { path: "/donate", minLen: 500 }, { path: "/donate/thank-you", minLen: 200 }, { path: "/billboard", minLen: 200 }, { path: "/boost", minLen: 200 }, { path: "/spotlight", minLen: 200 }, { path: "/cards", minLen: 200 }, { path: "/faq-board", minLen: 200 }, { path: "/missions", minLen: 200 }, { path: "/initiatives", minLen: 200 }, { path: "/vote/next-president", minLen: 200 }, { path: "/robots.txt", minLen: 50 }, { path: "/sitemap.xml", minLen: 100 }, { path: "/opengraph-image", minLen: 100 }, { path: "/favicon.ico" }, ]; for (const r of routes) { try { const res = await get(r.path); if (res.status === 200) { const body = await res.text(); const len = body.length; if (r.minLen && len < r.minLen) { fail(`GET ${r.path}`, `body too short: ${len} < ${r.minLen}`); } else { pass(`GET ${r.path}`, `${res.status} (${len} bytes)`); } } else { fail(`GET ${r.path}`, `HTTP ${res.status}`); } } catch (e) { fail(`GET ${r.path}`, String(e)); } } } // ───────────────────────────────────────── // 2. SEO / META TAGS // ───────────────────────────────────────── async function testSeoMeta() { console.log("\n── 2. SEO / META TAGS ────────────────────────────"); const res = await get("/"); const html = await res.text(); const checks: Array<{ name: string; pattern: RegExp }> = [ { name: "title tag", pattern: // }, { name: "meta description", pattern: /name="description"/ }, { name: "meta keywords", pattern: /name="keywords"/ }, { name: "og:title", pattern: /property="og:title"/ }, { name: "og:description", pattern: /property="og:description"/ }, { name: "og:image", pattern: /property="og:image"/ }, { name: "og:url", pattern: /property="og:url"/ }, { name: "twitter:card", pattern: /name="twitter:card"/ }, { name: "twitter:image", pattern: /name="twitter:image"/ }, { name: "canonical link", pattern: /rel="canonical"/ }, { name: "robots meta", pattern: /name="robots"/ }, { name: "JSON-LD script", pattern: /application\/ld\+json/ }, { name: "lang=en on html", pattern: /<html[^>]+lang="en"/ }, { name: "viewport meta", pattern: /name="viewport"/ }, ]; for (const c of checks) { if (c.pattern.test(html)) pass(`seo: ${c.name}`); else fail(`seo: ${c.name}`, "not found in /"); } // robots.txt content const robots = await (await get("/robots.txt")).text(); if (/Disallow.*\/wallet/.test(robots)) pass("robots.txt blocks /wallet"); else fail("robots.txt blocks /wallet", robots.substring(0, 200)); if (/Sitemap:/.test(robots)) pass("robots.txt has Sitemap directive"); else fail("robots.txt has Sitemap directive"); // sitemap.xml const sitemap = await (await get("/sitemap.xml")).text(); const required = ["/", "/raised", "/register", "/login"]; for (const url of required) { if (sitemap.includes(url)) pass(`sitemap: contains ${url}`); else fail(`sitemap: missing ${url}`); } } // ───────────────────────────────────────── // 3. LINK INTEGRITY (no dead ends) // ───────────────────────────────────────── async function testLinks() { console.log("\n── 3. LINK INTEGRITY ─────────────────────────────"); const pages = ["/", "/raised", "/register", "/login", "/forgot-password", "/donate", "/missions", "/initiatives"]; const knownExternal = new Set(["https://", "http://", "mailto:", "tel:"]); const checked = new Set<string>(); for (const page of pages) { const res = await get(page); const html = await res.text(); const links = extractLinks(html).filter( (l) => l.startsWith("/") && !l.startsWith("//") && !checked.has(l) ); for (const link of links) { // skip Next.js internals, API, and anchor-only hrefs if ( link.startsWith("/_next") || link.startsWith("/api/") || link === "/#" || link.startsWith("/#") ) continue; const clean = link.split("?")[0].split("#")[0]; if (!clean || checked.has(clean)) continue; checked.add(clean); try { const r = await fetch(`${BASE}${clean}`, { redirect: "manual", headers: { cookie: cookies, "user-agent": "SiteTestBot/1.0" }, }); // 200 (page renders), 3xx (auth/short-link redirect), all considered alive. if (r.status === 200 || (r.status >= 300 && r.status < 400)) { pass(`link: ${clean}`, `${r.status}`); } else { fail(`link: ${clean}`, `HTTP ${r.status}`); } } catch (e) { fail(`link: ${clean}`, String(e)); } } } } // ───────────────────────────────────────── // 4. IMAGES // ───────────────────────────────────────── async function testImages() { console.log("\n── 4. IMAGES ─────────────────────────────────────"); const res = await get("/"); const html = await res.text(); const srcs = extractImgSrcs(html); // Also check known static assets const staticAssets = [ "/favicon.ico", "/opengraph-image", ]; const allToCheck = [...new Set([...srcs.filter((s) => s.startsWith("/")), ...staticAssets])]; for (const src of allToCheck.slice(0, 20)) { // Skip data URIs and external if (src.startsWith("data:") || src.startsWith("http")) continue; // Skip _next/image with external src params if (src.includes("url=http")) continue; try { const r = await get(src); if (r.status === 200) pass(`image: ${src}`); else fail(`image: ${src}`, `HTTP ${r.status}`); } catch (e) { fail(`image: ${src}`, String(e)); } } if (allToCheck.length === 0) pass("images: no <img> tags found (CSS backgrounds / SVG components)"); } // ───────────────────────────────────────── // 5. WORD SPACING // ───────────────────────────────────────── async function testSpacing() { console.log("\n── 5. WORD SPACING ───────────────────────────────"); const pages = ["/", "/raised", "/register"]; for (const p of pages) { const html = await (await get(p)).text(); detectSpacingIssues(html, p); } } // ───────────────────────────────────────── // 6. AUTH — REGISTRATION FLOW // ───────────────────────────────────────── const TEST_EMAIL = `testbot_${Date.now()}@sitetest.local`; const TEST_PASS = "TestBot@1234!"; let CREATED_USERNAME = ""; // assigned by POST /api/register async function testRegistration() { console.log("\n── 6. REGISTRATION FLOW ──────────────────────────"); // Register new user const res = await postJson("/api/register", { name: "Site Test Bot", email: TEST_EMAIL, password: TEST_PASS, }); if (res.status === 201 || res.status === 200) { const body = (await res.json().catch(() => ({}))) as Record<string, unknown>; CREATED_USERNAME = typeof body.username === "string" ? body.username : ""; pass( "POST /api/register", `user created: ${typeof body.username === "string" ? `@${body.username}` : ""} ${body.email ?? ""}`.trim(), ); } else { const text = await res.text(); fail("POST /api/register", `${res.status}: ${text.substring(0, 120)}`); return; } // Try duplicate registration const dup = await postJson("/api/register", { name: "Site Test Bot 2", email: TEST_EMAIL, password: TEST_PASS, }); if (dup.status === 409 || dup.status === 400 || dup.status === 422) { pass("duplicate registration blocked", `${dup.status}`); } else { fail("duplicate registration blocked", `expected 4xx, got ${dup.status}`); } } // ───────────────────────────────────────── // 7. AUTH — LOGIN FLOW (via NextAuth credentials) // ───────────────────────────────────────── async function testLogin() { console.log("\n── 7. LOGIN FLOW ─────────────────────────────────"); // Get CSRF token const csrfRes = await get("/api/auth/csrf"); let csrfToken = ""; try { const j = await csrfRes.json() as { csrfToken: string }; csrfToken = j.csrfToken; pass("GET /api/auth/csrf", `token: ${csrfToken.substring(0, 12)}…`); } catch (e) { fail("GET /api/auth/csrf", String(e)); return; } // Store session cookie from CSRF const sc = csrfRes.headers.get("set-cookie"); if (sc) cookies += "; " + sc.split(";")[0]; // Sign in via credentials const formBody = new URLSearchParams({ email: TEST_EMAIL, password: TEST_PASS, csrfToken, callbackUrl: "/wallet", json: "true", }); const loginRes = await fetch(`${BASE}/api/auth/callback/credentials`, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", cookie: cookies, "user-agent": "SiteTestBot/1.0", }, body: formBody.toString(), redirect: "manual", }); const loginSc = loginRes.headers.get("set-cookie"); if (loginSc) cookies += "; " + loginSc.split(";")[0]; if (loginRes.status === 200 || loginRes.status === 302 || loginRes.status === 303) { pass("POST /api/auth/callback/credentials", `${loginRes.status} — session established`); if (/authjs\.session-token|__Secure-authjs\.session-token/.test(loginSc ?? "") && /Max-Age=/i.test(loginSc ?? "")) { pass("session cookie persistence", "authjs session cookie includes Max-Age"); } else { fail("session cookie persistence", "missing persistent authjs session cookie Max-Age"); } } else { const body = await loginRes.text().catch(() => ""); fail("POST /api/auth/callback/credentials", `${loginRes.status}: ${body.substring(0, 200)}`); } // Bad password should fail const badLogin = await fetch(`${BASE}/api/auth/callback/credentials`, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", cookie: cookies, "user-agent": "SiteTestBot/1.0", }, body: new URLSearchParams({ email: TEST_EMAIL, password: "wrongpassword999", csrfToken, callbackUrl: "/wallet", json: "true", }).toString(), redirect: "manual", }); // NextAuth redirects to error page on bad creds — 302 to /api/auth/error or /login?error= const badLocation = badLogin.headers.get("location") ?? ""; if (badLogin.status >= 400 || badLocation.includes("error") || badLocation.includes("Error")) { pass("bad password rejected", `${badLogin.status} → ${badLocation.substring(0, 60)}`); } else { fail("bad password rejected", `${badLogin.status} → ${badLocation.substring(0, 80)}`); } if (!CREATED_USERNAME) { fail("POST /api/auth/callback/credentials — username variant", "no CREATED_USERNAME from registration"); return; } const csrfResUser = await get("/api/auth/csrf"); let csrfTokUser = ""; try { csrfTokUser = ((await csrfResUser.json()) as { csrfToken: string }).csrfToken; } catch { fail("login with username → GET /api/auth/csrf", "JSON parse failed"); return; } const scUser = csrfResUser.headers.get("set-cookie"); if (scUser) cookies += "; " + scUser.split(";")[0]; const userLogin = await fetch(`${BASE}/api/auth/callback/credentials`, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", cookie: cookies, "user-agent": "SiteTestBot/1.0", }, body: new URLSearchParams({ email: CREATED_USERNAME, password: TEST_PASS, csrfToken: csrfTokUser, callbackUrl: "/wallet", json: "true", }).toString(), redirect: "manual", }); const userLoc = userLogin.headers.get("location") ?? ""; if (userLogin.status === 200 || userLogin.status === 302 || userLogin.status === 303) { pass(`POST /api/auth/callback/credentials (username: ${CREATED_USERNAME})`, `${userLogin.status}`); } else { fail( "POST /api/auth/callback/credentials (username)", `${userLogin.status}: ${userLoc.substring(0, 80)}`, ); } } // ───────────────────────────────────────── // 8. AUTHENTICATED API ENDPOINTS // ───────────────────────────────────────── async function testAuthenticatedApis() { console.log("\n── 8. AUTHENTICATED API ENDPOINTS ───────────────"); const endpoints: Array<{ method: string; path: string; expectCodes: number[]; body?: unknown }> = [ { method: "GET", path: "/api/wallet", expectCodes: [200] }, { method: "GET", path: "/api/wallet/ledger", expectCodes: [200] }, { method: "GET", path: "/api/wallet/donations", expectCodes: [200] }, { method: "GET", path: "/api/rewards/catalog", expectCodes: [200] }, { method: "GET", path: "/api/exchange/rate", expectCodes: [200] }, { method: "GET", path: "/api/public/stats", expectCodes: [200] }, ]; for (const e of endpoints) { try { const res = e.method === "GET" ? await get(e.path) : await postJson(e.path, e.body ?? {}); if (e.expectCodes.includes(res.status)) { const body = await res.text(); pass(`${e.method} ${e.path}`, `${res.status}`); } else { fail(`${e.method} ${e.path}`, `HTTP ${res.status} (expected one of ${e.expectCodes.join(",")})`); } } catch (err) { fail(`${e.method} ${e.path}`, String(err)); } } } // ───────────────────────────────────────── // 9. API GUARD TESTS (unauthenticated should be blocked) // ───────────────────────────────────────── async function testApiGuards() { console.log("\n── 9. API AUTH GUARDS ────────────────────────────"); // These require auth — call without cookies const guarded = [ "/api/wallet", "/api/wallet/ledger", "/api/wallet/donations", ]; for (const path of guarded) { const res = await fetch(`${BASE}${path}`, { headers: { "user-agent": "SiteTestBot/1.0" }, }); if (res.status === 401 || res.status === 403 || res.status === 302) { pass(`guard: ${path}`, `${res.status} — correctly blocked`); } else if (res.status === 200) { // Check if the response is empty/null (some Next.js auth returns 200 with null user) const body = await res.text(); if (body.includes('"user":null') || body.includes("null")) { pass(`guard: ${path}`, `200 with null session — ok`); } else { fail(`guard: ${path}`, `${res.status} — unguarded! body: ${body.substring(0, 80)}`); } } else { fail(`guard: ${path}`, `unexpected ${res.status}`); } } } // ───────────────────────────────────────── // 10. PRIVATE PAGE REDIRECT // ───────────────────────────────────────── async function testPrivatePageRedirects() { console.log("\n── 10. PRIVATE PAGE REDIRECTS / GUEST VIEWS ───────"); // /wallet renders a guest marketing view at 200; assert that view includes a sign-in CTA. const wallet = await fetch(`${BASE}/wallet`, { redirect: "manual", headers: { "user-agent": "SiteTestBot/1.0" }, }); if (wallet.status === 200) { const body = await wallet.text(); if (/sign in/i.test(body) && /register|join|enroll|create.*account/i.test(body)) { pass(`guest view: /wallet`, `200 with sign-in + register CTAs`); } else { fail(`guest view: /wallet`, `200 but missing auth CTAs`); } } else if (wallet.status === 302 || wallet.status === 307 || wallet.status === 308) { pass(`redirect: /wallet`, `→ ${wallet.headers.get("location") ?? ""}`); } else { fail(`redirect or guest view: /wallet`, `unexpected ${wallet.status}`); } // Casino subroutes are auth-gated. Confirm anonymous hit redirects to /login with callbackUrl. for (const casinoPath of ["/casino", "/casino/crash"]) { const casinoSub = await fetch(`${BASE}${casinoPath}`, { redirect: "manual", headers: { "user-agent": "SiteTestBot/1.0" }, }); const loc = casinoSub.headers.get("location") ?? ""; if (casinoSub.status >= 300 && casinoSub.status < 400) { if (loc.includes("callbackUrl") && loc.includes("casino")) { pass(`redirect: ${casinoPath}`, `${casinoSub.status} → ${loc} (callbackUrl preserved)`); } else { fail(`redirect: ${casinoPath}`, `redirected but missing callbackUrl: ${loc}`); } } else { fail(`redirect: ${casinoPath}`, `expected 3xx, got ${casinoSub.status}`); } } } // ───────────────────────────────────────── // 11. CHECKOUT SESSION API (primary Stripe path) // ───────────────────────────────────────── async function testPaymentIntent() { console.log("\n── 11. CHECKOUT SESSION API ──────────────────────"); const tiers = [500, 1000, 2000, 10000]; // cents for (const amount of tiers) { const res = await postJson("/api/stripe/create-checkout-session", { amountUsdCents: amount }); if (res.status === 200) { const body = await res.json().catch(() => ({})) as Record<string, unknown>; if (body.clientSecret && body.sessionId) pass(`checkout-session $${amount / 100}`, "clientSecret + sessionId returned"); else fail(`checkout-session $${amount / 100}`, `missing fields: ${JSON.stringify(Object.keys(body))}`); } else if (res.status === 401 || res.status === 403) { pass(`checkout-session $${amount / 100}`, `${res.status} — requires auth (expected)`); } else { const text = await res.text().catch(() => ""); fail(`checkout-session $${amount / 100}`, `${res.status}: ${text.substring(0, 100)}`); } } // Verify bad tier is rejected const bad = await postJson("/api/stripe/create-checkout-session", { amountUsdCents: 777 }); if (bad.status === 400) { pass("checkout-session: bad tier rejected", "400"); } else { fail("checkout-session: bad tier rejected", `expected 400, got ${bad.status}`); } // Session status retrieval (GET) with invalid id returns 404 const statusRes = await get("/api/stripe/create-checkout-session?session_id=cs_test_invalid_id"); if (statusRes.status === 404 || statusRes.status === 200) { pass("checkout-session GET status endpoint", `${statusRes.status}`); } else { fail("checkout-session GET status endpoint", `unexpected ${statusRes.status}`); } // Legacy payment-intent route still works for the home widget const legacyRes = await postJson("/api/stripe/create-payment-intent", { amountUsdCents: 1000 }); if (legacyRes.status === 200) { const body = await legacyRes.json().catch(() => ({})) as Record<string, unknown>; if (body.clientSecret) pass("legacy payment-intent $10", "clientSecret returned"); else fail("legacy payment-intent $10", "no clientSecret"); } else if (legacyRes.status === 401 || legacyRes.status === 403) { pass("legacy payment-intent $10", `${legacyRes.status} — requires auth (expected)`); } else { const text = await legacyRes.text().catch(() => ""); fail("legacy payment-intent $10", `${legacyRes.status}: ${text.substring(0, 100)}`); } } // ───────────────────────────────────────── // 12. EXCHANGE RATE API // ───────────────────────────────────────── async function testExchangeRate() { console.log("\n── 12. EXCHANGE RATE API ─────────────────────────"); const res = await get("/api/exchange/rate"); if (res.status !== 200) { fail("GET /api/exchange/rate", `${res.status}`); return; } const body = await res.json().catch(() => null) as Record<string, unknown> | null; if (!body) { fail("exchange rate: JSON parse", "no body"); return; } // API returns blwUsd (or mtkUsd alias) for the BLW/BWT exchange rate const rate = (body.blwUsd ?? body.mtkUsd ?? body.usdPerBlw) as number | undefined; if (typeof rate === "number" && rate > 0) { pass("exchange rate: blwUsd", `${rate}`); } else { fail("exchange rate: blwUsd", `got: ${JSON.stringify(body)}`); } } // ───────────────────────────────────────── // 13. PUBLIC STATS API // ───────────────────────────────────────── async function testPublicStats() { console.log("\n── 13. PUBLIC STATS API ──────────────────────────"); const res = await get("/api/public/stats"); if (res.status !== 200) { fail("GET /api/public/stats", `${res.status}`); return; } const body = await res.json().catch(() => null) as Record<string, unknown> | null; if (!body) { fail("public stats: JSON parse", "no body"); return; } const required = ["raisedUsd", "donationCount"]; for (const key of required) { if (key in body) pass(`public stats: ${key}`, String(body[key])); else fail(`public stats: ${key}`, `missing from ${JSON.stringify(body)}`); } } // ───────────────────────────────────────── // 14. 404 PAGE // ───────────────────────────────────────── async function test404() { console.log("\n── 14. 404 HANDLING ──────────────────────────────"); const paths = ["/does-not-exist", "/api/does-not-exist", "/raised/subpage"]; for (const path of paths) { const res = await get(path); if (res.status === 404) pass(`404: ${path}`); else if (res.status === 200 && path.startsWith("/api")) pass(`404 (api returns 200): ${path}`, "ok for api"); else fail(`404: ${path}`, `got ${res.status}`); } } // ───────────────────────────────────────── // 15. SECURITY HEADERS // ───────────────────────────────────────── async function testSecurityHeaders() { console.log("\n── 15. SECURITY HEADERS ──────────────────────────"); const res = await get("/"); const headers: Array<{ name: string; header: string }> = [ { name: "X-Content-Type-Options", header: "x-content-type-options" }, { name: "X-Frame-Options", header: "x-frame-options" }, { name: "Referrer-Policy", header: "referrer-policy" }, ]; for (const h of headers) { const val = res.headers.get(h.header); if (val) pass(`header: ${h.name}`, val); else fail(`header: ${h.name}`, "missing"); } } // ───────────────────────────────────────── // 16. FORGOT PASSWORD PAGE // ───────────────────────────────────────── async function testForgotPassword() { console.log("\n── 16. FORGOT PASSWORD PAGE ──────────────────────"); const res = await get("/forgot-password"); if (res.status !== 200) { fail("GET /forgot-password", `${res.status}`); return; } const html = await res.text(); if (html.includes("forgot") || html.includes("reset") || html.includes("email") || html.includes("password")) { pass("forgot-password: form content present"); } else { fail("forgot-password: form content present", "page seems empty"); } } // ───────────────────────────────────────── // MAIN // ───────────────────────────────────────── async function main() { console.log("═══════════════════════════════════════════════"); console.log(" Democracy Rising — Full Site Test Suite"); console.log(` Target: ${BASE}`); console.log(` Test user: ${TEST_EMAIL}`); console.log("═══════════════════════════════════════════════"); await testPublicRoutes(); await testSeoMeta(); await testLinks(); await testImages(); await testSpacing(); await testRegistration(); await testLogin(); await testAuthenticatedApis(); await testApiGuards(); await testPrivatePageRedirects(); await testPaymentIntent(); await testExchangeRate(); await testPublicStats(); await test404(); await testSecurityHeaders(); await testForgotPassword(); // ── SUMMARY ────────────────────────────────── console.log("\n═══════════════════════════════════════════════"); console.log(" RESULTS SUMMARY"); console.log("═══════════════════════════════════════════════"); const passed = results.filter((r) => r.pass); const failed = results.filter((r) => !r.pass); console.log(` ✅ Passed : ${passed.length}`); console.log(` ❌ Failed : ${failed.length}`); console.log(` 📋 Total : ${results.length}`); if (failed.length > 0) { console.log("\n FAILURES:"); for (const f of failed) { console.error(` ❌ ${f.name} → ${f.detail}`); } } console.log("\n═══════════════════════════════════════════════"); process.exit(failed.length > 0 ? 1 : 0); } main().catch((e) => { console.error("Test runner crashed:", e); process.exit(2); });