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.
@@ -1,3 +1,27 @@
1
+ /**
2
+ * Typed CI provider and canonical CI info.
3
+ *
4
+ * RawCIInfo.name = legacy transport string (kept for backward compat + schema).
5
+ * CIInfo.displayName = canonical display name for downstream consumers.
6
+ *
7
+ * All downstream code (HTML meta, notifications, history) uses CIInfo via mappers.
8
+ */
9
+
10
+ type CIProvider = "github" | "gitlab" | "circleci" | "jenkins" | "azure" | "buildkite" | "travis" | "unknown";
11
+ interface CIInfo {
12
+ provider: CIProvider;
13
+ displayName: string;
14
+ url?: string;
15
+ buildNumber?: string;
16
+ branch?: string;
17
+ commitSha?: string;
18
+ prNumber?: string;
19
+ }
20
+ /** Convert RawCIInfo (legacy transport) to canonical CIInfo. */
21
+ declare function toCIInfo(raw?: RawCIInfo): CIInfo | undefined;
22
+ /** Convert canonical CIInfo back to RawCIInfo (for serialization). */
23
+ declare function toRawCIInfo(ci?: CIInfo): RawCIInfo | undefined;
24
+
1
25
  /**
2
26
  * OTel span types for trace waterfall rendering.
3
27
  *
@@ -175,6 +199,7 @@ interface RawAttachment {
175
199
  /** Raw step event from framework (if available) */
176
200
  interface RawStepEvent {
177
201
  index?: number;
202
+ stepId?: string;
178
203
  title?: string;
179
204
  status?: RawStatus;
180
205
  durationMs?: number;
@@ -248,30 +273,6 @@ interface RawRun {
248
273
  ci?: RawCIInfo;
249
274
  }
250
275
 
251
- /**
252
- * Typed CI provider and canonical CI info.
253
- *
254
- * RawCIInfo.name = legacy transport string (kept for backward compat + schema).
255
- * CIInfo.displayName = canonical display name for downstream consumers.
256
- *
257
- * All downstream code (HTML meta, notifications, history) uses CIInfo via mappers.
258
- */
259
-
260
- type CIProvider = "github" | "gitlab" | "circleci" | "jenkins" | "azure" | "buildkite" | "travis" | "unknown";
261
- interface CIInfo {
262
- provider: CIProvider;
263
- displayName: string;
264
- url?: string;
265
- buildNumber?: string;
266
- branch?: string;
267
- commitSha?: string;
268
- prNumber?: string;
269
- }
270
- /** Convert RawCIInfo (legacy transport) to canonical CIInfo. */
271
- declare function toCIInfo(raw?: RawCIInfo): CIInfo | undefined;
272
- /** Convert canonical CIInfo back to RawCIInfo (for serialization). */
273
- declare function toRawCIInfo(ci?: CIInfo): RawCIInfo | undefined;
274
-
275
276
  /**
276
277
  * Jest Adapter - Layer 1.
277
278
  *
@@ -400,6 +401,15 @@ interface PlaywrightTestResult {
400
401
  errors: PlaywrightError[];
401
402
  attachments: PlaywrightAttachment[];
402
403
  retry: number;
404
+ /** Optional step events if caller captures Playwright step timing. */
405
+ stepEvents?: Array<{
406
+ index?: number;
407
+ stepId?: string;
408
+ title?: string;
409
+ status?: RawStatus;
410
+ durationMs?: number;
411
+ errorMessage?: string;
412
+ }>;
403
413
  }
404
414
  /** Playwright test case annotation */
