executable-stories-playwright 8.10.8 → 8.10.9

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.
@@ -40,6 +40,17 @@ declare class StoryReporter implements Reporter {
40
40
  private packageVersion;
41
41
  private gitSha;
42
42
  private projectRoot;
43
+ /**
44
+ * Where a relative *output* path is resolved from.
45
+ *
46
+ * `projectRoot` is Playwright's `rootDir`, the root of `testDir`. Scenario
47
+ * ids hash source paths made relative to it, so it must not move. Output
48
+ * paths are resolved from the cwd instead, like `outputDir` (written through
49
+ * Node's cwd-relative fs calls) and the Vitest adapter's `rawRunPath`.
50
+ * Resolving them against `rootDir` put `rawRunPath: "docs/run.json"` in
51
+ * `e2e/docs/` while `outputDir: "docs"` beside it wrote to `docs/`.
52
+ */
53
+ private resolveOutputPath;
43
54
  /**
44
55
  * Left unknown until onBegin sees a config. Claiming full coverage without
45
56
  * having looked would let a later merge retire scenarios on a guess.
package/dist/reporter.js CHANGED
@@ -1,6 +1,19 @@
1
1
  // src/reporter.ts
2
2
  import * as fs from "fs";
3
3
  import * as path from "path";
4
+ import {
5
+ canonicalizeRun,
6
+ detectCI,
7
+ loadHistory,
8
+ readGitSha,
9
+ readPackageVersion,
10
+ ReportGenerator,
11
+ saveHistory,
12
+ sendNotifications,
13
+ stripAnsi,
14
+ toCIInfo,
15
+ updateHistory
16
+ } from "executable-stories-formatters";
4
17
 
5
18
  // src/otel-reporter-spans.ts
6
19
  import { createRequire } from "module";
@@ -88,19 +101,6 @@ function createStepSpan(args, deps) {
88
101
  }
89
102
 
90
103
  // src/reporter.ts
91
- import {
92
- ReportGenerator,
93
- canonicalizeRun,
94
- readGitSha,
95
- readPackageVersion,
96
- detectCI,
97
- sendNotifications,
98
- toCIInfo,
99
- loadHistory,
100
- updateHistory,
101
- saveHistory,
102
- stripAnsi
103
- } from "executable-stories-formatters";
104
104
  function isNameFiltered(config) {
105
105
  if (!config) return false;
106
106
  if (config.shard != null) return true;
@@ -178,6 +178,19 @@ var StoryReporter = class {
178
178
  packageVersion;
179
179
  gitSha;
180
180
  projectRoot = process.cwd();
181
+ /**
182
+ * Where a relative *output* path is resolved from.
183
+ *
184
+ * `projectRoot` is Playwright's `rootDir`, the root of `testDir`. Scenario
185
+ * ids hash source paths made relative to it, so it must not move. Output
186
+ * paths are resolved from the cwd instead, like `outputDir` (written through
187
+ * Node's cwd-relative fs calls) and the Vitest adapter's `rawRunPath`.
188
+ * Resolving them against `rootDir` put `rawRunPath: "docs/run.json"` in
189
+ * `e2e/docs/` while `outputDir: "docs"` beside it wrote to `docs/`.
190
+ */
191
+ resolveOutputPath(filePath) {
192
+ return path.isAbsolute(filePath) ? filePath : path.resolve(process.cwd(), filePath);
193
+ }
181
194
  /**
182
195
  * Left unknown until onBegin sees a config. Claiming full coverage without
183
196
  * having looked would let a later merge retire scenarios on a guess.
@@ -279,12 +292,14 @@ var StoryReporter = class {
279
292
  this.testSpans.delete(test.id);
280
293
  }
281
294
  }
282
- const storyAnnotation = test.annotations.find((a) => a.type === "story-meta");
295
+ const storyAnnotation = test.annotations.find(
296
+ (a) => a.type === "story-meta"
297
+ );
283
298
  if (!storyAnnotation?.description) return;
284
299
  try {
285
300
  const meta = JSON.parse(storyAnnotation.description);
286
301
  const otelSpansAnnotation = test.annotations.find(
287
- (a) => a.type === "otel-spans"
302
+ (a) => a.type === "story-otel-spans"
288
303
  );
289
304
  if (otelSpansAnnotation?.description) {
290
305
  try {
@@ -312,33 +327,37 @@ var StoryReporter = class {
312
327
  const persistEnabled = this.options.attachments?.enabled ?? true;
313
328
  const inlineMaxBytes = this.options.attachments?.inlineMaxBytes ?? DEFAULT_ATTACHMENT_INLINE_MAX_BYTES;
314
329
  const attachmentDir = this.options.attachments?.dir ?? path.join(this.options.outputDir ?? "reports", "attachments");
315
- const allAttachments = (result.attachments ?? []).map((a) => {
316
- if (!persistEnabled) {
317
- let body;
318
- let encoding;
319
- if (a.body !== void 0) {
320
- if (typeof a.body === "string") {
321
- body = a.body;
322
- encoding = "IDENTITY";
323
- } else if (Buffer.isBuffer(a.body) || a.body instanceof Uint8Array) {
324
- body = Buffer.from(a.body).toString("base64");
325
- encoding = "BASE64";
330
+ const allAttachments = (result.attachments ?? []).map(
331
+ (a) => {
332
+ if (!persistEnabled) {
333
+ let body;
334
+ let encoding;
335
+ if (a.body !== void 0) {
336
+ if (typeof a.body === "string") {
337
+ body = a.body;
338
+ encoding = "IDENTITY";
339
+ } else if (Buffer.isBuffer(a.body) || a.body instanceof Uint8Array) {
340
+ body = Buffer.from(a.body).toString(
341
+ "base64"
342
+ );
343
+ encoding = "BASE64";
344
+ }
326
345
  }
346
+ return {
347
+ name: a.name,
348
+ mediaType: a.contentType,
349
+ path: a.path,
350
+ body,
351
+ encoding
352
+ };
327
353
  }
328
- return {
329
- name: a.name,
330
- mediaType: a.contentType,
331
- path: a.path,
332
- body,
333
- encoding
334
- };
354
+ return persistAttachment(a, {
355
+ testId: test.id,
356
+ attachmentDir,
357
+ inlineMaxBytes
358
+ });
335
359
  }
336
- return persistAttachment(a, {
337
- testId: test.id,
338
- attachmentDir,
339
- inlineMaxBytes
340
- });
341
- });
360
+ );
342
361
  const attachments = deduplicateVideoAttachments(allAttachments);
343
362
  const featureVideo = meta.meta?.featureVideo === true;
344
363
  if (featureVideo) {
@@ -357,12 +376,14 @@ var StoryReporter = class {
357
376
  });
358
377
  }
359
378
  }
360
- const stepEvents = meta.steps.filter((s) => s.durationMs !== void 0).map((s, i) => ({
361
- index: i,
362
- stepId: s.id,
363
- title: s.text,
364
- durationMs: s.durationMs
365
- }));
379
+ const stepEvents = meta.steps.filter((s) => s.durationMs !== void 0).map(
380
+ (s, i) => ({
381
+ index: i,
382
+ stepId: s.id,
383
+ title: s.text,
384
+ durationMs: s.durationMs
385
+ })
386
+ );
366
387
  this.scenarios.push({
367
388
  testId: test.id,
368
389
  meta,
@@ -394,9 +415,13 @@ var StoryReporter = class {
394
415
  collectPlannedTestCases() {
395
416
  if (!this.rootSuite) return [];
396
417
  const key = (projectName, sourceFile) => `${projectName ?? ""}\0${sourceFile}`;
397
- const storyFiles = new Set(this.scenarios.map((s) => key(s.projectName, s.sourceFile)));
418
+ const storyFiles = new Set(
419
+ this.scenarios.map((s) => key(s.projectName, s.sourceFile))
420
+ );
398
421
  if (storyFiles.size === 0) return [];
399
- const collectedIds = new Set(this.scenarios.map((s) => s.testId).filter(Boolean));
422
+ const collectedIds = new Set(
423
+ this.scenarios.map((s) => s.testId).filter(Boolean)
424
+ );
400
425
  const planned = [];
401
426
  for (const test of this.rootSuite.allTests()) {
402
427
  const isFixme = test.annotations.some((a) => a.type === "fixme");
@@ -409,7 +434,8 @@ var StoryReporter = class {
409
434
  if (!storyFiles.has(key(projectName, sourceFile))) continue;
410
435
  const suitePath = [];
411
436
  for (let parent = test.parent; parent; parent = parent.parent) {
412
- if (parent.type === "describe" && parent.title) suitePath.unshift(parent.title);
437
+ if (parent.type === "describe" && parent.title)
438
+ suitePath.unshift(parent.title);
413
439
  }
414
440
  planned.push({
415
441
  title: test.title,
@@ -431,14 +457,18 @@ var StoryReporter = class {
431
457
  return planned;
432
458
  }
433
459
  async onEnd(_result) {
434
- if (this.scenarios.length === 0 && this.coveredSourceFiles.size === 0) return;
460
+ if (this.scenarios.length === 0 && this.coveredSourceFiles.size === 0)
461
+ return;
435
462
  if (this.scenarios.length > 0) {
436
463
  const sampleScenario = this.scenarios[0];
437
464
  if ("tags" in sampleScenario) {
438
465
  this.debug("tags found at scenario level", Object.keys(sampleScenario));
439
466
  }
440
467
  if (sampleScenario.meta && "tags" in sampleScenario.meta) {
441
- this.debug("tags found inside meta (expected)", sampleScenario.meta.tags);
468
+ this.debug(
469
+ "tags found inside meta (expected)",
470
+ sampleScenario.meta.tags
471
+ );
442
472
  }
443
473
  }
444
474
  const rawTestCases = this.scenarios.map((scenario) => {
@@ -490,7 +520,7 @@ var StoryReporter = class {
490
520
  };
491
521
  const rawRunPath = this.options.rawRunPath;
492
522
  if (rawRunPath) {
493
- const absolutePath = path.isAbsolute(rawRunPath) ? rawRunPath : path.join(this.projectRoot, rawRunPath);
523
+ const absolutePath = this.resolveOutputPath(rawRunPath);
494
524
  const dir = path.dirname(absolutePath);
495
525
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
496
526
  const payload = { schemaVersion: 1, ...rawRun };
@@ -506,7 +536,7 @@ var StoryReporter = class {
506
536
  try {
507
537
  const histOpts = this.options.history;
508
538
  if (histOpts?.filePath) {
509
- const historyPath = path.isAbsolute(histOpts.filePath) ? histOpts.filePath : path.join(this.projectRoot, histOpts.filePath);
539
+ const historyPath = this.resolveOutputPath(histOpts.filePath);
510
540
  const store = loadHistory(
511
541
  { filePath: historyPath },
512
542
  {
@@ -520,12 +550,18 @@ var StoryReporter = class {
520
550
  logger: console
521
551
  }
522
552
  );
523
- const updated = updateHistory({ store, run: canonicalRun, maxRuns: histOpts.maxRuns ?? 10 });
553
+ const updated = updateHistory({
554
+ store,
555
+ run: canonicalRun,
556
+ maxRuns: histOpts.maxRuns ?? 10
557
+ });
524
558
  const dir = path.dirname(historyPath);
525
559
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
526
560
  saveHistory(
527
561
  { filePath: historyPath, store: updated },
528
- { writeFile: (p, c) => fs.writeFileSync(p, c, "utf8") }
562
+ {
563
+ writeFile: (p, c) => fs.writeFileSync(p, c, "utf8")
564
+ }
529
565
  );
530
566
  }
531
567
  } catch (err) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/reporter.ts","../src/otel-reporter-spans.ts"],"sourcesContent":["/**\n * Playwright reporter for executable-stories.\n * Generates reports using the executable-stories-formatters package.\n */\n\nimport type {\n Reporter,\n FullConfig,\n Suite,\n TestCase,\n TestResult,\n FullResult,\n TestStep,\n} from \"@playwright/test/reporter\";\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport type { StoryMeta } from \"executable-stories-formatters\";\nimport {\n tryLoadAutotel,\n shouldInstrumentStep,\n createTestSpan,\n createStepSpan,\n type AutotelApi,\n} from \"./otel-reporter-spans.js\";\n\n// Import from formatters package\nimport {\n ReportGenerator,\n canonicalizeRun,\n readGitSha,\n readPackageVersion,\n detectCI,\n sendNotifications,\n toCIInfo,\n loadHistory,\n updateHistory,\n saveHistory,\n stripAnsi,\n type RawRun,\n type RawTestCase,\n type RawAttachment,\n type RawStepEvent,\n type FormatterOptions,\n} from \"executable-stories-formatters\";\n\n// Re-export types from formatters for convenience\nexport type {\n OutputFormat,\n OutputMode,\n ColocatedStyle,\n OutputRule,\n FormatterOptions,\n} from \"executable-stories-formatters\";\n\n// ============================================================================\n// Reporter Options (delegates to FormatterOptions)\n// ============================================================================\n\nexport interface StoryReporterOptions extends FormatterOptions {\n /** If set, write raw run JSON (schemaVersion 1) to this path for use with the executable-stories CLI/binary */\n rawRunPath?: string;\n /**\n * Attachment persistence settings. Playwright keeps videos/screenshots/traces\n * inside its per-test outputDir; that directory may be cleaned before the\n * formatter (or a downstream CI job) runs, leaving reports with broken\n * <video>/<img> tags pointing at /home/runner/... paths. The reporter\n * eagerly persists each attachment at onTestEnd:\n * - small files (<= inlineMaxBytes) are base64-encoded into raw-run.json\n * - larger files are copied to <attachmentDir>/<test-id>/<filename>\n * so the bytes always survive even when the source dir is wiped.\n */\n attachments?: {\n /** Directory to copy non-inlined attachments to. Default: \"<outputDir>/attachments\" */\n dir?: string;\n /** Inline threshold in bytes. Default: 1 MB (1_048_576) */\n inlineMaxBytes?: number;\n /** Set false to skip persistence entirely. Default: true */\n enabled?: boolean;\n };\n /** Enable verbose reporter diagnostics. Default: false */\n debug?: boolean;\n}\n\n// ============================================================================\n// Internal Types\n// ============================================================================\n\ninterface CollectedScenario {\n /** Playwright's own test id, used to keep a runtime fixme from being counted twice. */\n testId?: string;\n meta: StoryMeta;\n sourceFile: string;\n sourceLine: number;\n status: \"passed\" | \"failed\" | \"skipped\" | \"timedOut\" | \"interrupted\";\n error?: string;\n errorStack?: string;\n durationMs: number;\n projectName?: string;\n retry: number;\n retries: number;\n attachments?: RawAttachment[];\n stepEvents?: RawStepEvent[];\n}\n\n// ============================================================================\n// Utility Functions\n// ============================================================================\n\n/**\n * Convert path to relative posix format.\n */\n/**\n * Whether the run was narrowed by title, via `--grep` / `--grep-invert` or the\n * config equivalents. Such a run reports a subset of each file it touches, so\n * consumers must not read it as those files' full contents.\n *\n * `grep` is always present on FullConfig and defaults to a match-everything\n * pattern, so presence is not the signal; only a pattern that actually narrows\n * the run is. `--shard` narrows just as much and must count too.\n */\nfunction isNameFiltered(config: Partial<FullConfig> | undefined): boolean {\n if (!config) return false;\n // Sharding splits a file's tests across machines, so this process sees only\n // some of them. Scenario ids do not carry the project, so project selection\n // is not a hazard the same way: every project reports the same scenario ids,\n // and a single-project run still names every scenario in a file.\n if (config.shard != null) return true;\n if (config.grepInvert != null) return true;\n const patterns = Array.isArray(config.grep) ? config.grep : config.grep ? [config.grep] : [];\n return patterns.some((pattern) => pattern.source !== \".*\");\n}\n\nfunction toRelativePosix(absolutePath: string, projectRoot: string): string {\n return path.relative(projectRoot, absolutePath).split(path.sep).join(\"/\");\n}\n\nconst DEFAULT_ATTACHMENT_INLINE_MAX_BYTES = 1024 * 1024; // 1 MB\n\n/**\n * Persist a single Playwright attachment so its bytes outlive Playwright's\n * per-test outputDir cleanup. Small files are base64-encoded inline; larger\n * files are copied to a stable directory and referenced by absolute path.\n *\n * Returns the resolved RawAttachment. On unexpected I/O failure falls back to\n * the original path-only mapping so behavior is no worse than before.\n */\nfunction persistAttachment(\n raw: { name: string; contentType: string; path?: string; body?: unknown },\n args: { testId: string; attachmentDir: string; inlineMaxBytes: number },\n): RawAttachment {\n // Attachment already has a body (either string content or a Buffer) — encode\n // it once and we're done. No filesystem I/O required.\n if (raw.body !== undefined) {\n if (typeof raw.body === \"string\") {\n return {\n name: raw.name,\n mediaType: raw.contentType,\n path: raw.path,\n body: raw.body,\n encoding: \"IDENTITY\",\n };\n }\n if (Buffer.isBuffer(raw.body) || raw.body instanceof Uint8Array) {\n return {\n name: raw.name,\n mediaType: raw.contentType,\n path: raw.path,\n body: Buffer.from(raw.body as Buffer | Uint8Array).toString(\"base64\"),\n encoding: \"BASE64\",\n };\n }\n }\n\n // Path-only attachment: read the file now while it still exists.\n if (raw.path) {\n try {\n if (fs.existsSync(raw.path)) {\n const stats = fs.statSync(raw.path);\n if (stats.size <= args.inlineMaxBytes) {\n const buf = fs.readFileSync(raw.path);\n return {\n name: raw.name,\n mediaType: raw.contentType,\n path: raw.path,\n body: buf.toString(\"base64\"),\n encoding: \"BASE64\",\n byteLength: stats.size,\n };\n }\n // Too large to inline — copy to stable location instead.\n const destDir = path.join(args.attachmentDir, args.testId);\n fs.mkdirSync(destDir, { recursive: true });\n const filename = path.basename(raw.path);\n const destPath = path.join(destDir, filename);\n fs.copyFileSync(raw.path, destPath);\n return {\n name: raw.name,\n mediaType: raw.contentType,\n path: destPath,\n byteLength: stats.size,\n };\n }\n } catch {\n // Fall through to original path-only mapping.\n }\n }\n\n return {\n name: raw.name,\n mediaType: raw.contentType,\n path: raw.path,\n };\n}\n\n// ============================================================================\n// Reporter Implementation\n// ============================================================================\n\nexport default class StoryReporter implements Reporter {\n private options: StoryReporterOptions;\n private scenarios: CollectedScenario[] = [];\n /** Kept from onBegin so onEnd can find tests that never ran (planned ones). */\n private rootSuite?: Suite;\n private startTime = 0;\n private packageVersion: string | undefined;\n private gitSha: string | undefined;\n private projectRoot: string = process.cwd();\n /**\n * Left unknown until onBegin sees a config. Claiming full coverage without\n * having looked would let a later merge retire scenarios on a guess.\n */\n private runScope: \"full\" | \"filtered\" | undefined;\n /**\n * Every spec file this run executed, story-bearing or not. Collected from all\n * tests rather than from the scenarios, so a file whose last story was\n * deleted is still known to have run and can have its report emptied.\n */\n private coveredSourceFiles = new Set<string>();\n /**\n * Files where a test ended badly without ever declaring its story — a hook\n * that threw before `story.init()`, a timeout during collection. Those\n * scenarios are missing because the run broke, not because they were deleted.\n */\n private incompleteSourceFiles = new Set<string>();\n private autotel: AutotelApi | null = null;\n private testSpans = new Map<\n string,\n { endSpan: (status: string, errorMessage?: string) => void }\n >();\n private stepSpanStacks = new Map<\n string,\n Array<{ endSpan: (errorMessage?: string) => void }>\n >();\n\n constructor(options: StoryReporterOptions = {}) {\n this.options = options;\n }\n\n private debug(...args: unknown[]): void {\n if (this.options.debug) {\n console.error(\"[executable-stories-playwright][debug]\", ...args);\n }\n }\n\n onBegin(config: FullConfig, suite: Suite): void {\n this.startTime = Date.now();\n this.rootSuite = suite;\n this.projectRoot = config.rootDir ?? process.cwd();\n if (config) this.runScope = isNameFiltered(config) ? \"filtered\" : \"full\";\n const includeMetadata = this.options.markdown?.includeMetadata ?? true;\n if (includeMetadata) {\n this.packageVersion = readPackageVersion(this.projectRoot);\n this.gitSha = readGitSha(this.projectRoot);\n }\n this.autotel = tryLoadAutotel();\n }\n\n onTestBegin(test: TestCase): void {\n if (!this.autotel) return;\n const sourceFile = test.location?.file;\n const sourceLine = (test.location as { line?: number })?.line;\n const titlePath = test.titlePath();\n // titlePath: [projectName, ...describes, testTitle]\n const suitePath = titlePath.slice(1, -1);\n const testTitle = titlePath[titlePath.length - 1] ?? test.title;\n\n const handle = createTestSpan(\n { testTitle, suitePath, sourceFile, sourceLine },\n { autotel: this.autotel },\n );\n this.testSpans.set(test.id, handle);\n this.stepSpanStacks.set(test.id, []);\n }\n\n onStepBegin(test: TestCase, _result: TestResult, step: TestStep): void {\n if (!this.autotel) return;\n if (!shouldInstrumentStep({ category: step.category, title: step.title }))\n return;\n\n const handle = createStepSpan(\n { stepTitle: step.title, stepCategory: step.category },\n { autotel: this.autotel },\n );\n const stack = this.stepSpanStacks.get(test.id);\n if (stack) {\n stack.push(handle);\n }\n }\n\n onStepEnd(test: TestCase, _result: TestResult, step: TestStep): void {\n if (!this.autotel) return;\n if (!shouldInstrumentStep({ category: step.category, title: step.title }))\n return;\n\n const stack = this.stepSpanStacks.get(test.id);\n if (stack && stack.length > 0) {\n const handle = stack.pop()!;\n handle.endSpan(step.error?.message);\n }\n }\n\n onTestEnd(test: TestCase, result: TestResult): void {\n // Record the file whether or not this test carries a story: the point is to\n // know the file ran, so a report emptied of stories can be retired.\n const file = test.location?.file;\n if (file) {\n const relative = toRelativePosix(file, this.projectRoot);\n this.coveredSourceFiles.add(relative);\n // No story annotation on a test that failed or timed out means the story\n // never got the chance to declare itself.\n const declared = test.annotations?.some((a) => a.type === \"story-meta\");\n const brokeEarly =\n result.status === \"failed\" ||\n result.status === \"timedOut\" ||\n result.status === \"interrupted\";\n if (!declared && brokeEarly) this.incompleteSourceFiles.add(relative);\n }\n\n // Defensive: unwind leftover step spans (interrupted/crash)\n if (this.autotel) {\n const stack = this.stepSpanStacks.get(test.id);\n if (stack) {\n while (stack.length > 0) {\n const handle = stack.pop()!;\n handle.endSpan(\"interrupted test\");\n }\n this.stepSpanStacks.delete(test.id);\n }\n // End test span\n const testHandle = this.testSpans.get(test.id);\n if (testHandle) {\n testHandle.endSpan(result.status, result.errors?.[0]?.message);\n this.testSpans.delete(test.id);\n }\n }\n\n // Find story-meta annotation\n const storyAnnotation = test.annotations.find((a) => a.type === \"story-meta\");\n if (!storyAnnotation?.description) return;\n\n try {\n const meta: StoryMeta = JSON.parse(storyAnnotation.description);\n\n // Read autotel OTel spans from annotations\n const otelSpansAnnotation = test.annotations.find(\n (a) => a.type === \"otel-spans\",\n );\n if (otelSpansAnnotation?.description) {\n try {\n const spans = JSON.parse(otelSpansAnnotation.description);\n if (Array.isArray(spans) && spans.length > 0) {\n const valid = spans.filter(\n (s: unknown) =>\n s != null &&\n typeof s === \"object\" &&\n typeof (s as Record<string, unknown>).spanId === \"string\" &&\n typeof (s as Record<string, unknown>).name === \"string\",\n );\n if (valid.length > 0) {\n meta.otelSpans = valid;\n }\n }\n } catch {\n /* ignore parse errors */\n }\n }\n\n // Get source file and line for sorting\n const sourceFile = test.location?.file\n ? toRelativePosix(test.location.file, this.projectRoot)\n : \"unknown\";\n const sourceLine = (test.location as { line?: number })?.line ?? 1;\n\n // Get error message if failed. Playwright populates these with ANSI\n // color codes; strip them so reports render clean text instead of\n // garbled escape sequences like \"[2mexpect([22m...\".\n let error: string | undefined;\n let errorStack: string | undefined;\n if (result.status === \"failed\" && result.errors?.length) {\n const err = result.errors[0];\n error = stripAnsi(err.message || String(err));\n errorStack = err.stack ? stripAnsi(err.stack) : undefined;\n }\n\n // Map Playwright result.attachments → RawAttachment[]. Eagerly persist\n // path-based attachments (videos/screenshots/traces) so their bytes\n // survive Playwright's per-test outputDir cleanup — see the\n // `persistAttachment` helper for the inline-vs-copy decision.\n const persistEnabled = this.options.attachments?.enabled ?? true;\n const inlineMaxBytes =\n this.options.attachments?.inlineMaxBytes ?? DEFAULT_ATTACHMENT_INLINE_MAX_BYTES;\n const attachmentDir =\n this.options.attachments?.dir ??\n path.join(this.options.outputDir ?? \"reports\", \"attachments\");\n const allAttachments: RawAttachment[] = (result.attachments ?? []).map((a) => {\n if (!persistEnabled) {\n let body: string | undefined;\n let encoding: \"BASE64\" | \"IDENTITY\" | undefined;\n if (a.body !== undefined) {\n if (typeof a.body === \"string\") {\n body = a.body;\n encoding = \"IDENTITY\";\n } else if (Buffer.isBuffer(a.body) || (a.body as unknown) instanceof Uint8Array) {\n body = Buffer.from(a.body as Buffer | Uint8Array).toString(\"base64\");\n encoding = \"BASE64\";\n }\n }\n return {\n name: a.name,\n mediaType: a.contentType,\n path: a.path,\n body,\n encoding,\n };\n }\n return persistAttachment(a, {\n testId: test.id,\n attachmentDir,\n inlineMaxBytes,\n });\n });\n\n // Deduplicate video attachments by name — Playwright may attach\n // multiple video files per test (e.g. video.webm and video-1.webm).\n // Keep only the last video attachment per name, which is the real recording.\n const attachments = deduplicateVideoAttachments(allAttachments);\n\n // Auto-promote the Playwright screen recording into a featured inline\n // video doc entry when story.init(..., { featureVideo: true }) was set.\n // The recording already rides along as an attachment; this surfaces it as\n // a playable walkthrough at the top of the scenario rather than a footer\n // attachment. Referenced by a path relative to the report output dir so\n // the generated HTML/Markdown resolves it alongside the report.\n const featureVideo =\n (meta.meta as { featureVideo?: boolean } | undefined)?.featureVideo === true;\n if (featureVideo) {\n const videoAtt = attachments.find(\n (a) => a.mediaType?.startsWith(\"video/\") && a.path,\n );\n if (videoAtt?.path) {\n const outDir = this.options.outputDir ?? \"reports\";\n const relPath = path\n .relative(outDir, videoAtt.path)\n .split(path.sep)\n .join(\"/\");\n meta.docs = meta.docs ?? [];\n meta.docs.unshift({\n kind: \"video\",\n path: relPath,\n caption: \"Recorded walkthrough\",\n phase: \"runtime\",\n });\n }\n }\n\n // Extract step events (timing) from story steps\n const stepEvents: RawStepEvent[] = meta.steps\n .filter((s: { durationMs?: number }) => s.durationMs !== undefined)\n .map((s: { durationMs?: number; text: string; id?: string }, i: number) => ({\n index: i,\n stepId: s.id,\n title: s.text,\n durationMs: s.durationMs,\n }));\n\n this.scenarios.push({\n testId: test.id,\n meta,\n sourceFile,\n sourceLine,\n status: result.status,\n error,\n errorStack,\n durationMs: result.duration,\n projectName: test.parent?.project()?.name,\n retry: result.retry,\n retries: test.retries,\n attachments: attachments.length > 0 ? attachments : undefined,\n stepEvents: stepEvents.length > 0 ? stepEvents : undefined,\n });\n } catch {\n // Ignore parse errors\n }\n }\n\n /**\n * `test.fixme(\"title\")` declares behaviour that is specified but not working\n * yet, which is what a planned scenario is. Those tests never run, so they\n * never call `story.init` and never reach `this.scenarios`; they have to be\n * read back off the suite instead.\n *\n * Only files that also contain story tests contribute, so a plain spec full\n * of fixmes does not leak into the generated docs. `test.skip` is left alone:\n * it means \"do not run this now\", not \"we have not built this yet\".\n */\n private collectPlannedTestCases(): RawTestCase[] {\n if (!this.rootSuite) return [];\n // Eligibility is per project AND file: the same spec can carry story tests\n // under one project and nothing under another.\n const key = (projectName: string | undefined, sourceFile: string) => `${projectName ?? \"\"}\\u0000${sourceFile}`;\n const storyFiles = new Set(this.scenarios.map((s) => key(s.projectName, s.sourceFile)));\n if (storyFiles.size === 0) return [];\n\n // A story that ran and then called test.fixme() at runtime is already\n // collected as a skipped scenario; it must not appear a second time as a\n // planned one.\n const collectedIds = new Set(this.scenarios.map((s) => s.testId).filter(Boolean));\n\n const planned: RawTestCase[] = [];\n for (const test of this.rootSuite.allTests()) {\n const isFixme = test.annotations.some((a) => a.type === \"fixme\");\n if (!isFixme) continue;\n if (collectedIds.has(test.id)) continue;\n\n const absolute = test.location?.file;\n if (!absolute) continue;\n const sourceFile = toRelativePosix(absolute, this.projectRoot);\n const projectName = test.parent?.project()?.name;\n if (!storyFiles.has(key(projectName, sourceFile))) continue;\n\n // Walk the parent chain rather than titlePath(): suite.type tells us\n // exactly which entries are describes, with no filename guessing.\n const suitePath: string[] = [];\n for (let parent: Suite | undefined = test.parent; parent; parent = parent.parent) {\n if (parent.type === \"describe\" && parent.title) suitePath.unshift(parent.title);\n }\n planned.push({\n title: test.title,\n titlePath: [...suitePath, test.title],\n story: {\n scenario: test.title,\n steps: [],\n ...(suitePath.length > 0 ? { suitePath } : {}),\n },\n sourceFile,\n sourceLine: test.location?.line ?? 1,\n status: \"todo\",\n durationMs: 0,\n projectName,\n retry: 0,\n retries: 0,\n });\n }\n return planned;\n }\n\n async onEnd(_result: FullResult): Promise<void> {\n // Nothing ran and nothing was covered: there is genuinely nothing to say.\n if (this.scenarios.length === 0 && this.coveredSourceFiles.size === 0) return;\n\n if (this.scenarios.length > 0) {\n const sampleScenario = this.scenarios[0];\n if (\"tags\" in sampleScenario) {\n this.debug(\"tags found at scenario level\", Object.keys(sampleScenario));\n }\n if (sampleScenario.meta && \"tags\" in sampleScenario.meta) {\n this.debug(\"tags found inside meta (expected)\", sampleScenario.meta.tags);\n }\n }\n\n // Collect test cases\n const rawTestCases: RawTestCase[] = this.scenarios.map((scenario) => {\n // Map Playwright status to raw status\n const statusMap: Record<string, RawTestCase[\"status\"]> = {\n passed: \"pass\",\n failed: \"fail\",\n skipped: \"skip\",\n timedOut: \"timeout\",\n interrupted: \"interrupted\",\n };\n\n const testCase = {\n title: scenario.meta.scenario,\n titlePath: scenario.meta.suitePath\n ? [...scenario.meta.suitePath, scenario.meta.scenario]\n : [scenario.meta.scenario],\n story: scenario.meta,\n sourceFile: scenario.sourceFile,\n sourceLine: Math.max(1, scenario.sourceLine),\n status: statusMap[scenario.status] ?? \"unknown\",\n durationMs: scenario.durationMs,\n error: scenario.error\n ? { message: scenario.error, stack: scenario.errorStack }\n : undefined,\n projectName: scenario.projectName,\n retry: scenario.retry,\n retries: scenario.retries,\n attachments: scenario.attachments,\n stepEvents: scenario.stepEvents,\n };\n\n return testCase;\n });\n\n if (rawTestCases.length > 0) {\n const sample = rawTestCases[0];\n if (\"tags\" in sample) {\n this.debug(\"tags found at rawTestCase level\", Object.keys(sample));\n }\n if (sample.story && \"tags\" in sample.story) {\n this.debug(\"tags found inside story (expected)\");\n }\n }\n\n rawTestCases.push(...this.collectPlannedTestCases());\n\n // Build RawRun\n const rawRun: RawRun = {\n testCases: rawTestCases,\n startedAtMs: this.startTime,\n finishedAtMs: Date.now(),\n projectRoot: this.projectRoot,\n ...(this.runScope ? { runScope: this.runScope } : {}),\n ...(this.coveredSourceFiles.size > 0\n ? { coveredSourceFiles: [...this.coveredSourceFiles].sort() }\n : {}),\n ...(this.incompleteSourceFiles.size > 0\n ? { incompleteSourceFiles: [...this.incompleteSourceFiles].sort() }\n : {}),\n packageVersion: this.packageVersion,\n gitSha: this.gitSha,\n ci: detectCI(),\n };\n\n // Optionally write raw run JSON for CLI/binary consumption\n const rawRunPath = this.options.rawRunPath;\n if (rawRunPath) {\n const absolutePath = path.isAbsolute(rawRunPath)\n ? rawRunPath\n : path.join(this.projectRoot, rawRunPath);\n const dir = path.dirname(absolutePath);\n if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });\n const payload = { schemaVersion: 1, ...rawRun };\n fs.writeFileSync(absolutePath, JSON.stringify(payload, null, 2), \"utf8\");\n }\n\n // Canonicalize\n const canonicalRun = canonicalizeRun(rawRun);\n\n // 1. Generate reports\n const generator = new ReportGenerator(this.options);\n try {\n await generator.generate(canonicalRun);\n } catch (err) {\n console.error(\"Failed to generate reports:\", err);\n }\n\n // 2. Update history (independent of report generation)\n try {\n const histOpts = this.options.history;\n if (histOpts?.filePath) {\n const historyPath = path.isAbsolute(histOpts.filePath)\n ? histOpts.filePath\n : path.join(this.projectRoot, histOpts.filePath);\n const store = loadHistory(\n { filePath: historyPath },\n {\n readFile: (p: string) => { try { return fs.readFileSync(p, \"utf8\"); } catch { return undefined; } },\n logger: console,\n },\n );\n const updated = updateHistory({ store, run: canonicalRun, maxRuns: histOpts.maxRuns ?? 10 });\n const dir = path.dirname(historyPath);\n if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });\n saveHistory(\n { filePath: historyPath, store: updated },\n { writeFile: (p: string, c: string) => fs.writeFileSync(p, c, \"utf8\") },\n );\n }\n } catch (err) {\n console.error(\"Failed to update history:\", err);\n }\n\n // 3. Send notifications (independent of both above)\n try {\n if (this.options.notification) {\n await sendNotifications(\n { run: canonicalRun, notification: this.options.notification },\n { fetch: globalThis.fetch, logger: console, toCIInfo },\n );\n }\n } catch (err) {\n console.error(\"Failed to send notifications:\", err);\n }\n }\n}\n\n/**\n * Deduplicate video attachments by name.\n *\n * Playwright with `video: \"on\"` may produce multiple video files per test\n * in the output directory (e.g. `video.webm` and `video-1.webm`), attaching\n * all of them to the test result. This leads to duplicate videos in reports.\n *\n * For each unique video attachment name, keep only the last occurrence —\n * Playwright appends the real recording after any stubs.\n * Non-video attachments are always preserved.\n */\nexport function deduplicateVideoAttachments(\n attachments: RawAttachment[],\n): RawAttachment[] {\n // Find the last index for each video attachment name\n const lastVideoIndex = new Map<string, number>();\n for (let i = 0; i < attachments.length; i++) {\n if (attachments[i].mediaType.startsWith(\"video/\")) {\n lastVideoIndex.set(attachments[i].name, i);\n }\n }\n\n // Keep non-video attachments and only the last video per name\n return attachments.filter((att, i) => {\n if (!att.mediaType.startsWith(\"video/\")) return true;\n return lastVideoIndex.get(att.name) === i;\n });\n}\n","/**\n * OTel span generation helpers for the Playwright reporter.\n *\n * Uses autotel for span creation with the same lazy-loading pattern\n * as story-api.ts (createRequire). All helpers follow the fn(args, deps)\n * convention for explicit dependency injection.\n */\n\nimport { createRequire } from \"node:module\";\n\n// ============================================================================\n// Autotel API surface\n// ============================================================================\n\n/** OTel span handle returned from autotel callback */\nexport interface AutotelSpan {\n end: () => void;\n setStatus: (status: { code: number; message?: string }) => void;\n setAttribute: (key: string, value: unknown) => void;\n}\n\n/** Minimal autotel API surface we use */\nexport interface AutotelApi {\n span: (\n name: string,\n fn: (span: AutotelSpan) => void,\n ) => void;\n SpanStatusCode: { UNSET: number; ERROR: number };\n}\n\n// ============================================================================\n// Lazy loader\n// ============================================================================\n\n/**\n * Lazy-load autotel. Returns null if unavailable.\n * Same createRequire pattern as story-api.ts.\n */\nexport function tryLoadAutotel(): AutotelApi | null {\n try {\n const reqUrl =\n import.meta.url ??\n (typeof __filename !== \"undefined\" ? `file://${__filename}` : undefined);\n if (!reqUrl) return null;\n const req = createRequire(reqUrl);\n const autotel = req(\"autotel\");\n if (typeof autotel?.span !== \"function\") return null;\n return autotel as AutotelApi;\n } catch {\n return null;\n }\n}\n\n// ============================================================================\n// Step filtering\n// ============================================================================\n\n/**\n * Step filtering — explicit, heavily tested.\n * Returns true for test.step category and story step keywords.\n */\nexport function shouldInstrumentStep(step: {\n category?: string;\n title?: string;\n}): boolean {\n return step.category === \"test.step\" || isStoryStep(step);\n}\n\n/**\n * Check if a step title starts with a story keyword.\n * Documented tradeoff: may match non-story steps starting with these words\n * (e.g. \"And this works\"). Acceptable for v1.\n */\nfunction isStoryStep(step: { title?: string }): boolean {\n if (!step.title) return false;\n return /^(Given|When|Then|And|But|Arrange|Act|Assert)\\s/.test(step.title);\n}\n\n// ============================================================================\n// Status mapping\n// ============================================================================\n\n/**\n * Map test/step status to OTel SpanStatusCode.\n * Single helper used by both test and step spans.\n *\n * \"passed\"/\"skipped\" -> UNSET\n * \"failed\"/\"timedOut\"/\"interrupted\" -> ERROR\n */\nfunction mapToSpanStatus(\n status: string,\n SpanStatusCode: { UNSET: number; ERROR: number },\n): { code: number; message?: string } {\n switch (status) {\n case \"passed\":\n case \"skipped\":\n return { code: SpanStatusCode.UNSET };\n case \"failed\":\n case \"timedOut\":\n case \"interrupted\":\n return { code: SpanStatusCode.ERROR, message: status };\n default:\n return { code: SpanStatusCode.UNSET };\n }\n}\n\n// ============================================================================\n// Test span\n// ============================================================================\n\n/**\n * Create a test-level span.\n *\n * Attribute naming convention:\n * - code.filepath, code.lineno -- OTel code conventions\n * - test.name, test.suite, test.status -- test attributes\n * - story.scenario, story.tags, story.tickets -- story-specific\n */\nexport function createTestSpan(\n args: {\n testTitle: string;\n suitePath?: string[];\n sourceFile?: string;\n sourceLine?: number;\n },\n deps: { autotel: AutotelApi },\n): { endSpan: (status: string, errorMessage?: string) => void } {\n let captured: AutotelSpan | undefined;\n deps.autotel.span(`test: ${args.testTitle}`, (s) => {\n captured = s;\n s.setAttribute(\"test.name\", args.testTitle);\n if (args.suitePath?.length) {\n s.setAttribute(\"test.suite\", args.suitePath.join(\" > \"));\n }\n if (args.sourceFile) {\n s.setAttribute(\"code.filepath\", args.sourceFile);\n }\n if (args.sourceLine !== undefined) {\n s.setAttribute(\"code.lineno\", args.sourceLine);\n }\n });\n const span = captured!;\n\n return {\n endSpan(status: string, errorMessage?: string) {\n span.setAttribute(\"test.status\", status);\n const spanStatus = mapToSpanStatus(status, deps.autotel.SpanStatusCode);\n if (errorMessage) {\n spanStatus.message = errorMessage;\n }\n span.setStatus(spanStatus);\n span.end();\n },\n };\n}\n\n// ============================================================================\n// Step span\n// ============================================================================\n\n/**\n * Create a step-level span.\n *\n * Attribute naming:\n * - test.step.name -- step title\n * - test.step.category -- step category\n */\nexport function createStepSpan(\n args: {\n stepTitle: string;\n stepCategory?: string;\n },\n deps: { autotel: AutotelApi },\n): { endSpan: (errorMessage?: string) => void } {\n let captured: AutotelSpan | undefined;\n deps.autotel.span(`step: ${args.stepTitle}`, (s) => {\n captured = s;\n s.setAttribute(\"test.step.name\", args.stepTitle);\n if (args.stepCategory) {\n s.setAttribute(\"test.step.category\", args.stepCategory);\n }\n });\n const span = captured!;\n\n return {\n endSpan(errorMessage?: string) {\n if (errorMessage) {\n span.setStatus({\n code: deps.autotel.SpanStatusCode.ERROR,\n message: errorMessage,\n });\n }\n span.end();\n },\n };\n}\n"],"mappings":"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","names":["relative"]}
