#!/usr/bin/env node /** * Runs the test suite with Vitest's JSON reporter (plus the normal live output) * and prints ONE compact, copy-pasteable summary at the end — pass/fail counts * and every failing test with a one-line reason. Paste the block between the * ===== markers to hand off the full picture without a wall of logs. * * Exits with the suite's own status, so `make test` / the pre-push hook still * block on failure. This file is the same across all my projects; only RUNS * differs (a project with several vitest configs lists one entry per config). */ import { spawnSync } from "node:child_process"; import { mkdtempSync, readFileSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; // The npm script(s) that together make up "the whole suite". Run through npm (not // `npx vitest` directly) so nested tooling in a globalSetup — e.g. `npx drizzle-kit // migrate` — resolves with the right PATH. A project split across several vitest // configs lists one entry per config. const RUNS = [{ label: "all", args: ["test"] }]; const reporterArgs = (out) => ["--", "--reporter=default", "--reporter=json", `--outputFile.json=${out}`]; const projectName = (() => { try { return JSON.parse(readFileSync("package.json", "utf8")).name ?? "project"; } catch { return "project"; } })(); const workDir = mkdtempSync(join(tmpdir(), "test-summary-")); const started = Date.now(); let status = 0; const reports = []; for (const run of RUNS) { const out = join(workDir, `${run.label}.json`); const res = spawnSync("npm", ["run", ...run.args, ...reporterArgs(out)], { stdio: "inherit", shell: process.platform === "win32", }); if (res.status !== 0) status = res.status ?? 1; try { reports.push(JSON.parse(readFileSync(out, "utf8"))); } catch { /* a crash before the report was written — status already non-zero */ } } let total = 0; let passed = 0; let failed = 0; let skipped = 0; let files = 0; let filesFailed = 0; const failures = []; const cwd = process.cwd(); for (const r of reports) { total += r.numTotalTests ?? 0; passed += r.numPassedTests ?? 0; failed += r.numFailedTests ?? 0; skipped += (r.numPendingTests ?? 0) + (r.numTodoTests ?? 0); for (const tr of r.testResults ?? []) { files += 1; const fileFailed = tr.status === "failed" || (tr.assertionResults ?? []).some((a) => a.status === "failed"); if (fileFailed) filesFailed += 1; for (const a of tr.assertionResults ?? []) { if (a.status !== "failed") continue; const file = (tr.name ?? "").replace(`${cwd}/`, ""); const name = a.fullName || [...(a.ancestorTitles ?? []), a.title].filter(Boolean).join(" › "); const reason = (a.failureMessages ?? []) .join("\n") .split("\n") .map((l) => l.trim()) .find((l) => l && !l.startsWith("at ")) ?? ""; failures.push({ file, name, reason: reason.slice(0, 200) }); } } } const elapsed = ((Date.now() - started) / 1000).toFixed(1); const ok = status === 0 && failed === 0 && reports.length > 0; const L = "=".repeat(38); const lines = []; lines.push(L); lines.push(`TEST SUMMARY — ${projectName} (${new Date().toISOString().slice(0, 16).replace("T", " ")})`); if (reports.length === 0) { lines.push("❌ CRASH — the test run failed before producing a report (see output above)."); } else { lines.push( `${ok ? "✅ PASS" : "❌ FAIL"} — ${passed}/${total} tests passed` + (failed ? `, ${failed} failed` : "") + (skipped ? `, ${skipped} skipped` : "") + ` · ${files} files (${filesFailed} failed) · ${elapsed}s`, ); if (failures.length) { lines.push(""); lines.push(`FAILED (${failures.length}):`); for (const f of failures) { lines.push(` ✗ ${f.file} › ${f.name}`); if (f.reason) lines.push(` ${f.reason}`); } } } lines.push(L); console.log(`\n${lines.join("\n")}`); rmSync(workDir, { recursive: true, force: true }); process.exit(status);