donobu 5.26.0 → 5.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/dist/cli/donobu-cli.js +203 -251
  2. package/dist/codegen/CodeGenerator.js +12 -16
  3. package/dist/esm/cli/donobu-cli.js +203 -251
  4. package/dist/esm/codegen/CodeGenerator.js +12 -16
  5. package/dist/esm/managers/DonobuFlowsManager.js +2 -1
  6. package/dist/esm/managers/TestsManager.js +2 -2
  7. package/dist/esm/models/CreateTest.d.ts +1 -1
  8. package/dist/esm/models/CreateTest.js +6 -0
  9. package/dist/esm/persistence/DonobuSqliteDb.js +102 -0
  10. package/dist/esm/persistence/TestConfigHash.d.ts +11 -0
  11. package/dist/esm/persistence/TestConfigHash.js +31 -0
  12. package/dist/esm/persistence/flows/FlowsPersistenceSqlite.d.ts +0 -9
  13. package/dist/esm/persistence/flows/FlowsPersistenceSqlite.js +4 -33
  14. package/dist/esm/persistence/normalizeFlowMetadata.d.ts +16 -0
  15. package/dist/esm/persistence/normalizeFlowMetadata.js +34 -0
  16. package/dist/esm/reporter/buildReport.d.ts +22 -0
  17. package/dist/esm/reporter/buildReport.js +106 -0
  18. package/dist/esm/reporter/html.d.ts +5 -9
  19. package/dist/esm/reporter/html.js +25 -101
  20. package/dist/esm/reporter/markdown.d.ts +33 -0
  21. package/dist/esm/reporter/markdown.js +62 -0
  22. package/dist/esm/reporter/merge.d.ts +33 -0
  23. package/dist/esm/reporter/merge.js +229 -0
  24. package/dist/esm/reporter/model.d.ts +101 -0
  25. package/dist/esm/reporter/model.js +27 -0
  26. package/dist/{cli/playwright-json-to-html.d.ts → esm/reporter/render.d.ts} +9 -14
  27. package/dist/esm/{cli/playwright-json-to-html.js → reporter/render.js} +52 -152
  28. package/dist/esm/reporter/renderMarkdown.d.ts +11 -0
  29. package/dist/esm/{cli/playwright-json-to-markdown.js → reporter/renderMarkdown.js} +31 -110
  30. package/dist/esm/reporter/renderSlack.d.ts +17 -0
  31. package/dist/esm/reporter/renderSlack.js +100 -0
  32. package/dist/esm/reporter/reportWalk.d.ts +28 -0
  33. package/dist/esm/reporter/reportWalk.js +61 -0
  34. package/dist/esm/reporter/slack.d.ts +93 -0
  35. package/dist/esm/reporter/slack.js +150 -0
  36. package/dist/esm/reporter/stateFile.d.ts +31 -0
  37. package/dist/esm/reporter/stateFile.js +70 -0
  38. package/dist/esm/tools/AssertPageTool.d.ts +2 -2
  39. package/dist/esm/utils/MiscUtils.d.ts +0 -13
  40. package/dist/esm/utils/MiscUtils.js +0 -21
  41. package/dist/esm/utils/displayName.d.ts +16 -0
  42. package/dist/esm/utils/displayName.js +28 -0
  43. package/dist/managers/DonobuFlowsManager.js +2 -1
  44. package/dist/managers/TestsManager.js +2 -2
  45. package/dist/models/CreateTest.d.ts +1 -1
  46. package/dist/models/CreateTest.js +6 -0
  47. package/dist/persistence/DonobuSqliteDb.js +102 -0
  48. package/dist/persistence/TestConfigHash.d.ts +11 -0
  49. package/dist/persistence/TestConfigHash.js +31 -0
  50. package/dist/persistence/flows/FlowsPersistenceSqlite.d.ts +0 -9
  51. package/dist/persistence/flows/FlowsPersistenceSqlite.js +4 -33
  52. package/dist/persistence/normalizeFlowMetadata.d.ts +16 -0
  53. package/dist/persistence/normalizeFlowMetadata.js +34 -0
  54. package/dist/reporter/buildReport.d.ts +22 -0
  55. package/dist/reporter/buildReport.js +106 -0
  56. package/dist/reporter/html.d.ts +5 -9
  57. package/dist/reporter/html.js +25 -101
  58. package/dist/reporter/markdown.d.ts +33 -0
  59. package/dist/reporter/markdown.js +62 -0
  60. package/dist/reporter/merge.d.ts +33 -0
  61. package/dist/reporter/merge.js +229 -0
  62. package/dist/reporter/model.d.ts +101 -0
  63. package/dist/reporter/model.js +27 -0
  64. package/dist/{esm/cli/playwright-json-to-html.d.ts → reporter/render.d.ts} +9 -14
  65. package/dist/{cli/playwright-json-to-html.js → reporter/render.js} +52 -152
  66. package/dist/reporter/renderMarkdown.d.ts +11 -0
  67. package/dist/{cli/playwright-json-to-markdown.js → reporter/renderMarkdown.js} +31 -110
  68. package/dist/reporter/renderSlack.d.ts +17 -0
  69. package/dist/reporter/renderSlack.js +100 -0
  70. package/dist/reporter/reportWalk.d.ts +28 -0
  71. package/dist/reporter/reportWalk.js +61 -0
  72. package/dist/reporter/slack.d.ts +93 -0
  73. package/dist/reporter/slack.js +150 -0
  74. package/dist/reporter/stateFile.d.ts +31 -0
  75. package/dist/reporter/stateFile.js +70 -0
  76. package/dist/tools/AssertPageTool.d.ts +2 -2
  77. package/dist/utils/MiscUtils.d.ts +0 -13
  78. package/dist/utils/MiscUtils.js +0 -21
  79. package/dist/utils/displayName.d.ts +16 -0
  80. package/dist/utils/displayName.js +28 -0
  81. package/package.json +11 -5
  82. package/dist/cli/playwright-json-to-markdown.d.ts +0 -43
  83. package/dist/cli/playwright-json-to-slack-json.d.ts +0 -3
  84. package/dist/cli/playwright-json-to-slack-json.js +0 -214
  85. package/dist/esm/cli/playwright-json-to-markdown.d.ts +0 -43
  86. package/dist/esm/cli/playwright-json-to-slack-json.d.ts +0 -3
  87. package/dist/esm/cli/playwright-json-to-slack-json.js +0 -214