1
+ {"version":3,"sources":["../src/reporter.ts","../src/otel-reporter-spans.ts"],"sourcesContent":["/**\n * Playwright reporter for executable-stories.\n * Generates reports using the executable-stories-formatters package.\n */\n\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport type {\n FullConfig,\n FullResult,\n Reporter,\n Suite,\n TestCase,\n TestResult,\n TestStep,\n} from '@playwright/test/reporter';\nimport type { StoryMeta } from 'executable-stories-formatters';\n// Import from formatters package\nimport {\n canonicalizeRun,\n detectCI,\n loadHistory,\n readGitSha,\n readPackageVersion,\n ReportGenerator,\n saveHistory,\n sendNotifications,\n stripAnsi,\n toCIInfo,\n updateHistory,\n type FormatterOptions,\n type RawAttachment,\n type RawRun,\n type RawStepEvent,\n type RawTestCase,\n} from 'executable-stories-formatters';\nimport {\n createStepSpan,\n createTestSpan,\n shouldInstrumentStep,\n tryLoadAutotel,\n type AutotelApi,\n} from './otel-reporter-spans.js';\n\n// Re-export types from formatters for convenience\nexport type {\n OutputFormat,\n OutputMode,\n ColocatedStyle,\n OutputRule,\n FormatterOptions,\n} from 'executable-stories-formatters';\n\n// ============================================================================\n// Reporter Options (delegates to FormatterOptions)\n// ============================================================================\n\nexport interface StoryReporterOptions extends FormatterOptions {\n /** If set, write raw run JSON (schemaVersion 1) to this path for use with the executable-stories CLI/binary */\n rawRunPath?: string;\n /**\n * Attachment persistence settings. Playwright keeps videos/screenshots/traces\n * inside its per-test outputDir; that directory may be cleaned before the\n * formatter (or a downstream CI job) runs, leaving reports with broken\n * <video>/<img> tags pointing at /home/runner/... paths. The reporter\n * eagerly persists each attachment at onTestEnd:\n * - small files (<= inlineMaxBytes) are base64-encoded into raw-run.json\n * - larger files are copied to <attachmentDir>/<test-id>/<filename>\n * so the bytes always survive even when the source dir is wiped.\n */\n attachments?: {\n /** Directory to copy non-inlined attachments to. Default: \"<outputDir>/attachments\" */\n dir?: string;\n /** Inline threshold in bytes. Default: 1 MB (1_048_576) */\n inlineMaxBytes?: number;\n /** Set false to skip persistence entirely. Default: true */\n enabled?: boolean;\n };\n /** Enable verbose reporter diagnostics. Default: false */\n debug?: boolean;\n}\n\n// ============================================================================\n// Internal Types\n// ============================================================================\n\ninterface CollectedScenario {\n /** Playwright's own test id, used to keep a runtime fixme from being counted twice. */\n testId?: string;\n meta: StoryMeta;\n sourceFile: string;\n sourceLine: number;\n status: 'passed' | 'failed' | 'skipped' | 'timedOut' | 'interrupted';\n error?: string;\n errorStack?: string;\n durationMs: number;\n projectName?: string;\n retry: number;\n retries: number;\n attachments?: RawAttachment[];\n stepEvents?: RawStepEvent[];\n}\n\n// ============================================================================\n// Utility Functions\n// ============================================================================\n\n/**\n * Convert path to relative posix format.\n */\n/**\n * Whether the run was narrowed by title, via `--grep` / `--grep-invert` or the\n * config equivalents. Such a run reports a subset of each file it touches, so\n * consumers must not read it as those files' full contents.\n *\n * `grep` is always present on FullConfig and defaults to a match-everything\n * pattern, so presence is not the signal; only a pattern that actually narrows\n * the run is. `--shard` narrows just as much and must count too.\n */\nfunction isNameFiltered(config: Partial<FullConfig> | undefined): boolean {\n if (!config) return false;\n // Sharding splits a file's tests across machines, so this process sees only\n // some of them. Scenario ids do not carry the project, so project selection\n // is not a hazard the same way: every project reports the same scenario ids,\n // and a single-project run still names every scenario in a file.\n if (config.shard != null) return true;\n if (config.grepInvert != null) return true;\n const patterns = Array.isArray(config.grep)\n ? config.grep\n : config.grep\n ? [config.grep]\n : [];\n return patterns.some((pattern) => pattern.source !== '.*');\n}\n\nfunction toRelativePosix(absolutePath: string, projectRoot: string): string {\n return path.relative(projectRoot, absolutePath).split(path.sep).join('/');\n}\n\nconst DEFAULT_ATTACHMENT_INLINE_MAX_BYTES = 1024 * 1024; // 1 MB\n\n/**\n * Persist a single Playwright attachment so its bytes outlive Playwright's\n * per-test outputDir cleanup. Small files are base64-encoded inline; larger\n * files are copied to a stable directory and referenced by absolute path.\n *\n * Returns the resolved RawAttachment. On unexpected I/O failure falls back to\n * the original path-only mapping so behavior is no worse than before.\n */\nfunction persistAttachment(\n raw: { name: string; contentType: string; path?: string; body?: unknown },\n args: { testId: string; attachmentDir: string; inlineMaxBytes: number },\n): RawAttachment {\n // Attachment already has a body (either string content or a Buffer) — encode\n // it once and we're done. No filesystem I/O required.\n if (raw.body !== undefined) {\n if (typeof raw.body === 'string') {\n return {\n name: raw.name,\n mediaType: raw.contentType,\n path: raw.path,\n body: raw.body,\n encoding: 'IDENTITY',\n };\n }\n if (Buffer.isBuffer(raw.body) || raw.body instanceof Uint8Array) {\n return {\n name: raw.name,\n mediaType: raw.contentType,\n path: raw.path,\n body: Buffer.from(raw.body as Buffer | Uint8Array).toString('base64'),\n encoding: 'BASE64',\n };\n }\n }\n\n // Path-only attachment: read the file now while it still exists.\n if (raw.path) {\n try {\n if (fs.existsSync(raw.path)) {\n const stats = fs.statSync(raw.path);\n if (stats.size <= args.inlineMaxBytes) {\n const buf = fs.readFileSync(raw.path);\n return {\n name: raw.name,\n mediaType: raw.contentType,\n path: raw.path,\n body: buf.toString('base64'),\n encoding: 'BASE64',\n byteLength: stats.size,\n };\n }\n // Too large to inline — copy to stable location instead.\n const destDir = path.join(args.attachmentDir, args.testId);\n fs.mkdirSync(destDir, { recursive: true });\n const filename = path.basename(raw.path);\n const destPath = path.join(destDir, filename);\n fs.copyFileSync(raw.path, destPath);\n return {\n name: raw.name,\n mediaType: raw.contentType,\n path: destPath,\n byteLength: stats.size,\n };\n }\n } catch {\n // Fall through to original path-only mapping.\n }\n }\n\n return {\n name: raw.name,\n mediaType: raw.contentType,\n path: raw.path,\n };\n}\n\n// ============================================================================\n// Reporter Implementation\n// ============================================================================\n\nexport default class StoryReporter implements Reporter {\n private options: StoryReporterOptions;\n private scenarios: CollectedScenario[] = [];\n /** Kept from onBegin so onEnd can find tests that never ran (planned ones). */\n private rootSuite?: Suite;\n private startTime = 0;\n private packageVersion: string | undefined;\n private gitSha: string | undefined;\n private projectRoot: string = process.cwd();\n\n /**\n * Where a relative *output* path is resolved from.\n *\n * `projectRoot` is Playwright's `rootDir`, the root of `testDir`. Scenario\n * ids hash source paths made relative to it, so it must not move. Output\n * paths are resolved from the cwd instead, like `outputDir` (written through\n * Node's cwd-relative fs calls) and the Vitest adapter's `rawRunPath`.\n * Resolving them against `rootDir` put `rawRunPath: \"docs/run.json\"` in\n * `e2e/docs/` while `outputDir: \"docs\"` beside it wrote to `docs/`.\n */\n private resolveOutputPath(filePath: string): string {\n return path.isAbsolute(filePath)\n ? filePath\n : path.resolve(process.cwd(), filePath);\n }\n /**\n * Left unknown until onBegin sees a config. Claiming full coverage without\n * having looked would let a later merge retire scenarios on a guess.\n */\n private runScope: 'full' | 'filtered' | undefined;\n /**\n * Every spec file this run executed, story-bearing or not. Collected from all\n * tests rather than from the scenarios, so a file whose last story was\n * deleted is still known to have run and can have its report emptied.\n */\n private coveredSourceFiles = new Set<string>();\n /**\n * Files where a test ended badly without ever declaring its story — a hook\n * that threw before `story.init()`, a timeout during collection. Those\n * scenarios are missing because the run broke, not because they were deleted.\n */\n private incompleteSourceFiles = new Set<string>();\n private autotel: AutotelApi | null = null;\n private testSpans = new Map<\n string,\n { endSpan: (status: string, errorMessage?: string) => void }\n >();\n private stepSpanStacks = new Map<\n string,\n Array<{ endSpan: (errorMessage?: string) => void }>\n >();\n\n constructor(options: StoryReporterOptions = {}) {\n this.options = options;\n }\n\n private debug(...args: unknown[]): void {\n if (this.options.debug) {\n console.error('[executable-stories-playwright][debug]', ...args);\n }\n }\n\n onBegin(config: FullConfig, suite: Suite): void {\n this.startTime = Date.now();\n this.rootSuite = suite;\n this.projectRoot = config.rootDir ?? process.cwd();\n if (config) this.runScope = isNameFiltered(config) ? 'filtered' : 'full';\n const includeMetadata = this.options.markdown?.includeMetadata ?? true;\n if (includeMetadata) {\n this.packageVersion = readPackageVersion(this.projectRoot);\n this.gitSha = readGitSha(this.projectRoot);\n }\n this.autotel = tryLoadAutotel();\n }\n\n onTestBegin(test: TestCase): void {\n if (!this.autotel) return;\n const sourceFile = test.location?.file;\n const sourceLine = (test.location as { line?: number })?.line;\n const titlePath = test.titlePath();\n // titlePath: [projectName, ...describes, testTitle]\n const suitePath = titlePath.slice(1, -1);\n const testTitle = titlePath[titlePath.length - 1] ?? test.title;\n\n const handle = createTestSpan(\n { testTitle, suitePath, sourceFile, sourceLine },\n { autotel: this.autotel },\n );\n this.testSpans.set(test.id, handle);\n this.stepSpanStacks.set(test.id, []);\n }\n\n onStepBegin(test: TestCase, _result: TestResult, step: TestStep): void {\n if (!this.autotel) return;\n if (!shouldInstrumentStep({ category: step.category, title: step.title }))\n return;\n\n const handle = createStepSpan(\n { stepTitle: step.title, stepCategory: step.category },\n { autotel: this.autotel },\n );\n const stack = this.stepSpanStacks.get(test.id);\n if (stack) {\n stack.push(handle);\n }\n }\n\n onStepEnd(test: TestCase, _result: TestResult, step: TestStep): void {\n if (!this.autotel) return;\n if (!shouldInstrumentStep({ category: step.category, title: step.title }))\n return;\n\n const stack = this.stepSpanStacks.get(test.id);\n if (stack && stack.length > 0) {\n const handle = stack.pop()!;\n handle.endSpan(step.error?.message);\n }\n }\n\n onTestEnd(test: TestCase, result: TestResult): void {\n // Record the file whether or not this test carries a story: the point is to\n // know the file ran, so a report emptied of stories can be retired.\n const file = test.location?.file;\n if (file) {\n const relative = toRelativePosix(file, this.projectRoot);\n this.coveredSourceFiles.add(relative);\n // No story annotation on a test that failed or timed out means the story\n // never got the chance to declare itself.\n const declared = test.annotations?.some((a) => a.type === 'story-meta');\n const brokeEarly =\n result.status === 'failed' ||\n result.status === 'timedOut' ||\n result.status === 'interrupted';\n if (!declared && brokeEarly) this.incompleteSourceFiles.add(relative);\n }\n\n // Defensive: unwind leftover step spans (interrupted/crash)\n if (this.autotel) {\n const stack = this.stepSpanStacks.get(test.id);\n if (stack) {\n while (stack.length > 0) {\n const handle = stack.pop()!;\n handle.endSpan('interrupted test');\n }\n this.stepSpanStacks.delete(test.id);\n }\n // End test span\n const testHandle = this.testSpans.get(test.id);\n if (testHandle) {\n testHandle.endSpan(result.status, result.errors?.[0]?.message);\n this.testSpans.delete(test.id);\n }\n }\n\n // Find story-meta annotation\n const storyAnnotation = test.annotations.find(\n (a) => a.type === 'story-meta',\n );\n if (!storyAnnotation?.description) return;\n\n try {\n const meta: StoryMeta = JSON.parse(storyAnnotation.description);\n\n // Read autotel OTel spans from annotations\n const otelSpansAnnotation = test.annotations.find(\n (a) => a.type === 'story-otel-spans',\n );\n if (otelSpansAnnotation?.description) {\n try {\n const spans = JSON.parse(otelSpansAnnotation.description);\n if (Array.isArray(spans) && spans.length > 0) {\n const valid = spans.filter(\n (s: unknown) =>\n s != null &&\n typeof s === 'object' &&\n typeof (s as Record<string, unknown>).spanId === 'string' &&\n typeof (s as Record<string, unknown>).name === 'string',\n );\n if (valid.length > 0) {\n meta.otelSpans = valid;\n }\n }\n } catch {\n /* ignore parse errors */\n }\n }\n\n // Get source file and line for sorting\n const sourceFile = test.location?.file\n ? toRelativePosix(test.location.file, this.projectRoot)\n : 'unknown';\n const sourceLine = (test.location as { line?: number })?.line ?? 1;\n\n // Get error message if failed. Playwright populates these with ANSI\n // color codes; strip them so reports render clean text instead of\n // garbled escape sequences like \"[2mexpect([22m...\".\n let error: string | undefined;\n let errorStack: string | undefined;\n if (result.status === 'failed' && result.errors?.length) {\n const err = result.errors[0];\n error = stripAnsi(err.message || String(err));\n errorStack = err.stack ? stripAnsi(err.stack) : undefined;\n }\n\n // Map Playwright result.attachments → RawAttachment[]. Eagerly persist\n // path-based attachments (videos/screenshots/traces) so their bytes\n // survive Playwright's per-test outputDir cleanup — see the\n // `persistAttachment` helper for the inline-vs-copy decision.\n const persistEnabled = this.options.attachments?.enabled ?? true;\n const inlineMaxBytes =\n this.options.attachments?.inlineMaxBytes ??\n DEFAULT_ATTACHMENT_INLINE_MAX_BYTES;\n const attachmentDir =\n this.options.attachments?.dir ??\n path.join(this.options.outputDir ?? 'reports', 'attachments');\n const allAttachments: RawAttachment[] = (result.attachments ?? []).map(\n (a) => {\n if (!persistEnabled) {\n let body: string | undefined;\n let encoding: 'BASE64' | 'IDENTITY' | undefined;\n if (a.body !== undefined) {\n if (typeof a.body === 'string') {\n body = a.body;\n encoding = 'IDENTITY';\n } else if (\n Buffer.isBuffer(a.body) ||\n (a.body as unknown) instanceof Uint8Array\n ) {\n body = Buffer.from(a.body as Buffer | Uint8Array).toString(\n 'base64',\n );\n encoding = 'BASE64';\n }\n }\n return {\n name: a.name,\n mediaType: a.contentType,\n path: a.path,\n body,\n encoding,\n };\n }\n return persistAttachment(a, {\n testId: test.id,\n attachmentDir,\n inlineMaxBytes,\n });\n },\n );\n\n // Deduplicate video attachments by name — Playwright may attach\n // multiple video files per test (e.g. video.webm and video-1.webm).\n // Keep only the last video attachment per name, which is the real recording.\n const attachments = deduplicateVideoAttachments(allAttachments);\n\n // Auto-promote the Playwright screen recording into a featured inline\n // video doc entry when story.init(..., { featureVideo: true }) was set.\n // The recording already rides along as an attachment; this surfaces it as\n // a playable walkthrough at the top of the scenario rather than a footer\n // attachment. Referenced by a path relative to the report output dir so\n // the generated HTML/Markdown resolves it alongside the report.\n const featureVideo =\n (meta.meta as { featureVideo?: boolean } | undefined)?.featureVideo ===\n true;\n if (featureVideo) {\n const videoAtt = attachments.find(\n (a) => a.mediaType?.startsWith('video/') && a.path,\n );\n if (videoAtt?.path) {\n const outDir = this.options.outputDir ?? 'reports';\n const relPath = path\n .relative(outDir, videoAtt.path)\n .split(path.sep)\n .join('/');\n meta.docs = meta.docs ?? [];\n meta.docs.unshift({\n kind: 'video',\n path: relPath,\n caption: 'Recorded walkthrough',\n phase: 'runtime',\n });\n }\n }\n\n // Extract step events (timing) from story steps\n const stepEvents: RawStepEvent[] = meta.steps\n .filter((s: { durationMs?: number }) => s.durationMs !== undefined)\n .map(\n (\n s: { durationMs?: number; text: string; id?: string },\n i: number,\n ) => ({\n index: i,\n stepId: s.id,\n title: s.text,\n durationMs: s.durationMs,\n }),\n );\n\n this.scenarios.push({\n testId: test.id,\n meta,\n sourceFile,\n sourceLine,\n status: result.status,\n error,\n errorStack,\n durationMs: result.duration,\n projectName: test.parent?.project()?.name,\n retry: result.retry,\n retries: test.retries,\n attachments: attachments.length > 0 ? attachments : undefined,\n stepEvents: stepEvents.length > 0 ? stepEvents : undefined,\n });\n } catch {\n // Ignore parse errors\n }\n }\n\n /**\n * `test.fixme(\"title\")` declares behaviour that is specified but not working\n * yet, which is what a planned scenario is. Those tests never run, so they\n * never call `story.init` and never reach `this.scenarios`; they have to be\n * read back off the suite instead.\n *\n * Only files that also contain story tests contribute, so a plain spec full\n * of fixmes does not leak into the generated docs. `test.skip` is left alone:\n * it means \"do not run this now\", not \"we have not built this yet\".\n */\n private collectPlannedTestCases(): RawTestCase[] {\n if (!this.rootSuite) return [];\n // Eligibility is per project AND file: the same spec can carry story tests\n // under one project and nothing under another.\n const key = (projectName: string | undefined, sourceFile: string) =>\n `${projectName ?? ''}\\u0000${sourceFile}`;\n const storyFiles = new Set(\n this.scenarios.map((s) => key(s.projectName, s.sourceFile)),\n );\n if (storyFiles.size === 0) return [];\n\n // A story that ran and then called test.fixme() at runtime is already\n // collected as a skipped scenario; it must not appear a second time as a\n // planned one.\n const collectedIds = new Set(\n this.scenarios.map((s) => s.testId).filter(Boolean),\n );\n\n const planned: RawTestCase[] = [];\n for (const test of this.rootSuite.allTests()) {\n const isFixme = test.annotations.some((a) => a.type === 'fixme');\n if (!isFixme) continue;\n if (collectedIds.has(test.id)) continue;\n\n const absolute = test.location?.file;\n if (!absolute) continue;\n const sourceFile = toRelativePosix(absolute, this.projectRoot);\n const projectName = test.parent?.project()?.name;\n if (!storyFiles.has(key(projectName, sourceFile))) continue;\n\n // Walk the parent chain rather than titlePath(): suite.type tells us\n // exactly which entries are describes, with no filename guessing.\n const suitePath: string[] = [];\n for (\n let parent: Suite | undefined = test.parent;\n parent;\n parent = parent.parent\n ) {\n if (parent.type === 'describe' && parent.title)\n suitePath.unshift(parent.title);\n }\n planned.push({\n title: test.title,\n titlePath: [...suitePath, test.title],\n story: {\n scenario: test.title,\n steps: [],\n ...(suitePath.length > 0 ? { suitePath } : {}),\n },\n sourceFile,\n sourceLine: test.location?.line ?? 1,\n status: 'todo',\n durationMs: 0,\n projectName,\n retry: 0,\n retries: 0,\n });\n }\n return planned;\n }\n\n async onEnd(_result: FullResult): Promise<void> {\n // Nothing ran and nothing was covered: there is genuinely nothing to say.\n if (this.scenarios.length === 0 && this.coveredSourceFiles.size === 0)\n return;\n\n if (this.scenarios.length > 0) {\n const sampleScenario = this.scenarios[0];\n if ('tags' in sampleScenario) {\n this.debug('tags found at scenario level', Object.keys(sampleScenario));\n }\n if (sampleScenario.meta && 'tags' in sampleScenario.meta) {\n this.debug(\n 'tags found inside meta (expected)',\n sampleScenario.meta.tags,\n );\n }\n }\n\n // Collect test cases\n const rawTestCases: RawTestCase[] = this.scenarios.map((scenario) => {\n // Map Playwright status to raw status\n const statusMap: Record<string, RawTestCase['status']> = {\n passed: 'pass',\n failed: 'fail',\n skipped: 'skip',\n timedOut: 'timeout',\n interrupted: 'interrupted',\n };\n\n const testCase = {\n title: scenario.meta.scenario,\n titlePath: scenario.meta.suitePath\n ? [...scenario.meta.suitePath, scenario.meta.scenario]\n : [scenario.meta.scenario],\n story: scenario.meta,\n sourceFile: scenario.sourceFile,\n sourceLine: Math.max(1, scenario.sourceLine),\n status: statusMap[scenario.status] ?? 'unknown',\n durationMs: scenario.durationMs,\n error: scenario.error\n ? { message: scenario.error, stack: scenario.errorStack }\n : undefined,\n projectName: scenario.projectName,\n retry: scenario.retry,\n retries: scenario.retries,\n attachments: scenario.attachments,\n stepEvents: scenario.stepEvents,\n };\n\n return testCase;\n });\n\n if (rawTestCases.length > 0) {\n const sample = rawTestCases[0];\n if ('tags' in sample) {\n this.debug('tags found at rawTestCase level', Object.keys(sample));\n }\n if (sample.story && 'tags' in sample.story) {\n this.debug('tags found inside story (expected)');\n }\n }\n\n rawTestCases.push(...this.collectPlannedTestCases());\n\n // Build RawRun\n const rawRun: RawRun = {\n testCases: rawTestCases,\n startedAtMs: this.startTime,\n finishedAtMs: Date.now(),\n projectRoot: this.projectRoot,\n ...(this.runScope ? { runScope: this.runScope } : {}),\n ...(this.coveredSourceFiles.size > 0\n ? { coveredSourceFiles: [...this.coveredSourceFiles].sort() }\n : {}),\n ...(this.incompleteSourceFiles.size > 0\n ? { incompleteSourceFiles: [...this.incompleteSourceFiles].sort() }\n : {}),\n packageVersion: this.packageVersion,\n gitSha: this.gitSha,\n ci: detectCI(),\n };\n\n // Optionally write raw run JSON for CLI/binary consumption\n const rawRunPath = this.options.rawRunPath;\n if (rawRunPath) {\n const absolutePath = this.resolveOutputPath(rawRunPath);\n const dir = path.dirname(absolutePath);\n if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });\n const payload = { schemaVersion: 1, ...rawRun };\n fs.writeFileSync(absolutePath, JSON.stringify(payload, null, 2), 'utf8');\n }\n\n // Canonicalize\n const canonicalRun = canonicalizeRun(rawRun);\n\n // 1. Generate reports\n const generator = new ReportGenerator(this.options);\n try {\n await generator.generate(canonicalRun);\n } catch (err) {\n console.error('Failed to generate reports:', err);\n }\n\n // 2. Update history (independent of report generation)\n try {\n const histOpts = this.options.history;\n if (histOpts?.filePath) {\n const historyPath = this.resolveOutputPath(histOpts.filePath);\n const store = loadHistory(\n { filePath: historyPath },\n {\n readFile: (p: string) => {\n try {\n return fs.readFileSync(p, 'utf8');\n } catch {\n return undefined;\n }\n },\n logger: console,\n },\n );\n const updated = updateHistory({\n store,\n run: canonicalRun,\n maxRuns: histOpts.maxRuns ?? 10,\n });\n const dir = path.dirname(historyPath);\n if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });\n saveHistory(\n { filePath: historyPath, store: updated },\n {\n writeFile: (p: string, c: string) => fs.writeFileSync(p, c, 'utf8'),\n },\n );\n }\n } catch (err) {\n console.error('Failed to update history:', err);\n }\n\n // 3. Send notifications (independent of both above)\n try {\n if (this.options.notification) {\n await sendNotifications(\n { run: canonicalRun, notification: this.options.notification },\n { fetch: globalThis.fetch, logger: console, toCIInfo },\n );\n }\n } catch (err) {\n console.error('Failed to send notifications:', err);\n }\n }\n}\n\n/**\n * Deduplicate video attachments by name.\n *\n * Playwright with `video: \"on\"` may produce multiple video files per test\n * in the output directory (e.g. `video.webm` and `video-1.webm`), attaching\n * all of them to the test result. This leads to duplicate videos in reports.\n *\n * For each unique video attachment name, keep only the last occurrence —\n * Playwright appends the real recording after any stubs.\n * Non-video attachments are always preserved.\n */\nexport function deduplicateVideoAttachments(\n attachments: RawAttachment[],\n): RawAttachment[] {\n // Find the last index for each video attachment name\n const lastVideoIndex = new Map<string, number>();\n for (let i = 0; i < attachments.length; i++) {\n if (attachments[i].mediaType.startsWith('video/')) {\n lastVideoIndex.set(attachments[i].name, i);\n }\n }\n\n // Keep non-video attachments and only the last video per name\n return attachments.filter((att, i) => {\n if (!att.mediaType.startsWith('video/')) return true;\n return lastVideoIndex.get(att.name) === i;\n });\n}\n","/**\n * OTel span generation helpers for the Playwright reporter.\n *\n * Uses autotel for span creation with the same lazy-loading pattern\n * as story-api.ts (createRequire). All helpers follow the fn(args, deps)\n * convention for explicit dependency injection.\n */\n\nimport { createRequire } from \"node:module\";\n\n// ============================================================================\n// Autotel API surface\n// ============================================================================\n\n/** OTel span handle returned from autotel callback */\nexport interface AutotelSpan {\n end: () => void;\n setStatus: (status: { code: number; message?: string }) => void;\n setAttribute: (key: string, value: unknown) => void;\n}\n\n/** Minimal autotel API surface we use */\nexport interface AutotelApi {\n span: (\n name: string,\n fn: (span: AutotelSpan) => void,\n ) => void;\n SpanStatusCode: { UNSET: number; ERROR: number };\n}\n\n// ============================================================================\n// Lazy loader\n// ============================================================================\n\n/**\n * Lazy-load autotel. Returns null if unavailable.\n * Same createRequire pattern as story-api.ts.\n */\nexport function tryLoadAutotel(): AutotelApi | null {\n try {\n const reqUrl =\n import.meta.url ??\n (typeof __filename !== \"undefined\" ? `file://${__filename}` : undefined);\n if (!reqUrl) return null;\n const req = createRequire(reqUrl);\n const autotel = req(\"autotel\");\n if (typeof autotel?.span !== \"function\") return null;\n return autotel as AutotelApi;\n } catch {\n return null;\n }\n}\n\n// ============================================================================\n// Step filtering\n// ============================================================================\n\n/**\n * Step filtering — explicit, heavily tested.\n * Returns true for test.step category and story step keywords.\n */\nexport function shouldInstrumentStep(step: {\n category?: string;\n title?: string;\n}): boolean {\n return step.category === \"test.step\" || isStoryStep(step);\n}\n\n/**\n * Check if a step title starts with a story keyword.\n * Documented tradeoff: may match non-story steps starting with these words\n * (e.g. \"And this works\"). Acceptable for v1.\n */\nfunction isStoryStep(step: { title?: string }): boolean {\n if (!step.title) return false;\n return /^(Given|When|Then|And|But|Arrange|Act|Assert)\\s/.test(step.title);\n}\n\n// ============================================================================\n// Status mapping\n// ============================================================================\n\n/**\n * Map test/step status to OTel SpanStatusCode.\n * Single helper used by both test and step spans.\n *\n * \"passed\"/\"skipped\" -> UNSET\n * \"failed\"/\"timedOut\"/\"interrupted\" -> ERROR\n */\nfunction mapToSpanStatus(\n status: string,\n SpanStatusCode: { UNSET: number; ERROR: number },\n): { code: number; message?: string } {\n switch (status) {\n case \"passed\":\n case \"skipped\":\n return { code: SpanStatusCode.UNSET };\n case \"failed\":\n case \"timedOut\":\n case \"interrupted\":\n return { code: SpanStatusCode.ERROR, message: status };\n default:\n return { code: SpanStatusCode.UNSET };\n }\n}\n\n// ============================================================================\n// Test span\n// ============================================================================\n\n/**\n * Create a test-level span.\n *\n * Attribute naming convention:\n * - code.filepath, code.lineno -- OTel code conventions\n * - test.name, test.suite, test.status -- test attributes\n * - story.scenario, story.tags, story.tickets -- story-specific\n */\nexport function createTestSpan(\n args: {\n testTitle: string;\n suitePath?: string[];\n sourceFile?: string;\n sourceLine?: number;\n },\n deps: { autotel: AutotelApi },\n): { endSpan: (status: string, errorMessage?: string) => void } {\n let captured: AutotelSpan | undefined;\n deps.autotel.span(`test: ${args.testTitle}`, (s) => {\n captured = s;\n s.setAttribute(\"test.name\", args.testTitle);\n if (args.suitePath?.length) {\n s.setAttribute(\"test.suite\", args.suitePath.join(\" > \"));\n }\n if (args.sourceFile) {\n s.setAttribute(\"code.filepath\", args.sourceFile);\n }\n if (args.sourceLine !== undefined) {\n s.setAttribute(\"code.lineno\", args.sourceLine);\n }\n });\n const span = captured!;\n\n return {\n endSpan(status: string, errorMessage?: string) {\n span.setAttribute(\"test.status\", status);\n const spanStatus = mapToSpanStatus(status, deps.autotel.SpanStatusCode);\n if (errorMessage) {\n spanStatus.message = errorMessage;\n }\n span.setStatus(spanStatus);\n span.end();\n },\n };\n}\n\n// ============================================================================\n// Step span\n// ============================================================================\n\n/**\n * Create a step-level span.\n *\n * Attribute naming:\n * - test.step.name -- step title\n * - test.step.category -- step category\n */\nexport function createStepSpan(\n args: {\n stepTitle: string;\n stepCategory?: string;\n },\n deps: { autotel: AutotelApi },\n): { endSpan: (errorMessage?: string) => void } {\n let captured: AutotelSpan | undefined;\n deps.autotel.span(`step: ${args.stepTitle}`, (s) => {\n captured = s;\n s.setAttribute(\"test.step.name\", args.stepTitle);\n if (args.stepCategory) {\n s.setAttribute(\"test.step.category\", args.stepCategory);\n }\n });\n const span = captured!;\n\n return {\n endSpan(errorMessage?: string) {\n if (errorMessage) {\n span.setStatus({\n code: deps.autotel.SpanStatusCode.ERROR,\n message: errorMessage,\n });\n }\n span.end();\n },\n };\n}\n"],"mappings":"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","names":["relative"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "executable-stories-playwright",
3
- "version": "8.10.8",
3
+ "version": "8.10.9",
4
4
  "license": "Apache-2.0",
5
5
  "description": "BDD-style executable stories for Playwright Test with documentation generation",
6
6
  "type": "module",
@@ -35,7 +35,7 @@
35
35
  },
36
36
  "dependencies": {
37
37
  "executable-stories-core": "0.26.0",
38
- "executable-stories-formatters": "1.19.0"
38
+ "executable-stories-formatters": "1.19.1"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@opentelemetry/api": "^1.9.1",