executable-stories-formatters 0.7.12 → 0.7.14

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);
@@ -3029,6 +3062,63 @@ body {
3029
3062
  opacity: 0.8;
3030
3063
  }
3031
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
+
3032
3122
  /* ============================================================================
3033
3123
  Documentation Entries - Custom
3034
3124
  ============================================================================ */
@@ -13151,6 +13241,22 @@ function renderDocScreenshot(entry, deps) {
13151
13241
  </div>`;
13152
13242
  }
13153
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
+ }
13154
13260
  const dataStr = JSON.stringify(entry.data, null, 2);
13155
13261
  return `<div class="doc-custom">
13156
13262
  <div class="doc-custom-type">${deps.escapeHtml(entry.type)}</div>
@@ -13219,8 +13325,11 @@ function renderStep(step, stepResult, index, deps) {
13219
13325
  </div>${stepDocs}`;
13220
13326
  }
13221
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
+ );
13222
13331
  const stepsHtml = args.steps.map((step, index) => {
13223
- 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);
13224
13333
  return renderStep(step, stepResult, index, deps);
13225
13334
  }).join("");
13226
13335
  return `<div class="steps">${stepsHtml}</div>`;
@@ -13273,6 +13382,7 @@ function renderScenario(args, deps) {
13273
13382
  const duration = tc.durationMs > 0 ? `${(tc.durationMs / 1e3).toFixed(2)}s` : "";
13274
13383
  const tags = tc.tags.map((t) => `<span class="tag">${deps.escapeHtml(t)}</span>`).join("");
13275
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>` : "";
13276
13386
  const otelMeta = tc.story.meta?.otel;
13277
13387
  let traceBadge = "";
13278
13388
  if (otelMeta?.traceId) {
@@ -13340,7 +13450,7 @@ function renderScenario(args, deps) {
13340
13450
  <span class="status-icon ${statusClass}">${statusIcon2}</span>
13341
13451
  <span class="scenario-name">${deps.escapeHtml(tc.story.scenario)}</span>
13342
13452
  </div>
13343
- <div class="scenario-meta">${tags}${tickets}${sourceLink}${traceBadge}${metricBadges}</div>
13453
+ <div class="scenario-meta">${tags}${tickets}${outcomeBadge}${sourceLink}${traceBadge}${metricBadges}</div>
13344
13454
  </div>
13345
13455
  <div class="scenario-actions">
13346
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>
@@ -14344,6 +14454,9 @@ var MarkdownFormatter = class {
14344
14454
  });
14345
14455
  meta.push(`Tickets: ${ticketLinks.join(", ")}`);
14346
14456
  }
14457
+ if (tc.rawStatus === "timeout" || tc.rawStatus === "interrupted") {
14458
+ meta.push(`Outcome: \`${tc.rawStatus}\``);
14459
+ }
14347
14460
  const otelMeta = tc.story.meta?.otel;
14348
14461
  if (otelMeta?.traceId) {
14349
14462
  const traceTemplate = this.options.traceUrlTemplate;
@@ -14506,6 +14619,16 @@ var MarkdownFormatter = class {
14506
14619
  lines.push(`${indent}![${entry.alt ?? "Screenshot"}](${entry.path})`);
14507
14620
  break;
14508
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
+ }
14509
14632
  lines.push(`${indent}**[${entry.type}]**`);
14510
14633
  lines.push(`${indent}`);
14511
14634
  lines.push(`${indent}\`\`\`json`);
@@ -17157,6 +17280,14 @@ function adaptPlaywrightRun(testResults, options = {}) {
17157
17280
  message: result.errors[0].message,
17158
17281
  stack: result.errors[0].stack
17159
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);
17160
17291
  testCases.push({
17161
17292
  externalId: test.titlePath().join(" > "),
17162
17293
  title: story.scenario,
@@ -17167,8 +17298,7 @@ function adaptPlaywrightRun(testResults, options = {}) {
17167
17298
  status: mapPlaywrightStatus(result.status),
17168
17299
  durationMs: result.duration,
17169
17300
  error,
17170
- stepEvents: void 0,
17171
- // Playwright could provide step info, but we don't capture it yet
17301
+ stepEvents: stepEvents.length > 0 ? stepEvents : void 0,
17172
17302
  attachments,
17173
17303
  meta: {
17174
17304
  playwrightStatus: result.status,