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/cli.js CHANGED
@@ -615,6 +615,7 @@ function deriveStepResults(steps, scenarioStatus, error) {
615
615
  if (scenarioStatus === "passed") {
616
616
  return steps.map((_, index) => ({
617
617
  index,
618
+ stepId: steps[index].id,
618
619
  status: "passed",
619
620
  durationMs: 0
620
621
  }));
@@ -622,6 +623,7 @@ function deriveStepResults(steps, scenarioStatus, error) {
622
623
  if (scenarioStatus === "skipped" || scenarioStatus === "pending") {
623
624
  return steps.map((_, index) => ({
624
625
  index,
626
+ stepId: steps[index].id,
625
627
  status: scenarioStatus,
626
628
  durationMs: 0
627
629
  }));
@@ -629,16 +631,17 @@ function deriveStepResults(steps, scenarioStatus, error) {
629
631
  const failingIndex = findFailingStepIndex(steps, error);
630
632
  return steps.map((_, index) => {
631
633
  if (index < failingIndex) {
632
- return { index, status: "passed", durationMs: 0 };
634
+ return { index, stepId: steps[index].id, status: "passed", durationMs: 0 };
633
635
  } else if (index === failingIndex) {
634
636
  return {
635
637
  index,
638
+ stepId: steps[index].id,
636
639
  status: "failed",
637
640
  durationMs: 0,
638
641
  errorMessage: error?.message
639
642
  };
640
643
  } else {
641
- return { index, status: "skipped", durationMs: 0 };
644
+ return { index, stepId: steps[index].id, status: "skipped", durationMs: 0 };
642
645
  }
643
646
  });
644
647
  }
@@ -660,18 +663,23 @@ function mergeStepResults(derived, events) {
660
663
  return derived;
661
664
  }
662
665
  const actualByIndex = /* @__PURE__ */ new Map();
666
+ const actualByStepId = /* @__PURE__ */ new Map();
663
667
  for (const event of events) {
664
668
  if (event.index !== void 0) {
665
669
  actualByIndex.set(event.index, event);
666
670
  }
671
+ if (event.stepId) {
672
+ actualByStepId.set(event.stepId, event);
673
+ }
667
674
  }
668
675
  return derived.map((step) => {
669
- const actual = actualByIndex.get(step.index);
676
+ const actual = (step.stepId ? actualByStepId.get(step.stepId) : void 0) ?? actualByIndex.get(step.index);
670
677
  if (!actual) {
671
678
  return step;
672
679
  }
673
680
  return {
674
681
  index: step.index,
682
+ stepId: step.stepId ?? actual.stepId,
675
683
  status: normalizeStepStatus(actual.status) ?? step.status,
676
684
  durationMs: actual.durationMs ?? step.durationMs,
677
685
  errorMessage: actual.errorMessage ?? step.errorMessage
@@ -805,6 +813,7 @@ function canonicalizeTestCase(raw, options, projectRoot) {
805
813
  derivedSteps,
806
814
  raw.stepEvents?.map((e) => ({
807
815
  index: e.index,
816
+ stepId: e.stepId,
808
817
  status: e.status,
809
818
  durationMs: e.durationMs,
810
819
  errorMessage: e.errorMessage
@@ -826,6 +835,7 @@ function canonicalizeTestCase(raw, options, projectRoot) {
826
835
  sourceFile,
827
836
  sourceLine: raw.sourceLine ?? 1,
828
837
  status,
838
+ rawStatus: raw.status,
829
839
  durationMs: raw.durationMs ?? 0,
830
840
  errorMessage: raw.error?.message,
831
841
  errorStack: raw.error?.stack,
@@ -1306,19 +1316,34 @@ ${doc.markdown}`,
1306
1316
  }
1307
1317
  };
1308
1318
  }
1309
- case "tag":
1319
+ case "custom":
1320
+ if (doc.type === "visual" && doc.data && typeof doc.data === "object") {
1321
+ const data = doc.data;
1322
+ const status = typeof data.status === "string" ? data.status : "unknown";
1323
+ const lines = [`Visual Check (${status})`];
1324
+ if (typeof data.baseline === "string") lines.push(`Baseline: ${data.baseline}`);
1325
+ if (typeof data.actual === "string") lines.push(`Actual: ${data.actual}`);
1326
+ if (typeof data.diff === "string") lines.push(`Diff: ${data.diff}`);
1327
+ return {
1328
+ doc_string: {
1329
+ content: lines.join("\n"),
1330
+ content_type: "text/plain",
1331
+ line: 0
1332
+ }
1333
+ };
1334
+ }
1310
1335
  return {
1311
1336
  doc_string: {
1312
- content: doc.names.map((n) => `@${n}`).join(" "),
1313
- content_type: "text/plain",
1337
+ content: JSON.stringify(doc.data, null, 2),
1338
+ content_type: "application/json",
1314
1339
  line: 0
1315
1340
  }
1316
1341
  };
1317
- case "custom":
1342
+ case "tag":
1318
1343
  return {
1319
1344
  doc_string: {
1320
- content: JSON.stringify(doc.data, null, 2),
1321
- content_type: "application/json",
1345
+ content: doc.names.map((n) => `@${n}`).join(" "),
1346
+ content_type: "text/plain",
1322
1347
  line: 0
1323
1348
  }
1324
1349
  };
@@ -2836,6 +2861,14 @@ body {
2836
2861
  font-family: var(--font-mono);
2837
2862
  }
2838
2863
 
2864
+ .outcome-tag {
2865
+ background: var(--status-fail-bg, color-mix(in srgb, var(--destructive, #ef4444) 12%, transparent));
2866
+ color: var(--destructive, #b91c1c);
2867
+ border-color: color-mix(in srgb, var(--destructive, #ef4444) 35%, transparent);
2868
+ text-transform: uppercase;
2869
+ letter-spacing: 0.04em;
2870
+ }
2871
+
2839
2872
  .scenario-duration {
2840
2873
  font-size: 0.75rem;
2841
2874
  color: var(--muted-foreground);
@@ -2992,6 +3025,27 @@ body {
2992
3025
  border: 1px solid var(--border);
2993
3026
  }
2994
3027
 
3028
+ .attachment-unavailable {
3029
+ padding: 0.75rem 1rem;
3030
+ border: 1px dashed var(--border);
3031
+ border-radius: calc(var(--radius) - 2px);
3032
+ background: var(--muted, transparent);
3033
+ color: var(--muted-foreground);
3034
+ font-size: 0.8125rem;
3035
+ }
3036
+
3037
+ .attachment-unavailable-label {
3038
+ font-weight: 600;
3039
+ margin-bottom: 0.25rem;
3040
+ }
3041
+
3042
+ .attachment-unavailable-path {
3043
+ font-family: var(--font-mono, ui-monospace, monospace);
3044
+ font-size: 0.75rem;
3045
+ word-break: break-all;
3046
+ opacity: 0.8;
3047
+ }
3048
+
2995
3049
  /* ============================================================================
2996
3050
  Chevron Icon - smooth rotation
2997
3051
  ============================================================================ */
@@ -3557,6 +3611,84 @@ body {
3557
3611
  font-style: italic;
3558
3612
  }
3559
3613
 
3614
+ .doc-screenshot-missing {
3615
+ padding: 0.75rem 1rem;
3616
+ border: 1px dashed var(--border);
3617
+ border-radius: calc(var(--radius) - 2px);
3618
+ background: var(--muted, transparent);
3619
+ color: var(--muted-foreground);
3620
+ font-size: 0.8125rem;
3621
+ }
3622
+
3623
+ .doc-screenshot-missing-label {
3624
+ font-weight: 600;
3625
+ margin-bottom: 0.25rem;
3626
+ }
3627
+
3628
+ .doc-screenshot-missing-path {
3629
+ font-family: var(--font-mono, ui-monospace, monospace);
3630
+ font-size: 0.75rem;
3631
+ word-break: break-all;
3632
+ opacity: 0.8;
3633
+ }
3634
+
3635
+ /* ============================================================================
3636
+ Documentation Entries - Visual Check
3637
+ ============================================================================ */
3638
+ .doc-visual {
3639
+ margin-bottom: 0.5rem;
3640
+ padding: 0.75rem;
3641
+ border: 1px solid var(--border);
3642
+ border-radius: calc(var(--radius) - 2px);
3643
+ background: var(--muted, transparent);
3644
+ }
3645
+
3646
+ .doc-visual:last-child {
3647
+ margin-bottom: 0;
3648
+ }
3649
+
3650
+ .doc-visual-header {
3651
+ font-size: 0.8125rem;
3652
+ font-weight: 600;
3653
+ margin-bottom: 0.5rem;
3654
+ display: flex;
3655
+ align-items: center;
3656
+ gap: 0.5rem;
3657
+ }
3658
+
3659
+ .doc-visual-status {
3660
+ font-size: 0.6875rem;
3661
+ font-family: var(--font-mono);
3662
+ font-weight: 500;
3663
+ padding: 0.125rem 0.5rem;
3664
+ border-radius: 9999px;
3665
+ background: var(--tag-bg);
3666
+ color: var(--tag-color);
3667
+ border: 1px solid var(--tag-border);
3668
+ text-transform: uppercase;
3669
+ letter-spacing: 0.04em;
3670
+ }
3671
+
3672
+ .doc-visual-grid {
3673
+ display: grid;
3674
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
3675
+ gap: 0.5rem;
3676
+ }
3677
+
3678
+ .doc-visual-item {
3679
+ display: flex;
3680
+ flex-direction: column;
3681
+ gap: 0.25rem;
3682
+ }
3683
+
3684
+ .doc-visual-label {
3685
+ font-size: 0.6875rem;
3686
+ font-weight: 600;
3687
+ color: var(--muted-foreground);
3688
+ text-transform: uppercase;
3689
+ letter-spacing: 0.04em;
3690
+ }
3691
+
3560
3692
  /* ============================================================================
3561
3693
  Documentation Entries - Custom
3562
3694
  ============================================================================ */
@@ -13531,11 +13663,18 @@ function renderTagBar(args, deps) {
13531
13663
  </div>`;
13532
13664
  }
13533
13665
 
13666
+ // src/notifiers/ansi-strip.ts
13667
+ function stripAnsi(text2) {
13668
+ return text2.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, "");
13669
+ }
13670
+
13534
13671
  // src/formatters/html/renderers/error-box.ts
13535
13672
  function renderErrorBox(args, deps) {
13536
- const body = args.stack != null ? `${deps.escapeHtml(args.message)}
13673
+ const message = stripAnsi(args.message);
13674
+ const stack = args.stack != null ? stripAnsi(args.stack) : void 0;
13675
+ const body = stack != null ? `${deps.escapeHtml(message)}
13537
13676
 
13538
- ${deps.escapeHtml(args.stack)}` : deps.escapeHtml(args.message);
13677
+ ${deps.escapeHtml(stack)}` : deps.escapeHtml(message);
13539
13678
  return `<div class="error-box">${body}</div>`;
13540
13679
  }
13541
13680
 
@@ -13548,6 +13687,7 @@ function renderAttachments(args, deps) {
13548
13687
  const isImage = att.mediaType.startsWith("image/");
13549
13688
  const isVideo = att.mediaType.startsWith("video/");
13550
13689
  const isBase64 = att.contentEncoding === "BASE64";
13690
+ const isUnreachableFsPath = deps.embedScreenshots && !isBase64 && typeof att.body === "string" && /^(?:[/\\]|[A-Za-z]:[/\\])/.test(att.body);
13551
13691
  if (isImage && deps.embedScreenshots && isBase64) {
13552
13692
  return `
13553
13693
  <div class="attachment">
@@ -13555,12 +13695,19 @@ function renderAttachments(args, deps) {
13555
13695
  <img class="attachment-image" src="data:${att.mediaType};base64,${att.body}" alt="${deps.escapeHtml(att.name)}" />
13556
13696
  </div>`;
13557
13697
  }
13558
- if (isVideo && deps.embedScreenshots) {
13698
+ if (isVideo && deps.embedScreenshots && !isUnreachableFsPath) {
13559
13699
  const src = isBase64 ? `data:${att.mediaType};base64,${att.body}` : att.body;
13560
13700
  return `
13561
13701
  <div class="attachment">
13562
13702
  ${deps.escapeHtml(att.name)}
13563
13703
  <video class="attachment-video" controls src="${deps.escapeHtml(src)}"></video>
13704
+ </div>`;
13705
+ }
13706
+ if (isUnreachableFsPath) {
13707
+ return `
13708
+ <div class="attachment attachment-unavailable">
13709
+ <div class="attachment-unavailable-label">${deps.escapeHtml(att.name)} unavailable</div>
13710
+ <div class="attachment-unavailable-path">${deps.escapeHtml(att.body)}</div>
13564
13711
  </div>`;
13565
13712
  }
13566
13713
  const href = isBase64 ? `data:${att.mediaType};base64,${att.body}` : att.body;
@@ -13643,13 +13790,40 @@ function renderDocScreenshot(entry, deps) {
13643
13790
  const alt = entry.alt ?? "Screenshot";
13644
13791
  const embedEnabled = deps.embedScreenshots ?? true;
13645
13792
  const isRemote = /^(?:https?:|data:)/i.test(entry.path);
13646
- const src = embedEnabled && !isRemote && deps.readScreenshot ? deps.readScreenshot(entry.path) ?? entry.path : entry.path;
13793
+ const embedAttempted = !isRemote && embedEnabled && !!deps.readScreenshot;
13794
+ const inlined = embedAttempted ? deps.readScreenshot(entry.path) : void 0;
13795
+ const isAbsoluteFsPath = !isRemote && /^(?:[/\\]|[A-Za-z]:[/\\])/.test(entry.path);
13796
+ if (embedAttempted && inlined === void 0 && isAbsoluteFsPath) {
13797
+ const captionHtml = entry.alt ? `<div class="doc-screenshot-caption">${deps.escapeHtml(entry.alt)}</div>` : "";
13798
+ return `<div class="doc-screenshot doc-screenshot-missing">
13799
+ <div class="doc-screenshot-missing-label">Screenshot unavailable</div>
13800
+ <div class="doc-screenshot-missing-path">${deps.escapeHtml(entry.path)}</div>
13801
+ ${captionHtml}
13802
+ </div>`;
13803
+ }
13804
+ const src = inlined ?? entry.path;
13647
13805
  return `<div class="doc-screenshot">
13648
13806
  <img src="${deps.escapeHtml(src)}" alt="${deps.escapeHtml(alt)}" class="doc-screenshot-img" />
13649
13807
  ${entry.alt ? `<div class="doc-screenshot-caption">${deps.escapeHtml(entry.alt)}</div>` : ""}
13650
13808
  </div>`;
13651
13809
  }
13652
13810
  function renderDocCustom(entry, deps) {
13811
+ if (entry.type === "visual" && entry.data && typeof entry.data === "object") {
13812
+ const data = entry.data;
13813
+ const status = typeof data.status === "string" ? data.status : "unknown";
13814
+ const baseline = typeof data.baseline === "string" ? data.baseline : void 0;
13815
+ const actual = typeof data.actual === "string" ? data.actual : void 0;
13816
+ const diff = typeof data.diff === "string" ? data.diff : void 0;
13817
+ 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>` : "";
13818
+ return `<div class="doc-visual">
13819
+ <div class="doc-visual-header">Visual Check <span class="doc-visual-status">${deps.escapeHtml(status)}</span></div>
13820
+ <div class="doc-visual-grid">
13821
+ ${maybeImg(baseline, "Baseline")}
13822
+ ${maybeImg(actual, "Actual")}
13823
+ ${maybeImg(diff, "Diff")}
13824
+ </div>
13825
+ </div>`;
13826
+ }
13653
13827
  const dataStr = JSON.stringify(entry.data, null, 2);
13654
13828
  return `<div class="doc-custom">
13655
13829
  <div class="doc-custom-type">${deps.escapeHtml(entry.type)}</div>
@@ -13718,8 +13892,11 @@ function renderStep(step, stepResult, index, deps) {
13718
13892
  </div>${stepDocs}`;
13719
13893
  }
13720
13894
  function renderSteps(args, deps) {
13895
+ const byStepId = new Map(
13896
+ args.stepResults.filter((sr) => typeof sr.stepId === "string" && sr.stepId.length > 0).map((sr) => [sr.stepId, sr])
13897
+ );
13721
13898
  const stepsHtml = args.steps.map((step, index) => {
13722
- const stepResult = args.stepResults.find((sr) => sr.index === index);
13899
+ const stepResult = (step.id ? byStepId.get(step.id) : void 0) ?? args.stepResults.find((sr) => sr.index === index);
13723
13900
  return renderStep(step, stepResult, index, deps);
13724
13901
  }).join("");
13725
13902
  return `<div class="steps">${stepsHtml}</div>`;
@@ -13767,6 +13944,7 @@ function renderScenario(args, deps) {
13767
13944
  const duration = tc.durationMs > 0 ? `${(tc.durationMs / 1e3).toFixed(2)}s` : "";
13768
13945
  const tags = tc.tags.map((t) => `<span class="tag">${deps.escapeHtml(t)}</span>`).join("");
13769
13946
  const tickets = (tc.story.tickets ?? []).map((t) => renderTicket(t, deps.ticketUrlTemplate, deps.escapeHtml)).join("");
13947
+ const outcomeBadge = tc.rawStatus === "timeout" || tc.rawStatus === "interrupted" ? `<span class="tag outcome-tag">${deps.escapeHtml(tc.rawStatus)}</span>` : "";
13770
13948
  const otelMeta = tc.story.meta?.otel;
13771
13949
  let traceBadge = "";
13772
13950
  if (otelMeta?.traceId) {
@@ -13834,7 +14012,7 @@ function renderScenario(args, deps) {
13834
14012
  <span class="status-icon ${statusClass}">${statusIcon2}</span>
13835
14013
  <span class="scenario-name">${deps.escapeHtml(tc.story.scenario)}</span>
13836
14014
  </div>
13837
- <div class="scenario-meta">${tags}${tickets}${sourceLink}${traceBadge}${metricBadges}</div>
14015
+ <div class="scenario-meta">${tags}${tickets}${outcomeBadge}${sourceLink}${traceBadge}${metricBadges}</div>
13838
14016
  </div>
13839
14017
  <div class="scenario-actions">
13840
14018
  <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>
@@ -14838,6 +15016,9 @@ var MarkdownFormatter = class {
14838
15016
  });
14839
15017
  meta.push(`Tickets: ${ticketLinks.join(", ")}`);
14840
15018
  }
15019
+ if (tc.rawStatus === "timeout" || tc.rawStatus === "interrupted") {
15020
+ meta.push(`Outcome: \`${tc.rawStatus}\``);
15021
+ }
14841
15022
  const otelMeta = tc.story.meta?.otel;
14842
15023
  if (otelMeta?.traceId) {
14843
15024
  const traceTemplate = this.options.traceUrlTemplate;
@@ -15000,6 +15181,16 @@ var MarkdownFormatter = class {
15000
15181
  lines.push(`${indent}![${entry.alt ?? "Screenshot"}](${entry.path})`);
15001
15182
  break;
15002
15183
  case "custom":
15184
+ if (entry.type === "visual" && entry.data && typeof entry.data === "object") {
15185
+ const data = entry.data;
15186
+ const status = typeof data.status === "string" ? data.status : "unknown";
15187
+ lines.push(`${indent}**Visual Check** (${status})`);
15188
+ if (typeof data.baseline === "string") lines.push(`${indent}- Baseline: ${data.baseline}`);
15189
+ if (typeof data.actual === "string") lines.push(`${indent}- Actual: ${data.actual}`);
15190
+ if (typeof data.diff === "string") lines.push(`${indent}- Diff: ${data.diff}`);
15191
+ lines.push(`${indent}`);
15192
+ break;
15193
+ }
15003
15194
  lines.push(`${indent}**[${entry.type}]**`);
15004
15195
  lines.push(`${indent}`);
15005
15196
  lines.push(`${indent}\`\`\`json`);
@@ -18052,11 +18243,6 @@ function pickleStepArgumentToDocs(ps) {
18052
18243
  return docs;
18053
18244
  }
18054
18245
 
18055
- // src/notifiers/ansi-strip.ts
18056
- function stripAnsi(text2) {
18057
- return text2.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, "");
18058
- }
18059
-
18060
18246
  // src/notifiers/slack.ts
18061
18247
  function truncate(text2, maxLen) {
18062
18248
  if (text2.length <= maxLen) return text2;