@@ -1,24 +1,19 @@
1
- #!/usr/bin/env node
2
1
  "use strict";
3
2
  /**
4
- * @fileoverview Playwright JSON Report to HTML Report Converter
3
+ * @fileoverview Donobu HTML report renderer.
5
4
  *
6
- * Converts Playwright JSON test reports (optionally enriched with Donobu triage
7
- * data) into a polished, self-contained HTML report for test writers, maintainers,
8
- * and debuggers.
9
- *
10
- * @usage
11
- * ```bash
12
- * npm exec playwright-json-to-html report.json -o report.html
13
- * npm exec playwright-json-to-html report.json --triage-dir ./donobu-triage/run-id/ -o report.html
14
- * cat merged-report.json | npx playwright-json-to-html --triage-dir ./triage/ -o report.html
15
- * ```
5
+ * Pure library that turns a `DonobuReport` (Playwright-JSON superset with
6
+ * optional triage data) into a self-contained HTML document. No filesystem
7
+ * writes, no CLI arg parsing, no environment variable reads — callers (the
8
+ * reporter and the auto-heal orchestrator) own I/O.
16
9
  */
17
10
  Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.stripAnsi = stripAnsi;
18
12
  exports.loadTriageData = loadTriageData;
19
- exports.generateHtml = generateHtml;
13
+ exports.renderHtml = renderHtml;
20
14
  const fs_1 = require("fs");
21
15
  const path_1 = require("path");
16
+ const reportWalk_1 = require("./reportWalk");
22
17
  // ---------------------------------------------------------------------------
23
18
  // Helpers
24
19
  // ---------------------------------------------------------------------------
@@ -132,41 +127,6 @@ function normalizeTestFile(filePath) {
132
127
  // normalize to "foo.test.ts".
133
128
  return (0, path_1.basename)(filePath);
134
129
  }
135
- // ---------------------------------------------------------------------------
136
- // CLI argument parsing
137
- // ---------------------------------------------------------------------------
138
- function parseArgs() {
139
- const args = process.argv.slice(2);
140
- let outputFile = null;
141
- let triageDir = null;
142
- let inputFile = null;
143
- for (let i = 0; i < args.length; i++) {
144
- if ((args[i] === '-o' || args[i] === '--output') && i + 1 < args.length) {
145
- outputFile = args[i + 1];
146
- i++;
147
- }
148
- else if (args[i] === '--triage-dir' && i + 1 < args.length) {
149
- triageDir = args[i + 1];
150
- i++;
151
- }
152
- else if (!args[i].startsWith('-')) {
153
- inputFile = args[i];
154
- }
155
- }
156
- const jsonData = inputFile
157
- ? JSON.parse((0, fs_1.readFileSync)(inputFile, 'utf8'))
158
- : JSON.parse((0, fs_1.readFileSync)(0, 'utf8'));
159
- // Default output to a sibling of the input JSON so asset relative paths work
160
- if (!outputFile && inputFile) {
161
- outputFile = (0, path_1.join)((0, path_1.dirname)((0, path_1.resolve)(inputFile)), 'index.html');
162
- }
163
- return {
164
- jsonData,
165
- outputFile,
166
- triageDir: triageDir ? (0, path_1.resolve)(triageDir) : null,
167
- inputFile,
168
- };
169
- }
170
130
  function loadTriageData(triageDir) {
171
131
  const plans = [];
172
132
  const evidence = [];
@@ -287,22 +247,13 @@ function parseStderrSteps(stderrEntries) {
287
247
  }
288
248
  return steps;
289
249
  }
