/** * Smoke report generator — R4.7 * * Reads the per-step telemetry produced by acadenice-smoke-full.spec.ts and * produces SMOKE-REPORT.md at the e2e root. Designed to run after the suite * regardless of pass/fail. * * Inputs (all optional — missing files produce empty sections): * smoke-telemetry/step-results.json * smoke-telemetry/network-errors.json * smoke-telemetry/console-errors.json * smoke-telemetry/page-errors.json * * Output: * SMOKE-REPORT.md */ import * as fs from "fs"; import * as path from "path"; interface StepResult { feature: string; status: "OK" | "KO" | "PARTIAL"; details: string; screenshot?: string; } interface NetworkError { url: string; status: number; method: string; testName: string; step: string; } interface ConsoleError { message: string; testName: string; step: string; } interface PageError { message: string; testName: string; step: string; } const TELEMETRY_DIR = path.resolve(__dirname, "../smoke-telemetry"); const OUTPUT = path.resolve(__dirname, "../SMOKE-REPORT.md"); function readJson(file: string, fallback: T): T { const full = path.join(TELEMETRY_DIR, file); if (!fs.existsSync(full)) return fallback; try { return JSON.parse(fs.readFileSync(full, "utf8")) as T; } catch (err) { console.warn(`[smoke-report] could not parse ${file}: ${String(err)}`); return fallback; } } function relScreenshot(p: string | undefined): string { if (!p) return ""; const e2eRoot = path.resolve(__dirname, ".."); return path.relative(e2eRoot, p); } function dedupe(arr: T[], key: (x: T) => string): T[] { const seen = new Set(); const out: T[] = []; for (const item of arr) { const k = key(item); if (!seen.has(k)) { seen.add(k); out.push(item); } } return out; } function main(): void { const steps = readJson("step-results.json", []); const network = readJson("network-errors.json", []); const consoleErrs = readJson("console-errors.json", []); const pageErrs = readJson("page-errors.json", []); const today = new Date().toISOString().slice(0, 10); const passed = steps.filter((s) => s.status === "OK").length; const failed = steps.filter((s) => s.status === "KO").length; const partial = steps.filter((s) => s.status === "PARTIAL").length; const lines: string[] = []; lines.push(`# AcadeDoc smoke report — ${today}`); lines.push(""); lines.push(`Stack: client :5173 — server :3001 — bridge :4000`); lines.push( `Result: ${passed} OK / ${failed} KO / ${partial} PARTIAL — total ${steps.length}`, ); lines.push(""); lines.push("## Feature matrix"); lines.push(""); lines.push("| Feature | Status | Details | Screenshot |"); lines.push("|---------|--------|---------|------------|"); if (steps.length === 0) { lines.push( "| (no telemetry) | - | suite did not run or failed before any step | - |", ); } for (const s of steps) { const screen = s.screenshot ? `\`${relScreenshot(s.screenshot)}\`` : "-"; // Collapse multi-line Playwright error logs into a single short summary. const safeDetails = s.details .replace(/\n+/g, " ") .replace(/=+ logs =+/g, "—") .replace(/=+/g, "") .replace(/\s+/g, " ") .replace(/\|/g, "\\|") .slice(0, 220); lines.push(`| ${s.feature} | ${s.status} | ${safeDetails} | ${screen} |`); } lines.push(""); lines.push("## Bugs confirmed"); lines.push(""); const bugs = steps.filter((s) => s.status === "KO"); if (bugs.length === 0) { lines.push("None — every step passed."); } else { for (const b of bugs) { const compact = b.details .replace(/\n+/g, " ") .replace(/=+ logs =+/g, "—") .replace(/=+/g, "") .replace(/\s+/g, " ") .slice(0, 400); lines.push(`- **${b.feature}** — ${compact}`); if (b.screenshot) { lines.push(` - screenshot: \`${relScreenshot(b.screenshot)}\``); } } } lines.push(""); lines.push("## Network errors (HTTP >= 400)"); lines.push(""); const dedupNet = dedupe( network, (n) => `${n.method} ${n.url} ${n.status}`, ); if (dedupNet.length === 0) { lines.push("None."); } else { for (const n of dedupNet.slice(0, 50)) { lines.push( `- \`${n.method} ${n.url}\` → ${n.status} *(during step: ${n.step})*`, ); } if (dedupNet.length > 50) { lines.push(`- ... and ${dedupNet.length - 50} more, truncated.`); } } lines.push(""); lines.push("## Console errors"); lines.push(""); const dedupCon = dedupe(consoleErrs, (c) => c.message); if (dedupCon.length === 0) { lines.push("None."); } else { for (const c of dedupCon.slice(0, 30)) { const oneLine = c.message.replace(/\n/g, " ").slice(0, 250); lines.push(`- \`${oneLine}\` *(step: ${c.step})*`); } if (dedupCon.length > 30) { lines.push(`- ... and ${dedupCon.length - 30} more, truncated.`); } } lines.push(""); lines.push("## Page errors (uncaught exceptions)"); lines.push(""); const dedupPage = dedupe(pageErrs, (p) => p.message); if (dedupPage.length === 0) { lines.push("None."); } else { for (const p of dedupPage.slice(0, 30)) { const oneLine = p.message.replace(/\n/g, " ").slice(0, 250); lines.push(`- \`${oneLine}\` *(step: ${p.step})*`); } } lines.push(""); lines.push("## How to reproduce"); lines.push(""); lines.push("```bash"); lines.push("cd formation-hub/e2e"); lines.push( "pnpm exec playwright test --config=playwright.smoke.config.ts", ); lines.push("pnpm exec tsx scripts/generate-smoke-report.ts"); lines.push("```"); lines.push(""); fs.writeFileSync(OUTPUT, lines.join("\n")); console.log(`[smoke-report] wrote ${OUTPUT}`); } main();