donobu
Version:
Create browser automations with an LLM agent and replay them as Playwright scripts.
182 lines • 8.43 kB
JavaScript
;
/**
* @fileoverview Donobu Markdown report renderer.
*
* Pure library that turns a `DonobuReport` into a Markdown string suitable
* for GitHub Actions step summaries, PR comments, or any other
* Markdown-consuming surface. No filesystem writes, no CLI arg parsing, no
* environment variable reads — callers own I/O.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.renderMarkdown = renderMarkdown;
const ansi_1 = require("../utils/ansi");
const reportWalk_1 = require("./reportWalk");
function formatDuration(ms) {
if (ms < 1000) {
return `${ms}ms`;
}
const seconds = Math.floor(ms / 1000);
if (seconds < 60) {
return `${seconds}s`;
}
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
return `${minutes}m ${remainingSeconds}s`;
}
function renderMarkdown(report) {
const suites = (report.suites ?? []);
let markdown = `# Playwright Test Report\n\n`;
if (report.metadata?.donobuMergedReport) {
markdown += `> ⚙️ Auto-heal summary generated by Donobu (merged initial and retry runs).\n\n`;
}
// Per-file summary table
markdown += `## Summary\n\n`;
markdown += `| File | Passed | Self-Healed | Failed | Timed Out | Skipped | Interrupted | Duration |\n`;
markdown += `| - | - | - | - | - | - | - | - |\n`;
let totalPassed = 0;
let totalFailed = 0;
let totalTimedOut = 0;
let totalSkipped = 0;
let totalInterrupted = 0;
let totalSelfHealed = 0;
let totalDuration = 0;
suites.forEach((suite) => {
let passed = 0;
let failed = 0;
let timedOut = 0;
let skipped = 0;
let interrupted = 0;
let selfHealed = 0;
const allSpecs = (0, reportWalk_1.collectSpecs)(suite);
const fileDuration = allSpecs.reduce((total, spec) => total +
(spec.tests ?? []).reduce((testTotal, test) => {
const result = test.results?.at(-1);
const healed = (0, reportWalk_1.isSelfHealed)(test);
if (test.status === 'skipped' ||
(!result && test.status === undefined)) {
skipped++;
}
else if (result) {
if (healed) {
selfHealed++;
}
else {
switch (result.status) {
case 'passed':
passed++;
break;
case 'failed':
failed++;
break;
case 'timedOut':
timedOut++;
break;
case 'skipped':
skipped++;
break;
case 'interrupted':
interrupted++;
break;
}
}
}
return testTotal + (result?.duration || 0);
}, 0), 0);
totalPassed += passed;
totalFailed += failed;
totalTimedOut += timedOut;
totalSkipped += skipped;
totalInterrupted += interrupted;
totalSelfHealed += selfHealed;
totalDuration += fileDuration;
markdown += `| ${suite.file} | ${passed ? passed + ' ✅' : ''} | ${selfHealed ? selfHealed + ' ❤️🩹' : ''} | ${failed ? failed + ' ❌' : ''} | ${timedOut ? timedOut + ' ⏰' : ''} | ${skipped ? skipped + ' ⏭️' : ''} | ${interrupted ? interrupted + ' ⚡' : ''} | ${formatDuration(fileDuration)} |\n`;
});
markdown += `| **TOTAL** | **${totalPassed + ' ✅'}** | **${totalSelfHealed + ' ❤️🩹'}** | **${totalFailed + ' ❌'}** | **${totalTimedOut + ' ⏰'}** | **${totalSkipped + ' ⏭️'}** | **${totalInterrupted + ' ⚡'}** | **${formatDuration(totalDuration)}** |\n`;
markdown += `\n`;
// Per-test details
suites.forEach((suite) => {
const fileName = suite.file;
markdown += `## ${fileName}\n\n`;
(0, reportWalk_1.collectSpecs)(suite).forEach((spec) => {
markdown += `### ${spec.title}\n\n`;
(spec.tests ?? []).forEach((test) => {
const result = test.results?.at(-1);
if (test.status === 'skipped' ||
(!result && test.status === undefined)) {
markdown += `**Status**: ⏭️ Skipped \n`;
markdown += `**Duration**: N/A \n`;
if (Array.isArray(test.tags) && test.tags.length > 0) {
markdown += `**Tags**: ${test.tags.join(' ')} \n`;
}
const objectiveAnnotation = test.annotations?.find((a) => a.type === 'objective');
if (objectiveAnnotation) {
const objective = (objectiveAnnotation.description || 'No objective provided').replace(/```/g, '\\`\\`\\`');
markdown += `**Objective**:\n\`\`\`\n${objective}\n\`\`\`\n`;
}
markdown += `\n---\n\n`;
return;
}
const healed = (0, reportWalk_1.isSelfHealed)(test);
let status;
if (healed) {
status = '❤️🩹 Healed';
}
else {
switch (result.status) {
case 'passed':
status = '✅ Passed';
break;
case 'failed':
status = '❌ Failed';
break;
case 'timedOut':
status = '⏰ Timed Out';
break;
case 'skipped':
status = '⏭️ Skipped';
break;
case 'interrupted':
status = '⚡ Interrupted';
break;
default:
status = `⚠️ ${result.status || 'Unknown'}`;
}
}
const duration = formatDuration(result.duration || 0);
markdown += `**Status**: ${status} \n`;
markdown += `**Duration**: ${duration} \n`;
if (Array.isArray(test.tags) && test.tags.length > 0) {
markdown += `**Tags**: ${test.tags.join(' ')} \n`;
}
if (healed) {
markdown += `> ❤️🩹 This test was automatically healed by re-running with Donobu treatment plan directives.\n\n`;
}
const objectiveAnnotation = test.annotations?.find((a) => a.type === 'objective');
if (objectiveAnnotation) {
const objective = (objectiveAnnotation.description || 'No objective provided').replace(/```/g, '\\`\\`\\`');
markdown += `**Objective**:\n\`\`\`\n${objective}\n\`\`\`\n`;
}
if (result.status === 'failed' && result.error) {
markdown += `\n<details>\n<summary>⚠️ Error Details</summary>\n\n`;
markdown += `\`\`\`\n${(0, ansi_1.stripAnsi)(result.error.message || '') || 'No error message available'}\n\`\`\`\n\n`;
if (result.error.snippet) {
markdown += `**Code Snippet**:\n\`\`\`\n${(0, ansi_1.stripAnsi)(result.error.snippet)}\n\`\`\`\n\n`;
}
markdown += `</details>\n\n`;
}
markdown += `\n---\n\n`;
});
});
});
if (Array.isArray(report.metadata?.donobuHealedTests) &&
report.metadata.donobuHealedTests.length > 0) {
markdown += `### Auto-Healed Tests\n\n`;
report.metadata.donobuHealedTests.forEach((entry) => {
markdown += `- ❤️🩹 ${entry}\n`;
});
markdown += `\n`;
}
markdown += `_Report generated on ${new Date().toLocaleString()} by Donobu_\n`;
return markdown;
}
//# sourceMappingURL=renderMarkdown.js.map