executable-stories-formatters 0.2.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/README.md CHANGED
@@ -119,7 +119,7 @@ const generator = new ReportGenerator({
119
119
 
120
120
  | Format | Description | File Extension |
121
121
  | --- | --- | --- |
122
- | `html` | Interactive HTML report with search and screenshots | `.html` |
122
+ | `html` | Interactive HTML report with search, screenshots, step parameter highlighting (quoted strings and numbers), syntax-highlighted code blocks, Mermaid diagrams, and Markdown in doc sections | `.html` |
123
123
  | `markdown` | Markdown user-story documentation | `.md` |
124
124
  | `junit` | JUnit XML for CI integration | `.junit.xml` |
125
125
  | `cucumber-json` | Cucumber JSON for tooling compatibility | `.cucumber.json` |
@@ -1 +1 @@
1
- export { J as JestAdapterOptions, i as JestAggregatedResult, j as JestFileResult, k as JestTestResult, P as PlaywrightAdapterOptions, l as PlaywrightAnnotation, m as PlaywrightAttachment, n as PlaywrightError, o as PlaywrightLocation, p as PlaywrightStatus, q as PlaywrightTestCase, r as PlaywrightTestResult, x as StoryFileReport, V as VitestAdapterOptions, y as VitestSerializedError, z as VitestState, A as VitestTestCase, B as VitestTestModule, C as VitestTestResult, e as adaptJestRun, f as adaptPlaywrightRun, g as adaptVitestRun } from './index-DCJ0NvAp.cjs';
1
+ export { J as JestAdapterOptions, i as JestAggregatedResult, j as JestFileResult, k as JestTestResult, P as PlaywrightAdapterOptions, m as PlaywrightAnnotation, n as PlaywrightAttachment, o as PlaywrightError, p as PlaywrightLocation, q as PlaywrightStatus, r as PlaywrightTestCase, s as PlaywrightTestResult, y as StoryFileReport, V as VitestAdapterOptions, z as VitestSerializedError, A as VitestState, B as VitestTestCase, C as VitestTestModule, E as VitestTestResult, e as adaptJestRun, f as adaptPlaywrightRun, g as adaptVitestRun } from './index-DyeUWfYK.cjs';
@@ -1 +1 @@
1
- export { J as JestAdapterOptions, i as JestAggregatedResult, j as JestFileResult, k as JestTestResult, P as PlaywrightAdapterOptions, l as PlaywrightAnnotation, m as PlaywrightAttachment, n as PlaywrightError, o as PlaywrightLocation, p as PlaywrightStatus, q as PlaywrightTestCase, r as PlaywrightTestResult, x as StoryFileReport, V as VitestAdapterOptions, y as VitestSerializedError, z as VitestState, A as VitestTestCase, B as VitestTestModule, C as VitestTestResult, e as adaptJestRun, f as adaptPlaywrightRun, g as adaptVitestRun } from './index-DCJ0NvAp.js';
1
+ export { J as JestAdapterOptions, i as JestAggregatedResult, j as JestFileResult, k as JestTestResult, P as PlaywrightAdapterOptions, m as PlaywrightAnnotation, n as PlaywrightAttachment, o as PlaywrightError, p as PlaywrightLocation, q as PlaywrightStatus, r as PlaywrightTestCase, s as PlaywrightTestResult, y as StoryFileReport, V as VitestAdapterOptions, z as VitestSerializedError, A as VitestState, B as VitestTestCase, C as VitestTestModule, E as VitestTestResult, e as adaptJestRun, f as adaptPlaywrightRun, g as adaptVitestRun } from './index-DyeUWfYK.js';
package/dist/cli.js CHANGED
@@ -1086,20 +1086,32 @@ function initCollapse() {
1086
1086
  }
1087
1087
  });
1088
1088
  });
1089
+
1090
+ document.querySelectorAll('.trace-view-header').forEach(header => {
1091
+ header.addEventListener('click', () => {
1092
+ toggleCollapse(header, header.closest('.trace-view'));
1093
+ });
1094
+ header.addEventListener('keydown', (e) => {
1095
+ if (e.key === 'Enter' || e.key === ' ') {
1096
+ e.preventDefault();
1097
+ toggleCollapse(header, header.closest('.trace-view'));
1098
+ }
1099
+ });
1100
+ });
1089
1101
  }
