executable-stories-formatters 0.1.0 → 0.3.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/index.cjs CHANGED
@@ -65,7 +65,9 @@ __export(src_exports, {
65
65
  readPackageVersion: () => readPackageVersion,
66
66
  resolveAttachment: () => resolveAttachment,
67
67
  resolveAttachments: () => resolveAttachments,
68
+ resolveTraceUrl: () => resolveTraceUrl,
68
69
  slugify: () => slugify,
70
+ tryGetActiveOtelContext: () => tryGetActiveOtelContext,
69
71
  validateCanonicalRun: () => validateCanonicalRun
70
72
  });
71
73
  module.exports = __toCommonJS(src_exports);
@@ -763,50 +765,162 @@ function initTheme() {
763
765
  }
764
766
  `;
765
767
  var JS_CORE = `
768
+ // Filter state
769
+ var activeTags = new Set();
770
+ var activeStatus = null;
771
+
766
772
  // Search functionality
767
773
  function initSearch() {
768
- const input = document.querySelector('.search-input');
774
+ var input = document.querySelector('.search-input');
769
775
  if (!input) return;
770
776
 
771
- let debounceTimer;
772
- input.addEventListener('input', (e) => {
777
+ var debounceTimer;
778
+ input.addEventListener('input', function() {
773
779
  clearTimeout(debounceTimer);
774
- debounceTimer = setTimeout(() => {
775
- filterScenarios(e.target.value.toLowerCase().trim());
780
+ debounceTimer = setTimeout(function() {
781
+ applyAllFilters();
776
782
  }, 150);
777
783
  });
778
784
 
779
785
  // Clear search on Escape
780
- input.addEventListener('keydown', (e) => {
786
+ input.addEventListener('keydown', function(e) {
781
787
  if (e.key === 'Escape') {
782
788
  e.target.value = '';
783
- filterScenarios('');
789
+ applyAllFilters();
784
790
  }
785
791
  });
786
792
  }
787
793
 
788
- function filterScenarios(query) {
789
- const features = document.querySelectorAll('.feature');
794
+ // Tag filter
795
+ function initTagFilter() {
796
+ document.querySelectorAll('.tag-pill').forEach(function(pill) {
797
+ pill.addEventListener('click', function() {
798
+ var tag = pill.dataset.tag;
799
+ if (activeTags.has(tag)) {
800
+ activeTags.delete(tag);
801
+ pill.classList.remove('active');
802
+ } else {
803
+ activeTags.add(tag);
804
+ pill.classList.add('active');
805
+ }
806
+ updateClearButton();
807
+ applyAllFilters();
808
+ });
809
+ });
790
810
 
791
- features.forEach(feature => {
792
- const scenarios = feature.querySelectorAll('.scenario');
793
- let visibleCount = 0;
811
+ var clearBtn = document.querySelector('.tag-bar-clear');
812
+ if (clearBtn) {
813
+ clearBtn.addEventListener('click', function() {
814
+ activeTags.clear();
815
+ document.querySelectorAll('.tag-pill.active').forEach(function(p) { p.classList.remove('active'); });
816
+ updateClearButton();
817
+ applyAllFilters();
818
+ });
819
+ }
820
+ }
794
821
 
795
- scenarios.forEach(scenario => {
796
- const title = scenario.querySelector('.scenario-title')?.textContent?.toLowerCase() || '';
797
- const tags = Array.from(scenario.querySelectorAll('.tag')).map(t => t.textContent.toLowerCase());
798
- const steps = Array.from(scenario.querySelectorAll('.step-text')).map(s => s.textContent.toLowerCase());
822
+ function updateClearButton() {
823
+ var clearBtn = document.querySelector('.tag-bar-clear');
824
+ if (clearBtn) {
825
+ clearBtn.style.display = activeTags.size > 0 ? '' : 'none';
826
+ }
827
+ }
799
828
 
800
- const matches = !query ||
801
- title.includes(query) ||
802
- tags.some(t => t.includes(query)) ||
803
- steps.some(s => s.includes(query));
829
+ // Status filter (clickable summary cards)
830
+ function initStatusFilter() {
831
+ document.querySelectorAll('.summary-card').forEach(function(card) {
832
+ card.style.cursor = 'pointer';
833
+ if (!card.classList.contains('passed') && !card.classList.contains('failed') && !card.classList.contains('skipped')) {
834
+ card.addEventListener('click', function() {
835
+ activeStatus = null;
836
+ document.querySelectorAll('.summary-card').forEach(function(c) { c.classList.remove('status-active'); });
837
+ applyAllFilters();
838
+ });
839
+ return;
840
+ }
841
+ card.addEventListener('click', function() {
842
+ var status = card.classList.contains('passed') ? 'passed' :
843
+ card.classList.contains('failed') ? 'failed' : 'skipped';
844
+ if (activeStatus === status) {
845
+ activeStatus = null;
846
+ card.classList.remove('status-active');
847
+ } else {
848
+ activeStatus = status;
849
+ document.querySelectorAll('.summary-card').forEach(function(c) { c.classList.remove('status-active'); });
850
+ card.classList.add('status-active');
851
+ }
852
+ applyAllFilters();
853
+ });
854
+ });
855
+ }
804
856
 
805
- scenario.style.display = matches ? '' : 'none';
806
- if (matches) visibleCount++;
857
+ // Unified filter: composes search + tags + status
858
+ function applyAllFilters() {
859
+ var searchInput = document.querySelector('.search-input');
860
+ var searchQuery = searchInput ? searchInput.value.toLowerCase().trim() : '';
861
+ var features = document.querySelectorAll('.feature');
862
+ var visibleCount = 0;
863
+ var totalCount = 0;
864
+
865
+ features.forEach(function(feature) {
866
+ var scenarios = feature.querySelectorAll('.scenario');
867
+ var featureVisible = 0;
868
+
869
+ scenarios.forEach(function(scenario) {
870
+ totalCount++;
871
+ var title = (scenario.querySelector('.scenario-title') || {}).textContent || '';
872
+ title = title.toLowerCase();
873
+ var tags = Array.from(scenario.querySelectorAll('.scenario-meta .tag')).map(function(t) { return t.textContent.toLowerCase(); });
874
+ var steps = Array.from(scenario.querySelectorAll('.step-text')).map(function(s) { return s.textContent.toLowerCase(); });
875
+ var statusEl = scenario.querySelector('.status-icon');
876
+ var status = statusEl && statusEl.classList.contains('status-passed') ? 'passed' :
877
+ statusEl && statusEl.classList.contains('status-failed') ? 'failed' :
878
+ statusEl && statusEl.classList.contains('status-skipped') ? 'skipped' : 'pending';
879
+
880
+ var matchesSearch = !searchQuery ||
881
+ title.includes(searchQuery) ||
882
+ tags.some(function(t) { return t.includes(searchQuery); }) ||
883
+ steps.some(function(s) { return s.includes(searchQuery); });
884
+
885
+ var matchesTags = activeTags.size === 0 ||
886
+ tags.some(function(t) { return activeTags.has(t); });
887
+
888
+ var matchesStatus = !activeStatus ||
889
+ status === activeStatus ||
890
+ (activeStatus === 'skipped' && status === 'pending');
891
+
892
+ var visible = matchesSearch && matchesTags && matchesStatus;
893
+ scenario.style.display = visible ? '' : 'none';
894
+ if (visible) { visibleCount++; featureVisible++; }
807
895
  });
808
896
 
809
- feature.style.display = visibleCount > 0 ? '' : 'none';
897
+ feature.style.display = featureVisible > 0 ? '' : 'none';
898
+ });
899
+
900
+ updateFilterResults(visibleCount, totalCount);
901
+ }
902
+
903
+ function updateFilterResults(visible, total) {
904
+ var el = document.querySelector('.filter-results');
905
+ if (!el) return;
906
+ var searchInput = document.querySelector('.search-input');
907
+ var isFiltering = activeTags.size > 0 || activeStatus ||
908
+ (searchInput && searchInput.value.trim().length > 0);
909
+ el.style.display = isFiltering ? '' : 'none';
910
+ var vc = el.querySelector('.visible-count');
911
+ var tc = el.querySelector('.total-count');
912
+ if (vc) vc.textContent = visible;
913
+ if (tc) tc.textContent = total;
914
+ }
915
+
916
+ // Keyboard shortcuts
917
+ function initKeyboardShortcuts() {
918
+ document.addEventListener('keydown', function(e) {
919
+ if (e.key === '/' && !e.ctrlKey && !e.metaKey && e.target.tagName !== 'INPUT') {
920
+ e.preventDefault();
921
+ var input = document.querySelector('.search-input');
922
+ if (input) input.focus();
923
+ }
810
924
  });
811
925
  }
812
926
 
@@ -841,6 +955,18 @@ function initCollapse() {
841
955
  }
842
956
  });
843
957
  });
958
+
959
+ document.querySelectorAll('.trace-view-header').forEach(header => {
960
+ header.addEventListener('click', () => {
961
+ toggleCollapse(header, header.closest('.trace-view'));
962
+ });
963
+ header.addEventListener('keydown', (e) => {
964
+ if (e.key === 'Enter' || e.key === ' ') {
965
+ e.preventDefault();
966
+ toggleCollapse(header, header.closest('.trace-view'));
967
+ }
968
+ });
969
+ });
844
970
  }
845
971
 
846
972
  function expandAll() {
@@ -891,6 +1017,9 @@ function generateScript(options) {
891
1017
  initCalls.push("initTheme();");
892
1018
  }
893
1019
  initCalls.push("initSearch();");
1020
+ initCalls.push("initTagFilter();");
1021
+ initCalls.push("initStatusFilter();");
1022
+ initCalls.push("initKeyboardShortcuts();");
894
1023
  initCalls.push("initCollapse();");
895
1024
  const initScript = `
896
1025
  // Initialize on load
@@ -1041,6 +1170,7 @@ var CSS_STYLES = `
1041
1170
  --tag-bg: hsl(145 55% 95%);
1042
1171
  --tag-color: hsl(145 63% 30%);
1043
1172
  --tag-border: hsl(145 55% 85%);
1173
+ --step-param-color: hsl(220 70% 50%);
1044
1174
 
1045
1175
  /* Accordion/Collapsible styling */
1046
1176
  --accordion-header-hover: hsl(0 0% 98%);
@@ -1099,6 +1229,7 @@ var CSS_STYLES = `
1099
1229
  --tag-bg: hsl(145 35% 14%);
1100
1230
  --tag-color: hsl(145 63% 60%);
1101
1231
  --tag-border: hsl(145 35% 22%);
1232
+ --step-param-color: hsl(220 70% 70%);
1102
1233
 
1103
1234
  /* Accordion/Collapsible styling */
1104
1235
  --accordion-header-hover: hsl(0 0% 11%);
@@ -1147,6 +1278,7 @@ var CSS_STYLES = `
1147
1278
  --tag-bg: hsl(145 35% 14%);
1148
1279
  --tag-color: hsl(145 63% 60%);
1149
1280
  --tag-border: hsl(145 35% 22%);
1281
+ --step-param-color: hsl(220 70% 70%);
1150
1282
  --accordion-header-hover: hsl(0 0% 11%);
1151
1283
  --accordion-content-bg: hsl(0 0% 7%);
1152
1284
  }
@@ -1374,6 +1506,98 @@ body {
1374
1506
  }
1375
1507
  .summary-card.pending .value { color: var(--pending); }
1376
1508
 
1509
+ /* ============================================================================
1510
+ Tag Filter Bar
1511
+ ============================================================================ */
1512
+ .tag-bar {
1513
+ margin-bottom: 1rem;
1514
+ padding: 0.75rem 1rem;
1515
+ background: var(--card);
1516
+ border: 1px solid var(--border);
1517
+ border-radius: var(--radius);
1518
+ position: sticky;
1519
+ top: 0;
1520
+ z-index: 10;
1521
+ }
1522
+
1523
+ .tag-bar-header {
1524
+ display: flex;
1525
+ justify-content: space-between;
1526
+ align-items: center;
1527
+ margin-bottom: 0.5rem;
1528
+ }
1529
+
1530
+ .tag-bar-label {
1531
+ font-size: 0.6875rem;
1532
+ text-transform: uppercase;
1533
+ letter-spacing: 0.05em;
1534
+ color: var(--muted-foreground);
1535
+ font-weight: 500;
1536
+ }
1537
+
1538
+ .tag-bar-clear {
1539
+ font-size: 0.75rem;
1540
+ font-weight: 500;
1541
+ color: var(--primary);
1542
+ background: none;
1543
+ border: none;
1544
+ cursor: pointer;
1545
+ padding: 0.125rem 0.5rem;
1546
+ border-radius: var(--radius);
1547
+ transition: all 0.15s ease;
1548
+ }
1549
+
1550
+ .tag-bar-clear:hover {
1551
+ background: var(--muted);
1552
+ }
1553
+
1554
+ .tag-bar-pills {
1555
+ display: flex;
1556
+ flex-wrap: wrap;
1557
+ gap: 0.375rem;
1558
+ }
1559
+
1560
+ .tag-pill {
1561
+ font-size: 0.75rem;
1562
+ font-weight: 500;
1563
+ padding: 0.25rem 0.625rem;
1564
+ background: var(--tag-bg);
1565
+ color: var(--tag-color);
1566
+ border: 1px solid var(--tag-border);
1567
+ border-radius: 9999px;
1568
+ font-family: var(--font-mono);
1569
+ cursor: pointer;
1570
+ transition: all 0.15s ease;
1571
+ }
1572
+
1573
+ .tag-pill:hover {
1574
+ background: var(--success-border);
1575
+ }
1576
+
1577
+ .tag-pill.active {
1578
+ background: var(--primary);
1579
+ color: var(--primary-foreground);
1580
+ border-color: var(--primary);
1581
+ }
1582
+
1583
+ /* ============================================================================
1584
+ Summary Card Status Filter
1585
+ ============================================================================ */
1586
+ .summary-card.status-active {
1587
+ box-shadow: 0 0 0 2px var(--background), 0 0 0 4px var(--ring);
1588
+ }
1589
+
1590
+ /* ============================================================================
1591
+ Filter Results Counter
1592
+ ============================================================================ */
1593
+ .filter-results {
1594
+ text-align: center;
1595
+ font-size: 0.8125rem;
1596
+ color: var(--muted-foreground);
1597
+ margin-bottom: 1rem;
1598
+ font-weight: 500;
1599
+ }
1600
+
1377
1601
  /* ============================================================================
1378
1602
  Feature Sections - shadcn accordion style
1379
1603
  ============================================================================ */
@@ -1600,6 +1824,12 @@ body {
1600
1824
  color: var(--foreground);
1601
1825
  }
1602
1826
 
1827
+ .step-param {
1828
+ font-style: italic;
1829
+ font-weight: 500;
1830
+ color: var(--step-param-color);
1831
+ }
1832
+
1603
1833
  .step-duration {
1604
1834
  color: var(--muted-foreground);
1605
1835
  font-size: 0.6875rem;
@@ -1761,8 +1991,10 @@ body {
1761
1991
  padding: 0;
1762
1992
  }
1763
1993
 
1764
- .header-actions {
1765
- display: none;
1994
+ .header-actions,
1995
+ .tag-bar,
1996
+ .filter-results {
1997
+ display: none !important;
1766
1998
  }
1767
1999
 
1768
2000
  .feature,
@@ -2274,6 +2506,7 @@ body {
2274
2506
  font-family: inherit;
2275
2507
  background: none;
2276
2508
  }
2509
+
2277
2510
  `;
2278
2511
 
2279
2512
  // src/formatters/html/renderers/status.ts
@@ -2336,6 +2569,28 @@ function renderSummary(args, _deps) {
2336
2569
  </div>`;
2337
2570
  }
2338
2571
 
2572
+ // src/formatters/html/renderers/tag-bar.ts
2573
+ function renderTagBar(args, deps) {
2574
+ const { tags, totalScenarios } = args;
2575
+ if (tags.length === 0) return "";
2576
+ const pills = tags.map(
2577
+ (tag) => `<button type="button" class="tag-pill" data-tag="${deps.escapeHtml(tag)}">${deps.escapeHtml(tag)}</button>`
2578
+ ).join("\n ");
2579
+ return `
2580
+ <div class="tag-bar">
2581
+ <div class="tag-bar-header">
2582
+ <span class="tag-bar-label">Filter by tag</span>
2583
+ <button type="button" class="tag-bar-clear" style="display:none">Clear</button>
2584
+ </div>
2585
+ <div class="tag-bar-pills">
2586
+ ${pills}
2587
+ </div>
2588
+ </div>
2589
+ <div class="filter-results" style="display:none">
2590
+ Showing <span class="visible-count">0</span> of <span class="total-count">${totalScenarios}</span> scenarios
2591
+ </div>`;
2592
+ }
2593
+
2339
2594
  // src/formatters/html/renderers/error-box.ts
2340
2595
  function renderErrorBox(args, deps) {
2341
2596
  const body = args.stack != null ? `${deps.escapeHtml(args.message)}
@@ -2496,10 +2751,11 @@ function renderStep(step, stepResult, index, deps) {
2496
2751
  const isContinuation = CONTINUATION_KEYWORDS.includes(keywordTrimmed);
2497
2752
  const stepClass = isContinuation ? "step continuation" : "step";
2498
2753
  const stepDocs = deps.renderDocs(step.docs, "step-docs");
2754
+ const textHtml = deps.highlightStepParams ? deps.highlightStepParams(step.text) : deps.escapeHtml(step.text);
2499
2755
  return `<div class="${stepClass}">
2500
2756
  <span class="step-status ${statusClass}">${statusIcon}</span>
2501
2757
  <span class="step-keyword">${deps.escapeHtml(step.keyword)}</span>
2502
- <span class="step-text">${deps.escapeHtml(step.text)}</span>
2758
+ <span class="step-text">${textHtml}</span>
2503
2759
  <span class="step-duration">${duration}</span>
2504
2760
  </div>${stepDocs}`;
2505
2761
  }
@@ -2511,6 +2767,30 @@ function renderSteps(args, deps) {
2511
2767
  return `<div class="steps">${stepsHtml}</div>`;
2512
2768
  }
2513
2769
 
2770
+ // src/formatters/html/renderers/step-params.ts
2771
+ var STEP_PARAM_PATTERN = /"[^"]*"|(?<![\w.\-])\d+(?:\.\d+)?(?![\w.\-])/g;
2772
+ function highlightStepParams(text, deps) {
2773
+ const matches = Array.from(text.matchAll(STEP_PARAM_PATTERN));
2774
+ if (matches.length === 0) {
2775
+ return deps.escapeHtml(text);
2776
+ }
2777
+ let result = "";
2778
+ let lastIndex = 0;
2779
+ for (const match of matches) {
2780
+ const matchStart = match.index;
2781
+ const matchEnd = matchStart + match[0].length;
2782
+ if (matchStart > lastIndex) {
2783
+ result += deps.escapeHtml(text.slice(lastIndex, matchStart));
2784
+ }
2785
+ result += `<span class="step-param">${deps.escapeHtml(match[0])}</span>`;
2786
+ lastIndex = matchEnd;
2787
+ }
2788
+ if (lastIndex < text.length) {
2789
+ result += deps.escapeHtml(text.slice(lastIndex));
2790
+ }
2791
+ return result;
2792
+ }
2793
+
2514
2794
  // src/formatters/html/renderers/scenario.ts
2515
2795
  function renderScenario(args, deps) {
2516
2796
  const { tc } = args;
@@ -2518,6 +2798,19 @@ function renderScenario(args, deps) {
2518
2798
  const statusClass = `status-${tc.status}`;
2519
2799
  const duration = tc.durationMs > 0 ? `${(tc.durationMs / 1e3).toFixed(2)}s` : "";
2520
2800
  const tags = tc.tags.map((t) => `<span class="tag">${deps.escapeHtml(t)}</span>`).join("");
2801
+ const otelMeta = tc.story.meta?.otel;
2802
+ let traceBadge = "";
2803
+ if (otelMeta?.traceId) {
2804
+ const shortId = otelMeta.traceId.slice(0, 16);
2805
+ const traceLink = tc.story.docs?.find(
2806
+ (d) => d.kind === "link" && d.label === "View Trace"
2807
+ );
2808
+ if (traceLink) {
2809
+ 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>`;
2810
+ } else {
2811
+ traceBadge = `<span class="tag trace-tag" title="${deps.escapeHtml(otelMeta.traceId)}">${deps.escapeHtml(shortId)}\u2026</span>`;
2812
+ }
2813
+ }
2521
2814
  const storyDocs = deps.renderDocs(tc.story.docs, "story-docs");
2522
2815
  const steps = deps.renderSteps(
2523
2816
  { steps: tc.story.steps, stepResults: tc.stepResults },
@@ -2548,7 +2841,7 @@ function renderScenario(args, deps) {
2548
2841
  <span class="status-icon ${statusClass}">${statusIcon}</span>
2549
2842
  <span class="scenario-name">${deps.escapeHtml(tc.story.scenario)}</span>
2550
2843
  </div>
2551
- <div class="scenario-meta">${tags}</div>
2844
+ <div class="scenario-meta">${tags}${traceBadge}</div>
2552
2845
  </div>
2553
2846
  <span class="scenario-duration">${duration}</span>
2554
2847
  </div>
@@ -2635,6 +2928,15 @@ function buildBody(args, deps) {
2635
2928
  deps.summaryDeps
2636
2929
  )
2637
2930
  );
2931
+ const allTags = [
2932
+ ...new Set(run.testCases.flatMap((tc) => tc.tags))
2933
+ ].sort();
2934
+ parts.push(
2935
+ deps.renderTagBar(
2936
+ { tags: allTags, totalScenarios: total },
2937
+ deps.tagBarDeps
2938
+ )
2939
+ );
2638
2940
  const byFile = groupBy(run.testCases, (tc) => tc.sourceFile);
2639
2941
  for (const [file, testCases] of byFile) {
2640
2942
  parts.push(
@@ -2673,7 +2975,8 @@ function createHtmlFormatter(options = {}) {
2673
2975
  const stepsDeps = {
2674
2976
  escapeHtml,
2675
2977
  getStatusIcon,
2676
- renderDocs
2978
+ renderDocs,
2979
+ highlightStepParams: (text) => highlightStepParams(text, { escapeHtml })
2677
2980
  };
2678
2981
  const scenarioDeps = {
2679
2982
  escapeHtml,
@@ -2691,12 +2994,15 @@ function createHtmlFormatter(options = {}) {
2691
2994
  renderScenario: (args) => renderScenario(args, scenarioDeps),
2692
2995
  scenarioDeps
2693
2996
  };
2997
+ const tagBarDeps = { escapeHtml };
2694
2998
  const bodyDeps = {
2695
2999
  renderMetaInfo,
2696
3000
  renderSummary,
3001
+ renderTagBar,
2697
3002
  renderFeature,
2698
3003
  metaDeps: { escapeHtml },
2699
3004
  summaryDeps: {},
3005
+ tagBarDeps,
2700
3006
  featureDeps
2701
3007
  };
2702
3008
  return {
@@ -2972,6 +3278,7 @@ var MarkdownFormatter = class {
2972
3278
  includeSummaryTable: options.includeSummaryTable ?? false,
2973
3279
  permalinkBaseUrl: options.permalinkBaseUrl,
2974
3280
  ticketUrlTemplate: options.ticketUrlTemplate,
3281
+ traceUrlTemplate: options.traceUrlTemplate,
2975
3282
  includeSourceLinks: options.includeSourceLinks ?? true,
2976
3283
  customRenderers: options.customRenderers
2977
3284
  };
@@ -3194,6 +3501,18 @@ var MarkdownFormatter = class {
3194
3501
  meta.push(`Tickets: ${tc.story.tickets.map((t) => `\`${t}\``).join(", ")}`);
3195
3502
  }
3196
3503
  }
3504
+ const otelMeta = tc.story.meta?.otel;
3505
+ if (otelMeta?.traceId) {
3506
+ const traceTemplate = this.options.traceUrlTemplate;
3507
+ if (traceTemplate) {
3508
+ const url = traceTemplate.replace(/\{traceId\}/g, otelMeta.traceId);
3509
+ meta.push(
3510
+ `Trace: [${otelMeta.traceId.slice(0, 16)}\u2026](${url})`
3511
+ );
3512
+ } else {
3513
+ meta.push(`Trace: \`${otelMeta.traceId}\``);
3514
+ }
3515
+ }
3197
3516
  if (meta.length > 0) {
3198
3517
  lines.push(meta.join(" | "));
3199
3518
  }
@@ -5136,6 +5455,32 @@ function detectCI4(env = process.env) {
5136
5455
  return void 0;
5137
5456
  }
5138
5457
 
5458
+ // src/utils/otel-detect.ts
5459
+ var import_node_module = require("module");
5460
+ var import_meta2 = {};
5461
+ function getRequire() {
5462
+ const url = import_meta2.url ?? (typeof __filename !== "undefined" ? `file://${__filename}` : void 0);
5463
+ if (!url) throw new Error("Cannot determine module URL");
5464
+ return (0, import_node_module.createRequire)(url);
5465
+ }
5466
+ function tryGetActiveOtelContext() {
5467
+ try {
5468
+ const api = getRequire()("@opentelemetry/api");
5469
+ const span = api.trace?.getActiveSpan?.();
5470
+ if (!span) return void 0;
5471
+ const ctx = span.spanContext?.();
5472
+ if (!ctx?.traceId || ctx.traceId === "00000000000000000000000000000000")
5473
+ return void 0;
5474
+ return { traceId: ctx.traceId, spanId: ctx.spanId };
5475
+ } catch {
5476
+ return void 0;
5477
+ }
5478
+ }
5479
+ function resolveTraceUrl(template, traceId) {
5480
+ if (!template) return void 0;
5481
+ return template.replace(/\{traceId\}/g, traceId);
5482
+ }
5483
+
5139
5484
  // src/index.ts
5140
5485
  var FORMAT_EXTENSIONS = {
5141
5486
  markdown: ".md",
@@ -5326,6 +5671,7 @@ var ReportGenerator = class {
5326
5671
  includeSummaryTable: options.markdown?.includeSummaryTable ?? false,
5327
5672
  permalinkBaseUrl: options.markdown?.permalinkBaseUrl,
5328
5673
  ticketUrlTemplate: options.markdown?.ticketUrlTemplate,
5674
+ traceUrlTemplate: options.markdown?.traceUrlTemplate,
5329
5675
  includeSourceLinks: options.markdown?.includeSourceLinks ?? true,
5330
5676
  customRenderers: options.markdown?.customRenderers
5331
5677
  }
@@ -5451,6 +5797,7 @@ var ReportGenerator = class {
5451
5797
  includeSummaryTable: this.options.markdown.includeSummaryTable,
5452
5798
  permalinkBaseUrl: this.options.markdown.permalinkBaseUrl,
5453
5799
  ticketUrlTemplate: this.options.markdown.ticketUrlTemplate,
5800
+ traceUrlTemplate: this.options.markdown.traceUrlTemplate,
5454
5801
  includeSourceLinks: this.options.markdown.includeSourceLinks,
5455
5802
  customRenderers: this.options.markdown.customRenderers
5456
5803
  });
@@ -5513,7 +5860,9 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
5513
5860
  readPackageVersion,
5514
5861
  resolveAttachment,
5515
5862
  resolveAttachments,
5863
+ resolveTraceUrl,
5516
5864
  slugify,
5865
+ tryGetActiveOtelContext,
5517
5866
  validateCanonicalRun
5518
5867
  });
5519
5868
  //# sourceMappingURL=index.cjs.map