executable-stories-formatters 0.3.0 → 0.4.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/adapters.d.cts +1 -1
- package/dist/adapters.d.ts +1 -1
- package/dist/cli.js +322 -4
- package/dist/cli.js.map +1 -1
- package/dist/{index-DCJ0NvAp.d.cts → index-DyeUWfYK.d.cts} +26 -1
- package/dist/{index-DCJ0NvAp.d.ts → index-DyeUWfYK.d.ts} +26 -1
- package/dist/index.cjs +324 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +324 -6
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OTel span types for trace waterfall rendering.
|
|
3
|
+
*
|
|
4
|
+
* Structurally compatible with autotel's SerializedSpan
|
|
5
|
+
* and raw OTel nanosecond formats. No import dependency on autotel.
|
|
6
|
+
*/
|
|
7
|
+
type OtelAttributeValue = string | number | boolean | string[] | number[] | boolean[];
|
|
8
|
+
interface OtelSpan {
|
|
9
|
+
spanId: string;
|
|
10
|
+
parentSpanId?: string;
|
|
11
|
+
name: string;
|
|
12
|
+
/** Preferred: epoch-based milliseconds (from autotel's SerializedSpan) */
|
|
13
|
+
startTimeMs?: number;
|
|
14
|
+
durationMs?: number;
|
|
15
|
+
/** Compatibility: raw OTel nanosecond timestamps */
|
|
16
|
+
startTimeUnixNano?: number;
|
|
17
|
+
endTimeUnixNano?: number;
|
|
18
|
+
status: "ok" | "error" | "unset";
|
|
19
|
+
statusMessage?: string;
|
|
20
|
+
attributes?: Record<string, OtelAttributeValue>;
|
|
21
|
+
}
|
|
22
|
+
|
|
1
23
|
/**
|
|
2
24
|
* Story types — the shared vocabulary for all framework adapters.
|
|
3
25
|
*
|
|
@@ -5,6 +27,7 @@
|
|
|
5
27
|
* They now live in formatters so every adapter can import them
|
|
6
28
|
* from the same place that defines RawRun (the output contract).
|
|
7
29
|
*/
|
|
30
|
+
|
|
8
31
|
/** BDD step keywords for scenario documentation */
|
|
9
32
|
type StepKeyword = "Given" | "When" | "Then" | "And" | "But";
|
|
10
33
|
/** Step execution mode for docs rendering */
|
|
@@ -103,6 +126,8 @@ interface StoryMeta {
|
|
|
103
126
|
docs?: DocEntry[];
|
|
104
127
|
/** Order in which story.init() was called (for source ordering) */
|
|
105
128
|
sourceOrder?: number;
|
|
129
|
+
/** OTel spans from autotel for trace waterfall rendering */
|
|
130
|
+
otelSpans?: OtelSpan[];
|
|
106
131
|
}
|
|
107
132
|
/** Key used to store StoryMeta in test metadata */
|
|
108
133
|
declare const STORY_META_KEY = "story";
|
|
@@ -375,4 +400,4 @@ interface PlaywrightAdapterOptions {
|
|
|
375
400
|
*/
|
|
376
401
|
declare function adaptPlaywrightRun(testResults: Array<[PlaywrightTestCase, PlaywrightTestResult]>, options?: PlaywrightAdapterOptions): RawRun;
|
|
377
402
|
|
|
378
|
-
export { type
|
|
403
|
+
export { type VitestState as A, type VitestTestCase as B, type VitestTestModule as C, type DocEntry as D, type VitestTestResult as E, type JestAdapterOptions as J, type OtelAttributeValue as O, type PlaywrightAdapterOptions as P, type RawStatus as R, type StoryMeta as S, type VitestAdapterOptions as V, type StoryStep as a, type RawAttachment as b, type RawRun as c, type RawCIInfo as d, adaptJestRun as e, adaptPlaywrightRun as f, adaptVitestRun as g, type DocPhase as h, type JestAggregatedResult as i, type JestFileResult as j, type JestTestResult as k, type OtelSpan as l, type PlaywrightAnnotation as m, type PlaywrightAttachment as n, type PlaywrightError as o, type PlaywrightLocation as p, type PlaywrightStatus as q, type PlaywrightTestCase as r, type PlaywrightTestResult as s, type RawStepEvent as t, type RawTestCase as u, STORY_META_KEY as v, type StepKeyword as w, type StepMode as x, type StoryFileReport as y, type VitestSerializedError as z };
|
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OTel span types for trace waterfall rendering.
|
|
3
|
+
*
|
|
4
|
+
* Structurally compatible with autotel's SerializedSpan
|
|
5
|
+
* and raw OTel nanosecond formats. No import dependency on autotel.
|
|
6
|
+
*/
|
|
7
|
+
type OtelAttributeValue = string | number | boolean | string[] | number[] | boolean[];
|
|
8
|
+
interface OtelSpan {
|
|
9
|
+
spanId: string;
|
|
10
|
+
parentSpanId?: string;
|
|
11
|
+
name: string;
|
|
12
|
+
/** Preferred: epoch-based milliseconds (from autotel's SerializedSpan) */
|
|
13
|
+
startTimeMs?: number;
|
|
14
|
+
durationMs?: number;
|
|
15
|
+
/** Compatibility: raw OTel nanosecond timestamps */
|
|
16
|
+
startTimeUnixNano?: number;
|
|
17
|
+
endTimeUnixNano?: number;
|
|
18
|
+
status: "ok" | "error" | "unset";
|
|
19
|
+
statusMessage?: string;
|
|
20
|
+
attributes?: Record<string, OtelAttributeValue>;
|
|
21
|
+
}
|
|
22
|
+
|
|
1
23
|
/**
|
|
2
24
|
* Story types — the shared vocabulary for all framework adapters.
|
|
3
25
|
*
|
|
@@ -5,6 +27,7 @@
|
|
|
5
27
|
* They now live in formatters so every adapter can import them
|
|
6
28
|
* from the same place that defines RawRun (the output contract).
|
|
7
29
|
*/
|
|
30
|
+
|
|
8
31
|
/** BDD step keywords for scenario documentation */
|
|
9
32
|
type StepKeyword = "Given" | "When" | "Then" | "And" | "But";
|
|
10
33
|
/** Step execution mode for docs rendering */
|
|
@@ -103,6 +126,8 @@ interface StoryMeta {
|
|
|
103
126
|
docs?: DocEntry[];
|
|
104
127
|
/** Order in which story.init() was called (for source ordering) */
|
|
105
128
|
sourceOrder?: number;
|
|
129
|
+
/** OTel spans from autotel for trace waterfall rendering */
|
|
130
|
+
otelSpans?: OtelSpan[];
|
|
106
131
|
}
|
|
107
132
|
/** Key used to store StoryMeta in test metadata */
|
|
108
133
|
declare const STORY_META_KEY = "story";
|
|
@@ -375,4 +400,4 @@ interface PlaywrightAdapterOptions {
|
|
|
375
400
|
*/
|
|
376
401
|
declare function adaptPlaywrightRun(testResults: Array<[PlaywrightTestCase, PlaywrightTestResult]>, options?: PlaywrightAdapterOptions): RawRun;
|
|
377
402
|
|
|
378
|
-
export { type
|
|
403
|
+
export { type VitestState as A, type VitestTestCase as B, type VitestTestModule as C, type DocEntry as D, type VitestTestResult as E, type JestAdapterOptions as J, type OtelAttributeValue as O, type PlaywrightAdapterOptions as P, type RawStatus as R, type StoryMeta as S, type VitestAdapterOptions as V, type StoryStep as a, type RawAttachment as b, type RawRun as c, type RawCIInfo as d, adaptJestRun as e, adaptPlaywrightRun as f, adaptVitestRun as g, type DocPhase as h, type JestAggregatedResult as i, type JestFileResult as j, type JestTestResult as k, type OtelSpan as l, type PlaywrightAnnotation as m, type PlaywrightAttachment as n, type PlaywrightError as o, type PlaywrightLocation as p, type PlaywrightStatus as q, type PlaywrightTestCase as r, type PlaywrightTestResult as s, type RawStepEvent as t, type RawTestCase as u, STORY_META_KEY as v, type StepKeyword as w, type StepMode as x, type StoryFileReport as y, type VitestSerializedError as z };
|
package/dist/index.cjs
CHANGED
|
@@ -48,7 +48,7 @@ __export(src_exports, {
|
|
|
48
48
|
deriveStepResults: () => deriveStepResults,
|
|
49
49
|
detectCI: () => detectCI4,
|
|
50
50
|
findGitDir: () => findGitDir,
|
|
51
|
-
formatDuration: () =>
|
|
51
|
+
formatDuration: () => formatDuration2,
|
|
52
52
|
generateRunId: () => generateRunId,
|
|
53
53
|
generateTestCaseId: () => generateTestCaseId,
|
|
54
54
|
mergeStepResults: () => mergeStepResults,
|
|
@@ -970,17 +970,17 @@ function initCollapse() {
|
|
|
970
970
|
}
|
|
971
971
|
|
|
972
972
|
function expandAll() {
|
|
973
|
-
document.querySelectorAll('.feature, .scenario').forEach(el => {
|
|
973
|
+
document.querySelectorAll('.feature, .scenario, .trace-view').forEach(el => {
|
|
974
974
|
el.classList.remove('collapsed');
|
|
975
|
-
const header = el.querySelector('.feature-header, .scenario-header');
|
|
975
|
+
const header = el.querySelector('.feature-header, .scenario-header, .trace-view-header');
|
|
976
976
|
header?.setAttribute('aria-expanded', 'true');
|
|
977
977
|
});
|
|
978
978
|
}
|
|
979
979
|
|
|
980
980
|
function collapseAll() {
|
|
981
|
-
document.querySelectorAll('.feature, .scenario').forEach(el => {
|
|
981
|
+
document.querySelectorAll('.feature, .scenario, .trace-view').forEach(el => {
|
|
982
982
|
el.classList.add('collapsed');
|
|
983
|
-
const header = el.querySelector('.feature-header, .scenario-header');
|
|
983
|
+
const header = el.querySelector('.feature-header, .scenario-header, .trace-view-header');
|
|
984
984
|
header?.setAttribute('aria-expanded', 'false');
|
|
985
985
|
});
|
|
986
986
|
}
|
|
@@ -2507,6 +2507,132 @@ body {
|
|
|
2507
2507
|
background: none;
|
|
2508
2508
|
}
|
|
2509
2509
|
|
|
2510
|
+
/* ============================================================================
|
|
2511
|
+
Trace View - OTel span waterfall
|
|
2512
|
+
============================================================================ */
|
|
2513
|
+
.trace-view {
|
|
2514
|
+
margin-top: 0.75rem;
|
|
2515
|
+
border: 1px solid var(--border);
|
|
2516
|
+
border-radius: calc(var(--radius) - 2px);
|
|
2517
|
+
overflow: hidden;
|
|
2518
|
+
}
|
|
2519
|
+
|
|
2520
|
+
.trace-view-header {
|
|
2521
|
+
display: flex;
|
|
2522
|
+
align-items: center;
|
|
2523
|
+
gap: 0.5rem;
|
|
2524
|
+
padding: 0.5rem 0.75rem;
|
|
2525
|
+
background: var(--card);
|
|
2526
|
+
cursor: pointer;
|
|
2527
|
+
user-select: none;
|
|
2528
|
+
font-size: 0.8125rem;
|
|
2529
|
+
font-weight: 500;
|
|
2530
|
+
color: var(--foreground);
|
|
2531
|
+
transition: background-color 0.15s ease;
|
|
2532
|
+
}
|
|
2533
|
+
|
|
2534
|
+
.trace-view-header:hover {
|
|
2535
|
+
background: var(--accordion-header-hover);
|
|
2536
|
+
}
|
|
2537
|
+
|
|
2538
|
+
.trace-view-count {
|
|
2539
|
+
font-size: 0.6875rem;
|
|
2540
|
+
font-weight: 500;
|
|
2541
|
+
padding: 0.125rem 0.5rem;
|
|
2542
|
+
background: var(--success-light);
|
|
2543
|
+
color: var(--success);
|
|
2544
|
+
border: 1px solid var(--success-border);
|
|
2545
|
+
border-radius: 9999px;
|
|
2546
|
+
font-family: var(--font-mono);
|
|
2547
|
+
}
|
|
2548
|
+
|
|
2549
|
+
.trace-view-content {
|
|
2550
|
+
border-top: 1px solid var(--border);
|
|
2551
|
+
padding: 0.5rem 0.75rem;
|
|
2552
|
+
background: var(--accordion-content-bg);
|
|
2553
|
+
}
|
|
2554
|
+
|
|
2555
|
+
.trace-view.collapsed .trace-view-content {
|
|
2556
|
+
display: none;
|
|
2557
|
+
}
|
|
2558
|
+
|
|
2559
|
+
.trace-view-axis {
|
|
2560
|
+
display: flex;
|
|
2561
|
+
justify-content: space-between;
|
|
2562
|
+
font-size: 0.625rem;
|
|
2563
|
+
font-family: var(--font-mono);
|
|
2564
|
+
color: var(--muted-foreground);
|
|
2565
|
+
padding-bottom: 0.375rem;
|
|
2566
|
+
margin-bottom: 0.375rem;
|
|
2567
|
+
border-bottom: 1px solid var(--border);
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
.trace-view-row {
|
|
2571
|
+
display: flex;
|
|
2572
|
+
align-items: center;
|
|
2573
|
+
gap: 0.5rem;
|
|
2574
|
+
padding: 0.1875rem 0;
|
|
2575
|
+
font-size: 0.75rem;
|
|
2576
|
+
}
|
|
2577
|
+
|
|
2578
|
+
.trace-view-name {
|
|
2579
|
+
width: 35%;
|
|
2580
|
+
flex-shrink: 0;
|
|
2581
|
+
display: flex;
|
|
2582
|
+
align-items: center;
|
|
2583
|
+
gap: 0.375rem;
|
|
2584
|
+
font-family: var(--font-mono);
|
|
2585
|
+
white-space: nowrap;
|
|
2586
|
+
overflow: hidden;
|
|
2587
|
+
text-overflow: ellipsis;
|
|
2588
|
+
color: var(--foreground);
|
|
2589
|
+
}
|
|
2590
|
+
|
|
2591
|
+
.trace-view-status-dot {
|
|
2592
|
+
width: 8px;
|
|
2593
|
+
height: 8px;
|
|
2594
|
+
border-radius: 50%;
|
|
2595
|
+
flex-shrink: 0;
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2598
|
+
.trace-view-status-ok { background: var(--success); }
|
|
2599
|
+
.trace-view-status-error { background: var(--error); }
|
|
2600
|
+
.trace-view-status-unset { background: var(--muted-foreground); }
|
|
2601
|
+
|
|
2602
|
+
.trace-view-bar-container {
|
|
2603
|
+
flex: 1;
|
|
2604
|
+
position: relative;
|
|
2605
|
+
height: 1.25rem;
|
|
2606
|
+
background: var(--muted);
|
|
2607
|
+
border-radius: 2px;
|
|
2608
|
+
}
|
|
2609
|
+
|
|
2610
|
+
.trace-view-bar {
|
|
2611
|
+
position: absolute;
|
|
2612
|
+
top: 0;
|
|
2613
|
+
height: 100%;
|
|
2614
|
+
border-radius: 2px;
|
|
2615
|
+
min-width: 2px;
|
|
2616
|
+
display: flex;
|
|
2617
|
+
align-items: center;
|
|
2618
|
+
padding: 0 0.375rem;
|
|
2619
|
+
font-size: 0.625rem;
|
|
2620
|
+
font-family: var(--font-mono);
|
|
2621
|
+
color: white;
|
|
2622
|
+
white-space: nowrap;
|
|
2623
|
+
overflow: hidden;
|
|
2624
|
+
}
|
|
2625
|
+
|
|
2626
|
+
.trace-view-bar-ok { background: var(--success); }
|
|
2627
|
+
.trace-view-bar-error { background: var(--error); }
|
|
2628
|
+
.trace-view-bar-unset { background: var(--muted-foreground); }
|
|
2629
|
+
|
|
2630
|
+
@media print {
|
|
2631
|
+
.trace-view.collapsed .trace-view-content {
|
|
2632
|
+
display: block;
|
|
2633
|
+
}
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2510
2636
|
`;
|
|
2511
2637
|
|
|
2512
2638
|
// src/formatters/html/renderers/status.ts
|
|
@@ -2831,6 +2957,10 @@ function renderScenario(args, deps) {
|
|
|
2831
2957
|
embedScreenshots: deps.embedScreenshots
|
|
2832
2958
|
}
|
|
2833
2959
|
);
|
|
2960
|
+
const traceView = deps.renderTraceView(
|
|
2961
|
+
{ spans: tc.story.otelSpans },
|
|
2962
|
+
{ escapeHtml: deps.escapeHtml }
|
|
2963
|
+
);
|
|
2834
2964
|
const collapsedClass = deps.startCollapsed ? " collapsed" : "";
|
|
2835
2965
|
const ariaExpanded = !deps.startCollapsed;
|
|
2836
2966
|
return `
|
|
@@ -2850,6 +2980,193 @@ function renderScenario(args, deps) {
|
|
|
2850
2980
|
${steps}
|
|
2851
2981
|
${error}
|
|
2852
2982
|
${attachments}
|
|
2983
|
+
${traceView}
|
|
2984
|
+
</div>
|
|
2985
|
+
</div>`;
|
|
2986
|
+
}
|
|
2987
|
+
|
|
2988
|
+
// src/formatters/html/renderers/trace-view.ts
|
|
2989
|
+
var VALID_STATUSES = /* @__PURE__ */ new Set(["ok", "error", "unset"]);
|
|
2990
|
+
var TOOLTIP_MAX_LENGTH = 4096;
|
|
2991
|
+
function safeStatus(status) {
|
|
2992
|
+
return VALID_STATUSES.has(status) ? status : "unset";
|
|
2993
|
+
}
|
|
2994
|
+
function formatDuration(ms) {
|
|
2995
|
+
if (ms >= 1e3) return `${(ms / 1e3).toFixed(2)}s`;
|
|
2996
|
+
return `${ms.toFixed(1)}ms`;
|
|
2997
|
+
}
|
|
2998
|
+
function clamp(value, min, max) {
|
|
2999
|
+
return Math.min(max, Math.max(min, value));
|
|
3000
|
+
}
|
|
3001
|
+
function normalizeSpans(spans) {
|
|
3002
|
+
const result = [];
|
|
3003
|
+
for (const span of spans) {
|
|
3004
|
+
if (!span || typeof span !== "object") continue;
|
|
3005
|
+
if (typeof span.spanId !== "string" || typeof span.name !== "string") continue;
|
|
3006
|
+
let startTimeMs;
|
|
3007
|
+
let durationMs;
|
|
3008
|
+
if (span.startTimeMs != null && span.durationMs != null) {
|
|
3009
|
+
startTimeMs = span.startTimeMs;
|
|
3010
|
+
durationMs = span.durationMs;
|
|
3011
|
+
} else if (span.startTimeUnixNano != null && span.endTimeUnixNano != null) {
|
|
3012
|
+
startTimeMs = span.startTimeUnixNano / 1e6;
|
|
3013
|
+
durationMs = (span.endTimeUnixNano - span.startTimeUnixNano) / 1e6;
|
|
3014
|
+
} else {
|
|
3015
|
+
continue;
|
|
3016
|
+
}
|
|
3017
|
+
durationMs = Math.max(0, durationMs);
|
|
3018
|
+
if (!isFinite(startTimeMs) || !isFinite(durationMs)) continue;
|
|
3019
|
+
result.push({
|
|
3020
|
+
spanId: span.spanId,
|
|
3021
|
+
parentSpanId: span.parentSpanId,
|
|
3022
|
+
name: span.name,
|
|
3023
|
+
startTimeMs,
|
|
3024
|
+
durationMs,
|
|
3025
|
+
status: safeStatus(span.status),
|
|
3026
|
+
statusMessage: span.statusMessage,
|
|
3027
|
+
attributes: span.attributes
|
|
3028
|
+
});
|
|
3029
|
+
}
|
|
3030
|
+
return result;
|
|
3031
|
+
}
|
|
3032
|
+
function buildTree(spans) {
|
|
3033
|
+
const byId = /* @__PURE__ */ new Map();
|
|
3034
|
+
for (const span of spans) {
|
|
3035
|
+
let key = span.spanId;
|
|
3036
|
+
if (byId.has(key)) {
|
|
3037
|
+
let suffix = 2;
|
|
3038
|
+
while (byId.has(`${span.spanId}__dup${suffix}`)) suffix++;
|
|
3039
|
+
key = `${span.spanId}__dup${suffix}`;
|
|
3040
|
+
}
|
|
3041
|
+
byId.set(key, { span: { ...span, spanId: key }, children: [], depth: 0 });
|
|
3042
|
+
}
|
|
3043
|
+
const roots = [];
|
|
3044
|
+
for (const node of byId.values()) {
|
|
3045
|
+
const parentId = node.span.parentSpanId;
|
|
3046
|
+
const parent = parentId ? byId.get(parentId) : void 0;
|
|
3047
|
+
if (parent && parent !== node) {
|
|
3048
|
+
parent.children.push(node);
|
|
3049
|
+
} else {
|
|
3050
|
+
roots.push(node);
|
|
3051
|
+
}
|
|
3052
|
+
}
|
|
3053
|
+
for (const node of byId.values()) {
|
|
3054
|
+
node.children.sort((a, b) => a.span.startTimeMs - b.span.startTimeMs);
|
|
3055
|
+
}
|
|
3056
|
+
roots.sort((a, b) => a.span.startTimeMs - b.span.startTimeMs);
|
|
3057
|
+
const visited = /* @__PURE__ */ new Set();
|
|
3058
|
+
function assignDepth(node, depth) {
|
|
3059
|
+
if (visited.has(node.span.spanId)) return;
|
|
3060
|
+
visited.add(node.span.spanId);
|
|
3061
|
+
node.depth = depth;
|
|
3062
|
+
for (const child of node.children) {
|
|
3063
|
+
assignDepth(child, depth + 1);
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
3066
|
+
for (const root of roots) {
|
|
3067
|
+
assignDepth(root, 0);
|
|
3068
|
+
}
|
|
3069
|
+
for (const node of byId.values()) {
|
|
3070
|
+
if (!visited.has(node.span.spanId)) {
|
|
3071
|
+
node.children = [];
|
|
3072
|
+
roots.push(node);
|
|
3073
|
+
assignDepth(node, 0);
|
|
3074
|
+
}
|
|
3075
|
+
}
|
|
3076
|
+
roots.sort((a, b) => a.span.startTimeMs - b.span.startTimeMs);
|
|
3077
|
+
return roots;
|
|
3078
|
+
}
|
|
3079
|
+
function flattenTree(roots) {
|
|
3080
|
+
const result = [];
|
|
3081
|
+
function walk(node) {
|
|
3082
|
+
result.push(node);
|
|
3083
|
+
for (const child of node.children) {
|
|
3084
|
+
walk(child);
|
|
3085
|
+
}
|
|
3086
|
+
}
|
|
3087
|
+
for (const root of roots) {
|
|
3088
|
+
walk(root);
|
|
3089
|
+
}
|
|
3090
|
+
return result;
|
|
3091
|
+
}
|
|
3092
|
+
function buildTooltip(span, escapeHtml2) {
|
|
3093
|
+
const parts = [];
|
|
3094
|
+
parts.push(`${span.name} (${formatDuration(span.durationMs)})`);
|
|
3095
|
+
if (span.statusMessage) {
|
|
3096
|
+
parts.push(`Status: ${span.statusMessage}`);
|
|
3097
|
+
}
|
|
3098
|
+
if (span.attributes) {
|
|
3099
|
+
const keys = Object.keys(span.attributes).sort();
|
|
3100
|
+
for (const key of keys) {
|
|
3101
|
+
const val = span.attributes[key];
|
|
3102
|
+
const formatted = Array.isArray(val) ? `[${val.map((v) => String(v)).join(", ")}]` : String(val);
|
|
3103
|
+
parts.push(`${key}=${formatted}`);
|
|
3104
|
+
}
|
|
3105
|
+
}
|
|
3106
|
+
let text = parts.join("\n");
|
|
3107
|
+
if (text.length > TOOLTIP_MAX_LENGTH) {
|
|
3108
|
+
text = text.slice(0, TOOLTIP_MAX_LENGTH - 3) + "...";
|
|
3109
|
+
}
|
|
3110
|
+
return escapeHtml2(text);
|
|
3111
|
+
}
|
|
3112
|
+
function renderTraceView(args, deps) {
|
|
3113
|
+
if (!args.spans || args.spans.length === 0) return "";
|
|
3114
|
+
const normalized = normalizeSpans(args.spans);
|
|
3115
|
+
if (normalized.length === 0) return "";
|
|
3116
|
+
const roots = buildTree(normalized);
|
|
3117
|
+
const flat = flattenTree(roots);
|
|
3118
|
+
let minStart = Infinity;
|
|
3119
|
+
let maxEnd = -Infinity;
|
|
3120
|
+
for (const node of flat) {
|
|
3121
|
+
const s = node.span.startTimeMs;
|
|
3122
|
+
const e = s + node.span.durationMs;
|
|
3123
|
+
if (s < minStart) minStart = s;
|
|
3124
|
+
if (e > maxEnd) maxEnd = e;
|
|
3125
|
+
}
|
|
3126
|
+
let totalDuration = maxEnd - minStart;
|
|
3127
|
+
if (totalDuration <= 0) totalDuration = 1;
|
|
3128
|
+
const rows = flat.map((node) => {
|
|
3129
|
+
const { span, depth } = node;
|
|
3130
|
+
const indent = depth * 16;
|
|
3131
|
+
const minWidth = 0.5;
|
|
3132
|
+
let spanLeft = clamp(
|
|
3133
|
+
(span.startTimeMs - minStart) / totalDuration * 100,
|
|
3134
|
+
0,
|
|
3135
|
+
100
|
|
3136
|
+
);
|
|
3137
|
+
if (spanLeft + minWidth > 100) {
|
|
3138
|
+
spanLeft = 100 - minWidth;
|
|
3139
|
+
}
|
|
3140
|
+
const spanWidth = clamp(
|
|
3141
|
+
span.durationMs / totalDuration * 100,
|
|
3142
|
+
minWidth,
|
|
3143
|
+
100 - spanLeft
|
|
3144
|
+
);
|
|
3145
|
+
const tooltip = buildTooltip(span, deps.escapeHtml);
|
|
3146
|
+
const durationLabel = formatDuration(span.durationMs);
|
|
3147
|
+
return ` <div class="trace-view-row">
|
|
3148
|
+
<div class="trace-view-name" style="padding-left: ${indent}px" title="${deps.escapeHtml(span.name)}">
|
|
3149
|
+
<span class="trace-view-status-dot trace-view-status-${span.status}"></span>
|
|
3150
|
+
${deps.escapeHtml(span.name)}
|
|
3151
|
+
</div>
|
|
3152
|
+
<div class="trace-view-bar-container">
|
|
3153
|
+
<div class="trace-view-bar trace-view-bar-${span.status}" style="left: ${spanLeft.toFixed(2)}%; width: ${spanWidth.toFixed(2)}%" title="${tooltip}">${durationLabel}</div>
|
|
3154
|
+
</div>
|
|
3155
|
+
</div>`;
|
|
3156
|
+
}).join("\n");
|
|
3157
|
+
const axisEnd = formatDuration(maxEnd - minStart);
|
|
3158
|
+
return `<div class="trace-view collapsed">
|
|
3159
|
+
<div class="trace-view-header" role="button" tabindex="0" aria-expanded="false">
|
|
3160
|
+
<span>Spans</span>
|
|
3161
|
+
<span class="trace-view-count">${flat.length}</span>
|
|
3162
|
+
<span class="chevron">▼</span>
|
|
3163
|
+
</div>
|
|
3164
|
+
<div class="trace-view-content">
|
|
3165
|
+
<div class="trace-view-axis">
|
|
3166
|
+
<span>0ms</span>
|
|
3167
|
+
<span>${axisEnd}</span>
|
|
3168
|
+
</div>
|
|
3169
|
+
${rows}
|
|
2853
3170
|
</div>
|
|
2854
3171
|
</div>`;
|
|
2855
3172
|
}
|
|
@@ -2986,6 +3303,7 @@ function createHtmlFormatter(options = {}) {
|
|
|
2986
3303
|
renderDocs,
|
|
2987
3304
|
renderErrorBox: (args, d) => renderErrorBox(args, d),
|
|
2988
3305
|
renderAttachments: (args, d) => renderAttachments(args, d),
|
|
3306
|
+
renderTraceView: (args, d) => renderTraceView(args, d),
|
|
2989
3307
|
embedScreenshots: opts.embedScreenshots
|
|
2990
3308
|
};
|
|
2991
3309
|
const featureDeps = {
|
|
@@ -5351,7 +5669,7 @@ function readBranchName(cwd = process.cwd()) {
|
|
|
5351
5669
|
}
|
|
5352
5670
|
|
|
5353
5671
|
// src/utils/duration.ts
|
|
5354
|
-
function
|
|
5672
|
+
function formatDuration2(ms) {
|
|
5355
5673
|
if (ms < 1e3) {
|
|
5356
5674
|
return `${Math.round(ms)} ms`;
|
|
5357
5675
|
}
|