290
- // Recursively collect all specs from a suite and its nested sub-suites
291
- function collectSpecs(suite) {
292
- const specs = [...(suite.specs ?? [])];
293
- for (const child of suite.suites ?? []) {
294
- specs.push(...collectSpecs(child));
295
- }
296
- return specs;
297
- }
298
250
  function extractTests(jsonData) {
299
251
  const tests = [];
300
252
  for (const suite of jsonData.suites ?? []) {
301
- for (const spec of collectSpecs(suite)) {
253
+ for (const spec of (0, reportWalk_1.collectSpecs)(suite)) {
302
254
  for (const test of spec.tests ?? []) {
303
255
  const annotations = test.annotations ?? [];
304
- const hasHealAnnotation = annotations.some((a) => a.type === 'self-healed');
305
- const isSelfHealed = hasHealAnnotation || test.donobuStatus === 'healed';
256
+ const isSelfHealed = (0, reportWalk_1.isSelfHealed)(test);
306
257
  let status;
307
258
  const lastResult = test.results?.at(-1);
308
259
  if (test.status === 'skipped' ||
@@ -621,25 +572,40 @@ function renderNativeStep(ns) {
621
572
  const locationStr = ns.location?.file
622
573
  ? esc(`${ns.location.file.replace(/.*[/\\]/, '')}:${ns.location.line}`)
623
574
  : '';
624
- let html = `<div class="filmstrip-step native-step">`;
625
- html += `<div class="filmstrip-header">`;
626
- html += statusIcon;
627
- html += `<span class="native-step-title">${esc(ns.title)}</span>`;
628
- html += categoryBadge;
629
- if (locationStr) {
630
- html += `<span class="native-step-location">${locationStr}</span>`;
631
- }
632
- html += `</div>`;
575
+ const snippet = ns.location?.file
576
+ ? readSourceSnippet(ns.location.file, ns.location.line)
577
+ : null;
578
+ const hasBody = snippet || (!ns.passed && ns.error?.message);
579
+ const renderHeader = (tag) => {
580
+ let header = `<${tag} class="filmstrip-header">`;
581
+ header += statusIcon;
582
+ header += `<span class="native-step-title">${esc(ns.title)}</span>`;
583
+ header += categoryBadge;
584
+ if (locationStr) {
585
+ header += `<span class="native-step-location">${locationStr}</span>`;
586
+ }
587
+ if (tag === 'summary') {
588
+ header +=
589
+ '<span class="native-step-chevron" aria-hidden="true">&#9656;</span>';
590
+ }
591
+ header += `</${tag}>`;
592
+ return header;
593
+ };
594
+ if (!hasBody) {
595
+ return `<div class="filmstrip-step native-step">${renderHeader('div')}</div>`;
596
+ }
597
+ // Failing steps render expanded so the error is immediately visible;
598
+ // passing steps collapse so a test with many expects stays scannable.
599
+ const passClass = ns.passed ? 'native-step--passed' : 'native-step--failed';
600
+ let html = `<details class="filmstrip-step native-step ${passClass}"${ns.passed ? '' : ' open'}>`;
601
+ html += renderHeader('summary');
633
602
  if (!ns.passed && ns.error?.message) {
634
603
  html += `<pre class="native-step-error">${ansiToHtml(ns.error.message)}</pre>`;
635
604
  }
636
- if (!ns.passed && ns.location?.file) {
637
- const snippet = readSourceSnippet(ns.location.file, ns.location.line);
638
- if (snippet) {
639
- html += snippet;
640
- }
605
+ if (snippet) {
606
+ html += snippet;
641
607
  }
642
- html += `</div>`;
608
+ html += `</details>`;
643
609
  return html;
644
610
  }
645
611
  const AUDIT_CHECK_DEFS = [
@@ -1223,7 +1189,8 @@ function renderResultTimeline(results, outputDir) {
1223
1189
  return html;
1224
1190
  }
1225
1191
  const LOGO_SVG = '<svg viewBox="0 0 245 238" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#logo-clip)"><path d="M8.75 123.921L21.3889 110.367H32.0833L40.8333 133.602L43.75 155.869L32.0833 158.774L19.4444 155.869L8.75 123.921Z" fill="#FF7F3A"/><path d="M237.225 125.857L224.586 112.303H213.891L205.141 135.539L202.225 157.806L213.891 160.71L226.53 157.806L237.225 125.857Z" fill="#FF7F3A"/><path d="M130.276 57.1198L55.415 78.4187L46.665 88.1V142.315V164.583L68.0539 206.212L93.3317 225.575L117.637 231.384C130.276 230.416 156.332 227.898 159.443 225.575C163.332 222.671 186.665 192.658 190.554 182.977C194.443 173.296 195.415 157.806 195.415 147.156C195.415 136.507 196.387 120.048 195.415 115.208C194.637 111.335 177.591 89.0682 169.165 78.4187L130.276 57.1198Z" fill="#FF7F3A"/><path d="M129.868 30.4824C131.882 28.9302 134.006 26.8043 135.359 24.6522C136.96 22.1107 137.166 19.9586 138.26 17.5486C138.487 17.0434 138.418 16.0226 139.353 16.8172C139.565 16.9961 141.99 20.9373 142.291 21.5214C144.854 26.5781 145.435 32.282 145.943 37.8597C168.385 44.6265 188.639 58.1076 200.824 78.3555C205.004 85.296 209.099 93.9466 210.108 101.971C224.772 94.6044 239.515 103.244 243.626 118.262C250.342 142.793 231.583 169.955 205.421 169.855C204.697 174.096 204.153 178.232 202.975 182.4C196.089 206.684 169.943 227.994 146.101 234.582C99.7856 247.374 45.9189 218.933 39.5938 169.782C13.2839 169.982-5.80773 142.093 1.61651 117.51C5.93895 103.192 20.5496 94.6201 34.5791 101.955C35.3453 101.903 36.3176 96.3197 36.6875 95.1358C41.9188 78.566 53.1846 63.2537 67.0819 52.8614C80.1232 43.1111 99.2202 37.8281 107.247 22.9421C111.099 15.7964 111.564 7.89294 110.333-.0157471C123.083 4.47268 130.724 17.1855 129.863 30.4719L129.868 30.4824ZM121.953 106.181C116.272 96.8144 103.95 89.0267 93.0272 87.4008C79.1774 85.3434 65.745 91.2788 58.8492 103.544C48.8727 121.288 56.6246 142.414 74.9606 150.349C50.8279 179.885 74.8179 211.972 107.157 218.949C130.492 223.985 160.295 216.781 173.69 195.828C183.032 181.216 180.908 163.446 169.98 150.349C195.931 140.352 197.643 103.692 172.971 90.8526C155.042 81.5232 133.636 89.8213 123.041 105.849L121.953 106.181ZM36.9517 154.538C35.9107 151.496 35.1075 148.334 34.4787 145.182C33.4219 139.889 32.328 133.38 32.1801 128.018C32.0057 121.793 35.266 115.552 26.954 113.774C15.5138 111.327 12.3803 123.834 13.2628 132.664C14.4306 144.345 24.3067 156.327 36.957 154.538H36.9517ZM217.876 113.658C216.338 113.968 212.423 116.331 212.195 118.036C212.053 119.12 212.734 120.425 212.761 121.666C212.882 127.481 212.407 133.658 211.53 139.41C210.8 144.193 209.553 149.255 208.063 153.864C207.921 155.969 213.178 154.69 214.436 154.401C222.732 152.512 229.21 144.277 231.055 136.253C233.237 126.734 231.261 110.937 217.871 113.663L217.876 113.658Z" fill="#1a1a1a"/><path d="M158.212 170.413C162.36 169.755 164.738 174.88 161.018 178.48C148.087 190.971 111.162 196.554 95.0185 189.509C91.848 188.125 91.1293 183.589 94.2998 181.958C96.5192 180.816 102.902 183.021 105.904 183.399C119.711 185.136 134.507 183.226 147.104 177.248C149.308 176.206 157.44 170.539 158.206 170.413H158.212Z" fill="#1a1a1a"/><path d="M150.305 114.72C163.975 111.91 165.682 135.542 153.835 137.152C140.884 138.915 139.943 116.846 150.305 114.72Z" fill="#1a1a1a"/><path d="M92.7645 114.721C103.808 112.437 108.141 129.933 99.2058 135.853C88.7326 142.788 79.628 123.75 90.0061 116.178C90.767 115.626 91.8503 114.91 92.7645 114.721Z" fill="#1a1a1a"/><path d="M132.355 145.198C137.211 144.109 140.059 148.608 137.713 152.643C136.217 155.222 126.283 159.526 124.841 156.653C123.715 152.88 128.74 146.008 132.355 145.198Z" fill="#1a1a1a"/><path d="M110.192 145.188C116.206 143.872 123.155 155.432 119.847 157.453C115.984 158.085 107.223 154.901 106.657 150.56C106.367 148.308 107.899 145.693 110.198 145.188H110.192Z" fill="#1a1a1a"/></g><defs><clipPath id="logo-clip"><rect width="245" height="237.65" fill="white"/></clipPath></defs></svg>';
1226
- function generateHtml(jsonData, triage, outputDir) {
1192
+ function renderHtml(report, triage, outputDir) {
1193
+ const jsonData = report;
1227
1194
  const tests = extractTests(jsonData);
1228
1195
  const isMergedReport = !!jsonData.metadata?.donobuMergedReport;
1229
1196
  // Match triage data to tests
@@ -1555,9 +1522,9 @@ body::before{content:'';position:fixed;top:-750px;left:50%;transform:translateX(
1555
1522
 
1556
1523
  /* Steps */
1557
1524
  .steps-section{margin:8px 0;border:1px solid var(--border-subtle);border-radius:var(--radius);overflow:hidden}
1558
- .steps-section summary{font-size:12px;font-weight:600;color:var(--text-muted);padding:8px 12px;cursor:pointer;user-select:none;background:var(--surface-raised)}
1525
+ .steps-section>summary{font-size:12px;font-weight:600;color:var(--text-muted);padding:8px 12px;cursor:pointer;user-select:none;background:var(--surface-raised)}
1559
1526
  .steps-section summary:hover{background:var(--surface)}
1560
- .steps-section[open] summary{border-bottom:1px solid var(--border-subtle)}
1527
+ .steps-section[open]>summary{border-bottom:1px solid var(--border-subtle)}
1561
1528
  .steps-list{padding:4px 0}
1562
1529
  .step-entry{display:flex;gap:8px;padding:4px 12px;font-size:12px;align-items:baseline}
1563
1530
  .step-time{color:var(--text-dim);font-family:var(--mono);font-size:11px;flex-shrink:0;width:85px}
@@ -1632,18 +1599,23 @@ body::before{content:'';position:fixed;top:-750px;left:50%;transform:translateX(
1632
1599
  .audit-a11y-more{font-size:10px;color:var(--text-dim);margin-top:2px}
1633
1600
 
1634
1601
  /* Native Playwright steps (expect / test.step) inside the filmstrip */
1635
- .native-step{background:var(--bg)}
1602
+ details.native-step>summary{list-style:none;cursor:pointer}
1603
+ details.native-step>summary::-webkit-details-marker{display:none}
1636
1604
  .native-step-title{font-size:12px;font-weight:500;color:var(--text);font-family:var(--mono);flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
1637
1605
  .native-step-badge{font-size:10px;font-weight:600;padding:1px 5px;border-radius:3px;white-space:nowrap;flex-shrink:0}
1638
1606
  .native-step-badge--expect{background:rgba(99,102,241,.12);color:#818cf8}
1639
1607
  .native-step-badge--test\.step{background:rgba(16,185,129,.10);color:#34d399}
1640
1608
  .native-step-location{font-size:10px;color:var(--text-dim);font-family:var(--mono);margin-left:auto;flex-shrink:0;white-space:nowrap}
1609
+ .native-step-chevron{font-size:10px;color:var(--text-dim);flex-shrink:0;transition:transform .12s;display:inline-block;margin-left:4px}
1610
+ details.native-step[open]>summary .native-step-chevron{transform:rotate(90deg)}
1641
1611
  .native-step-error{font-size:11px;font-family:var(--mono);padding:4px 0 2px 22px;margin:0;white-space:pre-wrap;word-break:break-word;color:var(--text-muted)}
1642
- .native-step-snippet{font-size:11px;font-family:var(--mono);margin:4px 0 2px 22px;border-radius:var(--radius);overflow:hidden;border:1px solid var(--border-subtle)}
1612
+ .native-step-snippet{font-size:11px;font-family:var(--mono);margin:4px 0 2px 22px;overflow:hidden}
1643
1613
  .snippet-line{display:flex;padding:1px 8px;white-space:pre}
1644
1614
  .snippet-line--target{background:rgba(239,68,68,.10)}
1645
1615
  .snippet-linenum{color:var(--text-dim);min-width:40px;user-select:none}
1646
1616
  .snippet-line--target .snippet-linenum{color:var(--red)}
1617
+ .native-step--passed .snippet-line--target{background:rgba(52,211,153,.10)}
1618
+ .native-step--passed .snippet-line--target .snippet-linenum{color:var(--green)}
1647
1619
  .snippet-code{color:var(--text)}
1648
1620
 
1649
1621
  /* Result timeline */
@@ -1871,76 +1843,4 @@ body::before{content:'';position:fixed;top:-750px;left:50%;transform:translateX(
1871
1843
  </body>
1872
1844
  </html>`;
1873
1845
  }
1874
- // ---------------------------------------------------------------------------
1875
- // Main
1876
- // ---------------------------------------------------------------------------
1877
- // Only run CLI logic when this file is executed directly (not imported as a module).
1878
- if (require.main === module) {
1879
- try {
1880
- const { jsonData, outputFile, triageDir, inputFile } = parseArgs();
1881
- // Auto-discover triage dir from merged report metadata if not explicitly given
1882
- let resolvedTriageDir = triageDir;
1883
- // Prefer the explicit triageRunDir recorded during the merge.
1884
- if (!resolvedTriageDir && jsonData.metadata?.triageRunDir) {
1885
- const candidate = jsonData.metadata.triageRunDir;
1886
- if ((0, fs_1.existsSync)(candidate)) {
1887
- resolvedTriageDir = candidate;
1888
- }
1889
- }
1890
- // Legacy fallback: derive triage dir from the initial report path (older
1891
- // merged reports stored the initial-playwright-report.json copy inside the
1892
- // triage run directory).
1893
- if (!resolvedTriageDir && jsonData.metadata?.sources?.initial) {
1894
- const initialPath = jsonData.metadata.sources.initial;
1895
- // Try the absolute path first (works when running in the same env as CI)
1896
- const candidate = (0, path_1.dirname)(initialPath);
1897
- if ((0, fs_1.existsSync)(candidate) && (0, path_1.basename)(candidate) !== '.') {
1898
- resolvedTriageDir = candidate;
1899
- }
1900
- else {
1901
- // Fallback: extract the donobu-triage/... suffix and resolve relative
1902
- // to the input file. This handles downloaded CI artifacts where the
1903
- // absolute CI path no longer exists.
1904
- const triageMatch = initialPath.match(/donobu-triage[/\\][^/\\]+[/\\]?.*$/);
1905
- if (triageMatch && inputFile) {
1906
- const localCandidate = (0, path_1.resolve)((0, path_1.dirname)((0, path_1.resolve)(inputFile)), (0, path_1.dirname)(triageMatch[0]));
1907
- if ((0, fs_1.existsSync)(localCandidate)) {
1908
- resolvedTriageDir = localCandidate;
1909
- }
1910
- }
1911
- }
1912
- }
1913
- // Last resort: scan for donobu-triage dirs next to the input file
1914
- if (!resolvedTriageDir && inputFile) {
1915
- const triageRoot = (0, path_1.resolve)((0, path_1.dirname)((0, path_1.resolve)(inputFile)), 'donobu-triage');
1916
- if ((0, fs_1.existsSync)(triageRoot)) {
1917
- // Use the most recent triage run directory
1918
- const subdirs = (0, fs_1.readdirSync)(triageRoot)
1919
- .filter((d) => (0, fs_1.existsSync)((0, path_1.resolve)(triageRoot, d, '.')))
1920
- .sort()
1921
- .reverse();
1922
- if (subdirs.length > 0) {
1923
- resolvedTriageDir = (0, path_1.resolve)(triageRoot, subdirs[0]);
1924
- }
1925
- }
1926
- }
1927
- const triage = resolvedTriageDir
1928
- ? loadTriageData(resolvedTriageDir)
1929
- : { plans: [], evidence: [] };
1930
- const outputDir = outputFile ? (0, path_1.dirname)((0, path_1.resolve)(outputFile)) : null;
1931
- const html = generateHtml(jsonData, triage, outputDir);
1932
- if (outputFile) {
1933
- (0, fs_1.writeFileSync)(outputFile, html, 'utf8');
1934
- console.error(`Report written to ${outputFile}`);
1935
- }
1936
- else {
1937
- console.log(html);
1938
- }
1939
- }
1940
- catch (error) {
1941
- console.error('Error processing JSON:', error.message);
1942
- process.exit(1);
1943
- }
1944
- }
1945
- // end of CLI guard
1946
- //# sourceMappingURL=playwright-json-to-html.js.map
1846
+ //# sourceMappingURL=render.js.map
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @fileoverview Donobu Markdown report renderer.
3
+ *
4
+ * Pure library that turns a `DonobuReport` into a Markdown string suitable
5
+ * for GitHub Actions step summaries, PR comments, or any other
6
+ * Markdown-consuming surface. No filesystem writes, no CLI arg parsing, no
7
+ * environment variable reads — callers own I/O.
8
+ */
9
+ import type { DonobuReport } from './model';
10
+ export declare function renderMarkdown(report: DonobuReport): string;
11
+ //# sourceMappingURL=renderMarkdown.d.ts.map
@@ -1,51 +1,16 @@
1
- #!/usr/bin/env node
2
1
  "use strict";
3
2
  /**
4
- * @fileoverview Playwright JSON Report to Markdown Converter
3
+ * @fileoverview Donobu Markdown report renderer.
5
4
  *
6
- * A command-line utility that converts Playwright JSON test reports into human-readable Markdown format.
7
- * This tool is designed to be used in CI/CD pipelines to generate formatted test reports for GitHub
8
- * Actions summaries, pull request comments, or documentation.
9
- *
10
- * @usage
11
- * Read from file:
12
- * ```bash
13
- * npm exec playwright-json-to-markdown report.json
14
- * ```
15
- *
16
- * Read from stdin (useful in pipelines):
17
- * ```bash
18
- * cat test-results/results.json | npx playwright-json-to-markdown
19
- * npm exec playwright test --reporter=json | npx playwright-json-to-markdown
20
- * ```
21
- *
22
- * @input
23
- * Expects a Playwright JSON report with the following structure:
24
- * - config: Test configuration including projects.
25
- * - suites: Array of test suites containing specs and tests.
26
- * - stats: Summary statistics (duration, expected, unexpected, flaky, skipped).
27
- *
28
- * @output
29
- * Generates a Markdown report containing:
30
- * - 📊 Summary table with test counts and duration.
31
- * - Overall pass/fail status with appropriate emoji.
32
- * - 📋 Detailed results organized by file and test suite.
33
- * - ❌ Error details and locations for failed tests.
34
- * - 🚀 List of configured projects.
35
- *
36
- * @features
37
- * - Emoji indicators for test status (✅ passed, ❌ failed).
38
- * - Human-readable duration formatting (ms, s, m s).
39
- * - Hierarchical organization (file → spec → test).
40
- * - Error message and location details for failures.
41
- * - Summary statistics in tabular format.
5
+ * Pure library that turns a `DonobuReport` into a Markdown string suitable
6
+ * for GitHub Actions step summaries, PR comments, or any other
7
+ * Markdown-consuming surface. No filesystem writes, no CLI arg parsing, no
8
+ * environment variable reads — callers own I/O.
42
9
  */
43
10
  Object.defineProperty(exports, "__esModule", { value: true });
44
- const fs_1 = require("fs");
45
- function stripAnsiCodes(str) {
46
- return str.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '');
47
- }
48
- // Function to format duration in a readable way
11
+ exports.renderMarkdown = renderMarkdown;
12
+ const render_1 = require("./render");
13
+ const reportWalk_1 = require("./reportWalk");
49
14
  function formatDuration(ms) {
50
15
  if (ms < 1000) {
51
16
  return `${ms}ms`;
@@ -58,38 +23,16 @@ function formatDuration(ms) {
58
23
  const remainingSeconds = seconds % 60;
59
24
  return `${minutes}m ${remainingSeconds}s`;
60
25
  }
61
- // Read the JSON data from file or stdin
62
- function readInput() {
63
- const args = process.argv.slice(2);
64
- if (args.length > 0) {
65
- return JSON.parse((0, fs_1.readFileSync)(args[0], 'utf8'));
66
- }
67
- else {
68
- return JSON.parse((0, fs_1.readFileSync)(0, 'utf8')); // Read from stdin
69
- }
70
- }
71
- // Recursively collect all specs from a suite and its nested sub-suites
72
- function collectSpecs(suite) {
73
- const specs = [...(suite.specs ?? [])];
74
- for (const child of suite.suites ?? []) {
75
- specs.push(...collectSpecs(child));
76
- }
77
- return specs;
78
- }
79
- // Process JSON and create markdown
80
- function generateMarkdown(jsonData) {
81
- const { suites } = jsonData;
82
- // Create report header
26
+ function renderMarkdown(report) {
27
+ const suites = (report.suites ?? []);
83
28
  let markdown = `# Playwright Test Report\n\n`;
84
- if (jsonData.metadata?.donobuMergedReport) {
29
+ if (report.metadata?.donobuMergedReport) {
85
30
  markdown += `> ⚙️ Auto-heal summary generated by Donobu (merged initial and retry runs).\n\n`;
86
31
  }
87
- // Tests by file
32
+ // Per-file summary table
88
33
  markdown += `## Summary\n\n`;
89
- // Create file summary table with status counts
90
34
  markdown += `| File | Passed | Self-Healed | Failed | Timed Out | Skipped | Interrupted | Duration |\n`;
91
35
  markdown += `| - | - | - | - | - | - | - | - |\n`;
92
- // Track totals for summary row
93
36
  let totalPassed = 0;
94
37
  let totalFailed = 0;
95
38
  let totalTimedOut = 0;
@@ -98,27 +41,23 @@ function generateMarkdown(jsonData) {
98
41
  let totalSelfHealed = 0;
99
42
  let totalDuration = 0;
100
43
  suites.forEach((suite) => {
101
- // Count tests by status for this file
102
44
  let passed = 0;
103
45
  let failed = 0;
104
46
  let timedOut = 0;
105
47
  let skipped = 0;
106
48
  let interrupted = 0;
107
49
  let selfHealed = 0;
108
- const allSpecs = collectSpecs(suite);
50
+ const allSpecs = (0, reportWalk_1.collectSpecs)(suite);
109
51
  const fileDuration = allSpecs.reduce((total, spec) => total +
110
- spec.tests.reduce((testTotal, test) => {
52
+ (spec.tests ?? []).reduce((testTotal, test) => {
111
53
  const result = test.results?.at(-1);
112
- const annotations = test.annotations ?? [];
113
- const hasAnnotation = annotations.some((a) => a.type === 'self-healed');
114
- const donobuHealed = test.donobuStatus === 'healed';
115
- const isSelfHealed = hasAnnotation || donobuHealed;
54
+ const healed = (0, reportWalk_1.isSelfHealed)(test);
116
55
  if (test.status === 'skipped' ||
117
56
  (!result && test.status === undefined)) {
118
57
  skipped++;
119
58
  }
120
59
  else if (result) {
121
- if (isSelfHealed) {
60
+ if (healed) {
122
61
  selfHealed++;
123
62
  }
124
63
  else {
@@ -143,7 +82,6 @@ function generateMarkdown(jsonData) {
143
82
  }
144
83
  return testTotal + (result?.duration || 0);
145
84
  }, 0), 0);
146
- // Add to totals
147
85
  totalPassed += passed;
148
86
  totalFailed += failed;
149
87
  totalTimedOut += timedOut;
@@ -153,35 +91,31 @@ function generateMarkdown(jsonData) {
153
91
  totalDuration += fileDuration;
154
92
  markdown += `| ${suite.file} | ${passed ? passed + ' ✅' : ''} | ${selfHealed ? selfHealed + ' ❤️‍🩹' : ''} | ${failed ? failed + ' ❌' : ''} | ${timedOut ? timedOut + ' ⏰' : ''} | ${skipped ? skipped + ' ⏭️' : ''} | ${interrupted ? interrupted + ' ⚡' : ''} | ${formatDuration(fileDuration)} |\n`;
155
93
  });
156
- // Add totals row
157
94
  markdown += `| **TOTAL** | **${totalPassed + ' ✅'}** | **${totalSelfHealed + ' ❤️‍🩹'}** | **${totalFailed + ' ❌'}** | **${totalTimedOut + ' ⏰'}** | **${totalSkipped + ' ⏭️'}** | **${totalInterrupted + ' ⚡'}** | **${formatDuration(totalDuration)}** |\n`;
158
95
  markdown += `\n`;
159
- // Generate test details sections
96
+ // Per-test details
160
97
  suites.forEach((suite) => {
161
98
  const fileName = suite.file;
162
99
  markdown += `## ${fileName}\n\n`;
163
- collectSpecs(suite).forEach((spec) => {
100
+ (0, reportWalk_1.collectSpecs)(suite).forEach((spec) => {
164
101
  markdown += `### ${spec.title}\n\n`;
165
- spec.tests.forEach((test) => {
102
+ (spec.tests ?? []).forEach((test) => {
166
103
  const result = test.results?.at(-1);
167
- if (test.status === 'skipped' || !result || test.status === undefined) {
104
+ if (test.status === 'skipped' ||
105
+ (!result && test.status === undefined)) {
168
106
  markdown += `**Status**: ⏭️ Skipped \n`;
169
107
  markdown += `**Duration**: N/A \n`;
170
108
  const objectiveAnnotation = test.annotations?.find((a) => a.type === 'objective');
171
109
  if (objectiveAnnotation) {
172
- const objective = (objectiveAnnotation.description || 'No objective provided').replace(/```/g, '\\`\\`\\`'); // escape triple backticks
110
+ const objective = (objectiveAnnotation.description || 'No objective provided').replace(/```/g, '\\`\\`\\`');
173
111
  markdown += `**Objective**:\n\`\`\`\n${objective}\n\`\`\`\n`;
174
112
  }
175
113
  markdown += `\n---\n\n`;
176
114
  return;
177
115
  }
178
- const annotations = test.annotations ?? [];
179
- const hasAnnotation = annotations.some((a) => a.type === 'self-healed');
180
- const donobuHealed = test.donobuStatus === 'healed';
181
- const isSelfHealed = hasAnnotation || donobuHealed;
182
- // Determine status based on result status and self-healing annotation
116
+ const healed = (0, reportWalk_1.isSelfHealed)(test);
183
117
  let status;
184
- if (isSelfHealed) {
118
+ if (healed) {
185
119
  status = '❤️‍🩹 Healed';
186
120
  }
187
121
  else {
@@ -208,21 +142,19 @@ function generateMarkdown(jsonData) {
208
142
  const duration = formatDuration(result.duration || 0);
209
143
  markdown += `**Status**: ${status} \n`;
210
144
  markdown += `**Duration**: ${duration} \n`;
211
- if (isSelfHealed) {
145
+ if (healed) {
212
146
  markdown += `> ❤️‍🩹 This test was automatically healed by re-running with Donobu treatment plan directives.\n\n`;
213
147
  }
214
148
  const objectiveAnnotation = test.annotations?.find((a) => a.type === 'objective');
215
149
  if (objectiveAnnotation) {
216
- const objective = (objectiveAnnotation.description || 'No objective provided').replace(/```/g, '\\`\\`\\`'); // escape triple backticks
150
+ const objective = (objectiveAnnotation.description || 'No objective provided').replace(/```/g, '\\`\\`\\`');
217
151
  markdown += `**Objective**:\n\`\`\`\n${objective}\n\`\`\`\n`;
218
152
  }
219
- // Add error details if test failed
220
153
  if (result.status === 'failed' && result.error) {
221
154
  markdown += `\n<details>\n<summary>⚠️ Error Details</summary>\n\n`;
222
155
  markdown += `\`\`\`\n${result.error.message || 'No error message available'}\n\`\`\`\n\n`;
223
- // Include code snippet if available
224
156
  if (result.error.snippet) {
225
- markdown += `**Code Snippet**:\n\`\`\`\n${stripAnsiCodes(result.error.snippet)}\n\`\`\`\n\n`;
157
+ markdown += `**Code Snippet**:\n\`\`\`\n${(0, render_1.stripAnsi)(result.error.snippet)}\n\`\`\`\n\n`;
226
158
  }
227
159
  markdown += `</details>\n\n`;
228
160
  }
@@ -230,26 +162,15 @@ function generateMarkdown(jsonData) {
230
162
  });
231
163
  });
232
164
  });
233
- if (Array.isArray(jsonData.metadata?.donobuHealedTests) &&
234
- jsonData.metadata.donobuHealedTests.length > 0) {
165
+ if (Array.isArray(report.metadata?.donobuHealedTests) &&
166
+ report.metadata.donobuHealedTests.length > 0) {
235
167
  markdown += `### Auto-Healed Tests\n\n`;
236
- jsonData.metadata.donobuHealedTests.forEach((entry) => {
168
+ report.metadata.donobuHealedTests.forEach((entry) => {
237
169
  markdown += `- ❤️‍🩹 ${entry}\n`;
238
170
  });
239
171
  markdown += `\n`;
240
172
  }
241
- // Add timestamp footer
242
173
  markdown += `_Report generated on ${new Date().toLocaleString()} by Donobu_\n`;
243
174
  return markdown;
244
175
  }
245
- // Main execution
246
- try {
247
- const jsonData = readInput();
248
- const markdown = generateMarkdown(jsonData);
249
- console.log(markdown);
250
- }
251
- catch (error) {
252
- console.error('Error processing JSON: ', error.message);
253
- process.exit(1);
254
- }
255
- //# sourceMappingURL=playwright-json-to-markdown.js.map
176
+ //# sourceMappingURL=renderMarkdown.js.map
@@ -0,0 +1,17 @@
1
+ /**
2
+ * @fileoverview Donobu Slack payload renderer.
3
+ *
4
+ * Pure library that turns a `DonobuReport` into a Slack Block Kit payload
5
+ * suitable for POSTing to an Incoming Webhook. No filesystem writes, no
6
+ * network calls, no env-var reads — callers own I/O.
7
+ */
8
+ import type { DonobuReport } from './model';
9
+ export interface RenderSlackOptions {
10
+ /** Optional URL linking back to the full HTML report or CI job. */
11
+ reportUrl?: string;
12
+ }
13
+ export interface SlackBlockPayload {
14
+ blocks: unknown[];
15
+ }
16
+ export declare function renderSlack(report: DonobuReport, options?: RenderSlackOptions): SlackBlockPayload;
17
+ //# sourceMappingURL=renderSlack.d.ts.map
@@ -0,0 +1,100 @@
1
+ "use strict";
2
+ /**
3
+ * @fileoverview Donobu Slack payload renderer.
4
+ *
5
+ * Pure library that turns a `DonobuReport` into a Slack Block Kit payload
6
+ * suitable for POSTing to an Incoming Webhook. No filesystem writes, no
7
+ * network calls, no env-var reads — callers own I/O.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.renderSlack = renderSlack;
11
+ const reportWalk_1 = require("./reportWalk");
12
+ function renderSlack(report, options = {}) {
13
+ const suites = (report.suites ?? []);
14
+ const blocks = [];
15
+ blocks.push({
16
+ type: 'header',
17
+ text: { type: 'plain_text', text: '🐵 Donobu Test Summary' },
18
+ });
19
+ let totalPassed = 0;
20
+ let totalFailed = 0;
21
+ let totalTimedOut = 0;
22
+ let totalSkipped = 0;
23
+ let totalInterrupted = 0;
24
+ let totalSelfHealed = 0;
25
+ suites.forEach((suite) => {
26
+ (0, reportWalk_1.collectSpecs)(suite).forEach((spec) => {
27
+ (spec.tests ?? []).forEach((test) => {
28
+ const result = test.results?.at(-1);
29
+ const healed = (0, reportWalk_1.isSelfHealed)(test);
30
+ if (test.status === 'skipped' ||
31
+ (!result && test.status === undefined)) {
32
+ totalSkipped++;
33
+ }
34
+ else if (result) {
35
+ if (healed) {
36
+ totalSelfHealed++;
37
+ }
38
+ else {
39
+ switch (result.status) {
40
+ case 'passed':
41
+ totalPassed++;
42
+ break;
43
+ case 'failed':
44
+ totalFailed++;
45
+ break;
46
+ case 'timedOut':
47
+ totalTimedOut++;
48
+ break;
49
+ case 'skipped':
50
+ totalSkipped++;
51
+ break;
52
+ case 'interrupted':
53
+ totalInterrupted++;
54
+ break;
55
+ }
56
+ }
57
+ }
58
+ });
59
+ });
60
+ });
61
+ const statusRows = [
62
+ { name: 'Passed', emoji: '✅', count: totalPassed },
63
+ { name: 'Self-Healed', emoji: '❤️‍🩹', count: totalSelfHealed },
64
+ { name: 'Failed', emoji: '❌', count: totalFailed },
65
+ { name: 'Timed Out', emoji: '⏰', count: totalTimedOut },
66
+ { name: 'Skipped', emoji: '⏭️', count: totalSkipped },
67
+ { name: 'Interrupted', emoji: '⚡', count: totalInterrupted },
68
+ ];
69
+ statusRows.forEach((row) => {
70
+ blocks.push({
71
+ type: 'section',
72
+ fields: [
73
+ { type: 'mrkdwn', text: `${row.emoji} ${row.name}` },
74
+ { type: 'mrkdwn', text: `${row.count}` },
75
+ ],
76
+ });
77
+ });
78
+ if (options.reportUrl) {
79
+ blocks.push({ type: 'divider' });
80
+ blocks.push({
81
+ type: 'section',
82
+ text: {
83
+ type: 'mrkdwn',
84
+ text: `📊 <${options.reportUrl}|View Full Report>`,
85
+ },
86
+ });
87
+ }
88
+ blocks.push({ type: 'divider' });
89
+ blocks.push({
90
+ type: 'context',
91
+ elements: [
92
+ {
93
+ type: 'mrkdwn',
94
+ text: `Report generated on ${new Date().toLocaleString()} by Donobu`,
95
+ },
96
+ ],
97
+ });
98
+ return { blocks };
99
+ }
100
+ //# sourceMappingURL=renderSlack.js.map