Wiki/e2e/scripts/generate-smoke-report.ts
Corentin JOGUET d245f31ab6 test(e2e): add acadenice full smoke suite — R4.7
Adds an autonomous Playwright suite (acadenice-smoke-full.spec.ts) that drives
the prod-like AcadeDoc stack via the UI to surface UI bugs without manual
screenshot diagnostic. Each step is wrapped in test.step and records
network/console/page errors plus per-step screenshots. A post-test script
(scripts/generate-smoke-report.ts) aggregates telemetry into SMOKE-REPORT.md.

The smoke runs against the real fork stack (client :5173, server :3001,
bridge :4000) using Corentin's prod-like Acadenice credentials. It does NOT
share state with the cross-stack e2e suite (no Baserow seeding, no
docker-compose-e2e dependency) — its dedicated playwright.smoke.config.ts
declares no setup project.

Coverage: login, create page, sub-page nesting, wikilink + backlink, slash
/database, slash /template, slash /sync-block, workspace graph, space graph.

Initial run results (baseline): 2 OK / 7 KO / 0 PARTIAL — confirms
SpaceSidebar renders-more-hooks crash when opening "Nouvelle page" submenu
(blank-screen Error Boundary), GET /api/acadenice/templates → 403, and
space-scoped graph entry point not reachable. Captured in SMOKE-REPORT.md
for follow-up by R4.5/R4.6.
2026-05-08 12:20:14 +02:00

209 lines
5.8 KiB
TypeScript

/**
* 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<T>(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<T>(arr: T[], key: (x: T) => string): T[] {
const seen = new Set<string>();
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<StepResult[]>("step-results.json", []);
const network = readJson<NetworkError[]>("network-errors.json", []);
const consoleErrs = readJson<ConsoleError[]>("console-errors.json", []);
const pageErrs = readJson<PageError[]>("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();