1090
1102
 
1091
1103
  function expandAll() {
1092
- document.querySelectorAll('.feature, .scenario').forEach(el => {
1104
+ document.querySelectorAll('.feature, .scenario, .trace-view').forEach(el => {
1093
1105
  el.classList.remove('collapsed');
1094
- const header = el.querySelector('.feature-header, .scenario-header');
1106
+ const header = el.querySelector('.feature-header, .scenario-header, .trace-view-header');
1095
1107
  header?.setAttribute('aria-expanded', 'true');
1096
1108
  });
1097
1109
  }
1098
1110
 
1099
1111
  function collapseAll() {
1100
- document.querySelectorAll('.feature, .scenario').forEach(el => {
1112
+ document.querySelectorAll('.feature, .scenario, .trace-view').forEach(el => {
1101
1113
  el.classList.add('collapsed');
1102
- const header = el.querySelector('.feature-header, .scenario-header');
1114
+ const header = el.querySelector('.feature-header, .scenario-header, .trace-view-header');
1103
1115
  header?.setAttribute('aria-expanded', 'false');
1104
1116
  });
1105
1117
  }
@@ -1289,6 +1301,7 @@ var CSS_STYLES = `
1289
1301
  --tag-bg: hsl(145 55% 95%);
1290
1302
  --tag-color: hsl(145 63% 30%);
1291
1303
  --tag-border: hsl(145 55% 85%);
1304
+ --step-param-color: hsl(220 70% 50%);
1292
1305
 
1293
1306
  /* Accordion/Collapsible styling */
1294
1307
  --accordion-header-hover: hsl(0 0% 98%);
@@ -1347,6 +1360,7 @@ var CSS_STYLES = `
1347
1360
  --tag-bg: hsl(145 35% 14%);
1348
1361
  --tag-color: hsl(145 63% 60%);
1349
1362
  --tag-border: hsl(145 35% 22%);
1363
+ --step-param-color: hsl(220 70% 70%);
1350
1364
 
1351
1365
  /* Accordion/Collapsible styling */
1352
1366
  --accordion-header-hover: hsl(0 0% 11%);
@@ -1395,6 +1409,7 @@ var CSS_STYLES = `
1395
1409
  --tag-bg: hsl(145 35% 14%);
1396
1410
  --tag-color: hsl(145 63% 60%);
1397
1411
  --tag-border: hsl(145 35% 22%);
1412
+ --step-param-color: hsl(220 70% 70%);
1398
1413
  --accordion-header-hover: hsl(0 0% 11%);
1399
1414
  --accordion-content-bg: hsl(0 0% 7%);
1400
1415
  }
@@ -1940,6 +1955,12 @@ body {
1940
1955
  color: var(--foreground);
1941
1956
  }
1942
1957
 
1958
+ .step-param {
1959
+ font-style: italic;
1960
+ font-weight: 500;
1961
+ color: var(--step-param-color);
1962
+ }
1963
+
1943
1964
  .step-duration {
1944
1965
  color: var(--muted-foreground);
1945
1966
  font-size: 0.6875rem;
@@ -2616,6 +2637,133 @@ body {
2616
2637
  font-family: inherit;
2617
2638
  background: none;
2618
2639
  }
2640
+
2641
+ /* ============================================================================
2642
+ Trace View - OTel span waterfall
2643
+ ============================================================================ */
2644
+ .trace-view {
2645
+ margin-top: 0.75rem;
2646
+ border: 1px solid var(--border);
2647
+ border-radius: calc(var(--radius) - 2px);
2648
+ overflow: hidden;
2649
+ }
2650
+
2651
+ .trace-view-header {
2652
+ display: flex;
2653
+ align-items: center;
2654
+ gap: 0.5rem;
2655
+ padding: 0.5rem 0.75rem;
2656
+ background: var(--card);
2657
+ cursor: pointer;
2658
+ user-select: none;
2659
+ font-size: 0.8125rem;
2660
+ font-weight: 500;
2661
+ color: var(--foreground);
2662
+ transition: background-color 0.15s ease;
2663
+ }
2664
+
2665
+ .trace-view-header:hover {
2666
+ background: var(--accordion-header-hover);
2667
+ }
2668
+
2669
+ .trace-view-count {
2670
+ font-size: 0.6875rem;
2671
+ font-weight: 500;
2672
+ padding: 0.125rem 0.5rem;
2673
+ background: var(--success-light);
2674
+ color: var(--success);
2675
+ border: 1px solid var(--success-border);
2676
+ border-radius: 9999px;
2677
+ font-family: var(--font-mono);
2678
+ }
2679
+
2680
+ .trace-view-content {
2681
+ border-top: 1px solid var(--border);
2682
+ padding: 0.5rem 0.75rem;
2683
+ background: var(--accordion-content-bg);
2684
+ }
2685
+
2686
+ .trace-view.collapsed .trace-view-content {
2687
+ display: none;
2688
+ }
2689
+
2690
+ .trace-view-axis {
2691
+ display: flex;
2692
+ justify-content: space-between;
2693
+ font-size: 0.625rem;
2694
+ font-family: var(--font-mono);
2695
+ color: var(--muted-foreground);
2696
+ padding-bottom: 0.375rem;
2697
+ margin-bottom: 0.375rem;
2698
+ border-bottom: 1px solid var(--border);
2699
+ }
2700
+
2701
+ .trace-view-row {
2702
+ display: flex;
2703
+ align-items: center;
2704
+ gap: 0.5rem;
2705
+ padding: 0.1875rem 0;
2706
+ font-size: 0.75rem;
2707
+ }
2708
+
2709
+ .trace-view-name {
2710
+ width: 35%;
2711
+ flex-shrink: 0;
2712
+ display: flex;
2713
+ align-items: center;
2714
+ gap: 0.375rem;
2715
+ font-family: var(--font-mono);
2716
+ white-space: nowrap;
2717
+ overflow: hidden;
2718
+ text-overflow: ellipsis;
2719
+ color: var(--foreground);
2720
+ }
2721
+
2722
+ .trace-view-status-dot {
2723
+ width: 8px;
2724
+ height: 8px;
2725
+ border-radius: 50%;
2726
+ flex-shrink: 0;
2727
+ }
2728
+
2729
+ .trace-view-status-ok { background: var(--success); }
2730
+ .trace-view-status-error { background: var(--error); }
2731
+ .trace-view-status-unset { background: var(--muted-foreground); }
2732
+
2733
+ .trace-view-bar-container {
2734
+ flex: 1;
2735
+ position: relative;
2736
+ height: 1.25rem;
2737
+ background: var(--muted);
2738
+ border-radius: 2px;
2739
+ }
2740
+
2741
+ .trace-view-bar {
2742
+ position: absolute;
2743
+ top: 0;
2744
+ height: 100%;
2745
+ border-radius: 2px;
2746
+ min-width: 2px;
2747
+ display: flex;
2748
+ align-items: center;
2749
+ padding: 0 0.375rem;
2750
+ font-size: 0.625rem;
2751
+ font-family: var(--font-mono);
2752
+ color: white;
2753
+ white-space: nowrap;
2754
+ overflow: hidden;
2755
+ }
2756
+
2757
+ .trace-view-bar-ok { background: var(--success); }
2758
+ .trace-view-bar-error { background: var(--error); }
2759
+ .trace-view-bar-unset { background: var(--muted-foreground); }
2760
+
2761
+ @media print {
2762
+ .trace-view.collapsed .trace-view-content {
2763
+ display: block;
2764
+ }
2765
+ }
2766
+
2619
2767
  `;
2620
2768
 
2621
2769
  // src/formatters/html/renderers/status.ts
@@ -2860,10 +3008,11 @@ function renderStep(step, stepResult, index, deps) {
2860
3008
  const isContinuation = CONTINUATION_KEYWORDS.includes(keywordTrimmed);
2861
3009
  const stepClass = isContinuation ? "step continuation" : "step";
2862
3010
  const stepDocs = deps.renderDocs(step.docs, "step-docs");
3011
+ const textHtml = deps.highlightStepParams ? deps.highlightStepParams(step.text) : deps.escapeHtml(step.text);
2863
3012
  return `<div class="${stepClass}">
2864
3013
  <span class="step-status ${statusClass}">${statusIcon}</span>
2865
3014
  <span class="step-keyword">${deps.escapeHtml(step.keyword)}</span>
2866
- <span class="step-text">${deps.escapeHtml(step.text)}</span>
3015
+ <span class="step-text">${textHtml}</span>
2867
3016
  <span class="step-duration">${duration}</span>
2868
3017
  </div>${stepDocs}`;
2869
3018
  }
@@ -2875,6 +3024,30 @@ function renderSteps(args, deps) {
2875
3024
  return `<div class="steps">${stepsHtml}</div>`;
2876
3025
  }
2877
3026
 
3027
+ // src/formatters/html/renderers/step-params.ts
3028
+ var STEP_PARAM_PATTERN = /"[^"]*"|(?<![\w.\-])\d+(?:\.\d+)?(?![\w.\-])/g;
3029
+ function highlightStepParams(text, deps) {
3030
+ const matches = Array.from(text.matchAll(STEP_PARAM_PATTERN));
3031
+ if (matches.length === 0) {
3032
+ return deps.escapeHtml(text);
3033
+ }
3034
+ let result = "";
3035
+ let lastIndex = 0;
3036
+ for (const match of matches) {
3037
+ const matchStart = match.index;
3038
+ const matchEnd = matchStart + match[0].length;
3039
+ if (matchStart > lastIndex) {
3040
+ result += deps.escapeHtml(text.slice(lastIndex, matchStart));
3041
+ }
3042
+ result += `<span class="step-param">${deps.escapeHtml(match[0])}</span>`;
3043
+ lastIndex = matchEnd;
3044
+ }
3045
+ if (lastIndex < text.length) {
3046
+ result += deps.escapeHtml(text.slice(lastIndex));
3047
+ }
3048
+ return result;
3049
+ }
3050
+
2878
3051
  // src/formatters/html/renderers/scenario.ts
2879
3052
  function renderScenario(args, deps) {
2880
3053
  const { tc } = args;
@@ -2882,6 +3055,19 @@ function renderScenario(args, deps) {
2882
3055
  const statusClass = `status-${tc.status}`;
2883
3056
  const duration = tc.durationMs > 0 ? `${(tc.durationMs / 1e3).toFixed(2)}s` : "";
2884
3057
  const tags = tc.tags.map((t) => `<span class="tag">${deps.escapeHtml(t)}</span>`).join("");
3058
+ const otelMeta = tc.story.meta?.otel;
3059
+ let traceBadge = "";
3060
+ if (otelMeta?.traceId) {
3061
+ const shortId = otelMeta.traceId.slice(0, 16);
3062
+ const traceLink = tc.story.docs?.find(
3063
+ (d) => d.kind === "link" && d.label === "View Trace"
3064
+ );
3065
+ if (traceLink) {
3066
+ traceBadge = `<a class="tag trace-tag" href="${deps.escapeHtml(traceLink.url)}" title="${deps.escapeHtml(otelMeta.traceId)}" target="_blank" rel="noopener">${deps.escapeHtml(shortId)}\u2026</a>`;
3067
+ } else {
3068
+ traceBadge = `<span class="tag trace-tag" title="${deps.escapeHtml(otelMeta.traceId)}">${deps.escapeHtml(shortId)}\u2026</span>`;
3069
+ }
3070
+ }
2885
3071
  const storyDocs = deps.renderDocs(tc.story.docs, "story-docs");
2886
3072
  const steps = deps.renderSteps(
2887
3073
  { steps: tc.story.steps, stepResults: tc.stepResults },
@@ -2902,6 +3088,10 @@ function renderScenario(args, deps) {
2902
3088
  embedScreenshots: deps.embedScreenshots
2903
3089
  }
2904
3090
  );
3091
+ const traceView = deps.renderTraceView(
3092
+ { spans: tc.story.otelSpans },
3093
+ { escapeHtml: deps.escapeHtml }
3094
+ );
2905
3095
  const collapsedClass = deps.startCollapsed ? " collapsed" : "";
2906
3096
  const ariaExpanded = !deps.startCollapsed;
2907
3097
  return `
@@ -2912,7 +3102,7 @@ function renderScenario(args, deps) {
2912
3102
  <span class="status-icon ${statusClass}">${statusIcon}</span>
2913
3103
  <span class="scenario-name">${deps.escapeHtml(tc.story.scenario)}</span>
2914
3104
  </div>
2915
- <div class="scenario-meta">${tags}</div>
3105
+ <div class="scenario-meta">${tags}${traceBadge}</div>
2916
3106
  </div>
2917
3107
  <span class="scenario-duration">${duration}</span>
2918
3108
  </div>
@@ -2921,6 +3111,193 @@ function renderScenario(args, deps) {
2921
3111
  ${steps}
2922
3112
  ${error}
2923
3113
  ${attachments}
3114
+ ${traceView}
3115
+ </div>
3116
+ </div>`;
3117
+ }
3118
+
3119
+ // src/formatters/html/renderers/trace-view.ts
3120
+ var VALID_STATUSES = /* @__PURE__ */ new Set(["ok", "error", "unset"]);
3121
+ var TOOLTIP_MAX_LENGTH = 4096;
3122
+ function safeStatus(status) {
3123
+ return VALID_STATUSES.has(status) ? status : "unset";
3124
+ }
3125
+ function formatDuration(ms) {
3126
+ if (ms >= 1e3) return `${(ms / 1e3).toFixed(2)}s`;
3127
+ return `${ms.toFixed(1)}ms`;
3128
+ }
3129
+ function clamp(value, min, max) {
3130
+ return Math.min(max, Math.max(min, value));
3131
+ }
3132
+ function normalizeSpans(spans) {
3133
+ const result = [];
3134
+ for (const span of spans) {
3135
+ if (!span || typeof span !== "object") continue;
3136
+ if (typeof span.spanId !== "string" || typeof span.name !== "string") continue;
3137
+ let startTimeMs;
3138
+ let durationMs;
3139
+ if (span.startTimeMs != null && span.durationMs != null) {
3140
+ startTimeMs = span.startTimeMs;
3141
+ durationMs = span.durationMs;
3142
+ } else if (span.startTimeUnixNano != null && span.endTimeUnixNano != null) {
3143
+ startTimeMs = span.startTimeUnixNano / 1e6;
3144
+ durationMs = (span.endTimeUnixNano - span.startTimeUnixNano) / 1e6;
3145
+ } else {
3146
+ continue;
3147
+ }
3148
+ durationMs = Math.max(0, durationMs);
3149
+ if (!isFinite(startTimeMs) || !isFinite(durationMs)) continue;
3150
+ result.push({
3151
+ spanId: span.spanId,
3152
+ parentSpanId: span.parentSpanId,
3153
+ name: span.name,
3154
+ startTimeMs,
3155
+ durationMs,
3156
+ status: safeStatus(span.status),
3157
+ statusMessage: span.statusMessage,
3158
+ attributes: span.attributes
3159
+ });
3160
+ }
3161
+ return result;
3162
+ }
3163
+ function buildTree(spans) {
3164
+ const byId = /* @__PURE__ */ new Map();
3165
+ for (const span of spans) {
3166
+ let key = span.spanId;
3167
+ if (byId.has(key)) {
3168
+ let suffix = 2;
3169
+ while (byId.has(`${span.spanId}__dup${suffix}`)) suffix++;
3170
+ key = `${span.spanId}__dup${suffix}`;
3171
+ }
3172
+ byId.set(key, { span: { ...span, spanId: key }, children: [], depth: 0 });
3173
+ }
3174
+ const roots = [];
3175
+ for (const node of byId.values()) {
3176
+ const parentId = node.span.parentSpanId;
3177
+ const parent = parentId ? byId.get(parentId) : void 0;
3178
+ if (parent && parent !== node) {
3179
+ parent.children.push(node);
3180
+ } else {
3181
+ roots.push(node);
3182
+ }
3183
+ }
3184
+ for (const node of byId.values()) {
3185
+ node.children.sort((a, b) => a.span.startTimeMs - b.span.startTimeMs);
3186
+ }
3187
+ roots.sort((a, b) => a.span.startTimeMs - b.span.startTimeMs);
3188
+ const visited = /* @__PURE__ */ new Set();
3189
+ function assignDepth(node, depth) {
3190
+ if (visited.has(node.span.spanId)) return;
3191
+ visited.add(node.span.spanId);
3192
+ node.depth = depth;
3193
+ for (const child of node.children) {
3194
+ assignDepth(child, depth + 1);
3195
+ }
3196
+ }
3197
+ for (const root of roots) {
3198
+ assignDepth(root, 0);
3199
+ }
3200
+ for (const node of byId.values()) {
3201
+ if (!visited.has(node.span.spanId)) {
3202
+ node.children = [];
3203
+ roots.push(node);
3204
+ assignDepth(node, 0);
3205
+ }
3206
+ }
3207
+ roots.sort((a, b) => a.span.startTimeMs - b.span.startTimeMs);
3208
+ return roots;
3209
+ }
3210
+ function flattenTree(roots) {
3211
+ const result = [];
3212
+ function walk(node) {
3213
+ result.push(node);
3214
+ for (const child of node.children) {
3215
+ walk(child);
3216
+ }
3217
+ }
3218
+ for (const root of roots) {
3219
+ walk(root);
3220
+ }
3221
+ return result;
3222
+ }
3223
+ function buildTooltip(span, escapeHtml2) {
3224
+ const parts = [];
3225
+ parts.push(`${span.name} (${formatDuration(span.durationMs)})`);
3226
+ if (span.statusMessage) {
3227
+ parts.push(`Status: ${span.statusMessage}`);
3228
+ }
3229
+ if (span.attributes) {
3230
+ const keys = Object.keys(span.attributes).sort();
3231
+ for (const key of keys) {
3232
+ const val = span.attributes[key];
3233
+ const formatted = Array.isArray(val) ? `[${val.map((v) => String(v)).join(", ")}]` : String(val);
3234
+ parts.push(`${key}=${formatted}`);
3235
+ }
3236
+ }
3237
+ let text = parts.join("\n");
3238
+ if (text.length > TOOLTIP_MAX_LENGTH) {
3239
+ text = text.slice(0, TOOLTIP_MAX_LENGTH - 3) + "...";
3240
+ }
3241
+ return escapeHtml2(text);
3242
+ }
3243
+ function renderTraceView(args, deps) {
3244
+ if (!args.spans || args.spans.length === 0) return "";
3245
+ const normalized = normalizeSpans(args.spans);
3246
+ if (normalized.length === 0) return "";
3247
+ const roots = buildTree(normalized);
3248
+ const flat = flattenTree(roots);
3249
+ let minStart = Infinity;
3250
+ let maxEnd = -Infinity;
3251
+ for (const node of flat) {
3252
+ const s = node.span.startTimeMs;
3253
+ const e = s + node.span.durationMs;
3254
+ if (s < minStart) minStart = s;
3255
+ if (e > maxEnd) maxEnd = e;
3256
+ }
3257
+ let totalDuration = maxEnd - minStart;
3258
+ if (totalDuration <= 0) totalDuration = 1;
3259
+ const rows = flat.map((node) => {
3260
+ const { span, depth } = node;
3261
+ const indent = depth * 16;
3262
+ const minWidth = 0.5;
3263
+ let spanLeft = clamp(
3264
+ (span.startTimeMs - minStart) / totalDuration * 100,
3265
+ 0,
3266
+ 100
3267
+ );
3268
+ if (spanLeft + minWidth > 100) {
3269
+ spanLeft = 100 - minWidth;
3270
+ }
3271
+ const spanWidth = clamp(
3272
+ span.durationMs / totalDuration * 100,
3273
+ minWidth,
3274
+ 100 - spanLeft
3275
+ );
3276
+ const tooltip = buildTooltip(span, deps.escapeHtml);
3277
+ const durationLabel = formatDuration(span.durationMs);
3278
+ return ` <div class="trace-view-row">
3279
+ <div class="trace-view-name" style="padding-left: ${indent}px" title="${deps.escapeHtml(span.name)}">
3280
+ <span class="trace-view-status-dot trace-view-status-${span.status}"></span>
3281
+ ${deps.escapeHtml(span.name)}
3282
+ </div>
3283
+ <div class="trace-view-bar-container">
3284
+ <div class="trace-view-bar trace-view-bar-${span.status}" style="left: ${spanLeft.toFixed(2)}%; width: ${spanWidth.toFixed(2)}%" title="${tooltip}">${durationLabel}</div>
3285
+ </div>
3286
+ </div>`;
3287
+ }).join("\n");
3288
+ const axisEnd = formatDuration(maxEnd - minStart);
3289
+ return `<div class="trace-view collapsed">
3290
+ <div class="trace-view-header" role="button" tabindex="0" aria-expanded="false">
3291
+ <span>Spans</span>
3292
+ <span class="trace-view-count">${flat.length}</span>
3293
+ <span class="chevron">&#9660;</span>
3294
+ </div>
3295
+ <div class="trace-view-content">
3296
+ <div class="trace-view-axis">
3297
+ <span>0ms</span>
3298
+ <span>${axisEnd}</span>
3299
+ </div>
3300
+ ${rows}
2924
3301
  </div>
2925
3302
  </div>`;
2926
3303
  }
@@ -3046,7 +3423,8 @@ function createHtmlFormatter(options = {}) {
3046
3423
  const stepsDeps = {
3047
3424
  escapeHtml,
3048
3425
  getStatusIcon,
3049
- renderDocs
3426
+ renderDocs,
3427
+ highlightStepParams: (text) => highlightStepParams(text, { escapeHtml })
3050
3428
  };
3051
3429
  const scenarioDeps = {
3052
3430
  escapeHtml,
@@ -3056,6 +3434,7 @@ function createHtmlFormatter(options = {}) {
3056
3434
  renderDocs,
3057
3435
  renderErrorBox: (args, d) => renderErrorBox(args, d),
3058
3436
  renderAttachments: (args, d) => renderAttachments(args, d),
3437
+ renderTraceView: (args, d) => renderTraceView(args, d),
3059
3438
  embedScreenshots: opts.embedScreenshots
3060
3439
  };
3061
3440
  const featureDeps = {
@@ -3348,6 +3727,7 @@ var MarkdownFormatter = class {
3348
3727
  includeSummaryTable: options.includeSummaryTable ?? false,
3349
3728
  permalinkBaseUrl: options.permalinkBaseUrl,
3350
3729
  ticketUrlTemplate: options.ticketUrlTemplate,
3730
+ traceUrlTemplate: options.traceUrlTemplate,
3351
3731
  includeSourceLinks: options.includeSourceLinks ?? true,
3352
3732
  customRenderers: options.customRenderers
3353
3733
  };
@@ -3570,6 +3950,18 @@ var MarkdownFormatter = class {
3570
3950
  meta.push(`Tickets: ${tc.story.tickets.map((t) => `\`${t}\``).join(", ")}`);
3571
3951
  }
3572
3952
  }
3953
+ const otelMeta = tc.story.meta?.otel;
3954
+ if (otelMeta?.traceId) {
3955
+ const traceTemplate = this.options.traceUrlTemplate;
3956
+ if (traceTemplate) {
3957
+ const url = traceTemplate.replace(/\{traceId\}/g, otelMeta.traceId);
3958
+ meta.push(
3959
+ `Trace: [${otelMeta.traceId.slice(0, 16)}\u2026](${url})`
3960
+ );
3961
+ } else {
3962
+ meta.push(`Trace: \`${otelMeta.traceId}\``);
3963
+ }
3964
+ }
3573
3965
  if (meta.length > 0) {
3574
3966
  lines.push(meta.join(" | "));
3575
3967
  }
@@ -5124,6 +5516,7 @@ var ReportGenerator = class {
5124
5516
  includeSummaryTable: options.markdown?.includeSummaryTable ?? false,
5125
5517
  permalinkBaseUrl: options.markdown?.permalinkBaseUrl,
5126
5518
  ticketUrlTemplate: options.markdown?.ticketUrlTemplate,
5519
+ traceUrlTemplate: options.markdown?.traceUrlTemplate,
5127
5520
  includeSourceLinks: options.markdown?.includeSourceLinks ?? true,
5128
5521
  customRenderers: options.markdown?.customRenderers
5129
5522
  }
@@ -5249,6 +5642,7 @@ var ReportGenerator = class {
5249
5642
  includeSummaryTable: this.options.markdown.includeSummaryTable,
5250
5643
  permalinkBaseUrl: this.options.markdown.permalinkBaseUrl,
5251
5644
  ticketUrlTemplate: this.options.markdown.ticketUrlTemplate,
5645
+ traceUrlTemplate: this.options.markdown.traceUrlTemplate,
5252
5646
  includeSourceLinks: this.options.markdown.includeSourceLinks,
5253
5647
  customRenderers: this.options.markdown.customRenderers
5254
5648
  });