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.
209 lines
5.8 KiB
TypeScript
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();
|