UNPKG

donobu

Version:

Create browser automations with an LLM agent and replay them as Playwright scripts.

182 lines 8.43 kB
"use strict"; /** * @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