donobu 5.8.4 → 5.9.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.
- package/dist/cli/donobu-cli.js +17 -2
- package/dist/cli/donobu-cli.js.map +1 -1
- package/dist/cli/playwright-json-to-html.d.ts +133 -0
- package/dist/cli/playwright-json-to-html.d.ts.map +1 -1
- package/dist/cli/playwright-json-to-html.js +370 -187
- package/dist/cli/playwright-json-to-html.js.map +1 -1
- package/dist/esm/cli/donobu-cli.js +17 -2
- package/dist/esm/cli/donobu-cli.js.map +1 -1
- package/dist/esm/cli/playwright-json-to-html.d.ts +133 -0
- package/dist/esm/cli/playwright-json-to-html.d.ts.map +1 -1
- package/dist/esm/cli/playwright-json-to-html.js +370 -187
- package/dist/esm/cli/playwright-json-to-html.js.map +1 -1
- package/dist/esm/lib/test/testExtension.js +58 -0
- package/dist/esm/lib/test/testExtension.js.map +1 -1
- package/dist/esm/reporter/html.d.ts +57 -0
- package/dist/esm/reporter/html.d.ts.map +1 -0
- package/dist/esm/reporter/html.js +144 -0
- package/dist/esm/reporter/html.js.map +1 -0
- package/dist/lib/test/testExtension.js +58 -0
- package/dist/lib/test/testExtension.js.map +1 -1
- package/dist/reporter/html.d.ts +57 -0
- package/dist/reporter/html.d.ts.map +1 -0
- package/dist/reporter/html.js +144 -0
- package/dist/reporter/html.js.map +1 -0
- package/package.json +11 -5
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
* ```
|
|
16
16
|
*/
|
|
17
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.loadTriageData = loadTriageData;
|
|
19
|
+
exports.generateHtml = generateHtml;
|
|
18
20
|
const fs_1 = require("fs");
|
|
19
21
|
const path_1 = require("path");
|
|
20
22
|
// ---------------------------------------------------------------------------
|
|
@@ -23,6 +25,76 @@ const path_1 = require("path");
|
|
|
23
25
|
function stripAnsi(str) {
|
|
24
26
|
return str.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '');
|
|
25
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Convert ANSI SGR color codes to HTML spans. Handles the subset Playwright's
|
|
30
|
+
* expect formatter emits: red (31), green (32), dim (2), and resets (0/22/39).
|
|
31
|
+
* Text segments are HTML-escaped before wrapping.
|
|
32
|
+
*/
|
|
33
|
+
function ansiToHtml(str) {
|
|
34
|
+
const colorMap = {
|
|
35
|
+
'31': 'var(--red)',
|
|
36
|
+
'32': 'var(--green)',
|
|
37
|
+
'2': 'var(--text-dim)',
|
|
38
|
+
};
|
|
39
|
+
const resetCodes = new Set(['0', '22', '39', '49', '']);
|
|
40
|
+
let html = '';
|
|
41
|
+
let openSpans = 0;
|
|
42
|
+
// Split on ESC [ ... m sequences; odd indices are the captured code group
|
|
43
|
+
const parts = str.split(/\x1b\[([0-9;]*)m/);
|
|
44
|
+
for (let i = 0; i < parts.length; i++) {
|
|
45
|
+
if (i % 2 === 0) {
|
|
46
|
+
html += esc(parts[i]);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
const code = parts[i];
|
|
50
|
+
if (resetCodes.has(code)) {
|
|
51
|
+
if (openSpans > 0) {
|
|
52
|
+
html += '</span>'.repeat(openSpans);
|
|
53
|
+
openSpans = 0;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
const color = colorMap[code];
|
|
58
|
+
if (color) {
|
|
59
|
+
html += `<span style="color:${color}">`;
|
|
60
|
+
openSpans++;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (openSpans > 0) {
|
|
66
|
+
html += '</span>'.repeat(openSpans);
|
|
67
|
+
}
|
|
68
|
+
return html;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Read a few lines of source around `targetLine` (1-based) and return an HTML
|
|
72
|
+
* snippet block, or null if the file is unreadable.
|
|
73
|
+
*/
|
|
74
|
+
function readSourceSnippet(file, targetLine) {
|
|
75
|
+
try {
|
|
76
|
+
const source = (0, fs_1.readFileSync)(file, 'utf8');
|
|
77
|
+
const lines = source.split('\n');
|
|
78
|
+
const context = 2;
|
|
79
|
+
const start = Math.max(0, targetLine - 1 - context);
|
|
80
|
+
const end = Math.min(lines.length - 1, targetLine - 1 + context);
|
|
81
|
+
let html = '<div class="native-step-snippet">';
|
|
82
|
+
for (let i = start; i <= end; i++) {
|
|
83
|
+
const lineNum = i + 1;
|
|
84
|
+
const isTarget = lineNum === targetLine;
|
|
85
|
+
const marker = isTarget ? '>' : ' ';
|
|
86
|
+
html += `<div class="snippet-line${isTarget ? ' snippet-line--target' : ''}">`;
|
|
87
|
+
html += `<span class="snippet-linenum">${marker} ${String(lineNum).padStart(3)}</span>`;
|
|
88
|
+
html += `<span class="snippet-code"> ${esc(lines[i] ?? '')}</span>`;
|
|
89
|
+
html += '</div>';
|
|
90
|
+
}
|
|
91
|
+
html += '</div>';
|
|
92
|
+
return html;
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
26
98
|
function esc(str) {
|
|
27
99
|
return str
|
|
28
100
|
.replace(/&/g, '&')
|
|
@@ -277,6 +349,18 @@ function extractTests(jsonData) {
|
|
|
277
349
|
// Ignore parse failures
|
|
278
350
|
}
|
|
279
351
|
}
|
|
352
|
+
// Parse native Playwright steps from donobu-native-steps attachment
|
|
353
|
+
let nativeSteps = [];
|
|
354
|
+
const nativeAtt = attachments.find((a) => a.name === 'donobu-native-steps');
|
|
355
|
+
if (nativeAtt?.body) {
|
|
356
|
+
try {
|
|
357
|
+
const decoded = Buffer.from(nativeAtt.body, 'base64').toString('utf8');
|
|
358
|
+
nativeSteps = JSON.parse(decoded);
|
|
359
|
+
}
|
|
360
|
+
catch {
|
|
361
|
+
// Ignore parse failures
|
|
362
|
+
}
|
|
363
|
+
}
|
|
280
364
|
return {
|
|
281
365
|
index: i,
|
|
282
366
|
status: r.status,
|
|
@@ -286,13 +370,21 @@ function extractTests(jsonData) {
|
|
|
286
370
|
errors: (r.errors ?? (r.error ? [r.error] : [])).map((e) => ({
|
|
287
371
|
message: e.message,
|
|
288
372
|
stack: e.stack,
|
|
289
|
-
snippet: e.snippet
|
|
373
|
+
snippet: e.snippet ?? undefined,
|
|
290
374
|
actual: e.actual,
|
|
291
375
|
expected: e.expected,
|
|
376
|
+
location: e.location
|
|
377
|
+
? {
|
|
378
|
+
file: e.location.file ?? '',
|
|
379
|
+
line: e.location.line ?? 0,
|
|
380
|
+
column: e.location.column ?? 0,
|
|
381
|
+
}
|
|
382
|
+
: undefined,
|
|
292
383
|
})),
|
|
293
384
|
attachments,
|
|
294
385
|
steps: parseStderrSteps(r.stderr ?? []),
|
|
295
386
|
stepScreenshots,
|
|
387
|
+
nativeSteps,
|
|
296
388
|
};
|
|
297
389
|
});
|
|
298
390
|
tests.push({
|
|
@@ -491,16 +583,14 @@ function renderErrors(errors) {
|
|
|
491
583
|
let html = '';
|
|
492
584
|
for (const err of errors) {
|
|
493
585
|
if (err.message) {
|
|
494
|
-
|
|
586
|
+
// Use a neutral container so ANSI-derived colors (green Expected, red
|
|
587
|
+
// Received, dim call log) render correctly without a red tint fighting them.
|
|
588
|
+
html += `<pre class="error-block-ansi">${ansiToHtml(err.message)}</pre>`;
|
|
495
589
|
}
|
|
496
590
|
if (err.snippet) {
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
err.actual !== undefined &&
|
|
501
|
-
err.expected !== null &&
|
|
502
|
-
err.expected !== undefined) {
|
|
503
|
-
html += `<div class="expect-actual"><span class="expect-label">Expected:</span> <code>${esc(err.expected)}</code><br/><span class="expect-label">Actual:</span> <code>${esc(err.actual)}</code></div>`;
|
|
591
|
+
// Playwright's pre-formatted snippet (pipe/arrow markers) — convert ANSI
|
|
592
|
+
// highlight codes to HTML spans so the failing line shows in color.
|
|
593
|
+
html += `<pre class="snippet-block">${ansiToHtml(err.snippet)}</pre>`;
|
|
504
594
|
}
|
|
505
595
|
if (err.stack && err.stack !== err.message) {
|
|
506
596
|
html += `<details class="stack-details"><summary>Stack trace</summary><pre class="stack-block">${esc(stripAnsi(err.stack))}</pre></details>`;
|
|
@@ -508,93 +598,142 @@ function renderErrors(errors) {
|
|
|
508
598
|
}
|
|
509
599
|
return html;
|
|
510
600
|
}
|
|
511
|
-
function
|
|
601
|
+
function renderNativeStep(ns) {
|
|
602
|
+
const statusIcon = ns.passed
|
|
603
|
+
? '<span class="step-status-ok">✓</span>'
|
|
604
|
+
: '<span class="step-status-fail">✗</span>';
|
|
605
|
+
const categoryBadge = `<span class="native-step-badge native-step-badge--${ns.category}">${esc(ns.category)}</span>`;
|
|
606
|
+
const locationStr = ns.location?.file
|
|
607
|
+
? esc(`${ns.location.file.replace(/.*[/\\]/, '')}:${ns.location.line}`)
|
|
608
|
+
: '';
|
|
609
|
+
let html = `<div class="filmstrip-step native-step">`;
|
|
610
|
+
html += `<div class="filmstrip-header">`;
|
|
611
|
+
html += statusIcon;
|
|
612
|
+
html += `<span class="native-step-title">${esc(ns.title)}</span>`;
|
|
613
|
+
html += categoryBadge;
|
|
614
|
+
if (locationStr) {
|
|
615
|
+
html += `<span class="native-step-location">${locationStr}</span>`;
|
|
616
|
+
}
|
|
617
|
+
html += `</div>`;
|
|
618
|
+
if (!ns.passed && ns.error?.message) {
|
|
619
|
+
html += `<pre class="native-step-error">${ansiToHtml(ns.error.message)}</pre>`;
|
|
620
|
+
}
|
|
621
|
+
if (!ns.passed && ns.location?.file) {
|
|
622
|
+
const snippet = readSourceSnippet(ns.location.file, ns.location.line);
|
|
623
|
+
if (snippet) {
|
|
624
|
+
html += snippet;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
html += `</div>`;
|
|
628
|
+
return html;
|
|
629
|
+
}
|
|
630
|
+
function renderFilmstripStep(ss, outputDir) {
|
|
631
|
+
const duration = ss.completedAt - ss.startedAt;
|
|
632
|
+
const durationStr = duration >= 1000 ? `${(duration / 1000).toFixed(1)}s` : `${duration}ms`;
|
|
633
|
+
const statusIcon = ss.success
|
|
634
|
+
? '<span class="step-status-ok">✓</span>'
|
|
635
|
+
: '<span class="step-status-fail">✗</span>';
|
|
636
|
+
let imgSrc = null;
|
|
637
|
+
if (ss.imagePath && (0, fs_1.existsSync)(ss.imagePath)) {
|
|
638
|
+
imgSrc = outputDir ? (0, path_1.relative)(outputDir, ss.imagePath) : ss.imagePath;
|
|
639
|
+
}
|
|
640
|
+
else if (ss.imageBody && ss.imageContentType) {
|
|
641
|
+
imgSrc = `data:${ss.imageContentType};base64,${ss.imageBody}`;
|
|
642
|
+
}
|
|
643
|
+
const hasDetail = !!(ss.parameters || ss.outcome);
|
|
644
|
+
let jsonBlock = '';
|
|
645
|
+
if (hasDetail) {
|
|
646
|
+
const jsonObj = {
|
|
647
|
+
toolName: ss.toolName,
|
|
648
|
+
page: ss.page,
|
|
649
|
+
};
|
|
650
|
+
if (ss.parameters) {
|
|
651
|
+
jsonObj.parameters = ss.parameters;
|
|
652
|
+
}
|
|
653
|
+
if (ss.outcome) {
|
|
654
|
+
jsonObj.outcome = ss.outcome;
|
|
655
|
+
}
|
|
656
|
+
jsonBlock = `<pre class="step-json">${esc(JSON.stringify(jsonObj, null, 2))}</pre>`;
|
|
657
|
+
}
|
|
658
|
+
const hasExpandable = !!(imgSrc || hasDetail);
|
|
659
|
+
const expandId = `step-expand-${uid()}`;
|
|
660
|
+
let html = `<div class="filmstrip-step${hasExpandable ? ' expandable' : ''}">`;
|
|
661
|
+
html += `<div class="filmstrip-header">`;
|
|
662
|
+
html += statusIcon;
|
|
663
|
+
html += `<span class="filmstrip-tool">${esc(ss.toolName)}</span>`;
|
|
664
|
+
html += `<span class="filmstrip-duration">${durationStr}</span>`;
|
|
665
|
+
if (hasExpandable) {
|
|
666
|
+
html += `<span class="filmstrip-chevron">▸</span>`;
|
|
667
|
+
}
|
|
668
|
+
html += `</div>`;
|
|
669
|
+
if (ss.summary) {
|
|
670
|
+
html += `<div class="filmstrip-summary">${esc(ss.summary)}</div>`;
|
|
671
|
+
}
|
|
672
|
+
if (hasExpandable) {
|
|
673
|
+
html += `<div class="filmstrip-detail" id="${expandId}">`;
|
|
674
|
+
if (imgSrc) {
|
|
675
|
+
html += `<a href="${esc(imgSrc)}" target="_blank"><img src="${esc(imgSrc)}" alt="Step ${ss.index}: ${esc(ss.toolName)}" loading="lazy" class="step-screenshot" /></a>`;
|
|
676
|
+
}
|
|
677
|
+
if (jsonBlock) {
|
|
678
|
+
html += jsonBlock;
|
|
679
|
+
}
|
|
680
|
+
html += `</div>`;
|
|
681
|
+
}
|
|
682
|
+
html += `</div>`;
|
|
683
|
+
return html;
|
|
684
|
+
}
|
|
685
|
+
function renderSteps(steps, stepScreenshots, nativeSteps, outputDir) {
|
|
512
686
|
const meaningful = steps.filter((s) => s.type === 'action' || s.type === 'result');
|
|
513
687
|
const hasScreenshots = stepScreenshots.length > 0;
|
|
514
|
-
|
|
688
|
+
const hasNative = nativeSteps.length > 0;
|
|
689
|
+
if (!meaningful.length && !hasScreenshots && !hasNative) {
|
|
515
690
|
return '';
|
|
516
691
|
}
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
692
|
+
if (hasScreenshots || hasNative) {
|
|
693
|
+
const timeline = [
|
|
694
|
+
...stepScreenshots.map((ss) => ({
|
|
695
|
+
t: ss.completedAt,
|
|
696
|
+
kind: 'donobu',
|
|
697
|
+
ss,
|
|
698
|
+
})),
|
|
699
|
+
...nativeSteps.map((ns) => ({
|
|
700
|
+
t: ns.endWallTime,
|
|
701
|
+
kind: 'native',
|
|
702
|
+
ns,
|
|
703
|
+
})),
|
|
704
|
+
];
|
|
705
|
+
timeline.sort((a, b) => a.t - b.t);
|
|
706
|
+
const stepCount = timeline.length;
|
|
707
|
+
let html = '<details class="steps-section"><summary>Steps (' +
|
|
708
|
+
stepCount +
|
|
709
|
+
')</summary>';
|
|
525
710
|
html += '<div class="step-filmstrip">';
|
|
526
|
-
for (const
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
const statusIcon = ss.success
|
|
530
|
-
? '<span class="step-status-ok">✓</span>'
|
|
531
|
-
: '<span class="step-status-fail">✗</span>';
|
|
532
|
-
// Resolve screenshot src
|
|
533
|
-
let imgSrc = null;
|
|
534
|
-
if (ss.imagePath && (0, fs_1.existsSync)(ss.imagePath)) {
|
|
535
|
-
imgSrc = outputDir ? (0, path_1.relative)(outputDir, ss.imagePath) : ss.imagePath;
|
|
536
|
-
}
|
|
537
|
-
else if (ss.imageBody && ss.imageContentType) {
|
|
538
|
-
imgSrc = `data:${ss.imageContentType};base64,${ss.imageBody}`;
|
|
539
|
-
}
|
|
540
|
-
// Build JSON object if available
|
|
541
|
-
const hasDetail = !!(ss.parameters || ss.outcome);
|
|
542
|
-
let jsonBlock = '';
|
|
543
|
-
if (hasDetail) {
|
|
544
|
-
const jsonObj = {
|
|
545
|
-
toolName: ss.toolName,
|
|
546
|
-
page: ss.page,
|
|
547
|
-
};
|
|
548
|
-
if (ss.parameters) {
|
|
549
|
-
jsonObj.parameters = ss.parameters;
|
|
550
|
-
}
|
|
551
|
-
if (ss.outcome) {
|
|
552
|
-
jsonObj.outcome = ss.outcome;
|
|
553
|
-
}
|
|
554
|
-
jsonBlock = `<pre class="step-json">${esc(JSON.stringify(jsonObj, null, 2))}</pre>`;
|
|
711
|
+
for (const entry of timeline) {
|
|
712
|
+
if (entry.kind === 'donobu') {
|
|
713
|
+
html += renderFilmstripStep(entry.ss, outputDir);
|
|
555
714
|
}
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
html += `<div class="filmstrip-step${hasExpandable ? ' expandable' : ''}"${hasExpandable ? ` onclick="this.classList.toggle('open')"` : ''}>`;
|
|
559
|
-
html += `<div class="filmstrip-header">`;
|
|
560
|
-
html += `${statusIcon}`;
|
|
561
|
-
html += `<span class="filmstrip-tool">${esc(ss.toolName)}</span>`;
|
|
562
|
-
html += `<span class="filmstrip-duration">${durationStr}</span>`;
|
|
563
|
-
if (hasExpandable) {
|
|
564
|
-
html += `<span class="filmstrip-chevron">▸</span>`;
|
|
565
|
-
}
|
|
566
|
-
html += `</div>`;
|
|
567
|
-
// Summary line
|
|
568
|
-
if (ss.summary) {
|
|
569
|
-
html += `<div class="filmstrip-summary">${esc(ss.summary)}</div>`;
|
|
570
|
-
}
|
|
571
|
-
// Expandable detail area (screenshot + JSON together)
|
|
572
|
-
if (hasExpandable) {
|
|
573
|
-
html += `<div class="filmstrip-detail" id="${expandId}">`;
|
|
574
|
-
if (imgSrc) {
|
|
575
|
-
html += `<a href="${esc(imgSrc)}" target="_blank" onclick="event.stopPropagation()"><img src="${esc(imgSrc)}" alt="Step ${ss.index}: ${esc(ss.toolName)}" loading="lazy" class="step-screenshot" /></a>`;
|
|
576
|
-
}
|
|
577
|
-
if (jsonBlock) {
|
|
578
|
-
html += jsonBlock;
|
|
579
|
-
}
|
|
580
|
-
html += `</div>`;
|
|
715
|
+
else {
|
|
716
|
+
html += renderNativeStep(entry.ns);
|
|
581
717
|
}
|
|
582
|
-
html += `</div>`;
|
|
583
718
|
}
|
|
584
719
|
html += '</div>';
|
|
720
|
+
html += '</details>';
|
|
721
|
+
return html;
|
|
585
722
|
}
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
}
|
|
723
|
+
// Fallback: no filmstrip or native step data — render log-based steps
|
|
724
|
+
const stepCount = meaningful.length;
|
|
725
|
+
let html = '<details class="steps-section"><summary>Steps (' +
|
|
726
|
+
stepCount +
|
|
727
|
+
')</summary>';
|
|
728
|
+
html += '<div class="steps-list">';
|
|
729
|
+
for (const step of meaningful) {
|
|
730
|
+
const typeClass = step.type === 'action' ? 'step-action' : 'step-result';
|
|
731
|
+
html += `<div class="step-entry ${typeClass}">`;
|
|
732
|
+
html += `<span class="step-time">${esc(step.time)}</span>`;
|
|
733
|
+
html += `<span class="step-text">${step.text}</span>`;
|
|
596
734
|
html += '</div>';
|
|
597
735
|
}
|
|
736
|
+
html += '</div>';
|
|
598
737
|
html += '</details>';
|
|
599
738
|
return html;
|
|
600
739
|
}
|
|
@@ -703,7 +842,7 @@ function renderQuickActions(test) {
|
|
|
703
842
|
html += `<span class="qa-label">${esc(label)}</span>`;
|
|
704
843
|
html += `<div class="qa-cmd-wrapper">`;
|
|
705
844
|
html += `<code class="qa-cmd" id="${cmdId}">${esc(cmd)}</code>`;
|
|
706
|
-
html += `<button class="qa-copy"
|
|
845
|
+
html += `<button class="qa-copy" data-copy="${cmdId}" title="Copy to clipboard">Copy</button>`;
|
|
707
846
|
html += `</div></div>`;
|
|
708
847
|
}
|
|
709
848
|
html += sourceHtml;
|
|
@@ -813,7 +952,7 @@ function renderEvidenceDeepDive(evidence, outputDir) {
|
|
|
813
952
|
const baseSrc = outputDir
|
|
814
953
|
? (0, path_1.relative)(outputDir, (0, path_1.resolve)(baseScreenshot))
|
|
815
954
|
: baseScreenshot;
|
|
816
|
-
content += `<a href="${esc(baseSrc)}" target="_blank"
|
|
955
|
+
content += `<a href="${esc(baseSrc)}" target="_blank"><img src="${esc(baseSrc)}" alt="Baseline screenshot" loading="lazy" class="screenshot triage-screenshot" /></a>`;
|
|
817
956
|
}
|
|
818
957
|
else {
|
|
819
958
|
content +=
|
|
@@ -828,7 +967,7 @@ function renderEvidenceDeepDive(evidence, outputDir) {
|
|
|
828
967
|
const failSrc = outputDir
|
|
829
968
|
? (0, path_1.relative)(outputDir, (0, path_1.resolve)(failScreenshot))
|
|
830
969
|
: failScreenshot;
|
|
831
|
-
content += `<a href="${esc(failSrc)}" target="_blank"
|
|
970
|
+
content += `<a href="${esc(failSrc)}" target="_blank"><img src="${esc(failSrc)}" alt="Failure screenshot" loading="lazy" class="screenshot triage-screenshot" /></a>`;
|
|
832
971
|
}
|
|
833
972
|
else {
|
|
834
973
|
content +=
|
|
@@ -878,7 +1017,7 @@ function renderResultTimeline(results, outputDir) {
|
|
|
878
1017
|
html += `<div class="timeline-errors">${renderErrors(r.errors)}</div>`;
|
|
879
1018
|
}
|
|
880
1019
|
html += renderAttachments(r.attachments, outputDir, r.stepScreenshots);
|
|
881
|
-
html += renderSteps(r.steps, r.stepScreenshots, outputDir);
|
|
1020
|
+
html += renderSteps(r.steps, r.stepScreenshots, r.nativeSteps, outputDir);
|
|
882
1021
|
html += '</div></div>';
|
|
883
1022
|
}
|
|
884
1023
|
html += '</div>';
|
|
@@ -954,7 +1093,7 @@ function generateHtml(jsonData, triage, outputDir) {
|
|
|
954
1093
|
continue;
|
|
955
1094
|
}
|
|
956
1095
|
const sc = cfg(card.key);
|
|
957
|
-
statCardsHtml += `<button class="stat-card" data-filter="${card.key}"
|
|
1096
|
+
statCardsHtml += `<button class="stat-card" data-filter="${card.key}" title="Click to filter"><div class="stat-count" style="color:${sc.color}">${count}</div><div class="stat-label">${card.label}</div></button>`;
|
|
958
1097
|
}
|
|
959
1098
|
// File groups + test cards
|
|
960
1099
|
let testSectionsHtml = '';
|
|
@@ -1009,7 +1148,7 @@ function generateHtml(jsonData, triage, outputDir) {
|
|
|
1009
1148
|
}
|
|
1010
1149
|
// 6. Steps — detailed forensics
|
|
1011
1150
|
if (!hasMultipleResults && lastResult) {
|
|
1012
|
-
detailsHtml += renderSteps(lastResult.steps, lastResult.stepScreenshots, outputDir);
|
|
1151
|
+
detailsHtml += renderSteps(lastResult.steps, lastResult.stepScreenshots, lastResult.nativeSteps, outputDir);
|
|
1013
1152
|
}
|
|
1014
1153
|
// 7. Triage details — remediation steps (expandable)
|
|
1015
1154
|
if (test.plan) {
|
|
@@ -1026,7 +1165,7 @@ function generateHtml(jsonData, triage, outputDir) {
|
|
|
1026
1165
|
: '<span class="chevron-spacer"></span>';
|
|
1027
1166
|
const totalTestDuration = test.results.reduce((s, r) => s + r.duration, 0);
|
|
1028
1167
|
testsHtml += `
|
|
1029
|
-
<div class="test-row ${sc.label.toLowerCase().replace(/ /g, '')} ${expandableClass}" data-status="${test.status}" ${hasDetails ? `
|
|
1168
|
+
<div class="test-row ${sc.label.toLowerCase().replace(/ /g, '')} ${expandableClass}" data-status="${test.status}" ${hasDetails ? `data-detail="${testId}"` : ''}>
|
|
1030
1169
|
<div class="test-summary">
|
|
1031
1170
|
${chevron}
|
|
1032
1171
|
<span class="status-dot" style="background:${sc.color}" title="${sc.label}"></span>
|
|
@@ -1039,7 +1178,7 @@ function generateHtml(jsonData, triage, outputDir) {
|
|
|
1039
1178
|
}
|
|
1040
1179
|
testSectionsHtml += `
|
|
1041
1180
|
<div class="file-group ${fileHasFailure ? 'has-failure' : ''}" data-file="${esc(file)}">
|
|
1042
|
-
<div class="file-header"
|
|
1181
|
+
<div class="file-header" data-toggle-file-group>
|
|
1043
1182
|
<span class="file-chevron">▾</span>
|
|
1044
1183
|
<span class="file-name">${esc(file)}</span>
|
|
1045
1184
|
<div class="file-badges">${fileBadgesHtml}</div>
|
|
@@ -1162,8 +1301,9 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica N
|
|
|
1162
1301
|
.healed-icon{font-size:16px}
|
|
1163
1302
|
|
|
1164
1303
|
/* Error / snippet blocks */
|
|
1165
|
-
.error-block,.snippet-block,.stack-block,.snapshot-block{font-size:12px;font-family:var(--mono);padding:12px 14px;border-radius:var(--radius);overflow-x:auto;white-space:pre-wrap;word-break:break-word;line-height:1.6;max-height:400px;overflow-y:auto}
|
|
1304
|
+
.error-block,.error-block-ansi,.snippet-block,.stack-block,.snapshot-block{font-size:12px;font-family:var(--mono);padding:12px 14px;border-radius:var(--radius);overflow-x:auto;white-space:pre-wrap;word-break:break-word;line-height:1.6;max-height:400px;overflow-y:auto}
|
|
1166
1305
|
.error-block{background:rgba(239,68,68,.06);border:1px solid rgba(239,68,68,.15);color:#fca5a5}
|
|
1306
|
+
.error-block-ansi{background:var(--bg);border:1px solid var(--border-subtle);color:var(--text-muted)}
|
|
1167
1307
|
.snippet-block{background:var(--bg);border:1px solid var(--border-subtle);color:var(--text)}
|
|
1168
1308
|
.stack-block{background:var(--bg);border:1px solid var(--border-subtle);color:var(--text-muted);font-size:11px}
|
|
1169
1309
|
.snapshot-block{background:var(--bg);border:1px solid var(--border-subtle);color:var(--text-muted);font-size:11px;max-height:300px}
|
|
@@ -1205,6 +1345,21 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica N
|
|
|
1205
1345
|
.step-json{font-size:11px;font-family:var(--mono);background:var(--bg);border:1px solid var(--border-subtle);border-radius:var(--radius);padding:8px 12px;margin:0;color:var(--text-muted);overflow-x:auto;max-height:300px;overflow-y:auto}
|
|
1206
1346
|
.step-screenshot{max-width:100%;max-height:250px;width:auto;height:auto;object-fit:contain;border-radius:var(--radius);border:1px solid var(--border);cursor:pointer}
|
|
1207
1347
|
|
|
1348
|
+
/* Native Playwright steps (expect / test.step) inside the filmstrip */
|
|
1349
|
+
.native-step{background:var(--bg)}
|
|
1350
|
+
.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}
|
|
1351
|
+
.native-step-badge{font-size:10px;font-weight:600;padding:1px 5px;border-radius:3px;white-space:nowrap;flex-shrink:0}
|
|
1352
|
+
.native-step-badge--expect{background:rgba(99,102,241,.12);color:#818cf8}
|
|
1353
|
+
.native-step-badge--test\.step{background:rgba(16,185,129,.10);color:#34d399}
|
|
1354
|
+
.native-step-location{font-size:10px;color:var(--text-dim);font-family:var(--mono);margin-left:auto;flex-shrink:0;white-space:nowrap}
|
|
1355
|
+
.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)}
|
|
1356
|
+
.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)}
|
|
1357
|
+
.snippet-line{display:flex;padding:1px 8px;white-space:pre}
|
|
1358
|
+
.snippet-line--target{background:rgba(239,68,68,.10)}
|
|
1359
|
+
.snippet-linenum{color:var(--text-dim);min-width:40px;user-select:none}
|
|
1360
|
+
.snippet-line--target .snippet-linenum{color:var(--red)}
|
|
1361
|
+
.snippet-code{color:var(--text)}
|
|
1362
|
+
|
|
1208
1363
|
/* Result timeline */
|
|
1209
1364
|
.result-timeline{position:relative;padding-left:20px}
|
|
1210
1365
|
.timeline-entry{position:relative;padding-bottom:16px;padding-left:16px;border-left:2px solid var(--border)}
|
|
@@ -1349,7 +1504,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica N
|
|
|
1349
1504
|
|
|
1350
1505
|
<div class="filter-bar" id="filterBar">
|
|
1351
1506
|
<span>Showing: <strong id="filterLabel"></strong></span>
|
|
1352
|
-
<button class="clear-filter"
|
|
1507
|
+
<button class="clear-filter" data-clear-filter>Clear filter</button>
|
|
1353
1508
|
</div>
|
|
1354
1509
|
|
|
1355
1510
|
${testSectionsHtml}
|
|
@@ -1358,51 +1513,75 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica N
|
|
|
1358
1513
|
<div class="report-footer">Report generated by Donobu</div>
|
|
1359
1514
|
</div>
|
|
1360
1515
|
|
|
1361
|
-
<div class="lightbox" id="lightbox"
|
|
1516
|
+
<div class="lightbox" id="lightbox" data-lightbox>
|
|
1362
1517
|
<img id="lightboxImg" src="" alt="Screenshot" />
|
|
1363
1518
|
</div>
|
|
1364
1519
|
|
|
1365
1520
|
<script>
|
|
1366
|
-
function
|
|
1367
|
-
|
|
1521
|
+
(function(){
|
|
1522
|
+
var activeFilter=null;
|
|
1523
|
+
function filterByStatus(s){
|
|
1524
|
+
if(activeFilter===s){clearFilter();return}
|
|
1525
|
+
activeFilter=s;
|
|
1526
|
+
document.querySelectorAll('.stat-card').forEach(function(c){c.classList.toggle('active-filter',c.getAttribute('data-filter')===s)});
|
|
1527
|
+
document.getElementById('filterBar').classList.add('visible');
|
|
1528
|
+
document.getElementById('filterLabel').textContent=s.charAt(0).toUpperCase()+s.slice(1);
|
|
1529
|
+
document.querySelectorAll('.test-row').forEach(function(r){r.classList.toggle('hidden-by-filter',r.getAttribute('data-status')!==s)});
|
|
1530
|
+
document.querySelectorAll('.file-group').forEach(function(g){
|
|
1531
|
+
var vis=g.querySelectorAll('.test-row:not(.hidden-by-filter)');
|
|
1532
|
+
g.classList.toggle('hidden',vis.length===0);
|
|
1533
|
+
g.classList.remove('collapsed');
|
|
1534
|
+
});
|
|
1535
|
+
}
|
|
1536
|
+
function clearFilter(){
|
|
1537
|
+
activeFilter=null;
|
|
1538
|
+
document.querySelectorAll('.stat-card').forEach(function(c){c.classList.remove('active-filter')});
|
|
1539
|
+
document.getElementById('filterBar').classList.remove('visible');
|
|
1540
|
+
document.querySelectorAll('.test-row').forEach(function(r){r.classList.remove('hidden-by-filter')});
|
|
1541
|
+
document.querySelectorAll('.file-group').forEach(function(g){g.classList.remove('hidden')});
|
|
1542
|
+
}
|
|
1543
|
+
function closeLightbox(){document.getElementById('lightbox').classList.remove('open')}
|
|
1368
1544
|
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1545
|
+
document.addEventListener('click',function(e){
|
|
1546
|
+
// Close lightbox when clicking its backdrop
|
|
1547
|
+
if(e.target.closest('[data-lightbox]')&&!e.target.closest('#lightboxImg')){closeLightbox();return}
|
|
1548
|
+
// Open lightbox for screenshot images
|
|
1549
|
+
var img=e.target.closest('.screenshot');
|
|
1550
|
+
if(img){
|
|
1551
|
+
e.preventDefault();e.stopPropagation();
|
|
1552
|
+
var src=img.closest('a')?img.closest('a').href:img.src;
|
|
1553
|
+
document.getElementById('lightboxImg').src=src;
|
|
1554
|
+
document.getElementById('lightbox').classList.add('open');
|
|
1555
|
+
return;
|
|
1556
|
+
}
|
|
1557
|
+
// Copy button
|
|
1558
|
+
var copyBtn=e.target.closest('[data-copy]');
|
|
1559
|
+
if(copyBtn){
|
|
1560
|
+
var el=document.getElementById(copyBtn.getAttribute('data-copy'));
|
|
1561
|
+
if(el){navigator.clipboard.writeText(el.textContent);copyBtn.textContent='Copied!';setTimeout(function(){copyBtn.textContent='Copy'},1500)}
|
|
1562
|
+
return;
|
|
1563
|
+
}
|
|
1564
|
+
// Stat card filter
|
|
1565
|
+
var card=e.target.closest('.stat-card[data-filter]');
|
|
1566
|
+
if(card){filterByStatus(card.getAttribute('data-filter'));return}
|
|
1567
|
+
// Clear filter
|
|
1568
|
+
if(e.target.closest('[data-clear-filter]')){clearFilter();return}
|
|
1569
|
+
// Filmstrip step expand (skip if clicking a link inside)
|
|
1570
|
+
var step=e.target.closest('.filmstrip-step.expandable');
|
|
1571
|
+
if(step&&!e.target.closest('a')){step.classList.toggle('open');return}
|
|
1572
|
+
// File group collapse/expand
|
|
1573
|
+
var fileHeader=e.target.closest('[data-toggle-file-group]');
|
|
1574
|
+
if(fileHeader){fileHeader.closest('.file-group').classList.toggle('collapsed');return}
|
|
1575
|
+
// Test row expand
|
|
1576
|
+
var row=e.target.closest('.test-row[data-detail]');
|
|
1577
|
+
if(row&&!e.target.closest('.test-detail')){row.classList.toggle('expanded');return}
|
|
1381
1578
|
});
|
|
1382
|
-
}
|
|
1383
|
-
function clearFilter(){
|
|
1384
|
-
activeFilter=null;
|
|
1385
|
-
document.querySelectorAll('.stat-card').forEach(function(c){c.classList.remove('active-filter')});
|
|
1386
|
-
document.getElementById('filterBar').classList.remove('visible');
|
|
1387
|
-
document.querySelectorAll('.test-row').forEach(function(r){r.classList.remove('hidden-by-filter')});
|
|
1388
|
-
document.querySelectorAll('.file-group').forEach(function(g){g.classList.remove('hidden')});
|
|
1389
|
-
}
|
|
1390
1579
|
|
|
1391
|
-
|
|
1392
|
-
document.addEventListener('click',function(e){
|
|
1393
|
-
var img=e.target.closest('.screenshot');
|
|
1394
|
-
if(img){
|
|
1395
|
-
e.preventDefault();e.stopPropagation();
|
|
1396
|
-
var src=img.closest('a')?img.closest('a').href:img.src;
|
|
1397
|
-
document.getElementById('lightboxImg').src=src;
|
|
1398
|
-
document.getElementById('lightbox').classList.add('open');
|
|
1399
|
-
}
|
|
1400
|
-
});
|
|
1401
|
-
function closeLightbox(){document.getElementById('lightbox').classList.remove('open')}
|
|
1402
|
-
document.addEventListener('keydown',function(e){if(e.key==='Escape'){closeLightbox();clearFilter()}});
|
|
1580
|
+
document.addEventListener('keydown',function(e){if(e.key==='Escape'){closeLightbox();clearFilter()}});
|
|
1403
1581
|
|
|
1404
|
-
// Auto-expand failed/timedout/interrupted/healed tests
|
|
1405
|
-
document.querySelectorAll('.test-row.failed,.test-row.timedout,.test-row.interrupted,.test-row.healed').forEach(function(r){r.classList.add('expanded')});
|
|
1582
|
+
// Auto-expand failed/timedout/interrupted/healed tests
|
|
1583
|
+
document.querySelectorAll('.test-row.failed,.test-row.timedout,.test-row.interrupted,.test-row.healed').forEach(function(r){r.classList.add('expanded')});
|
|
1584
|
+
})();
|
|
1406
1585
|
</script>
|
|
1407
1586
|
</body>
|
|
1408
1587
|
</html>`;
|
|
@@ -1410,69 +1589,73 @@ document.querySelectorAll('.test-row.failed,.test-row.timedout,.test-row.interru
|
|
|
1410
1589
|
// ---------------------------------------------------------------------------
|
|
1411
1590
|
// Main
|
|
1412
1591
|
// ---------------------------------------------------------------------------
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
if (
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
// merged reports stored the initial-playwright-report.json copy inside the
|
|
1426
|
-
// triage run directory).
|
|
1427
|
-
if (!resolvedTriageDir && jsonData.metadata?.sources?.initial) {
|
|
1428
|
-
const initialPath = jsonData.metadata.sources.initial;
|
|
1429
|
-
// Try the absolute path first (works when running in the same env as CI)
|
|
1430
|
-
const candidate = (0, path_1.dirname)(initialPath);
|
|
1431
|
-
if ((0, fs_1.existsSync)(candidate) && (0, path_1.basename)(candidate) !== '.') {
|
|
1432
|
-
resolvedTriageDir = candidate;
|
|
1592
|
+
// Only run CLI logic when this file is executed directly (not imported as a module).
|
|
1593
|
+
if (require.main === module) {
|
|
1594
|
+
try {
|
|
1595
|
+
const { jsonData, outputFile, triageDir, inputFile } = parseArgs();
|
|
1596
|
+
// Auto-discover triage dir from merged report metadata if not explicitly given
|
|
1597
|
+
let resolvedTriageDir = triageDir;
|
|
1598
|
+
// Prefer the explicit triageRunDir recorded during the merge.
|
|
1599
|
+
if (!resolvedTriageDir && jsonData.metadata?.triageRunDir) {
|
|
1600
|
+
const candidate = jsonData.metadata.triageRunDir;
|
|
1601
|
+
if ((0, fs_1.existsSync)(candidate)) {
|
|
1602
|
+
resolvedTriageDir = candidate;
|
|
1603
|
+
}
|
|
1433
1604
|
}
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
const
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1605
|
+
// Legacy fallback: derive triage dir from the initial report path (older
|
|
1606
|
+
// merged reports stored the initial-playwright-report.json copy inside the
|
|
1607
|
+
// triage run directory).
|
|
1608
|
+
if (!resolvedTriageDir && jsonData.metadata?.sources?.initial) {
|
|
1609
|
+
const initialPath = jsonData.metadata.sources.initial;
|
|
1610
|
+
// Try the absolute path first (works when running in the same env as CI)
|
|
1611
|
+
const candidate = (0, path_1.dirname)(initialPath);
|
|
1612
|
+
if ((0, fs_1.existsSync)(candidate) && (0, path_1.basename)(candidate) !== '.') {
|
|
1613
|
+
resolvedTriageDir = candidate;
|
|
1614
|
+
}
|
|
1615
|
+
else {
|
|
1616
|
+
// Fallback: extract the donobu-triage/... suffix and resolve relative
|
|
1617
|
+
// to the input file. This handles downloaded CI artifacts where the
|
|
1618
|
+
// absolute CI path no longer exists.
|
|
1619
|
+
const triageMatch = initialPath.match(/donobu-triage[/\\][^/\\]+[/\\]?.*$/);
|
|
1620
|
+
if (triageMatch && inputFile) {
|
|
1621
|
+
const localCandidate = (0, path_1.resolve)((0, path_1.dirname)((0, path_1.resolve)(inputFile)), (0, path_1.dirname)(triageMatch[0]));
|
|
1622
|
+
if ((0, fs_1.existsSync)(localCandidate)) {
|
|
1623
|
+
resolvedTriageDir = localCandidate;
|
|
1624
|
+
}
|
|
1443
1625
|
}
|
|
1444
1626
|
}
|
|
1445
1627
|
}
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
.
|
|
1456
|
-
|
|
1457
|
-
|
|
1628
|
+
// Last resort: scan for donobu-triage dirs next to the input file
|
|
1629
|
+
if (!resolvedTriageDir && inputFile) {
|
|
1630
|
+
const triageRoot = (0, path_1.resolve)((0, path_1.dirname)((0, path_1.resolve)(inputFile)), 'donobu-triage');
|
|
1631
|
+
if ((0, fs_1.existsSync)(triageRoot)) {
|
|
1632
|
+
// Use the most recent triage run directory
|
|
1633
|
+
const subdirs = (0, fs_1.readdirSync)(triageRoot)
|
|
1634
|
+
.filter((d) => (0, fs_1.existsSync)((0, path_1.resolve)(triageRoot, d, '.')))
|
|
1635
|
+
.sort()
|
|
1636
|
+
.reverse();
|
|
1637
|
+
if (subdirs.length > 0) {
|
|
1638
|
+
resolvedTriageDir = (0, path_1.resolve)(triageRoot, subdirs[0]);
|
|
1639
|
+
}
|
|
1458
1640
|
}
|
|
1459
1641
|
}
|
|
1642
|
+
const triage = resolvedTriageDir
|
|
1643
|
+
? loadTriageData(resolvedTriageDir)
|
|
1644
|
+
: { plans: [], evidence: [] };
|
|
1645
|
+
const outputDir = outputFile ? (0, path_1.dirname)((0, path_1.resolve)(outputFile)) : null;
|
|
1646
|
+
const html = generateHtml(jsonData, triage, outputDir);
|
|
1647
|
+
if (outputFile) {
|
|
1648
|
+
(0, fs_1.writeFileSync)(outputFile, html, 'utf8');
|
|
1649
|
+
console.error(`Report written to ${outputFile}`);
|
|
1650
|
+
}
|
|
1651
|
+
else {
|
|
1652
|
+
console.log(html);
|
|
1653
|
+
}
|
|
1460
1654
|
}
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
const outputDir = outputFile ? (0, path_1.dirname)((0, path_1.resolve)(outputFile)) : null;
|
|
1465
|
-
const html = generateHtml(jsonData, triage, outputDir);
|
|
1466
|
-
if (outputFile) {
|
|
1467
|
-
(0, fs_1.writeFileSync)(outputFile, html, 'utf8');
|
|
1468
|
-
console.error(`Report written to ${outputFile}`);
|
|
1469
|
-
}
|
|
1470
|
-
else {
|
|
1471
|
-
console.log(html);
|
|
1655
|
+
catch (error) {
|
|
1656
|
+
console.error('Error processing JSON:', error.message);
|
|
1657
|
+
process.exit(1);
|
|
1472
1658
|
}
|
|
1473
1659
|
}
|
|
1474
|
-
|
|
1475
|
-
console.error('Error processing JSON:', error.message);
|
|
1476
|
-
process.exit(1);
|
|
1477
|
-
}
|
|
1660
|
+
// end of CLI guard
|
|
1478
1661
|
//# sourceMappingURL=playwright-json-to-html.js.map
|