executable-stories-formatters 0.7.11 → 0.7.13

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.d.cts CHANGED
@@ -1,12 +1,5 @@
1
- import { S as StoryMeta, C as CIInfo$1, a as StoryStep, D as DocEntry, N as NormalizedTicket, O as OtelSpan, b as CIProvider, R as RawStatus, c as RawAttachment, d as RawRun, e as RawCIInfo, f as adaptJestRun, g as adaptPlaywrightRun, h as adaptVitestRun } from './index-C0OOaaiK.cjs';
2
- export { i as DocPhase, J as JestAdapterOptions, j as JestAggregatedResult, k as JestFileResult, l as JestTestResult, m as OtelAttributeValue, P as PlaywrightAdapterOptions, n as PlaywrightAnnotation, o as PlaywrightAttachment, p as PlaywrightError, q as PlaywrightLocation, r as PlaywrightStatus, s as PlaywrightTestCase, t as PlaywrightTestResult, u as RawStepEvent, v as RawTestCase, w as STORY_META_KEY, x as StepKeyword, y as StepMode, z as StoryFileReport, V as VitestAdapterOptions, A as VitestSerializedError, B as VitestState, E as VitestTestCase, F as VitestTestModule, G as VitestTestResult, H as toCIInfo, I as toRawCIInfo } from './index-C0OOaaiK.cjs';
3
-
4
- /**
5
- * Canonical types for Layer 2: Anti-Corruption Layer output.
6
- *
7
- * These types are strict and have all required fields populated.
8
- * Formatters (Layer 3) accept only these canonical types.
9
- */
1
+ import { S as StoryMeta, R as RawStatus, C as CIInfo$1, a as StoryStep, D as DocEntry, N as NormalizedTicket, O as OtelSpan, b as CIProvider, c as RawAttachment, d as RawRun, e as RawCIInfo, f as adaptJestRun, g as adaptPlaywrightRun, h as adaptVitestRun } from './index-fqrm5-Xr.cjs';
2
+ export { i as DocPhase, J as JestAdapterOptions, j as JestAggregatedResult, k as JestFileResult, l as JestTestResult, m as OtelAttributeValue, P as PlaywrightAdapterOptions, n as PlaywrightAnnotation, o as PlaywrightAttachment, p as PlaywrightError, q as PlaywrightLocation, r as PlaywrightStatus, s as PlaywrightTestCase, t as PlaywrightTestResult, u as RawStepEvent, v as RawTestCase, w as STORY_META_KEY, x as StepKeyword, y as StepMode, z as StoryFileReport, V as VitestAdapterOptions, A as VitestSerializedError, B as VitestState, E as VitestTestCase, F as VitestTestModule, G as VitestTestResult, H as toCIInfo, I as toRawCIInfo } from './index-fqrm5-Xr.cjs';
10
3
 
11
4
  /** Canonical test status (Cucumber-compatible) */
12
5
  type TestStatus = "passed" | "failed" | "skipped" | "pending";
