donobu 5.32.0 → 5.33.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.
|
@@ -451,15 +451,25 @@ exports.test = test_1.test.extend({
|
|
|
451
451
|
* 3. `_steps` is fully populated by the time `finalizeTest()` runs (the
|
|
452
452
|
* test body has already completed).
|
|
453
453
|
*/
|
|
454
|
-
function collectNativeSteps(rawSteps) {
|
|
454
|
+
function collectNativeSteps(rawSteps, startTimes) {
|
|
455
455
|
const result = [];
|
|
456
456
|
for (const step of rawSteps) {
|
|
457
457
|
const cat = step.category ?? '';
|
|
458
|
+
const childRaw = Array.isArray(step.steps) ? step.steps : [];
|
|
459
|
+
const collectedChildren = collectNativeSteps(childRaw, startTimes);
|
|
458
460
|
if (cat === 'expect' || cat === 'test.step') {
|
|
461
|
+
// Start time comes from the onStepBegin payload (captured by the
|
|
462
|
+
// pwApiStepLogger fixture into startTimes). If unavailable, fall
|
|
463
|
+
// back to the step's endWallTime so the window is zero-width and
|
|
464
|
+
// sibling steps don't get falsely nested inside.
|
|
465
|
+
const endWallTime = step.endWallTime ?? Date.now();
|
|
466
|
+
const startWallTime = (typeof step.stepId === 'string' && startTimes.get(step.stepId)) ||
|
|
467
|
+
endWallTime;
|
|
459
468
|
result.push({
|
|
460
469
|
title: step.title ?? '',
|
|
461
470
|
category: cat,
|
|
462
|
-
|
|
471
|
+
startWallTime,
|
|
472
|
+
endWallTime,
|
|
463
473
|
passed: !step.error,
|
|
464
474
|
error: step.error
|
|
465
475
|
? { message: step.error.message, stack: step.error.stack }
|
|
@@ -471,15 +481,16 @@ function collectNativeSteps(rawSteps) {
|
|
|
471
481
|
column: step.location.column ?? 0,
|
|
472
482
|
}
|
|
473
483
|
: undefined,
|
|
484
|
+
children: collectedChildren,
|
|
474
485
|
});
|
|
475
486
|
}
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
result.push(...
|
|
487
|
+
else {
|
|
488
|
+
// Parent is not a kept category (e.g. pw:api) — promote any qualifying
|
|
489
|
+
// descendants so a nested expect() still appears in the report.
|
|
490
|
+
result.push(...collectedChildren);
|
|
480
491
|
}
|
|
481
492
|
}
|
|
482
|
-
result.sort((a, b) => a.
|
|
493
|
+
result.sort((a, b) => a.startWallTime - b.startWallTime);
|
|
483
494
|
return result;
|
|
484
495
|
}
|
|
485
496
|
// ---------------------------------------------------------------------------
|
|
@@ -689,9 +700,20 @@ function installPlaywrightStepLogger(testInfo) {
|
|
|
689
700
|
const originalOnStepEnd = callbacks && typeof callbacks.onStepEnd === 'function'
|
|
690
701
|
? callbacks.onStepEnd
|
|
691
702
|
: null;
|
|
703
|
+
const originalOnStepBegin = callbacks && typeof callbacks.onStepBegin === 'function'
|
|
704
|
+
? callbacks.onStepBegin
|
|
705
|
+
: null;
|
|
692
706
|
if (!callbacks || !originalOnStepEnd || !stepMap) {
|
|
693
707
|
return () => { };
|
|
694
708
|
}
|
|
709
|
+
// Stash a stepId -> wallTime map on testInfo so collectNativeSteps can
|
|
710
|
+
// look up start times when building the report hierarchy. Playwright
|
|
711
|
+
// doesn't store wallTime on the step object itself — it's only emitted
|
|
712
|
+
// via the onStepBegin payload — so we capture it here. Without this,
|
|
713
|
+
// `test.step` blocks have no recoverable start time and any preceding
|
|
714
|
+
// tool-call step gets falsely nested inside them.
|
|
715
|
+
const startTimes = new Map();
|
|
716
|
+
ti.__donobuStepStartTimes = startTimes;
|
|
695
717
|
let installed = false;
|
|
696
718
|
try {
|
|
697
719
|
callbacks.onStepEnd = function patchedOnStepEnd(payload) {
|
|
@@ -710,6 +732,21 @@ function installPlaywrightStepLogger(testInfo) {
|
|
|
710
732
|
}
|
|
711
733
|
return ret;
|
|
712
734
|
};
|
|
735
|
+
callbacks.onStepBegin = function patchedOnStepBegin(payload) {
|
|
736
|
+
const ret = originalOnStepBegin
|
|
737
|
+
? originalOnStepBegin.call(this, payload)
|
|
738
|
+
: undefined;
|
|
739
|
+
try {
|
|
740
|
+
if (typeof payload?.stepId === 'string' &&
|
|
741
|
+
typeof payload?.wallTime === 'number') {
|
|
742
|
+
startTimes.set(payload.stepId, payload.wallTime);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
catch (err) {
|
|
746
|
+
Logger_1.appLogger.debug('Failed to record Playwright step start time', err);
|
|
747
|
+
}
|
|
748
|
+
return ret;
|
|
749
|
+
};
|
|
713
750
|
installed = true;
|
|
714
751
|
}
|
|
715
752
|
catch (err) {
|
|
@@ -718,6 +755,9 @@ function installPlaywrightStepLogger(testInfo) {
|
|
|
718
755
|
return () => {
|
|
719
756
|
if (installed) {
|
|
720
757
|
callbacks.onStepEnd = originalOnStepEnd;
|
|
758
|
+
if (originalOnStepBegin) {
|
|
759
|
+
callbacks.onStepBegin = originalOnStepBegin;
|
|
760
|
+
}
|
|
721
761
|
}
|
|
722
762
|
};
|
|
723
763
|
}
|
|
@@ -825,7 +865,8 @@ async function finalizeTest(page, testInfo, logBuffer, videoOption) {
|
|
|
825
865
|
// Attach native Playwright steps (expect assertions, test.step blocks)
|
|
826
866
|
// so the HTML report can show a unified timeline alongside AI tool calls.
|
|
827
867
|
try {
|
|
828
|
-
const
|
|
868
|
+
const startTimes = testInfo.__donobuStepStartTimes ?? new Map();
|
|
869
|
+
const nativeSteps = collectNativeSteps(testInfo._steps ?? [], startTimes);
|
|
829
870
|
if (nativeSteps.length > 0) {
|
|
830
871
|
await testInfo.attach('donobu-native-steps', {
|
|
831
872
|
body: JSON.stringify(nativeSteps),
|
|
@@ -562,7 +562,7 @@ function renderErrors(errors) {
|
|
|
562
562
|
}
|
|
563
563
|
return html;
|
|
564
564
|
}
|
|
565
|
-
function renderNativeStep(ns) {
|
|
565
|
+
function renderNativeStep(ns, childrenHtml) {
|
|
566
566
|
const statusIcon = ns.passed
|
|
567
567
|
? '<span class="step-status-ok">✓</span>'
|
|
568
568
|
: '<span class="step-status-fail">✗</span>';
|
|
@@ -573,7 +573,8 @@ function renderNativeStep(ns) {
|
|
|
573
573
|
const snippet = ns.location?.file
|
|
574
574
|
? readSourceSnippet(ns.location.file, ns.location.line)
|
|
575
575
|
: null;
|
|
576
|
-
const
|
|
576
|
+
const hasError = !ns.passed && !!ns.error?.message;
|
|
577
|
+
const hasBody = !!snippet || hasError || !!childrenHtml;
|
|
577
578
|
const renderHeader = (tag) => {
|
|
578
579
|
let header = `<${tag} class="filmstrip-header">`;
|
|
579
580
|
header += statusIcon;
|
|
@@ -592,17 +593,23 @@ function renderNativeStep(ns) {
|
|
|
592
593
|
if (!hasBody) {
|
|
593
594
|
return `<div class="filmstrip-step native-step">${renderHeader('div')}</div>`;
|
|
594
595
|
}
|
|
595
|
-
//
|
|
596
|
-
//
|
|
596
|
+
// Failures always render expanded so the error is immediately visible.
|
|
597
|
+
// test.step blocks with nested content also default open so users see
|
|
598
|
+
// what's inside; bare passing expects with just a snippet collapse to
|
|
599
|
+
// keep tests with many assertions scannable.
|
|
600
|
+
const defaultOpen = !ns.passed || (ns.category === 'test.step' && !!childrenHtml);
|
|
597
601
|
const passClass = ns.passed ? 'native-step--passed' : 'native-step--failed';
|
|
598
|
-
let html = `<details class="filmstrip-step native-step ${passClass}"${
|
|
602
|
+
let html = `<details class="filmstrip-step native-step ${passClass}"${defaultOpen ? ' open' : ''}>`;
|
|
599
603
|
html += renderHeader('summary');
|
|
600
|
-
if (
|
|
604
|
+
if (hasError) {
|
|
601
605
|
html += `<pre class="native-step-error">${ansiToHtml(ns.error.message)}</pre>`;
|
|
602
606
|
}
|
|
603
607
|
if (snippet) {
|
|
604
608
|
html += snippet;
|
|
605
609
|
}
|
|
610
|
+
if (childrenHtml) {
|
|
611
|
+
html += childrenHtml;
|
|
612
|
+
}
|
|
606
613
|
html += `</details>`;
|
|
607
614
|
return html;
|
|
608
615
|
}
|
|
@@ -853,31 +860,71 @@ function renderSteps(steps, stepScreenshots, nativeSteps, outputDir) {
|
|
|
853
860
|
return '';
|
|
854
861
|
}
|
|
855
862
|
if (hasScreenshots || hasNative) {
|
|
856
|
-
const
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
863
|
+
const buildNativeTree = (nss) => nss.map((ns) => ({
|
|
864
|
+
kind: 'native',
|
|
865
|
+
ns,
|
|
866
|
+
t: ns.startWallTime,
|
|
867
|
+
tEnd: ns.endWallTime,
|
|
868
|
+
children: buildNativeTree(ns.children),
|
|
869
|
+
}));
|
|
870
|
+
const roots = buildNativeTree(nativeSteps);
|
|
871
|
+
// Place each Donobu screenshot under the deepest native step whose
|
|
872
|
+
// [start, end] window contains it. Falls back to top level if none.
|
|
873
|
+
const placeDonobu = (nodes, d) => {
|
|
874
|
+
for (const n of nodes) {
|
|
875
|
+
if (n.kind !== 'native') {
|
|
876
|
+
continue;
|
|
877
|
+
}
|
|
878
|
+
if (d.ss.startedAt >= n.t && d.ss.completedAt <= n.tEnd) {
|
|
879
|
+
if (!placeDonobu(n.children, d)) {
|
|
880
|
+
n.children.push(d);
|
|
881
|
+
}
|
|
882
|
+
return true;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
return false;
|
|
886
|
+
};
|
|
887
|
+
for (const ss of stepScreenshots) {
|
|
888
|
+
const d = { kind: 'donobu', ss, t: ss.startedAt };
|
|
889
|
+
if (!placeDonobu(roots, d)) {
|
|
890
|
+
roots.push(d);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
const sortTree = (nodes) => {
|
|
894
|
+
nodes.sort((a, b) => a.t - b.t);
|
|
895
|
+
for (const n of nodes) {
|
|
896
|
+
if (n.kind === 'native') {
|
|
897
|
+
sortTree(n.children);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
};
|
|
901
|
+
sortTree(roots);
|
|
902
|
+
const countNodes = (nodes) => {
|
|
903
|
+
let c = 0;
|
|
904
|
+
for (const n of nodes) {
|
|
905
|
+
c += 1;
|
|
906
|
+
if (n.kind === 'native') {
|
|
907
|
+
c += countNodes(n.children);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
return c;
|
|
911
|
+
};
|
|
912
|
+
const renderNode = (node) => {
|
|
913
|
+
if (node.kind === 'donobu') {
|
|
914
|
+
return renderFilmstripStep(node.ss, outputDir);
|
|
915
|
+
}
|
|
916
|
+
const childrenHtml = node.children.length > 0
|
|
917
|
+
? `<div class="native-step-children">${node.children.map(renderNode).join('')}</div>`
|
|
918
|
+
: '';
|
|
919
|
+
return renderNativeStep(node.ns, childrenHtml);
|
|
920
|
+
};
|
|
921
|
+
const stepCount = countNodes(roots);
|
|
870
922
|
let html = '<details class="steps-section"><summary>Steps (' +
|
|
871
923
|
stepCount +
|
|
872
924
|
')</summary>';
|
|
873
925
|
html += '<div class="step-filmstrip">';
|
|
874
|
-
for (const
|
|
875
|
-
|
|
876
|
-
html += renderFilmstripStep(entry.ss, outputDir);
|
|
877
|
-
}
|
|
878
|
-
else {
|
|
879
|
-
html += renderNativeStep(entry.ns);
|
|
880
|
-
}
|
|
926
|
+
for (const node of roots) {
|
|
927
|
+
html += renderNode(node);
|
|
881
928
|
}
|
|
882
929
|
html += '</div>';
|
|
883
930
|
html += '</details>';
|
|
@@ -1610,6 +1657,8 @@ details.native-step>summary::-webkit-details-marker{display:none}
|
|
|
1610
1657
|
details.native-step[open]>summary .native-step-chevron{transform:rotate(90deg)}
|
|
1611
1658
|
.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)}
|
|
1612
1659
|
.native-step-snippet{font-size:11px;font-family:var(--mono);margin:4px 0 2px 22px;overflow:hidden}
|
|
1660
|
+
.native-step-children{display:flex;flex-direction:column;margin:4px 0 0 10px;border-left:1px solid var(--border-subtle);padding-left:8px}
|
|
1661
|
+
.native-step-children>.filmstrip-step{padding-left:8px}
|
|
1613
1662
|
.snippet-line{display:flex;padding:1px 8px;white-space:pre}
|
|
1614
1663
|
.snippet-line--target{background:rgba(239,68,68,.10)}
|
|
1615
1664
|
.snippet-linenum{color:var(--text-dim);min-width:40px;user-select:none}
|
|
@@ -451,15 +451,25 @@ exports.test = test_1.test.extend({
|
|
|
451
451
|
* 3. `_steps` is fully populated by the time `finalizeTest()` runs (the
|
|
452
452
|
* test body has already completed).
|
|
453
453
|
*/
|
|
454
|
-
function collectNativeSteps(rawSteps) {
|
|
454
|
+
function collectNativeSteps(rawSteps, startTimes) {
|
|
455
455
|
const result = [];
|
|
456
456
|
for (const step of rawSteps) {
|
|
457
457
|
const cat = step.category ?? '';
|
|
458
|
+
const childRaw = Array.isArray(step.steps) ? step.steps : [];
|
|
459
|
+
const collectedChildren = collectNativeSteps(childRaw, startTimes);
|
|
458
460
|
if (cat === 'expect' || cat === 'test.step') {
|
|
461
|
+
// Start time comes from the onStepBegin payload (captured by the
|
|
462
|
+
// pwApiStepLogger fixture into startTimes). If unavailable, fall
|
|
463
|
+
// back to the step's endWallTime so the window is zero-width and
|
|
464
|
+
// sibling steps don't get falsely nested inside.
|
|
465
|
+
const endWallTime = step.endWallTime ?? Date.now();
|
|
466
|
+
const startWallTime = (typeof step.stepId === 'string' && startTimes.get(step.stepId)) ||
|
|
467
|
+
endWallTime;
|
|
459
468
|
result.push({
|
|
460
469
|
title: step.title ?? '',
|
|
461
470
|
category: cat,
|
|
462
|
-
|
|
471
|
+
startWallTime,
|
|
472
|
+
endWallTime,
|
|
463
473
|
passed: !step.error,
|
|
464
474
|
error: step.error
|
|
465
475
|
? { message: step.error.message, stack: step.error.stack }
|
|
@@ -471,15 +481,16 @@ function collectNativeSteps(rawSteps) {
|
|
|
471
481
|
column: step.location.column ?? 0,
|
|
472
482
|
}
|
|
473
483
|
: undefined,
|
|
484
|
+
children: collectedChildren,
|
|
474
485
|
});
|
|
475
486
|
}
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
result.push(...
|
|
487
|
+
else {
|
|
488
|
+
// Parent is not a kept category (e.g. pw:api) — promote any qualifying
|
|
489
|
+
// descendants so a nested expect() still appears in the report.
|
|
490
|
+
result.push(...collectedChildren);
|
|
480
491
|
}
|
|
481
492
|
}
|
|
482
|
-
result.sort((a, b) => a.
|
|
493
|
+
result.sort((a, b) => a.startWallTime - b.startWallTime);
|
|
483
494
|
return result;
|
|
484
495
|
}
|
|
485
496
|
// ---------------------------------------------------------------------------
|
|
@@ -689,9 +700,20 @@ function installPlaywrightStepLogger(testInfo) {
|
|
|
689
700
|
const originalOnStepEnd = callbacks && typeof callbacks.onStepEnd === 'function'
|
|
690
701
|
? callbacks.onStepEnd
|
|
691
702
|
: null;
|
|
703
|
+
const originalOnStepBegin = callbacks && typeof callbacks.onStepBegin === 'function'
|
|
704
|
+
? callbacks.onStepBegin
|
|
705
|
+
: null;
|
|
692
706
|
if (!callbacks || !originalOnStepEnd || !stepMap) {
|
|
693
707
|
return () => { };
|
|
694
708
|
}
|
|
709
|
+
// Stash a stepId -> wallTime map on testInfo so collectNativeSteps can
|
|
710
|
+
// look up start times when building the report hierarchy. Playwright
|
|
711
|
+
// doesn't store wallTime on the step object itself — it's only emitted
|
|
712
|
+
// via the onStepBegin payload — so we capture it here. Without this,
|
|
713
|
+
// `test.step` blocks have no recoverable start time and any preceding
|
|
714
|
+
// tool-call step gets falsely nested inside them.
|
|
715
|
+
const startTimes = new Map();
|
|
716
|
+
ti.__donobuStepStartTimes = startTimes;
|
|
695
717
|
let installed = false;
|
|
696
718
|
try {
|
|
697
719
|
callbacks.onStepEnd = function patchedOnStepEnd(payload) {
|
|
@@ -710,6 +732,21 @@ function installPlaywrightStepLogger(testInfo) {
|
|
|
710
732
|
}
|
|
711
733
|
return ret;
|
|
712
734
|
};
|
|
735
|
+
callbacks.onStepBegin = function patchedOnStepBegin(payload) {
|
|
736
|
+
const ret = originalOnStepBegin
|
|
737
|
+
? originalOnStepBegin.call(this, payload)
|
|
738
|
+
: undefined;
|
|
739
|
+
try {
|
|
740
|
+
if (typeof payload?.stepId === 'string' &&
|
|
741
|
+
typeof payload?.wallTime === 'number') {
|
|
742
|
+
startTimes.set(payload.stepId, payload.wallTime);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
catch (err) {
|
|
746
|
+
Logger_1.appLogger.debug('Failed to record Playwright step start time', err);
|
|
747
|
+
}
|
|
748
|
+
return ret;
|
|
749
|
+
};
|
|
713
750
|
installed = true;
|
|
714
751
|
}
|
|
715
752
|
catch (err) {
|
|
@@ -718,6 +755,9 @@ function installPlaywrightStepLogger(testInfo) {
|
|
|
718
755
|
return () => {
|
|
719
756
|
if (installed) {
|
|
720
757
|
callbacks.onStepEnd = originalOnStepEnd;
|
|
758
|
+
if (originalOnStepBegin) {
|
|
759
|
+
callbacks.onStepBegin = originalOnStepBegin;
|
|
760
|
+
}
|
|
721
761
|
}
|
|
722
762
|
};
|
|
723
763
|
}
|
|
@@ -825,7 +865,8 @@ async function finalizeTest(page, testInfo, logBuffer, videoOption) {
|
|
|
825
865
|
// Attach native Playwright steps (expect assertions, test.step blocks)
|
|
826
866
|
// so the HTML report can show a unified timeline alongside AI tool calls.
|
|
827
867
|
try {
|
|
828
|
-
const
|
|
868
|
+
const startTimes = testInfo.__donobuStepStartTimes ?? new Map();
|
|
869
|
+
const nativeSteps = collectNativeSteps(testInfo._steps ?? [], startTimes);
|
|
829
870
|
if (nativeSteps.length > 0) {
|
|
830
871
|
await testInfo.attach('donobu-native-steps', {
|
|
831
872
|
body: JSON.stringify(nativeSteps),
|
package/dist/reporter/render.js
CHANGED
|
@@ -562,7 +562,7 @@ function renderErrors(errors) {
|
|
|
562
562
|
}
|
|
563
563
|
return html;
|
|
564
564
|
}
|
|
565
|
-
function renderNativeStep(ns) {
|
|
565
|
+
function renderNativeStep(ns, childrenHtml) {
|
|
566
566
|
const statusIcon = ns.passed
|
|
567
567
|
? '<span class="step-status-ok">✓</span>'
|
|
568
568
|
: '<span class="step-status-fail">✗</span>';
|
|
@@ -573,7 +573,8 @@ function renderNativeStep(ns) {
|
|
|
573
573
|
const snippet = ns.location?.file
|
|
574
574
|
? readSourceSnippet(ns.location.file, ns.location.line)
|
|
575
575
|
: null;
|
|
576
|
-
const
|
|
576
|
+
const hasError = !ns.passed && !!ns.error?.message;
|
|
577
|
+
const hasBody = !!snippet || hasError || !!childrenHtml;
|
|
577
578
|
const renderHeader = (tag) => {
|
|
578
579
|
let header = `<${tag} class="filmstrip-header">`;
|
|
579
580
|
header += statusIcon;
|
|
@@ -592,17 +593,23 @@ function renderNativeStep(ns) {
|
|
|
592
593
|
if (!hasBody) {
|
|
593
594
|
return `<div class="filmstrip-step native-step">${renderHeader('div')}</div>`;
|
|
594
595
|
}
|
|
595
|
-
//
|
|
596
|
-
//
|
|
596
|
+
// Failures always render expanded so the error is immediately visible.
|
|
597
|
+
// test.step blocks with nested content also default open so users see
|
|
598
|
+
// what's inside; bare passing expects with just a snippet collapse to
|
|
599
|
+
// keep tests with many assertions scannable.
|
|
600
|
+
const defaultOpen = !ns.passed || (ns.category === 'test.step' && !!childrenHtml);
|
|
597
601
|
const passClass = ns.passed ? 'native-step--passed' : 'native-step--failed';
|
|
598
|
-
let html = `<details class="filmstrip-step native-step ${passClass}"${
|
|
602
|
+
let html = `<details class="filmstrip-step native-step ${passClass}"${defaultOpen ? ' open' : ''}>`;
|
|
599
603
|
html += renderHeader('summary');
|
|
600
|
-
if (
|
|
604
|
+
if (hasError) {
|
|
601
605
|
html += `<pre class="native-step-error">${ansiToHtml(ns.error.message)}</pre>`;
|
|
602
606
|
}
|
|
603
607
|
if (snippet) {
|
|
604
608
|
html += snippet;
|
|
605
609
|
}
|
|
610
|
+
if (childrenHtml) {
|
|
611
|
+
html += childrenHtml;
|
|
612
|
+
}
|
|
606
613
|
html += `</details>`;
|
|
607
614
|
return html;
|
|
608
615
|
}
|
|
@@ -853,31 +860,71 @@ function renderSteps(steps, stepScreenshots, nativeSteps, outputDir) {
|
|
|
853
860
|
return '';
|
|
854
861
|
}
|
|
855
862
|
if (hasScreenshots || hasNative) {
|
|
856
|
-
const
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
863
|
+
const buildNativeTree = (nss) => nss.map((ns) => ({
|
|
864
|
+
kind: 'native',
|
|
865
|
+
ns,
|
|
866
|
+
t: ns.startWallTime,
|
|
867
|
+
tEnd: ns.endWallTime,
|
|
868
|
+
children: buildNativeTree(ns.children),
|
|
869
|
+
}));
|
|
870
|
+
const roots = buildNativeTree(nativeSteps);
|
|
871
|
+
// Place each Donobu screenshot under the deepest native step whose
|
|
872
|
+
// [start, end] window contains it. Falls back to top level if none.
|
|
873
|
+
const placeDonobu = (nodes, d) => {
|
|
874
|
+
for (const n of nodes) {
|
|
875
|
+
if (n.kind !== 'native') {
|
|
876
|
+
continue;
|
|
877
|
+
}
|
|
878
|
+
if (d.ss.startedAt >= n.t && d.ss.completedAt <= n.tEnd) {
|
|
879
|
+
if (!placeDonobu(n.children, d)) {
|
|
880
|
+
n.children.push(d);
|
|
881
|
+
}
|
|
882
|
+
return true;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
return false;
|
|
886
|
+
};
|
|
887
|
+
for (const ss of stepScreenshots) {
|
|
888
|
+
const d = { kind: 'donobu', ss, t: ss.startedAt };
|
|
889
|
+
if (!placeDonobu(roots, d)) {
|
|
890
|
+
roots.push(d);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
const sortTree = (nodes) => {
|
|
894
|
+
nodes.sort((a, b) => a.t - b.t);
|
|
895
|
+
for (const n of nodes) {
|
|
896
|
+
if (n.kind === 'native') {
|
|
897
|
+
sortTree(n.children);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
};
|
|
901
|
+
sortTree(roots);
|
|
902
|
+
const countNodes = (nodes) => {
|
|
903
|
+
let c = 0;
|
|
904
|
+
for (const n of nodes) {
|
|
905
|
+
c += 1;
|
|
906
|
+
if (n.kind === 'native') {
|
|
907
|
+
c += countNodes(n.children);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
return c;
|
|
911
|
+
};
|
|
912
|
+
const renderNode = (node) => {
|
|
913
|
+
if (node.kind === 'donobu') {
|
|
914
|
+
return renderFilmstripStep(node.ss, outputDir);
|
|
915
|
+
}
|
|
916
|
+
const childrenHtml = node.children.length > 0
|
|
917
|
+
? `<div class="native-step-children">${node.children.map(renderNode).join('')}</div>`
|
|
918
|
+
: '';
|
|
919
|
+
return renderNativeStep(node.ns, childrenHtml);
|
|
920
|
+
};
|
|
921
|
+
const stepCount = countNodes(roots);
|
|
870
922
|
let html = '<details class="steps-section"><summary>Steps (' +
|
|
871
923
|
stepCount +
|
|
872
924
|
')</summary>';
|
|
873
925
|
html += '<div class="step-filmstrip">';
|
|
874
|
-
for (const
|
|
875
|
-
|
|
876
|
-
html += renderFilmstripStep(entry.ss, outputDir);
|
|
877
|
-
}
|
|
878
|
-
else {
|
|
879
|
-
html += renderNativeStep(entry.ns);
|
|
880
|
-
}
|
|
926
|
+
for (const node of roots) {
|
|
927
|
+
html += renderNode(node);
|
|
881
928
|
}
|
|
882
929
|
html += '</div>';
|
|
883
930
|
html += '</details>';
|
|
@@ -1610,6 +1657,8 @@ details.native-step>summary::-webkit-details-marker{display:none}
|
|
|
1610
1657
|
details.native-step[open]>summary .native-step-chevron{transform:rotate(90deg)}
|
|
1611
1658
|
.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)}
|
|
1612
1659
|
.native-step-snippet{font-size:11px;font-family:var(--mono);margin:4px 0 2px 22px;overflow:hidden}
|
|
1660
|
+
.native-step-children{display:flex;flex-direction:column;margin:4px 0 0 10px;border-left:1px solid var(--border-subtle);padding-left:8px}
|
|
1661
|
+
.native-step-children>.filmstrip-step{padding-left:8px}
|
|
1613
1662
|
.snippet-line{display:flex;padding:1px 8px;white-space:pre}
|
|
1614
1663
|
.snippet-line--target{background:rgba(239,68,68,.10)}
|
|
1615
1664
|
.snippet-linenum{color:var(--text-dim);min-width:40px;user-select:none}
|