405
415
  interface PlaywrightAnnotation {
@@ -1,3 +1,27 @@
1
+ /**
2
+ * Typed CI provider and canonical CI info.
3
+ *
4
+ * RawCIInfo.name = legacy transport string (kept for backward compat + schema).
5
+ * CIInfo.displayName = canonical display name for downstream consumers.
6
+ *
7
+ * All downstream code (HTML meta, notifications, history) uses CIInfo via mappers.
8
+ */
9
+
10
+ type CIProvider = "github" | "gitlab" | "circleci" | "jenkins" | "azure" | "buildkite" | "travis" | "unknown";
11
+ interface CIInfo {
12
+ provider: CIProvider;
13
+ displayName: string;
14
+ url?: string;
15
+ buildNumber?: string;
16
+ branch?: string;
17
+ commitSha?: string;
18
+ prNumber?: string;
19
+ }
20
+ /** Convert RawCIInfo (legacy transport) to canonical CIInfo. */
21
+ declare function toCIInfo(raw?: RawCIInfo): CIInfo | undefined;
22
+ /** Convert canonical CIInfo back to RawCIInfo (for serialization). */
23
+ declare function toRawCIInfo(ci?: CIInfo): RawCIInfo | undefined;
24
+
1
25
  /**
2
26
  * OTel span types for trace waterfall rendering.
3
27
  *
@@ -175,6 +199,7 @@ interface RawAttachment {
175
199
  /** Raw step event from framework (if available) */
176
200
  interface RawStepEvent {
177
201
  index?: number;
202
+ stepId?: string;
178
203
  title?: string;
179
204
  status?: RawStatus;
180
205
  durationMs?: number;
@@ -248,30 +273,6 @@ interface RawRun {
248
273
  ci?: RawCIInfo;
249
274
  }
250
275
 
251
- /**
252
- * Typed CI provider and canonical CI info.
253
- *
254
- * RawCIInfo.name = legacy transport string (kept for backward compat + schema).
255
- * CIInfo.displayName = canonical display name for downstream consumers.
256
- *
257
- * All downstream code (HTML meta, notifications, history) uses CIInfo via mappers.
258
- */
259
-
260
- type CIProvider = "github" | "gitlab" | "circleci" | "jenkins" | "azure" | "buildkite" | "travis" | "unknown";
261
- interface CIInfo {
262
- provider: CIProvider;
263
- displayName: string;
264
- url?: string;
265
- buildNumber?: string;
266
- branch?: string;
267
- commitSha?: string;
268
- prNumber?: string;
269
- }
270
- /** Convert RawCIInfo (legacy transport) to canonical CIInfo. */
271
- declare function toCIInfo(raw?: RawCIInfo): CIInfo | undefined;
272
- /** Convert canonical CIInfo back to RawCIInfo (for serialization). */
273
- declare function toRawCIInfo(ci?: CIInfo): RawCIInfo | undefined;
274
-
275
276
  /**
276
277
  * Jest Adapter - Layer 1.
277
278
  *
@@ -400,6 +401,15 @@ interface PlaywrightTestResult {
400
401
  errors: PlaywrightError[];
401
402
  attachments: PlaywrightAttachment[];
402
403
  retry: number;
404
+ /** Optional step events if caller captures Playwright step timing. */
405
+ stepEvents?: Array<{
406
+ index?: number;
407
+ stepId?: string;
408
+ title?: string;
409
+ status?: RawStatus;
410
+ durationMs?: number;
411
+ errorMessage?: string;
412
+ }>;
403
413
  }
404
414
  /** Playwright test case annotation */
405
415
  interface PlaywrightAnnotation {
package/dist/index.cjs CHANGED
@@ -158,6 +158,7 @@ function deriveStepResults(steps, scenarioStatus, error) {
158
158
  if (scenarioStatus === "passed") {
159
159
  return steps.map((_, index) => ({
160
160
  index,
161
+ stepId: steps[index].id,
161
162
  status: "passed",
162
163
  durationMs: 0
163
164
  }));
@@ -165,6 +166,7 @@ function deriveStepResults(steps, scenarioStatus, error) {
165
166
  if (scenarioStatus === "skipped" || scenarioStatus === "pending") {
166
167
  return steps.map((_, index) => ({
167
168
  index,
169
+ stepId: steps[index].id,
168
170
  status: scenarioStatus,
169
171
  durationMs: 0
170
172
  }));
@@ -172,16 +174,17 @@ function deriveStepResults(steps, scenarioStatus, error) {
172
174
  const failingIndex = findFailingStepIndex(steps, error);
173
175
  return steps.map((_, index) => {
174
176
  if (index < failingIndex) {
175
- return { index, status: "passed", durationMs: 0 };
177
+ return { index, stepId: steps[index].id, status: "passed", durationMs: 0 };
176
178
  } else if (index === failingIndex) {
177
179
  return {
178
180
  index,
181
+ stepId: steps[index].id,
179
182
  status: "failed",
180
183
  durationMs: 0,
181
184
  errorMessage: error?.message
182
185
  };
183
186
  } else {
184
- return { index, status: "skipped", durationMs: 0 };
187
+ return { index, stepId: steps[index].id, status: "skipped", durationMs: 0 };
185
188
  }
186
189
  });
187
190
  }
@@ -203,18 +206,23 @@ function mergeStepResults(derived, events) {
203
206
  return derived;
204
207
  }
205
208
  const actualByIndex = /* @__PURE__ */ new Map();
209
+ const actualByStepId = /* @__PURE__ */ new Map();
206
210
  for (const event of events) {
207
211
  if (event.index !== void 0) {
208
212
  actualByIndex.set(event.index, event);
209
213
  }
214
+ if (event.stepId) {
215
+ actualByStepId.set(event.stepId, event);
216
+ }
210
217
  }
211
218
  return derived.map((step) => {
212
- const actual = actualByIndex.get(step.index);
219
+ const actual = (step.stepId ? actualByStepId.get(step.stepId) : void 0) ?? actualByIndex.get(step.index);
213
220
  if (!actual) {
214
221
  return step;
215
222
  }
216
223
  return {
217
224
  index: step.index,
225
+ stepId: step.stepId ?? actual.stepId,
218
226
  status: normalizeStepStatus(actual.status) ?? step.status,
219
227
  durationMs: actual.durationMs ?? step.durationMs,
220
228
  errorMessage: actual.errorMessage ?? step.errorMessage
@@ -348,6 +356,7 @@ function canonicalizeTestCase(raw, options, projectRoot) {
348
356
  derivedSteps,
349
357
  raw.stepEvents?.map((e) => ({
350
358
  index: e.index,
359
+ stepId: e.stepId,
351
360
  status: e.status,
352
361
  durationMs: e.durationMs,
353
362
  errorMessage: e.errorMessage
@@ -369,6 +378,7 @@ function canonicalizeTestCase(raw, options, projectRoot) {
369
378
  sourceFile,
370
379
  sourceLine: raw.sourceLine ?? 1,
371
380
  status,
381
+ rawStatus: raw.status,
372
382
  durationMs: raw.durationMs ?? 0,
373
383
  errorMessage: raw.error?.message,
374
384
  errorStack: raw.error?.stack,
@@ -736,19 +746,34 @@ ${doc.markdown}`,
736
746
  }
737
747
  };
738
748
  }
739
- case "tag":
749
+ case "custom":
750
+ if (doc.type === "visual" && doc.data && typeof doc.data === "object") {
751
+ const data = doc.data;
752
+ const status = typeof data.status === "string" ? data.status : "unknown";
753
+ const lines = [`Visual Check (${status})`];
754
+ if (typeof data.baseline === "string") lines.push(`Baseline: ${data.baseline}`);
755
+ if (typeof data.actual === "string") lines.push(`Actual: ${data.actual}`);
756
+ if (typeof data.diff === "string") lines.push(`Diff: ${data.diff}`);
757
+ return {
758
+ doc_string: {
759
+ content: lines.join("\n"),
760
+ content_type: "text/plain",
761
+ line: 0
762
+ }
763
+ };
764
+ }
740
765
  return {
741
766
  doc_string: {
742
- content: doc.names.map((n) => `@${n}`).join(" "),
743
- content_type: "text/plain",
767
+ content: JSON.stringify(doc.data, null, 2),
768
+ content_type: "application/json",
744
769
  line: 0
745
770
  }
746
771
  };
747
- case "custom":
772
+ case "tag":
748
773
  return {
749
774
  doc_string: {
750
- content: JSON.stringify(doc.data, null, 2),
751
- content_type: "application/json",
775
+ content: doc.names.map((n) => `@${n}`).join(" "),
776
+ content_type: "text/plain",
752
777
  line: 0
753
778
  }
754
779
  };
@@ -2266,6 +2291,14 @@ body {
2266
2291
  font-family: var(--font-mono);
2267
2292
  }
2268
2293
 
2294
+ .outcome-tag {
2295
+ background: var(--status-fail-bg, color-mix(in srgb, var(--destructive, #ef4444) 12%, transparent));
2296
+ color: var(--destructive, #b91c1c);
2297
+ border-color: color-mix(in srgb, var(--destructive, #ef4444) 35%, transparent);
2298
+ text-transform: uppercase;
2299
+ letter-spacing: 0.04em;
2300
+ }
2301
+
2269
2302
  .scenario-duration {
2270
2303
  font-size: 0.75rem;
2271
2304
  color: var(--muted-foreground);
@@ -2422,6 +2455,27 @@ body {
2422
2455
  border: 1px solid var(--border);
2423
2456
  }
2424
2457
 
2458
+ .attachment-unavailable {
2459
+ padding: 0.75rem 1rem;
2460
+ border: 1px dashed var(--border);
2461
+ border-radius: calc(var(--radius) - 2px);
2462
+ background: var(--muted, transparent);
2463
+ color: var(--muted-foreground);
2464
+ font-size: 0.8125rem;
2465
+ }
2466
+
2467
+ .attachment-unavailable-label {
2468
+ font-weight: 600;
2469
+ margin-bottom: 0.25rem;
2470
+ }
2471
+
2472
+ .attachment-unavailable-path {
2473
+ font-family: var(--font-mono, ui-monospace, monospace);
2474
+ font-size: 0.75rem;
2475
+ word-break: break-all;
2476
+ opacity: 0.8;
2477
+ }
2478
+
2425
2479
  /* ============================================================================
2426
2480
  Chevron Icon - smooth rotation
2427
2481
  ============================================================================ */
@@ -2987,6 +3041,84 @@ body {
2987
3041
  font-style: italic;
2988
3042
  }
2989
3043
 
3044
+ .doc-screenshot-missing {
3045
+ padding: 0.75rem 1rem;
3046
+ border: 1px dashed var(--border);
3047
+ border-radius: calc(var(--radius) - 2px);
3048
+ background: var(--muted, transparent);
3049
+ color: var(--muted-foreground);
3050
+ font-size: 0.8125rem;
3051
+ }
3052
+
3053
+ .doc-screenshot-missing-label {
3054
+ font-weight: 600;
3055
+ margin-bottom: 0.25rem;
3056
+ }
3057
+
3058
+ .doc-screenshot-missing-path {
3059
+ font-family: var(--font-mono, ui-monospace, monospace);
3060
+ font-size: 0.75rem;
3061
+ word-break: break-all;
3062
+ opacity: 0.8;
3063
+ }
3064
+
3065
+ /* ============================================================================
3066
+ Documentation Entries - Visual Check
3067
+ ============================================================================ */
3068
+ .doc-visual {
3069
+ margin-bottom: 0.5rem;
3070
+ padding: 0.75rem;
3071
+ border: 1px solid var(--border);
3072
+ border-radius: calc(var(--radius) - 2px);
3073
+ background: var(--muted, transparent);
3074
+ }
3075
+
3076
+ .doc-visual:last-child {
3077
+ margin-bottom: 0;
3078
+ }
3079
+
3080
+ .doc-visual-header {
3081
+ font-size: 0.8125rem;
3082
+ font-weight: 600;
3083
+ margin-bottom: 0.5rem;
3084
+ display: flex;
3085
+ align-items: center;
3086
+ gap: 0.5rem;
3087
+ }
3088
+
3089
+ .doc-visual-status {
3090
+ font-size: 0.6875rem;
3091
+ font-family: var(--font-mono);
3092
+ font-weight: 500;
3093
+ padding: 0.125rem 0.5rem;
3094
+ border-radius: 9999px;
3095
+ background: var(--tag-bg);
3096
+ color: var(--tag-color);
3097
+ border: 1px solid var(--tag-border);
3098
+ text-transform: uppercase;
3099
+ letter-spacing: 0.04em;
3100
+ }
3101
+
3102
+ .doc-visual-grid {
3103
+ display: grid;
3104
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
3105
+ gap: 0.5rem;
3106
+ }
3107
+
3108
+ .doc-visual-item {
3109
+ display: flex;
3110
+ flex-direction: column;
3111
+ gap: 0.25rem;
3112
+ }
3113
+
3114
+ .doc-visual-label {
3115
+ font-size: 0.6875rem;
3116
+ font-weight: 600;
3117
+ color: var(--muted-foreground);
3118
+ text-transform: uppercase;
3119
+ letter-spacing: 0.04em;
3120
+ }
3121
+
2990
3122
  /* ============================================================================
2991
3123
  Documentation Entries - Custom
2992
3124
  ============================================================================ */
@@ -12964,11 +13096,18 @@ function renderTagBar(args, deps) {
12964
13096
  </div>`;
12965
13097
  }
12966
13098
 
13099
+ // src/notifiers/ansi-strip.ts
13100
+ function stripAnsi(text2) {
13101
+ return text2.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, "");
13102
+ }
13103
+
12967
13104
  // src/formatters/html/renderers/error-box.ts
12968
13105
  function renderErrorBox(args, deps) {
12969
- const body = args.stack != null ? `${deps.escapeHtml(args.message)}
13106
+ const message = stripAnsi(args.message);
13107
+ const stack = args.stack != null ? stripAnsi(args.stack) : void 0;
13108
+ const body = stack != null ? `${deps.escapeHtml(message)}
12970
13109
 
12971
- ${deps.escapeHtml(args.stack)}` : deps.escapeHtml(args.message);
13110
+ ${deps.escapeHtml(stack)}` : deps.escapeHtml(message);
12972
13111
  return `<div class="error-box">${body}</div>`;
12973
13112
  }
12974
13113
 
@@ -12981,6 +13120,7 @@ function renderAttachments(args, deps) {
12981
13120
  const isImage = att.mediaType.startsWith("image/");
12982
13121
  const isVideo = att.mediaType.startsWith("video/");
12983
13122
  const isBase64 = att.contentEncoding === "BASE64";
13123
+ const isUnreachableFsPath = deps.embedScreenshots && !isBase64 && typeof att.body === "string" && /^(?:[/\\]|[A-Za-z]:[/\\])/.test(att.body);
12984
13124
  if (isImage && deps.embedScreenshots && isBase64) {
12985
13125
  return `
12986
13126
  <div class="attachment">
@@ -12988,12 +13128,19 @@ function renderAttachments(args, deps) {
12988
13128
  <img class="attachment-image" src="data:${att.mediaType};base64,${att.body}" alt="${deps.escapeHtml(att.name)}" />
12989
13129
  </div>`;
12990
13130
  }
12991
- if (isVideo && deps.embedScreenshots) {
13131
+ if (isVideo && deps.embedScreenshots && !isUnreachableFsPath) {
12992
13132
  const src = isBase64 ? `data:${att.mediaType};base64,${att.body}` : att.body;
12993
13133
  return `
12994
13134
  <div class="attachment">
12995
13135
  ${deps.escapeHtml(att.name)}
12996
13136
  <video class="attachment-video" controls src="${deps.escapeHtml(src)}"></video>
13137
+ </div>`;
13138
+ }
13139
+ if (isUnreachableFsPath) {
13140
+ return `
13141
+ <div class="attachment attachment-unavailable">
13142
+ <div class="attachment-unavailable-label">${deps.escapeHtml(att.name)} unavailable</div>
13143
+ <div class="attachment-unavailable-path">${deps.escapeHtml(att.body)}</div>
12997
13144
  </div>`;
12998
13145
  }
12999
13146
  const href = isBase64 ? `data:${att.mediaType};base64,${att.body}` : att.body;
@@ -13076,13 +13223,40 @@ function renderDocScreenshot(entry, deps) {
13076
13223
  const alt = entry.alt ?? "Screenshot";
13077
13224
  const embedEnabled = deps.embedScreenshots ?? true;
13078
13225
  const isRemote = /^(?:https?:|data:)/i.test(entry.path);
13079
- const src = embedEnabled && !isRemote && deps.readScreenshot ? deps.readScreenshot(entry.path) ?? entry.path : entry.path;
13226
+ const embedAttempted = !isRemote && embedEnabled && !!deps.readScreenshot;
13227
+ const inlined = embedAttempted ? deps.readScreenshot(entry.path) : void 0;
13228
+ const isAbsoluteFsPath = !isRemote && /^(?:[/\\]|[A-Za-z]:[/\\])/.test(entry.path);
13229
+ if (embedAttempted && inlined === void 0 && isAbsoluteFsPath) {
13230
+ const captionHtml = entry.alt ? `<div class="doc-screenshot-caption">${deps.escapeHtml(entry.alt)}</div>` : "";
13231
+ return `<div class="doc-screenshot doc-screenshot-missing">
13232
+ <div class="doc-screenshot-missing-label">Screenshot unavailable</div>
13233
+ <div class="doc-screenshot-missing-path">${deps.escapeHtml(entry.path)}</div>
13234
+ ${captionHtml}
13235
+ </div>`;
13236
+ }
13237
+ const src = inlined ?? entry.path;
13080
13238
  return `<div class="doc-screenshot">
13081
13239
  <img src="${deps.escapeHtml(src)}" alt="${deps.escapeHtml(alt)}" class="doc-screenshot-img" />
13082
13240
  ${entry.alt ? `<div class="doc-screenshot-caption">${deps.escapeHtml(entry.alt)}</div>` : ""}
13083
13241
  </div>`;
13084
13242
  }
13085
13243
  function renderDocCustom(entry, deps) {
13244
+ if (entry.type === "visual" && entry.data && typeof entry.data === "object") {
13245
+ const data = entry.data;
13246
+ const status = typeof data.status === "string" ? data.status : "unknown";
13247
+ const baseline = typeof data.baseline === "string" ? data.baseline : void 0;
13248
+ const actual = typeof data.actual === "string" ? data.actual : void 0;
13249
+ const diff = typeof data.diff === "string" ? data.diff : void 0;
13250
+ 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>` : "";
13251
+ return `<div class="doc-visual">
13252
+ <div class="doc-visual-header">Visual Check <span class="doc-visual-status">${deps.escapeHtml(status)}</span></div>
13253
+ <div class="doc-visual-grid">
13254
+ ${maybeImg(baseline, "Baseline")}
13255
+ ${maybeImg(actual, "Actual")}
13256
+ ${maybeImg(diff, "Diff")}
13257
+ </div>
13258
+ </div>`;
13259
+ }
13086
13260
  const dataStr = JSON.stringify(entry.data, null, 2);
13087
13261
  return `<div class="doc-custom">
13088
13262
  <div class="doc-custom-type">${deps.escapeHtml(entry.type)}</div>
@@ -13151,8 +13325,11 @@ function renderStep(step, stepResult, index, deps) {
13151
13325
  </div>${stepDocs}`;
13152
13326
  }
13153
13327
  function renderSteps(args, deps) {
13328
+ const byStepId = new Map(
13329
+ args.stepResults.filter((sr) => typeof sr.stepId === "string" && sr.stepId.length > 0).map((sr) => [sr.stepId, sr])
13330
+ );
13154
13331
  const stepsHtml = args.steps.map((step, index) => {
13155
- const stepResult = args.stepResults.find((sr) => sr.index === index);
13332
+ const stepResult = (step.id ? byStepId.get(step.id) : void 0) ?? args.stepResults.find((sr) => sr.index === index);
13156
13333
  return renderStep(step, stepResult, index, deps);
13157
13334
  }).join("");
13158
13335
  return `<div class="steps">${stepsHtml}</div>`;
@@ -13205,6 +13382,7 @@ function renderScenario(args, deps) {
13205
13382
  const duration = tc.durationMs > 0 ? `${(tc.durationMs / 1e3).toFixed(2)}s` : "";
13206
13383
  const tags = tc.tags.map((t) => `<span class="tag">${deps.escapeHtml(t)}</span>`).join("");
13207
13384
  const tickets = (tc.story.tickets ?? []).map((t) => renderTicket(t, deps.ticketUrlTemplate, deps.escapeHtml)).join("");
13385
+ const outcomeBadge = tc.rawStatus === "timeout" || tc.rawStatus === "interrupted" ? `<span class="tag outcome-tag">${deps.escapeHtml(tc.rawStatus)}</span>` : "";
13208
13386
  const otelMeta = tc.story.meta?.otel;
13209
13387
  let traceBadge = "";
13210
13388
  if (otelMeta?.traceId) {
@@ -13272,7 +13450,7 @@ function renderScenario(args, deps) {
13272
13450
  <span class="status-icon ${statusClass}">${statusIcon2}</span>
13273
13451
  <span class="scenario-name">${deps.escapeHtml(tc.story.scenario)}</span>
13274
13452
  </div>
13275
- <div class="scenario-meta">${tags}${tickets}${sourceLink}${traceBadge}${metricBadges}</div>
13453
+ <div class="scenario-meta">${tags}${tickets}${outcomeBadge}${sourceLink}${traceBadge}${metricBadges}</div>
13276
13454
  </div>
13277
13455
  <div class="scenario-actions">
13278
13456
  <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>
@@ -14276,6 +14454,9 @@ var MarkdownFormatter = class {
14276
14454
  });
14277
14455
  meta.push(`Tickets: ${ticketLinks.join(", ")}`);
14278
14456
  }
14457
+ if (tc.rawStatus === "timeout" || tc.rawStatus === "interrupted") {
14458
+ meta.push(`Outcome: \`${tc.rawStatus}\``);
14459
+ }
14279
14460
  const otelMeta = tc.story.meta?.otel;
14280
14461
  if (otelMeta?.traceId) {
14281
14462
  const traceTemplate = this.options.traceUrlTemplate;
@@ -14438,6 +14619,16 @@ var MarkdownFormatter = class {
14438
14619
  lines.push(`${indent}![${entry.alt ?? "Screenshot"}](${entry.path})`);
14439
14620
  break;
14440
14621
  case "custom":
14622
+ if (entry.type === "visual" && entry.data && typeof entry.data === "object") {
14623
+ const data = entry.data;
14624
+ const status = typeof data.status === "string" ? data.status : "unknown";
14625
+ lines.push(`${indent}**Visual Check** (${status})`);
14626
+ if (typeof data.baseline === "string") lines.push(`${indent}- Baseline: ${data.baseline}`);
14627
+ if (typeof data.actual === "string") lines.push(`${indent}- Actual: ${data.actual}`);
14628
+ if (typeof data.diff === "string") lines.push(`${indent}- Diff: ${data.diff}`);
14629
+ lines.push(`${indent}`);
14630
+ break;
14631
+ }
14441
14632
  lines.push(`${indent}**[${entry.type}]**`);
14442
14633
  lines.push(`${indent}`);
14443
14634
  lines.push(`${indent}\`\`\`json`);
@@ -17089,6 +17280,14 @@ function adaptPlaywrightRun(testResults, options = {}) {
17089
17280
  message: result.errors[0].message,
17090
17281
  stack: result.errors[0].stack
17091
17282
  } : void 0;
17283
+ const stepEvents = result.stepEvents?.length ? result.stepEvents : story.steps.map(
17284
+ (step, index) => step.durationMs !== void 0 ? {
17285
+ index,
17286
+ stepId: step.id,
17287
+ title: step.text,
17288
+ durationMs: step.durationMs
17289
+ } : void 0
17290
+ ).filter((e) => e !== void 0);
17092
17291
  testCases.push({
17093
17292
  externalId: test.titlePath().join(" > "),
17094
17293
  title: story.scenario,
@@ -17099,8 +17298,7 @@ function adaptPlaywrightRun(testResults, options = {}) {
17099
17298
  status: mapPlaywrightStatus(result.status),
17100
17299
  durationMs: result.duration,
17101
17300
  error,
17102
- stepEvents: void 0,
17103
- // Playwright could provide step info, but we don't capture it yet
17301
+ stepEvents: stepEvents.length > 0 ? stepEvents : void 0,
17104
17302
  attachments,
17105
17303
  meta: {
17106
17304
  playwrightStatus: result.status,
@@ -18121,11 +18319,6 @@ function resolveTraceUrl(template, traceId) {
18121
18319
  return template.replace(/\{traceId\}/g, traceId);
18122
18320
  }
18123
18321
 
18124
- // src/notifiers/ansi-strip.ts
18125
- function stripAnsi(text2) {
18126
- return text2.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, "");
18127
- }
18128
-
18129
18322
  // src/notifiers/slack.ts
18130
18323
  function truncate(text2, maxLen) {
18131
18324
  if (text2.length <= maxLen) return text2;