@@ -14,6 +7,8 @@ type TestStatus = "passed" | "failed" | "skipped" | "pending";
14
7
  interface StepResult {
15
8
  /** Step index (0-based) */
16
9
  index: number;
10
+ /** Stable step ID when available */
11
+ stepId?: string;
17
12
  /** Step status */
18
13
  status: TestStatus;
19
14
  /** Duration in milliseconds (default 0) */
@@ -57,6 +52,8 @@ interface TestCaseResult {
57
52
  sourceLine: number;
58
53
  /** Test status (required) */
59
54
  status: TestStatus;
55
+ /** Original adapter/framework status (preserved for diagnostics). */
56
+ rawStatus?: RawStatus;
60
57
  /** Duration in milliseconds (required, default 0) */
61
58
  durationMs: number;
62
59
  /** Error message if failed */
@@ -1094,6 +1091,7 @@ declare function deriveStepResults(steps: StoryStep[], scenarioStatus: TestStatu
1094
1091
  */
1095
1092
  declare function mergeStepResults(derived: StepResult[], events?: Array<{
1096
1093
  index?: number;
1094
+ stepId?: string;
1097
1095
  status?: string;
1098
1096
  durationMs?: number;
1099
1097
  errorMessage?: string;
package/dist/index.d.ts CHANGED
@@ -1,12 +1,5 @@
1
- import { S as StoryMeta, C as CIInfo$1, a as StoryStep, D as DocEntry, N as NormalizedTicket, O as OtelSpan, b as CIProvider, R as RawStatus, c as RawAttachment, d as RawRun, e as RawCIInfo, f as adaptJestRun, g as adaptPlaywrightRun, h as adaptVitestRun } from './index-C0OOaaiK.js';
2
- export { i as DocPhase, J as JestAdapterOptions, j as JestAggregatedResult, k as JestFileResult, l as JestTestResult, m as OtelAttributeValue, P as PlaywrightAdapterOptions, n as PlaywrightAnnotation, o as PlaywrightAttachment, p as PlaywrightError, q as PlaywrightLocation, r as PlaywrightStatus, s as PlaywrightTestCase, t as PlaywrightTestResult, u as RawStepEvent, v as RawTestCase, w as STORY_META_KEY, x as StepKeyword, y as StepMode, z as StoryFileReport, V as VitestAdapterOptions, A as VitestSerializedError, B as VitestState, E as VitestTestCase, F as VitestTestModule, G as VitestTestResult, H as toCIInfo, I as toRawCIInfo } from './index-C0OOaaiK.js';
3
-
4
- /**
5
- * Canonical types for Layer 2: Anti-Corruption Layer output.
6
- *
7
- * These types are strict and have all required fields populated.
8
- * Formatters (Layer 3) accept only these canonical types.
9
- */
1
+ import { S as StoryMeta, R as RawStatus, C as CIInfo$1, a as StoryStep, D as DocEntry, N as NormalizedTicket, O as OtelSpan, b as CIProvider, c as RawAttachment, d as RawRun, e as RawCIInfo, f as adaptJestRun, g as adaptPlaywrightRun, h as adaptVitestRun } from './index-fqrm5-Xr.js';
2
+ export { i as DocPhase, J as JestAdapterOptions, j as JestAggregatedResult, k as JestFileResult, l as JestTestResult, m as OtelAttributeValue, P as PlaywrightAdapterOptions, n as PlaywrightAnnotation, o as PlaywrightAttachment, p as PlaywrightError, q as PlaywrightLocation, r as PlaywrightStatus, s as PlaywrightTestCase, t as PlaywrightTestResult, u as RawStepEvent, v as RawTestCase, w as STORY_META_KEY, x as StepKeyword, y as StepMode, z as StoryFileReport, V as VitestAdapterOptions, A as VitestSerializedError, B as VitestState, E as VitestTestCase, F as VitestTestModule, G as VitestTestResult, H as toCIInfo, I as toRawCIInfo } from './index-fqrm5-Xr.js';
10
3
 
11
4
  /** Canonical test status (Cucumber-compatible) */
12
5
  type TestStatus = "passed" | "failed" | "skipped" | "pending";
@@ -14,6 +7,8 @@ type TestStatus = "passed" | "failed" | "skipped" | "pending";
14
7
  interface StepResult {
15
8
  /** Step index (0-based) */
16
9
  index: number;
10
+ /** Stable step ID when available */
11
+ stepId?: string;
17
12
  /** Step status */
18
13
  status: TestStatus;
19
14
  /** Duration in milliseconds (default 0) */
@@ -57,6 +52,8 @@ interface TestCaseResult {
57
52
  sourceLine: number;
58
53
  /** Test status (required) */
59
54
  status: TestStatus;
55
+ /** Original adapter/framework status (preserved for diagnostics). */
56
+ rawStatus?: RawStatus;
60
57
  /** Duration in milliseconds (required, default 0) */
61
58
  durationMs: number;
62
59
  /** Error message if failed */
@@ -1094,6 +1091,7 @@ declare function deriveStepResults(steps: StoryStep[], scenarioStatus: TestStatu
1094
1091
  */
1095
1092
  declare function mergeStepResults(derived: StepResult[], events?: Array<{
1096
1093
  index?: number;
1094
+ stepId?: string;
1097
1095
  status?: string;
1098
1096
  durationMs?: number;
1099
1097
  errorMessage?: string;
package/dist/index.js CHANGED
@@ -51,6 +51,7 @@ function deriveStepResults(steps, scenarioStatus, error) {
51
51
  if (scenarioStatus === "passed") {
52
52
  return steps.map((_, index) => ({
53
53
  index,
54
+ stepId: steps[index].id,
54
55
  status: "passed",
55
56
  durationMs: 0
56
57
  }));
@@ -58,6 +59,7 @@ function deriveStepResults(steps, scenarioStatus, error) {
58
59
  if (scenarioStatus === "skipped" || scenarioStatus === "pending") {
59
60
  return steps.map((_, index) => ({
60
61
  index,
62
+ stepId: steps[index].id,
61
63
  status: scenarioStatus,
62
64
  durationMs: 0
63
65
  }));
@@ -65,16 +67,17 @@ function deriveStepResults(steps, scenarioStatus, error) {
65
67
  const failingIndex = findFailingStepIndex(steps, error);
66
68
  return steps.map((_, index) => {
67
69
  if (index < failingIndex) {
68
- return { index, status: "passed", durationMs: 0 };
70
+ return { index, stepId: steps[index].id, status: "passed", durationMs: 0 };
69
71
  } else if (index === failingIndex) {
70
72
  return {
71
73
  index,
74
+ stepId: steps[index].id,
72
75
  status: "failed",
73
76
  durationMs: 0,
74
77
  errorMessage: error?.message
75
78
  };
76
79
  } else {
77
- return { index, status: "skipped", durationMs: 0 };
80
+ return { index, stepId: steps[index].id, status: "skipped", durationMs: 0 };
78
81
  }
79
82
  });
80
83
  }
@@ -96,18 +99,23 @@ function mergeStepResults(derived, events) {
96
99
  return derived;
97
100
  }
98
101
  const actualByIndex = /* @__PURE__ */ new Map();
102
+ const actualByStepId = /* @__PURE__ */ new Map();
99
103
  for (const event of events) {
100
104
  if (event.index !== void 0) {
101
105
  actualByIndex.set(event.index, event);
102
106
  }
107
+ if (event.stepId) {
108
+ actualByStepId.set(event.stepId, event);
109
+ }
103
110
  }
104
111
  return derived.map((step) => {
105
- const actual = actualByIndex.get(step.index);
112
+ const actual = (step.stepId ? actualByStepId.get(step.stepId) : void 0) ?? actualByIndex.get(step.index);
106
113
  if (!actual) {
107
114
  return step;
108
115
  }
109
116
  return {
110
117
  index: step.index,
118
+ stepId: step.stepId ?? actual.stepId,
111
119
  status: normalizeStepStatus(actual.status) ?? step.status,
112
120
  durationMs: actual.durationMs ?? step.durationMs,
113
121
  errorMessage: actual.errorMessage ?? step.errorMessage
@@ -241,6 +249,7 @@ function canonicalizeTestCase(raw, options, projectRoot) {
241
249
  derivedSteps,
242
250
  raw.stepEvents?.map((e) => ({
243
251
  index: e.index,
252
+ stepId: e.stepId,
244
253
  status: e.status,
245
254
  durationMs: e.durationMs,
246
255
  errorMessage: e.errorMessage
@@ -262,6 +271,7 @@ function canonicalizeTestCase(raw, options, projectRoot) {
262
271
  sourceFile,
263
272
  sourceLine: raw.sourceLine ?? 1,
264
273
  status,
274
+ rawStatus: raw.status,
265
275
  durationMs: raw.durationMs ?? 0,
266
276
  errorMessage: raw.error?.message,
267
277
  errorStack: raw.error?.stack,
@@ -629,19 +639,34 @@ ${doc.markdown}`,
629
639
  }
630
640
  };
631
641
  }
632
- case "tag":
642
+ case "custom":
643
+ if (doc.type === "visual" && doc.data && typeof doc.data === "object") {
644
+ const data = doc.data;
645
+ const status = typeof data.status === "string" ? data.status : "unknown";
646
+ const lines = [`Visual Check (${status})`];
647
+ if (typeof data.baseline === "string") lines.push(`Baseline: ${data.baseline}`);
648
+ if (typeof data.actual === "string") lines.push(`Actual: ${data.actual}`);
649
+ if (typeof data.diff === "string") lines.push(`Diff: ${data.diff}`);
650
+ return {
651
+ doc_string: {
652
+ content: lines.join("\n"),
653
+ content_type: "text/plain",
654
+ line: 0
655
+ }
656
+ };
657
+ }
633
658
  return {
634
659
  doc_string: {
635
- content: doc.names.map((n) => `@${n}`).join(" "),
636
- content_type: "text/plain",
660
+ content: JSON.stringify(doc.data, null, 2),
661
+ content_type: "application/json",
637
662
  line: 0
638
663
  }
639
664
  };
640
- case "custom":
665
+ case "tag":
641
666
  return {
642
667
  doc_string: {
643
- content: JSON.stringify(doc.data, null, 2),
644
- content_type: "application/json",
668
+ content: doc.names.map((n) => `@${n}`).join(" "),
669
+ content_type: "text/plain",
645
670
  line: 0
646
671
  }
647
672
  };
@@ -2159,6 +2184,14 @@ body {
2159
2184
  font-family: var(--font-mono);
2160
2185
  }
2161
2186
 
2187
+ .outcome-tag {
2188
+ background: var(--status-fail-bg, color-mix(in srgb, var(--destructive, #ef4444) 12%, transparent));
2189
+ color: var(--destructive, #b91c1c);
2190
+ border-color: color-mix(in srgb, var(--destructive, #ef4444) 35%, transparent);
2191
+ text-transform: uppercase;
2192
+ letter-spacing: 0.04em;
2193
+ }
2194
+
2162
2195
  .scenario-duration {
2163
2196
  font-size: 0.75rem;
2164
2197
  color: var(--muted-foreground);
@@ -2315,6 +2348,27 @@ body {
2315
2348
  border: 1px solid var(--border);
2316
2349
  }
2317
2350
 
2351
+ .attachment-unavailable {
2352
+ padding: 0.75rem 1rem;
2353
+ border: 1px dashed var(--border);
2354
+ border-radius: calc(var(--radius) - 2px);
2355
+ background: var(--muted, transparent);
2356
+ color: var(--muted-foreground);
2357
+ font-size: 0.8125rem;
2358
+ }
2359
+
2360
+ .attachment-unavailable-label {
2361
+ font-weight: 600;
2362
+ margin-bottom: 0.25rem;
2363
+ }
2364
+
2365
+ .attachment-unavailable-path {
2366
+ font-family: var(--font-mono, ui-monospace, monospace);
2367
+ font-size: 0.75rem;
2368
+ word-break: break-all;
2369
+ opacity: 0.8;
2370
+ }
2371
+
2318
2372
  /* ============================================================================
2319
2373
  Chevron Icon - smooth rotation
2320
2374
  ============================================================================ */
@@ -2880,6 +2934,84 @@ body {
2880
2934
  font-style: italic;
2881
2935
  }
2882
2936
 
2937
+ .doc-screenshot-missing {
2938
+ padding: 0.75rem 1rem;
2939
+ border: 1px dashed var(--border);
2940
+ border-radius: calc(var(--radius) - 2px);
2941
+ background: var(--muted, transparent);
2942
+ color: var(--muted-foreground);
2943
+ font-size: 0.8125rem;
2944
+ }
2945
+
2946
+ .doc-screenshot-missing-label {
2947
+ font-weight: 600;
2948
+ margin-bottom: 0.25rem;
2949
+ }
2950
+
2951
+ .doc-screenshot-missing-path {
2952
+ font-family: var(--font-mono, ui-monospace, monospace);
2953
+ font-size: 0.75rem;
2954
+ word-break: break-all;
2955
+ opacity: 0.8;
2956
+ }
2957
+
2958
+ /* ============================================================================
2959
+ Documentation Entries - Visual Check
2960
+ ============================================================================ */
2961
+ .doc-visual {
2962
+ margin-bottom: 0.5rem;
2963
+ padding: 0.75rem;
2964
+ border: 1px solid var(--border);
2965
+ border-radius: calc(var(--radius) - 2px);
2966
+ background: var(--muted, transparent);
2967
+ }
2968
+
2969
+ .doc-visual:last-child {
2970
+ margin-bottom: 0;
2971
+ }
2972
+
2973
+ .doc-visual-header {
2974
+ font-size: 0.8125rem;
2975
+ font-weight: 600;
2976
+ margin-bottom: 0.5rem;
2977
+ display: flex;
2978
+ align-items: center;
2979
+ gap: 0.5rem;
2980
+ }
2981
+
2982
+ .doc-visual-status {
2983
+ font-size: 0.6875rem;
2984
+ font-family: var(--font-mono);
2985
+ font-weight: 500;
2986
+ padding: 0.125rem 0.5rem;
2987
+ border-radius: 9999px;
2988
+ background: var(--tag-bg);
2989
+ color: var(--tag-color);
2990
+ border: 1px solid var(--tag-border);
2991
+ text-transform: uppercase;
2992
+ letter-spacing: 0.04em;
2993
+ }
2994
+
2995
+ .doc-visual-grid {
2996
+ display: grid;
2997
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
2998
+ gap: 0.5rem;
2999
+ }
3000
+
3001
+ .doc-visual-item {
3002
+ display: flex;
3003
+ flex-direction: column;
3004
+ gap: 0.25rem;
3005
+ }
3006
+
3007
+ .doc-visual-label {
3008
+ font-size: 0.6875rem;
3009
+ font-weight: 600;
3010
+ color: var(--muted-foreground);
3011
+ text-transform: uppercase;
3012
+ letter-spacing: 0.04em;
3013
+ }
3014
+
2883
3015
  /* ============================================================================
2884
3016
  Documentation Entries - Custom
2885
3017
  ============================================================================ */
@@ -12857,11 +12989,18 @@ function renderTagBar(args, deps) {
12857
12989
  </div>`;
12858
12990
  }
12859
12991
 
12992
+ // src/notifiers/ansi-strip.ts
12993
+ function stripAnsi(text2) {
12994
+ return text2.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, "");
12995
+ }
12996
+
12860
12997
  // src/formatters/html/renderers/error-box.ts
12861
12998
  function renderErrorBox(args, deps) {
12862
- const body = args.stack != null ? `${deps.escapeHtml(args.message)}
12999
+ const message = stripAnsi(args.message);
13000
+ const stack = args.stack != null ? stripAnsi(args.stack) : void 0;
13001
+ const body = stack != null ? `${deps.escapeHtml(message)}
12863
13002
 
12864
- ${deps.escapeHtml(args.stack)}` : deps.escapeHtml(args.message);
13003
+ ${deps.escapeHtml(stack)}` : deps.escapeHtml(message);
12865
13004
  return `<div class="error-box">${body}</div>`;
12866
13005
  }
12867
13006
 
@@ -12874,6 +13013,7 @@ function renderAttachments(args, deps) {
12874
13013
  const isImage = att.mediaType.startsWith("image/");
12875
13014
  const isVideo = att.mediaType.startsWith("video/");
12876
13015
  const isBase64 = att.contentEncoding === "BASE64";
13016
+ const isUnreachableFsPath = deps.embedScreenshots && !isBase64 && typeof att.body === "string" && /^(?:[/\\]|[A-Za-z]:[/\\])/.test(att.body);
12877
13017
  if (isImage && deps.embedScreenshots && isBase64) {
12878
13018
  return `
12879
13019
  <div class="attachment">
@@ -12881,12 +13021,19 @@ function renderAttachments(args, deps) {
12881
13021
  <img class="attachment-image" src="data:${att.mediaType};base64,${att.body}" alt="${deps.escapeHtml(att.name)}" />
12882
13022
  </div>`;
12883
13023
  }
12884
- if (isVideo && deps.embedScreenshots) {
13024
+ if (isVideo && deps.embedScreenshots && !isUnreachableFsPath) {
12885
13025
  const src = isBase64 ? `data:${att.mediaType};base64,${att.body}` : att.body;
12886
13026
  return `
12887
13027
  <div class="attachment">
12888
13028
  ${deps.escapeHtml(att.name)}
12889
13029
  <video class="attachment-video" controls src="${deps.escapeHtml(src)}"></video>
13030
+ </div>`;
13031
+ }
13032
+ if (isUnreachableFsPath) {
13033
+ return `
13034
+ <div class="attachment attachment-unavailable">
13035
+ <div class="attachment-unavailable-label">${deps.escapeHtml(att.name)} unavailable</div>
13036
+ <div class="attachment-unavailable-path">${deps.escapeHtml(att.body)}</div>
12890
13037
  </div>`;
12891
13038
  }
12892
13039
  const href = isBase64 ? `data:${att.mediaType};base64,${att.body}` : att.body;
@@ -12969,13 +13116,40 @@ function renderDocScreenshot(entry, deps) {
12969
13116
  const alt = entry.alt ?? "Screenshot";
12970
13117
  const embedEnabled = deps.embedScreenshots ?? true;
12971
13118
  const isRemote = /^(?:https?:|data:)/i.test(entry.path);
12972
- const src = embedEnabled && !isRemote && deps.readScreenshot ? deps.readScreenshot(entry.path) ?? entry.path : entry.path;
13119
+ const embedAttempted = !isRemote && embedEnabled && !!deps.readScreenshot;
13120
+ const inlined = embedAttempted ? deps.readScreenshot(entry.path) : void 0;
13121
+ const isAbsoluteFsPath = !isRemote && /^(?:[/\\]|[A-Za-z]:[/\\])/.test(entry.path);
13122
+ if (embedAttempted && inlined === void 0 && isAbsoluteFsPath) {
13123
+ const captionHtml = entry.alt ? `<div class="doc-screenshot-caption">${deps.escapeHtml(entry.alt)}</div>` : "";
13124
+ return `<div class="doc-screenshot doc-screenshot-missing">
13125
+ <div class="doc-screenshot-missing-label">Screenshot unavailable</div>
13126
+ <div class="doc-screenshot-missing-path">${deps.escapeHtml(entry.path)}</div>
13127
+ ${captionHtml}
13128
+ </div>`;
13129
+ }
13130
+ const src = inlined ?? entry.path;
12973
13131
  return `<div class="doc-screenshot">
12974
13132
  <img src="${deps.escapeHtml(src)}" alt="${deps.escapeHtml(alt)}" class="doc-screenshot-img" />
12975
13133
  ${entry.alt ? `<div class="doc-screenshot-caption">${deps.escapeHtml(entry.alt)}</div>` : ""}
12976
13134
  </div>`;
12977
13135
  }
12978
13136
  function renderDocCustom(entry, deps) {
13137
+ if (entry.type === "visual" && entry.data && typeof entry.data === "object") {
13138
+ const data = entry.data;
13139
+ const status = typeof data.status === "string" ? data.status : "unknown";
13140
+ const baseline = typeof data.baseline === "string" ? data.baseline : void 0;
13141
+ const actual = typeof data.actual === "string" ? data.actual : void 0;
13142
+ const diff = typeof data.diff === "string" ? data.diff : void 0;
13143
+ const maybeImg = (src, label) => src ? `<div class="doc-visual-item"><div class="doc-visual-label">${deps.escapeHtml(label ?? "")}</div><img src="${deps.escapeHtml(src)}" alt="${deps.escapeHtml(label ?? "visual image")}" class="doc-screenshot-img" /></div>` : "";
13144
+ return `<div class="doc-visual">
13145
+ <div class="doc-visual-header">Visual Check <span class="doc-visual-status">${deps.escapeHtml(status)}</span></div>
13146
+ <div class="doc-visual-grid">
13147
+ ${maybeImg(baseline, "Baseline")}
13148
+ ${maybeImg(actual, "Actual")}
13149
+ ${maybeImg(diff, "Diff")}
13150
+ </div>
13151
+ </div>`;
13152
+ }
12979
13153
  const dataStr = JSON.stringify(entry.data, null, 2);
12980
13154
  return `<div class="doc-custom">
12981
13155
  <div class="doc-custom-type">${deps.escapeHtml(entry.type)}</div>
@@ -13044,8 +13218,11 @@ function renderStep(step, stepResult, index, deps) {
13044
13218
  </div>${stepDocs}`;
13045
13219
  }
13046
13220
  function renderSteps(args, deps) {
13221
+ const byStepId = new Map(
13222
+ args.stepResults.filter((sr) => typeof sr.stepId === "string" && sr.stepId.length > 0).map((sr) => [sr.stepId, sr])
13223
+ );
13047
13224
  const stepsHtml = args.steps.map((step, index) => {
13048
- const stepResult = args.stepResults.find((sr) => sr.index === index);
13225
+ const stepResult = (step.id ? byStepId.get(step.id) : void 0) ?? args.stepResults.find((sr) => sr.index === index);
13049
13226
  return renderStep(step, stepResult, index, deps);
13050
13227
  }).join("");
13051
13228
  return `<div class="steps">${stepsHtml}</div>`;
@@ -13098,6 +13275,7 @@ function renderScenario(args, deps) {
13098
13275
  const duration = tc.durationMs > 0 ? `${(tc.durationMs / 1e3).toFixed(2)}s` : "";
13099
13276
  const tags = tc.tags.map((t) => `<span class="tag">${deps.escapeHtml(t)}</span>`).join("");
13100
13277
  const tickets = (tc.story.tickets ?? []).map((t) => renderTicket(t, deps.ticketUrlTemplate, deps.escapeHtml)).join("");
13278
+ const outcomeBadge = tc.rawStatus === "timeout" || tc.rawStatus === "interrupted" ? `<span class="tag outcome-tag">${deps.escapeHtml(tc.rawStatus)}</span>` : "";
13101
13279
  const otelMeta = tc.story.meta?.otel;
13102
13280
  let traceBadge = "";
13103
13281
  if (otelMeta?.traceId) {
@@ -13165,7 +13343,7 @@ function renderScenario(args, deps) {
13165
13343
  <span class="status-icon ${statusClass}">${statusIcon2}</span>
13166
13344
  <span class="scenario-name">${deps.escapeHtml(tc.story.scenario)}</span>
13167
13345
  </div>
13168
- <div class="scenario-meta">${tags}${tickets}${sourceLink}${traceBadge}${metricBadges}</div>
13346
+ <div class="scenario-meta">${tags}${tickets}${outcomeBadge}${sourceLink}${traceBadge}${metricBadges}</div>
13169
13347
  </div>
13170
13348
  <div class="scenario-actions">
13171
13349
  <button class="copy-scenario-btn" onclick="copyScenarioAsMarkdown('scenario-${tc.id}')" aria-label="Copy scenario as markdown" title="Copy as Markdown"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button>
@@ -14169,6 +14347,9 @@ var MarkdownFormatter = class {
14169
14347
  });
14170
14348
  meta.push(`Tickets: ${ticketLinks.join(", ")}`);
14171
14349
  }
14350
+ if (tc.rawStatus === "timeout" || tc.rawStatus === "interrupted") {
14351
+ meta.push(`Outcome: \`${tc.rawStatus}\``);
14352
+ }
14172
14353
  const otelMeta = tc.story.meta?.otel;
14173
14354
  if (otelMeta?.traceId) {
14174
14355
  const traceTemplate = this.options.traceUrlTemplate;
@@ -14331,6 +14512,16 @@ var MarkdownFormatter = class {
14331
14512
  lines.push(`${indent}![${entry.alt ?? "Screenshot"}](${entry.path})`);
14332
14513
  break;
14333
14514
  case "custom":
14515
+ if (entry.type === "visual" && entry.data && typeof entry.data === "object") {
14516
+ const data = entry.data;
14517
+ const status = typeof data.status === "string" ? data.status : "unknown";
14518
+ lines.push(`${indent}**Visual Check** (${status})`);
14519
+ if (typeof data.baseline === "string") lines.push(`${indent}- Baseline: ${data.baseline}`);
14520
+ if (typeof data.actual === "string") lines.push(`${indent}- Actual: ${data.actual}`);
14521
+ if (typeof data.diff === "string") lines.push(`${indent}- Diff: ${data.diff}`);
14522
+ lines.push(`${indent}`);
14523
+ break;
14524
+ }
14334
14525
  lines.push(`${indent}**[${entry.type}]**`);
14335
14526
  lines.push(`${indent}`);
14336
14527
  lines.push(`${indent}\`\`\`json`);
@@ -16982,6 +17173,14 @@ function adaptPlaywrightRun(testResults, options = {}) {
16982
17173
  message: result.errors[0].message,
16983
17174
  stack: result.errors[0].stack
16984
17175
  } : void 0;
17176
+ const stepEvents = result.stepEvents?.length ? result.stepEvents : story.steps.map(
17177
+ (step, index) => step.durationMs !== void 0 ? {
17178
+ index,
17179
+ stepId: step.id,
17180
+ title: step.text,
17181
+ durationMs: step.durationMs
17182
+ } : void 0
17183
+ ).filter((e) => e !== void 0);
16985
17184
  testCases.push({
16986
17185
  externalId: test.titlePath().join(" > "),
16987
17186
  title: story.scenario,
@@ -16992,8 +17191,7 @@ function adaptPlaywrightRun(testResults, options = {}) {
16992
17191
  status: mapPlaywrightStatus(result.status),
16993
17192
  durationMs: result.duration,
16994
17193
  error,
16995
- stepEvents: void 0,
16996
- // Playwright could provide step info, but we don't capture it yet
17194
+ stepEvents: stepEvents.length > 0 ? stepEvents : void 0,
16997
17195
  attachments,
16998
17196
  meta: {
16999
17197
  playwrightStatus: result.status,
@@ -18013,11 +18211,6 @@ function resolveTraceUrl(template, traceId) {
18013
18211
  return template.replace(/\{traceId\}/g, traceId);
18014
18212
  }
18015
18213
 
18016
- // src/notifiers/ansi-strip.ts
18017
- function stripAnsi(text2) {
18018
- return text2.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, "");
18019
- }
18020
-
18021
18214
  // src/notifiers/slack.ts
18022
18215
  function truncate(text2, maxLen) {
18023
18216
  if (text2.length <= maxLen) return text2;