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.
@@ -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 VitestTestCase as A, type VitestTestModule as B, type VitestTestResult as C, type DocEntry as D, type JestAdapterOptions as J, 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 PlaywrightAnnotation as l, type PlaywrightAttachment as m, type PlaywrightError as n, type PlaywrightLocation as o, type PlaywrightStatus as p, type PlaywrightTestCase as q, type PlaywrightTestResult as r, type RawStepEvent as s, type RawTestCase as t, STORY_META_KEY as u, type StepKeyword as v, type StepMode as w, type StoryFileReport as x, type VitestSerializedError as y, type VitestState as z };
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 VitestTestCase as A, type VitestTestModule as B, type VitestTestResult as C, type DocEntry as D, type JestAdapterOptions as J, 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 PlaywrightAnnotation as l, type PlaywrightAttachment as m, type PlaywrightError as n, type PlaywrightLocation as o, type PlaywrightStatus as p, type PlaywrightTestCase as q, type PlaywrightTestResult as r, type RawStepEvent as s, type RawTestCase as t, STORY_META_KEY as u, type StepKeyword as v, type StepMode as w, type StoryFileReport as x, type VitestSerializedError as y, type VitestState as z };
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: () => 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">&#9660;</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 formatDuration(ms) {
5672
+ function formatDuration2(ms) {
5355
5673
  if (ms < 1e3) {
5356
5674
  return `${Math.round(ms)} ms`;
5357
5675
  }