Files

115 lines
4.0 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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);