executable-stories-playwright 8.9.0 → 8.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -154,6 +154,16 @@ story.init(testInfo, {
154
154
 
155
155
  ## Developer experience
156
156
 
157
+ The reporter maintains one canonical JSON report per test source under
158
+ `<outputDir>/by-file/`. Documentation formats render the accumulated suite; JUnit,
159
+ Cucumber, and release manifests contain only the current execution. Playwright `grep`,
160
+ `grepInvert`, and sharding are detected as filtered runs. Incomplete collection preserves
161
+ earlier scenarios for affected files rather than treating them as deleted.
162
+
163
+ Playwright's live assertion counter is attributed to marker-style steps and
164
+ `story.expect` wrappers. A passing observable claim with zero assertions is marked in
165
+ Markdown and HTML and grades `none` in Evidence Review.
166
+
157
167
  - **API:** Top-level step functions: `given`, `when`, `then`, `and`, `but` from `executable-stories-playwright`, plus `story` object with `story.init()` and the same steps on `story` for a consistent entry point. Use `story.init(testInfo)` and then `story.given` / `story.when` / `story.then`, etc.
158
168
  - **Modifiers:** Use Playwright’s native test modifiers — `test.skip`, `test.only`, `test.fixme`, `test.slow`, `test.fail` (and `.todo`) — for scenario-level control. The `story` object does not provide its own modifiers.
159
169
  - **Attach story to a plain test:** Call `story.init(testInfo)` inside a normal `test()` so that test appears in generated docs. Playwright does not export `doc`; scenario title comes from the Playwright test title.
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@ import { createRequire } from "module";
3
3
  import * as fs from "fs";
4
4
  import * as path from "path";
5
5
  import { fileURLToPath } from "url";
6
+ import { test as test2, expect } from "@playwright/test";
6
7
  import { tryGetActiveOtelContext, resolveTraceUrl } from "executable-stories-core/utils/otel-detect";
7
8
  import { buildHtmlDocEntry } from "executable-stories-core/utils/doc-builders";
8
9
 
@@ -40,6 +41,37 @@ function isAsyncFunction(fn2) {
40
41
  }
41
42
 
42
43
  // src/story-api.ts
44
+ function readAssertionCount() {
45
+ try {
46
+ const state = expect.getState?.();
47
+ return typeof state?.assertionCalls === "number" ? state.assertionCalls : void 0;
48
+ } catch {
49
+ return void 0;
50
+ }
51
+ }
52
+ function recordAssertions(step, before) {
53
+ if (before === void 0) return;
54
+ const after = readAssertionCount();
55
+ if (after === void 0 || after < before) return;
56
+ step.assertions = after - before;
57
+ }
58
+ function flushPendingAssertions() {
59
+ const ctx = activeContext;
60
+ if (!ctx) return;
61
+ const step = ctx.pendingStep;
62
+ const before = ctx.pendingFrom;
63
+ ctx.pendingStep = null;
64
+ ctx.pendingFrom = void 0;
65
+ if (!step) return;
66
+ recordAssertions(step, before);
67
+ syncAnnotationToTest();
68
+ }
69
+ try {
70
+ test2.afterEach(() => {
71
+ flushPendingAssertions();
72
+ });
73
+ } catch {
74
+ }
43
75
  var activeContext = null;
44
76
  var activeTestInfo = null;
45
77
  var sourceOrderCounter = 0;
@@ -283,8 +315,13 @@ function createStepMarker(keyword) {
283
315
  docs: stepDocs,
284
316
  ...isCallback ? { wrapped: true } : {}
285
317
  };
318
+ flushPendingAssertions();
286
319
  ctx.meta.steps.push(step);
287
320
  ctx.currentStep = step;
321
+ if (!isCallback) {
322
+ ctx.pendingStep = step;
323
+ ctx.pendingFrom = readAssertionCount();
324
+ }
288
325
  syncAnnotationToTest();
289
326
  if (isChildrenArray) {
290
327
  const children = docsOrBody;
@@ -305,6 +342,7 @@ function createStepMarker(keyword) {
305
342
  const body = docsOrBody;
306
343
  const label = `${step.keyword}: ${text}`;
307
344
  const start = performance.now();
345
+ const assertionsBefore = readAssertionCount();
308
346
  if (ctx.fixtures !== void 0 && (isAsyncFunction(body) || body.length >= 2)) {
309
347
  const fixtures = ctx.fixtures;
310
348
  const result = runStep(
@@ -316,11 +354,13 @@ function createStepMarker(keyword) {
316
354
  return result.then(
317
355
  (val) => {
318
356
  step.durationMs = performance.now() - start;
357
+ recordAssertions(step, assertionsBefore);
319
358
  syncAnnotationToTest();
320
359
  return val;
321
360
  },
322
361
  (err) => {
323
362
  step.durationMs = performance.now() - start;
363
+ recordAssertions(step, assertionsBefore);
324
364
  syncAnnotationToTest();
325
365
  throw err;
326
366
  }
@@ -332,11 +372,13 @@ function createStepMarker(keyword) {
332
372
  return result.then(
333
373
  (val) => {
334
374
  step.durationMs = performance.now() - start;
375
+ recordAssertions(step, assertionsBefore);
335
376
  syncAnnotationToTest();
336
377
  return val;
337
378
  },
338
379
  (err) => {
339
380
  step.durationMs = performance.now() - start;
381
+ recordAssertions(step, assertionsBefore);
340
382
  syncAnnotationToTest();
341
383
  throw err;
342
384
  }
@@ -424,7 +466,8 @@ function init(first, second, third) {
424
466
  activeTimers: /* @__PURE__ */ new Map(),
425
467
  timerCounter: 0,
426
468
  fixtures,
427
- traceUrlTemplate
469
+ traceUrlTemplate,
470
+ pendingStep: null
428
471
  };
429
472
  activeTestInfo = testInfo;
430
473
  }
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/story-api.ts","../src/step-runner.ts","../src/index.ts"],"sourcesContent":["/**\n * Playwright story.* API for executable-stories.\n *\n * Uses native Playwright test() with opt-in documentation:\n *\n * @example\n * ```ts\n * import { test, expect } from '@playwright/test';\n * import { story } from 'executable-stories-playwright';\n *\n * test.describe('Calculator', () => {\n * test('adds two numbers', async ({ page }, testInfo) => {\n * story.init(testInfo);\n *\n * story.given('two numbers 5 and 3');\n * const a = 5;\n * const b = 3;\n *\n * story.when('I add them together');\n * const result = a + b;\n *\n * story.then('the result is 8');\n * expect(result).toBe(8);\n * });\n * });\n * ```\n */\n\nimport { createRequire } from 'node:module';\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport type { TestInfo, PlaywrightTestArgs, PlaywrightTestOptions, Page } from '@playwright/test';\nimport { tryGetActiveOtelContext, resolveTraceUrl } from 'executable-stories-core/utils/otel-detect';\nimport { buildHtmlDocEntry } from 'executable-stories-core/utils/doc-builders';\nimport type {\n StepKeyword,\n StoryMeta,\n StoryStep,\n DocEntry,\n FeatureInput,\n NormalizedTicket,\n TicketInput,\n} from './types';\nimport type {\n StoryDocs,\n StoryOptions,\n AttachmentOptions,\n ScopedAttachment,\n KvOptions,\n JsonOptions,\n StateOptions,\n CodeOptions,\n TableOptions,\n LinkOptions,\n SectionOptions,\n MermaidOptions,\n ScreenshotOptions,\n VideoOptions,\n HtmlOptions,\n CustomOptions,\n ConsoleOptions,\n ObservePageErrorsOptions,\n} from './types';\nimport { runStep, isAsyncFunction } from './step-runner';\nimport type { TestStepInfo } from './step-runner';\n\n// Re-export types for consumers\nexport type {\n StoryMeta,\n StoryStep,\n DocEntry,\n StepKeyword,\n StoryDocs,\n StoryOptions,\n AttachmentOptions,\n} from './types';\n\n// ============================================================================\n// Internal types\n// ============================================================================\n\n/** Fixture type for step callbacks: Playwright test args + options; custom extend() fixtures as unknown. */\ntype PlaywrightFixtures = PlaywrightTestArgs & PlaywrightTestOptions & Record<string, unknown>;\n\ninterface TimerEntry {\n start: number;\n stepIndex?: number;\n stepId?: string;\n consumed: boolean;\n}\n\ninterface StoryContext {\n meta: StoryMeta;\n currentStep: StoryStep | null;\n stepCounter: number;\n attachments: ScopedAttachment[];\n activeTimers: Map<number, TimerEntry>;\n timerCounter: number;\n fixtures?: Record<string, unknown>;\n /** Trace-link URL template, captured at init for later use by attachSpans. */\n traceUrlTemplate?: string;\n}\n\n// ============================================================================\n// Playwright-specific context\n// ============================================================================\n\n/** Active story context - set by story.init() */\nlet activeContext: StoryContext | null = null;\n\n/** Reference to testInfo for attaching metadata */\nlet activeTestInfo: TestInfo | null = null;\n\n/** Counter to track source order of stories (increments on each story.init call) */\nlet sourceOrderCounter = 0;\n\n/**\n * Get the current story context. Throws if story.init() wasn't called.\n */\nfunction getContext(): StoryContext {\n if (!activeContext) {\n throw new Error(\n \"story.init(testInfo) must be called first. Use: test('name', async ({ page }, testInfo) => { story.init(testInfo); ... });\",\n );\n }\n return activeContext;\n}\n\n// ============================================================================\n// Helper functions (inlined from core)\n// ============================================================================\n\nfunction normalizeTickets(\n ticket: TicketInput | TicketInput[] | undefined,\n): NormalizedTicket[] | undefined {\n if (!ticket) return undefined;\n const arr = Array.isArray(ticket) ? ticket : [ticket];\n return arr.map((t) => (typeof t === 'string' ? { id: t } : t));\n}\n\nfunction convertStoryDocsToEntries(docs: StoryDocs): DocEntry[] {\n const entries: DocEntry[] = [];\n\n if (docs.note) {\n entries.push({ kind: 'note', text: docs.note, phase: 'runtime' });\n }\n if (docs.tag) {\n const names = Array.isArray(docs.tag) ? docs.tag : [docs.tag];\n entries.push({ kind: 'tag', names, phase: 'runtime' });\n }\n if (docs.kv) {\n for (const [label, value] of Object.entries(docs.kv)) {\n entries.push({ kind: 'kv', label, value, phase: 'runtime' });\n }\n }\n if (docs.code) {\n entries.push({\n kind: 'code',\n label: docs.code.label,\n content: docs.code.content,\n lang: docs.code.lang,\n phase: 'runtime',\n });\n }\n if (docs.json) {\n entries.push({\n kind: 'code',\n label: docs.json.label,\n content: JSON.stringify(docs.json.value, null, 2),\n lang: 'json',\n phase: 'runtime',\n });\n }\n if (docs.state) {\n warnIfStateLarge(docs.state.label, docs.state.value);\n entries.push({\n kind: 'state',\n label: docs.state.label,\n value: docs.state.value,\n phase: 'runtime',\n });\n }\n if (docs.table) {\n entries.push({\n kind: 'table',\n label: docs.table.label,\n columns: docs.table.columns,\n rows: docs.table.rows,\n phase: 'runtime',\n });\n }\n if (docs.link) {\n entries.push({\n kind: 'link',\n label: docs.link.label,\n url: docs.link.url,\n phase: 'runtime',\n });\n }\n if (docs.section) {\n entries.push({\n kind: 'section',\n title: docs.section.title,\n markdown: docs.section.markdown,\n phase: 'runtime',\n });\n }\n if (docs.mermaid) {\n entries.push({\n kind: 'mermaid',\n code: docs.mermaid.code,\n title: docs.mermaid.title,\n phase: 'runtime',\n });\n }\n if (docs.screenshot) {\n if (docs.screenshot.page) {\n throw new Error(\n 'story.screenshot({ page }) is not supported inside inline step docs ' +\n '(e.g. story.then(text, { screenshot: { page } })) because capturing a ' +\n 'screenshot is asynchronous and step markers run synchronously. Call ' +\n 'story.screenshot({ page, alt }) as its own statement instead.',\n );\n }\n if (!docs.screenshot.path) {\n throw new Error('story docs screenshot requires a `path` (or use story.screenshot({ page }) instead).');\n }\n entries.push({\n kind: 'screenshot',\n // Inline file bytes as a `data:` URI here too — this is the same\n // capture-vs-cleanup race that story.screenshot() guards against, and\n // this path used to skip it, so a screenshot passed via inline step\n // docs would silently never embed no matter how the file was captured.\n path: inlineScreenshotIfPossible(docs.screenshot.path),\n alt: docs.screenshot.alt,\n phase: 'runtime',\n });\n }\n if (docs.video) {\n entries.push({\n kind: 'video',\n path: docs.video.path,\n caption: docs.video.caption,\n poster: docs.video.poster,\n phase: 'runtime',\n });\n }\n if (docs.html) {\n entries.push(buildHtmlEntry(docs.html));\n }\n if (docs.custom) {\n entries.push({\n kind: 'custom',\n type: docs.custom.type,\n data: docs.custom.data,\n phase: 'runtime',\n });\n }\n\n return entries;\n}\n\n/**\n * Capture-time inlining for the html doc kind. Playwright is the only adapter\n * that pre-resolves a source before building the entry; the exactly-one-of rule\n * and the entry shape come from the canonical `buildHtmlDocEntry`.\n *\n * An *absolute* local path (e.g. `/home/runner/work/.../x.html`, or anything\n * under `testInfo.outputPath()`) is read now and inlined as iframe srcdoc: that\n * path 404s once the HTML report is downloaded as a CI artifact and opened on\n * another machine, so we capture the bytes while they exist — the same\n * reasoning as screenshot data-URI inlining.\n *\n * A *relative* path is left as-is so it behaves exactly like the Vitest adapter:\n * the formatter reads it at format time (default), or copies it as a hashed\n * asset under `assetMode: \"copy\"`. Inlining relative paths early would needlessly\n * foreclose copy mode. The embedded HTML must be self-contained (single-file):\n * relative references to sibling CSS/JS are not rewritten (directory bundling is planned).\n */\nfunction buildHtmlEntry(options: HtmlOptions): DocEntry {\n let { path: htmlPath, content } = options;\n // Only inline when `path` is the lone source: that keeps an invalid\n // multi-source call (e.g. path + content) reaching the canonical validator\n // instead of being silently \"fixed\" by overwriting content from disk. Listing\n // `htmlPath !== undefined` first in the chain narrows it for the rest.\n if (\n htmlPath !== undefined &&\n options.url === undefined &&\n content === undefined &&\n !/^https?:/i.test(htmlPath) &&\n path.isAbsolute(htmlPath)\n ) {\n try {\n if (fs.existsSync(htmlPath)) {\n content = fs.readFileSync(htmlPath, 'utf8');\n htmlPath = undefined;\n }\n } catch {\n // keep the path — the formatter retries at format time\n }\n }\n return buildHtmlDocEntry({ ...options, path: htmlPath, content });\n}\n\nconst SCREENSHOT_MIME_BY_EXT: Record<string, string> = {\n png: 'image/png',\n jpg: 'image/jpeg',\n jpeg: 'image/jpeg',\n gif: 'image/gif',\n webp: 'image/webp',\n svg: 'image/svg+xml',\n avif: 'image/avif',\n bmp: 'image/bmp',\n};\n\n/**\n * Read a screenshot file and return a `data:` URI; fall back to the original\n * path on any failure (remote URL, missing file, unknown extension).\n *\n * A missing/unreadable local path almost always means the screenshot was\n * never taken at that path — e.g. `story.screenshot({ path })` called without\n * a preceding `page.screenshot({ path })`, or the two paths drifted apart.\n * That failure otherwise surfaces minutes later as a broken image in a PR\n * comment or a \"Screenshot unavailable\" placeholder in the HTML report, far\n * from the line that caused it — so warn immediately, at the call site.\n */\nfunction inlineScreenshotIfPossible(filePath: string): string {\n if (/^(?:https?:|data:)/i.test(filePath)) return filePath;\n const ext = path.extname(filePath).slice(1).toLowerCase();\n const mime = SCREENSHOT_MIME_BY_EXT[ext];\n if (!mime) return filePath;\n try {\n if (!fs.existsSync(filePath)) {\n warnScreenshotUnavailable(filePath);\n return filePath;\n }\n const buf = fs.readFileSync(filePath);\n return `data:${mime};base64,${buf.toString('base64')}`;\n } catch (err) {\n warnScreenshotUnavailable(filePath, err);\n return filePath;\n }\n}\n\nfunction warnScreenshotUnavailable(filePath: string, cause?: unknown): void {\n const causeMessage = cause instanceof Error ? cause.message : cause ? String(cause) : undefined;\n console.warn(\n `[executable-stories-playwright] story.screenshot(): could not read \"${filePath}\" — ` +\n 'the report will show a \"Screenshot unavailable\" placeholder instead of the image. ' +\n 'Make sure a screenshot is written to this exact path before story.screenshot() runs ' +\n '(e.g. `await page.screenshot({ path })`), or capture it directly with ' +\n '`story.screenshot({ page, alt })`.' +\n (causeMessage ? ` Cause: ${causeMessage}` : ''),\n );\n}\n\n/**\n * story.video() never reads/inlines the file (video bytes are too large for\n * a `data:` URI) — it always defers to the formatter's asset bundler, which\n * runs later, at report-generation time. A *relative* path is legitimately\n * unresolvable yet (the bundler resolves it against the report output), so\n * this only warns for an *absolute* local path missing right now — that can\n * never resolve later either, since it's not relative to anything the\n * bundler controls. The most common cause: passing a path nothing has\n * written to (e.g. a typo, or the video hasn't finished flushing to disk —\n * see Playwright's `page.video()?.path()`, which resolves only after\n * `page.close()`).\n */\nfunction warnIfAbsoluteVideoPathMissing(filePath: string): void {\n if (/^(?:https?:|data:)/i.test(filePath)) return;\n if (!path.isAbsolute(filePath)) return;\n if (fs.existsSync(filePath)) return;\n console.warn(\n `[executable-stories-playwright] story.video(): \"${filePath}\" does not exist yet. ` +\n 'Video bytes are never inlined, so this exact path must resolve to a real file by the time the ' +\n 'report is built, or the report will show a \"Video unavailable\" placeholder instead of the clip.',\n );\n}\n\n/**\n * Warn (non-fatal) when a state snapshot serializes past 100KB — the entry is\n * still recorded, but a projection is usually what the storyboard needs.\n */\nfunction warnIfStateLarge(label: string | undefined, value: unknown): void {\n try {\n const len = JSON.stringify(value)?.length ?? 0;\n if (len > 100_000) {\n console.warn(\n `[executable-stories] state \"${label ?? ''}\" is ${Math.round(len / 1024)}KB — consider capturing a projection`,\n );\n }\n } catch {\n // Non-serializable value: recorded as-is, size unknown.\n }\n}\n\nfunction attachDoc(entry: DocEntry, children?: DocEntry[]): DocEntry {\n const ctx = getContext();\n if (children && children.length > 0) {\n entry.children = children;\n const childSet = new Set<DocEntry>(children);\n const filterDocs = (docs: DocEntry[]) => docs.filter((d) => !childSet.has(d));\n // Remove children from ALL containers (story-level + every step)\n ctx.meta.docs = filterDocs(ctx.meta.docs ?? []);\n for (const step of ctx.meta.steps) {\n if (step.docs) step.docs = filterDocs(step.docs);\n }\n }\n if (ctx.currentStep) {\n ctx.currentStep.docs ??= [];\n ctx.currentStep.docs.push(entry);\n } else {\n ctx.meta.docs ??= [];\n ctx.meta.docs.push(entry);\n }\n syncAnnotationToTest();\n return entry;\n}\n\n// ============================================================================\n// Suite path extraction\n// ============================================================================\n\n/**\n * Extract the suite path from testInfo.titlePath.\n * Playwright's titlePath includes: [projectName, ...describeTitles, testTitle]\n * We want just the describe titles (excluding project and test name).\n */\nfunction extractSuitePath(testInfo: TestInfo): string[] | undefined {\n const titlePath = testInfo.titlePath;\n if (titlePath.length <= 2) {\n return undefined;\n }\n const suitePath = titlePath.slice(1, -1);\n return suitePath.length > 0 ? suitePath : undefined;\n}\n\n// ============================================================================\n// Step markers\n// ============================================================================\n\nfunction createStepMarker(keyword: StepKeyword) {\n function stepMarker(text: string, docs?: StoryDocs): void;\n function stepMarker(text: string, children: DocEntry[]): void;\n function stepMarker<T>(text: string, body: (fixtures: PlaywrightFixtures, step?: TestStepInfo) => T): T;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n function stepMarker<T>(text: string, docsOrBody?: StoryDocs | DocEntry[] | ((...args: any[]) => T)): T | void {\n const ctx = getContext();\n const isCallback = typeof docsOrBody === 'function';\n const isChildrenArray = Array.isArray(docsOrBody);\n\n const resolvedKeyword: StepKeyword =\n (keyword === 'Given' || keyword === 'When' || keyword === 'Then') &&\n ctx.meta.steps.some((s) => s.keyword === keyword)\n ? 'And'\n : keyword;\n\n let stepDocs: DocEntry[] = [];\n if (!isCallback && !isChildrenArray && docsOrBody) {\n stepDocs = convertStoryDocsToEntries(docsOrBody as StoryDocs);\n }\n\n const step: StoryStep = {\n id: `step-${ctx.stepCounter++}`,\n keyword: resolvedKeyword,\n text,\n docs: stepDocs,\n ...(isCallback ? { wrapped: true } : {}),\n };\n\n ctx.meta.steps.push(step);\n ctx.currentStep = step;\n syncAnnotationToTest();\n\n // Handle DocEntry[] children: attach as step docs and deduplicate from story-level\n if (isChildrenArray) {\n const children = docsOrBody as DocEntry[];\n if (children.length > 0) {\n const childSet = new Set<DocEntry>(children);\n // Deduplicate from story-level docs\n ctx.meta.docs = (ctx.meta.docs ?? []).filter((d) => !childSet.has(d));\n // Deduplicate from step docs of earlier steps\n for (const prevStep of ctx.meta.steps) {\n if (prevStep !== step && prevStep.docs) {\n prevStep.docs = prevStep.docs.filter((d) => !childSet.has(d));\n }\n }\n step.docs = [...(step.docs ?? []), ...children];\n }\n syncAnnotationToTest();\n return;\n }\n\n if (!isCallback) return;\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const body = docsOrBody as (fixtures?: PlaywrightFixtures, stepInfo?: TestStepInfo) => T;\n const label = `${step.keyword}: ${text}`;\n const start = performance.now();\n\n // ── Async or stepInfo-aware callbacks: route through runStep() for Playwright-native integrations ──\n // Integrations: screencast chapters (v1.59), test.step/TestStepInfo (v1.51),\n // tracing.group (v1.49). Activated when fixtures are available AND either:\n // 1. callback is an async function, OR\n // 2. callback expects TestStepInfo (arity >= 2)\n if (ctx.fixtures !== undefined && (isAsyncFunction(body) || body.length >= 2)) {\n const fixtures = ctx.fixtures as Record<string, unknown>;\n const result = runStep(\n label,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n body as unknown as (fixtures: Record<string, unknown>, step?: TestStepInfo) => Promise<any>,\n fixtures,\n );\n return result.then(\n (val: T) => { step.durationMs = performance.now() - start; syncAnnotationToTest(); return val; },\n (err: unknown) => { step.durationMs = performance.now() - start; syncAnnotationToTest(); throw err; },\n ) as T;\n }\n\n // ── Sync callbacks or no-fixture context: existing behaviour ─────────────\n try {\n const result = ctx.fixtures !== undefined ? body(ctx.fixtures as PlaywrightFixtures) : body();\n if (result instanceof Promise) {\n return result.then(\n (val) => { step.durationMs = performance.now() - start; syncAnnotationToTest(); return val; },\n (err) => { step.durationMs = performance.now() - start; syncAnnotationToTest(); throw err; },\n ) as T;\n }\n step.durationMs = performance.now() - start;\n syncAnnotationToTest();\n return result;\n } catch (err) {\n step.durationMs = performance.now() - start;\n syncAnnotationToTest();\n throw err;\n }\n }\n return stepMarker;\n}\n\n// ============================================================================\n// story.init() - Playwright-specific\n// ============================================================================\n\nfunction isTestInfo(x: unknown): x is TestInfo {\n return (\n typeof x === 'object' &&\n x !== null &&\n 'title' in x &&\n 'annotations' in x &&\n Array.isArray((x as TestInfo).annotations)\n );\n}\n\n/** init(testInfo) or init(fixtures, testInfo) or init(testInfo, { fixtures }). */\nfunction init(\n first: TestInfo | unknown,\n second?: StoryOptions | TestInfo,\n third?: StoryOptions,\n): void {\n let testInfo: TestInfo;\n let options: StoryOptions | undefined;\n let fixtures: unknown;\n\n if (second !== undefined && isTestInfo(second)) {\n fixtures = first;\n testInfo = second;\n options = third;\n } else {\n testInfo = first as TestInfo;\n options = second;\n fixtures = options?.fixtures;\n }\n\n const meta: StoryMeta = {\n scenario: testInfo.title,\n steps: [],\n suitePath: extractSuitePath(testInfo),\n tags: options?.tags,\n tickets: normalizeTickets(options?.ticket),\n covers: options?.covers,\n meta: options?.meta,\n sourceOrder: sourceOrderCounter++,\n };\n\n // Flag the scenario so the reporter promotes the Playwright screen recording\n // into a featured inline video doc entry (see reporter onTestEnd).\n if (options?.featureVideo) {\n meta.meta = { ...meta.meta, featureVideo: true };\n }\n\n // OTel bridge: detect active span, flow data bidirectionally\n const traceUrlTemplate =\n options?.traceUrlTemplate ?? process.env.OTEL_TRACE_URL_TEMPLATE;\n const otelCtx = tryGetActiveOtelContext();\n if (otelCtx) {\n // OTel -> Story: capture traceId + docs from the active span.\n applyTraceToMeta(meta, otelCtx.traceId, otelCtx.spanId, traceUrlTemplate);\n\n // Story -> OTel: enrich active span with story attributes\n try {\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n const reqUrl = import.meta.url\n ?? (typeof __filename !== 'undefined' ? `file://${__filename}` : undefined);\n const req = createRequire(reqUrl!);\n const api = req('@opentelemetry/api');\n const span = api.trace?.getActiveSpan?.();\n if (span) {\n span.setAttribute('story.scenario', testInfo.title);\n if (options?.tags?.length) span.setAttribute('story.tags', options.tags);\n if (options?.ticket) {\n const tickets = Array.isArray(options.ticket) ? options.ticket : [options.ticket];\n span.setAttribute('story.tickets', tickets.map((t) => typeof t === 'string' ? t : t.id));\n }\n }\n } catch { /* OTel not available */ }\n }\n\n testInfo.annotations.push({\n type: 'story-meta',\n description: JSON.stringify(meta),\n });\n\n const declaredFeature =\n declaredFeatures.get(fileKey(testInfo.file)) ?? declaredFeatures.get(UNRESOLVED_FILE);\n if (declaredFeature) {\n testInfo.annotations.push({\n type: 'story-feature',\n description: JSON.stringify(declaredFeature),\n });\n }\n\n // ── Feature: Tag sync (v1.43) ─────────────────────────────────────────────\n // Sync story tags to Playwright's native annotation system so they appear in\n // UI Mode tag filters and the HTML reporter's tag display.\n for (const tag of options?.tags ?? []) {\n testInfo.annotations.push({ type: 'tag', description: tag });\n }\n\n activeContext = {\n meta,\n currentStep: null,\n stepCounter: 0,\n attachments: [],\n activeTimers: new Map(),\n timerCounter: 0,\n fixtures: fixtures as Record<string, unknown> | undefined,\n traceUrlTemplate,\n };\n activeTestInfo = testInfo;\n}\n\n/**\n * Update the story-meta annotation on testInfo with the current meta (including steps).\n * Called after each step/doc so the reporter sees the full story in onTestEnd.\n */\nfunction syncAnnotationToTest(): void {\n if (!activeTestInfo || !activeContext) return;\n const annotation = activeTestInfo.annotations.find(\n (a) => a.type === 'story-meta',\n );\n if (annotation) {\n annotation.description = JSON.stringify(activeContext.meta);\n }\n}\n\n/**\n * Bridge an OTel trace into the story's meta + docs: structured `otel` meta, a\n * \"Trace ID\" key-value, and a \"View Trace\" link when the template resolves.\n * Idempotent — once a traceId is recorded it is not overwritten, so the\n * active-span path in init() and the explicit path in attachSpans() compose\n * without duplicating entries.\n */\nfunction applyTraceToMeta(\n meta: StoryMeta,\n traceId: string,\n spanId: string | undefined,\n template: string | undefined,\n): void {\n const existing = (meta.meta as { otel?: { traceId?: string } } | undefined)\n ?.otel;\n if (existing?.traceId) return;\n\n meta.meta = { ...meta.meta, otel: { traceId, spanId } };\n meta.docs = meta.docs ?? [];\n meta.docs.push({\n kind: 'kv',\n label: 'Trace ID',\n value: traceId,\n phase: 'runtime',\n });\n const url = resolveTraceUrl(template, traceId);\n if (url) {\n meta.docs.push({ kind: 'link', label: 'View Trace', url, phase: 'runtime' });\n }\n}\n\n// ============================================================================\n// story.fn() and story.expect()\n// ============================================================================\n\n/**\n * Wrap a function as a step with timing and error capture.\n * Records the step with `wrapped: true` and `durationMs`.\n */\nfunction fn<T>(keyword: StepKeyword, text: string, body: (fixtures: PlaywrightFixtures) => T): T;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction fn<T>(keyword: StepKeyword, text: string, body: (...args: any[]) => T): T {\n const ctx = getContext();\n const resolvedKeyword: StepKeyword =\n (keyword === 'Given' || keyword === 'When' || keyword === 'Then') &&\n ctx.meta.steps.some((s) => s.keyword === keyword)\n ? 'And'\n : keyword;\n const step: StoryStep = {\n id: `step-${ctx.stepCounter++}`,\n keyword: resolvedKeyword,\n text,\n docs: [],\n wrapped: true,\n };\n ctx.meta.steps.push(step);\n ctx.currentStep = step;\n syncAnnotationToTest();\n\n const start = performance.now();\n try {\n const result = ctx.fixtures !== undefined ? body(ctx.fixtures as PlaywrightFixtures) : body();\n if (result instanceof Promise) {\n return result.then(\n (val) => {\n step.durationMs = performance.now() - start;\n syncAnnotationToTest();\n return val;\n },\n (err) => {\n step.durationMs = performance.now() - start;\n syncAnnotationToTest();\n throw err;\n },\n ) as T;\n }\n step.durationMs = performance.now() - start;\n syncAnnotationToTest();\n return result;\n } catch (err) {\n step.durationMs = performance.now() - start;\n syncAnnotationToTest();\n throw err;\n }\n}\n\n/**\n * Wrap an assertion as a Then step with timing and error capture.\n * Shorthand for `story.fn('Then', text, body)`.\n */\nfunction storyExpect<T>(text: string, body: () => T): T {\n return fn('Then', text, body);\n}\n\n// ============================================================================\n// Playwright-specific attach\n// ============================================================================\n\nfunction playwrightAttach(options: AttachmentOptions): void {\n const ctx = getContext();\n const stepIndex = ctx.currentStep\n ? ctx.meta.steps.indexOf(ctx.currentStep)\n : undefined;\n ctx.attachments.push({\n ...options,\n stepId: ctx.currentStep?.id,\n });\n syncAnnotationToTest();\n\n if (activeTestInfo) {\n const attachOptions: { name: string; contentType: string; path?: string; body?: string | Buffer } = {\n name: options.name,\n contentType: options.mediaType,\n };\n if (options.path) attachOptions.path = options.path;\n if (options.body) attachOptions.body = options.body;\n activeTestInfo.attach(options.name, attachOptions);\n }\n}\n\n// ============================================================================\n// story.screenshot() — capture-and-attach, or attach-an-existing-file\n// ============================================================================\n\n/**\n * Two ways to attach a screenshot:\n * - `story.screenshot({ page, alt })` captures a fresh screenshot and inlines\n * it directly from the in-memory buffer — no filesystem round-trip, so\n * there's no path to fall out of sync and nothing for Playwright's output\n * cleanup to delete before the report is built. Prefer this form.\n * - `story.screenshot({ path, alt })` attaches a screenshot that already\n * exists on disk (e.g. one taken earlier for another purpose). The caller\n * is responsible for making sure something wrote a file to `path` first.\n */\nfunction screenshotImpl(options: ScreenshotOptions & { page: Page }, children?: DocEntry[]): Promise<DocEntry>;\nfunction screenshotImpl(\n options: ScreenshotOptions & { path: string; page?: undefined },\n children?: DocEntry[],\n): DocEntry;\nfunction screenshotImpl(options: ScreenshotOptions, children?: DocEntry[]): DocEntry | Promise<DocEntry> {\n if (options.page) {\n return options.page\n .screenshot(options.path ? { path: options.path, fullPage: options.fullPage ?? true } : { fullPage: options.fullPage ?? true })\n .then((buffer) => {\n const dataUri = `data:image/png;base64,${buffer.toString('base64')}`;\n return attachDoc({ kind: 'screenshot', path: dataUri, alt: options.alt, phase: 'runtime' }, children);\n });\n }\n if (!options.path) {\n throw new Error('story.screenshot() requires either `path` (an existing file) or `page` (to capture one).');\n }\n // Inline file bytes as a `data:` URI so the screenshot survives Playwright's\n // per-test outputDir cleanup (passing tests have their `test-results/<test>/`\n // directory deleted before the formatter runs). Falls back to the original\n // path for remote URLs or unreadable files.\n const resolvedPath = inlineScreenshotIfPossible(options.path);\n return attachDoc({ kind: 'screenshot', path: resolvedPath, alt: options.alt, phase: 'runtime' }, children);\n}\n\n// ============================================================================\n// Export story object\n// ============================================================================\n\nexport const story = {\n init,\n feature,\n\n // BDD step markers\n given: createStepMarker('Given'),\n when: createStepMarker('When'),\n then: createStepMarker('Then'),\n and: createStepMarker('And'),\n but: createStepMarker('But'),\n\n // AAA pattern aliases\n arrange: createStepMarker('Given'),\n act: createStepMarker('When'),\n assert: createStepMarker('Then'),\n\n // Additional aliases\n setup: createStepMarker('Given'),\n context: createStepMarker('Given'),\n execute: createStepMarker('When'),\n action: createStepMarker('When'),\n verify: createStepMarker('Then'),\n\n // Standalone doc methods\n note(text: string, children?: DocEntry[]): DocEntry {\n return attachDoc({ kind: 'note', text, phase: 'runtime' }, children);\n },\n\n tag(name: string | string[], children?: DocEntry[]): DocEntry {\n const names = Array.isArray(name) ? name : [name];\n return attachDoc({ kind: 'tag', names, phase: 'runtime' }, children);\n },\n\n kv(options: KvOptions, children?: DocEntry[]): DocEntry {\n return attachDoc({ kind: 'kv', label: options.label, value: options.value, phase: 'runtime' }, children);\n },\n\n json(options: JsonOptions, children?: DocEntry[]): DocEntry {\n const content = JSON.stringify(options.value, null, 2);\n return attachDoc({ kind: 'code', label: options.label, content, lang: 'json', phase: 'runtime' }, children);\n },\n\n /**\n * Capture what the world looks like at this step: a JSON-serializable data\n * snapshot that becomes a storyboard frame. Consecutive states with the same\n * label are diffed downstream, so snapshot the same projection at each step.\n *\n * A step can carry BOTH a screenshot and a state — the screen and the\n * backend record for the same moment:\n *\n * @example\n * story.when('the user completes checkout');\n * await story.screenshot({ page, alt: 'Payment form' });\n * story.state({ label: 'order', value: { id: 1042, status: 'paid', total: '£25' } });\n */\n state(options: StateOptions, children?: DocEntry[]): DocEntry {\n warnIfStateLarge(options.label, options.value);\n return attachDoc({ kind: 'state', label: options.label, value: options.value, phase: 'runtime' }, children);\n },\n\n code(options: CodeOptions, children?: DocEntry[]): DocEntry {\n return attachDoc({ kind: 'code', label: options.label, content: options.content, lang: options.lang, phase: 'runtime' }, children);\n },\n\n table(options: TableOptions, children?: DocEntry[]): DocEntry {\n return attachDoc({ kind: 'table', label: options.label, columns: options.columns, rows: options.rows, phase: 'runtime' }, children);\n },\n\n link(options: LinkOptions, children?: DocEntry[]): DocEntry {\n return attachDoc({ kind: 'link', label: options.label, url: options.url, phase: 'runtime' }, children);\n },\n\n section(options: SectionOptions, children?: DocEntry[]): DocEntry {\n return attachDoc({ kind: 'section', title: options.title, markdown: options.markdown, phase: 'runtime' }, children);\n },\n\n mermaid(options: MermaidOptions, children?: DocEntry[]): DocEntry {\n return attachDoc({ kind: 'mermaid', code: options.code, title: options.title, phase: 'runtime' }, children);\n },\n\n screenshot: screenshotImpl,\n\n video(options: VideoOptions, children?: DocEntry[]): DocEntry {\n // Unlike screenshots, video bytes are never inlined as a data URI — they're\n // too large. The path is kept as-is so the formatter's asset bundler copies\n // the file into the report/docs site. Pass a path relative to the output, or\n // use `featureVideo: true` on story.init() to auto-promote the Playwright\n // recording instead.\n warnIfAbsoluteVideoPathMissing(options.path);\n return attachDoc(\n { kind: 'video', path: options.path, caption: options.caption, poster: options.poster, phase: 'runtime' },\n children,\n );\n },\n\n /**\n * Embed HTML in a sandboxed iframe in the current step or story-level docs.\n * Exactly one of path/url/content is required. Local files are inlined so\n * the report stays self-contained — the HTML must therefore be\n * self-contained too (use your tool's single-file mode); relative sub-asset\n * references won't resolve.\n * @example story.html({ path: './coverage/index.html', title: 'Coverage' })\n * @example story.html({ url: 'https://dash.example.com/run/42', height: 600 })\n * @example story.html({ content: chartHtml, title: 'Latency chart' })\n */\n html(options: HtmlOptions, children?: DocEntry[]): DocEntry {\n return attachDoc(buildHtmlEntry(options), children);\n },\n\n custom(options: CustomOptions, children?: DocEntry[]): DocEntry {\n return attachDoc({ kind: 'custom', type: options.type, data: options.data, phase: 'runtime' }, children);\n },\n\n // ── Feature: Console capture (v1.56) ────────────────────────────────────\n /**\n * Snapshot the current page console messages (and optionally page errors)\n * and attach them as a code doc entry.\n *\n * Uses page.consoleMessages() and page.pageErrors() introduced in Playwright v1.56.\n * Safe to call on any Playwright version – silently produces empty output if the\n * APIs are not present.\n *\n * @example\n * story.when('the form is submitted', async ({ page }) => {\n * await page.click('#submit');\n * story.console({ page, label: 'Submit console output' });\n * });\n */\n console(options: ConsoleOptions, children?: DocEntry[]): DocEntry {\n const p = options.page as {\n consoleMessages?: () => Array<{ type(): string; text(): string }>;\n pageErrors?: () => Error[];\n };\n\n const lines: string[] = [];\n\n if (typeof p?.consoleMessages === 'function') {\n for (const msg of p.consoleMessages()) {\n lines.push(`[${msg.type()}] ${msg.text()}`);\n }\n }\n\n if (options.includeErrors === true && typeof p?.pageErrors === 'function') {\n for (const err of p.pageErrors()) {\n lines.push(`[error] ${err.message}`);\n }\n }\n\n return attachDoc(\n {\n kind: 'code',\n label: options.label ?? 'Console',\n content: lines.length > 0 ? lines.join('\\n') : '(no console output)',\n lang: 'log',\n phase: 'runtime',\n },\n children,\n );\n },\n\n /**\n * Capture current page runtime errors and attach as a structured doc entry.\n *\n * Collects from Playwright v1.56+ page.pageErrors() and page.consoleMessages().\n * This is intentionally snapshot-based so tests can call it at critical points\n * (after submit, after navigation) and keep evidence near relevant steps.\n */\n observePageErrors(options: ObservePageErrorsOptions, children?: DocEntry[]): DocEntry {\n const p = options.page as {\n consoleMessages?: () => Array<{ type(): string; text(): string }>;\n pageErrors?: () => Error[];\n };\n const ignore = options.ignore ?? [];\n const lines: string[] = [];\n\n if (typeof p?.pageErrors === 'function') {\n for (const err of p.pageErrors()) {\n const msg = err?.message ?? String(err);\n if (!ignore.some((rx) => rx.test(msg))) lines.push(`[pageerror] ${msg}`);\n }\n }\n if (typeof p?.consoleMessages === 'function') {\n for (const msg of p.consoleMessages()) {\n if (msg.type() !== 'error') continue;\n const text = msg.text();\n if (!ignore.some((rx) => rx.test(text))) lines.push(`[console.error] ${text}`);\n }\n }\n\n return attachDoc(\n {\n kind: 'code',\n label: options.label ?? 'Browser Runtime Errors',\n content: lines.length > 0 ? lines.join('\\n') : '(no runtime errors observed)',\n lang: 'log',\n phase: 'runtime',\n },\n children,\n );\n },\n\n // Attachments\n attach: playwrightAttach,\n\n // OTel span attachment\n attachSpans(\n spans: ReadonlyArray<Record<string, unknown>>,\n options?: { traceId?: string; spanId?: string },\n ): void {\n if (!activeTestInfo) return;\n const existing = activeTestInfo.annotations.find(\n (a) => a.type === 'story-otel-spans',\n );\n const description = JSON.stringify(spans);\n if (existing) {\n existing.description = description;\n } else {\n activeTestInfo.annotations.push({\n type: 'story-otel-spans',\n description,\n });\n }\n // Capture-then-attach: wire the trace badge + \"View Trace\" link when the\n // trace was created after init() (so the init-time bridge couldn't see it).\n if (options?.traceId && activeContext) {\n applyTraceToMeta(\n activeContext.meta,\n options.traceId,\n options.spanId,\n activeContext.traceUrlTemplate,\n );\n syncAnnotationToTest();\n }\n },\n\n // Step timing\n startTimer(): number {\n const ctx = getContext();\n const token = ctx.timerCounter++;\n const stepIndex = ctx.currentStep\n ? ctx.meta.steps.indexOf(ctx.currentStep)\n : undefined;\n ctx.activeTimers.set(token, {\n start: performance.now(),\n stepIndex: stepIndex !== undefined && stepIndex >= 0 ? stepIndex : undefined,\n stepId: ctx.currentStep?.id,\n consumed: false,\n });\n syncAnnotationToTest();\n return token;\n },\n\n endTimer(token: number): void {\n const ctx = getContext();\n const entry = ctx.activeTimers.get(token);\n if (!entry || entry.consumed) return;\n\n entry.consumed = true;\n const durationMs = performance.now() - entry.start;\n\n let step: StoryStep | undefined;\n if (entry.stepId) {\n step = ctx.meta.steps.find((s) => s.id === entry.stepId);\n }\n if (!step && entry.stepIndex !== undefined) {\n step = ctx.meta.steps[entry.stepIndex];\n }\n\n if (step) {\n step.durationMs = durationMs;\n }\n syncAnnotationToTest();\n },\n\n // Step wrappers\n fn,\n expect: storyExpect,\n};\n\nexport type Story = typeof story;\n\n/**\n * Declarations by the spec file that made them.\n *\n * A worker loads one spec file at a time, but it is reused across files. A\n * single \"last declaration wins\" slot therefore hands file A's feature to file\n * B when B declares none of its own, and the reporter keys features by\n * `test.location.file`, so B ends up with a feature it never wrote. Keying on\n * the declaring file is what stops that.\n */\nconst declaredFeatures = new Map<string, FeatureInput>();\n\n/**\n * One canonical key for a spec file, whichever way it reached us.\n *\n * Under ESM a V8 stack frame carries a file: URL, so the declaring file\n * arrives as `file:///C:/app/spec.ts` while `testInfo.file` is `C:\\app\\spec.ts`.\n * Compared as text those never match and the declaration is silently dropped.\n * Windows paths are also case-insensitive, and the two sources do not always\n * agree on the drive letter's case.\n */\nfunction fileKey(raw: string): string {\n let file = raw;\n if (file.startsWith('file://')) {\n try {\n file = fileURLToPath(file);\n } catch {\n // Not a URL we can decode; fall through with the raw text.\n }\n }\n const resolved = path.resolve(file);\n return process.platform === 'win32' ? resolved.toLowerCase() : resolved;\n}\n\n/**\n * Best-effort source file of whoever called into this module.\n *\n * `story.feature(...)` runs at module scope, before any test is running, so\n * the stack is the only place the spec file is available at that point.\n */\nfunction callerFile(): string | undefined {\n const stack = new Error().stack;\n if (!stack) return undefined;\n\n const files = stack\n .split('\\n')\n .slice(1)\n .map((line) => {\n const match = /\\(((?:file:\\/\\/)?[^()]+?):\\d+:\\d+\\)/.exec(line)\n ?? /at ((?:file:\\/\\/)?[^()\\s]+?):\\d+:\\d+/.exec(line);\n const file = match?.[1];\n // node: builtins are not files and must not be normalized into one.\n return file && !file.startsWith('node:') ? fileKey(file) : undefined;\n });\n\n // The first frame is this function, so its file is this module — in a build\n // that could be dist/index.js or src/story-api.ts. Comparing against it\n // beats matching the package name, which would also exclude a spec file\n // living inside this package (its own tests) or any user path that happens\n // to contain it.\n const self = files[0];\n return files.find((file) => file !== undefined && file !== self);\n}\n\n/**\n * Declare what the file's scenarios are for, before any of them run.\n *\n * Scenarios say what the system does. This says why the feature exists and who\n * it serves, so a reader meets the intent before the examples. Call it once per\n * spec file, at module scope or at the top of the outermost `test.describe`.\n *\n * @example\n * ```ts\n * story.feature({\n * kind: 'ability',\n * title: 'Shoppers can check out without an account',\n * narrative: 'Forcing a signup before payment is where most carts are abandoned.',\n * });\n * ```\n */\nfunction feature(input: FeatureInput): void {\n const file = callerFile();\n // Unresolvable file: keep the declaration rather than lose it silently, and\n // let the next test claim it. That is the old behaviour, now the exception.\n declaredFeatures.set(file ?? UNRESOLVED_FILE, input);\n}\n\n/** Bucket for a declaration whose file the stack could not name. */\nconst UNRESOLVED_FILE = '\\0unresolved';\n","/**\n * Playwright-native step execution helper.\n *\n * Centralises the cross-cutting concerns for fixture-aware step callbacks:\n * 1. page.screencast.showChapter() – narrated chapter markers in video recordings (v1.59)\n * 2. test.step() – TestStepInfo access + trace/report visibility (v1.51)\n * 3. context.tracing.group() – BDD phase grouping in trace viewer (v1.49)\n *\n * Used for callbacks that are either async functions or expect TestStepInfo.\n * Sync callbacks that don't need TestStepInfo follow the faster sync path.\n * All three integrations degrade gracefully if the API is absent.\n */\n\nimport { test } from '@playwright/test';\nimport type { TestStepInfo } from '@playwright/test';\n\nexport type { TestStepInfo };\n\n/** Async step callback that optionally receives TestStepInfo as a second argument. */\nexport type AsyncStepCallback<T = unknown> = (\n fixtures: Record<string, unknown>,\n step?: TestStepInfo,\n) => Promise<T>;\n\n/**\n * Execute an async step callback with full Playwright-native integration.\n *\n * Call order:\n * 1. page.screencast.showChapter(label) – sets chapter in the recording before the step runs\n * 2. test.step(label, …) – wraps execution for trace/report visibility\n * 3. context.tracing.group(label, …) – groups trace actions under the step label\n * 4. body(fixtures, stepInfo) – user callback with injected TestStepInfo\n */\nexport async function runStep<T>(\n label: string,\n body: AsyncStepCallback<T>,\n fixtures: Record<string, unknown>,\n): Promise<T> {\n const page = fixtures.page as Record<string, unknown> | undefined;\n // Derive context from fixtures or from page.context() (Playwright sync method)\n const context =\n (fixtures.context as Record<string, unknown> | undefined) ??\n (typeof (page as { context?: () => Record<string, unknown> })?.context === 'function'\n ? (page as { context: () => Record<string, unknown> }).context()\n : undefined);\n\n // ── Feature 1: Screencast chapter (v1.59) ─────────────────────────────────\n // Show the chapter BEFORE the step body runs so the recording reflects the\n // BDD step title at the right moment. Silently skipped on older Playwright.\n const screencast = page?.screencast as\n | { showChapter?: (label: string) => Promise<void> }\n | undefined;\n if (screencast?.showChapter) {\n try {\n await screencast.showChapter(label);\n } catch {\n // Graceful degradation: screencast not started or API unavailable\n }\n }\n\n // ── Feature 2: test.step (v1.51) + Feature 3: tracing.group (v1.49) ──────\n // test.step provides TestStepInfo for the callback and makes the step visible\n // in the Playwright trace viewer and HTML report as a named action.\n // tracing.group inside it groups the step's child actions under the label.\n return test.step(label, async (stepInfo) => {\n const tracing = context?.tracing as\n | { group?: <R>(label: string, fn: () => Promise<R>) => Promise<R> }\n | undefined;\n\n if (tracing?.group) {\n // Track whether body was invoked to avoid double-execution\n // when body throws inside tracing.group\n let bodyInvoked = false;\n try {\n return await tracing.group(label, async () => {\n bodyInvoked = true;\n return body(fixtures, stepInfo);\n });\n } catch (e) {\n // If body was invoked, it threw - re-throw (don't retry)\n if (bodyInvoked) throw e;\n // Otherwise, tracing.group itself threw (e.g., tracing not recording)\n // Fall back to calling body without tracing.group\n return body(fixtures, stepInfo);\n }\n }\n\n return body(fixtures, stepInfo);\n });\n}\n\n/**\n * Returns true if fn is an async function (declared with `async`).\n * Used along with callback arity to decide whether to route a step callback through runStep().\n *\n * Callbacks are routed through runStep() if they are async functions OR if they\n * have arity >= 2 (meaning they expect TestStepInfo as the second argument).\n */\nexport function isAsyncFunction(fn: unknown): boolean {\n return (\n typeof fn === 'function' &&\n (fn as { constructor?: { name?: string } }).constructor?.name === 'AsyncFunction'\n );\n}\n","/**\n * Playwright Executable Stories\n *\n * BDD-style executable documentation for Playwright Test.\n *\n * @example\n * ```ts\n * import { test, expect } from '@playwright/test';\n * import { story } from 'executable-stories-playwright';\n *\n * test.describe('Calculator', () => {\n * test('adds two numbers', async ({ page }, testInfo) => {\n * story.init(testInfo);\n *\n * story.given('two numbers 5 and 3');\n * const a = 5;\n * const b = 3;\n *\n * story.when('I add them together');\n * const result = a + b;\n *\n * story.then('the result is 8');\n * expect(result).toBe(8);\n * });\n * });\n * ```\n */\n\n// Story API\nimport { story } from './story-api';\nexport { story };\nexport type { Story } from './story-api';\n\n// Top-level step helpers (framework contract)\nexport const given = story.given;\nexport const when = story.when;\nexport const then = story.then;\nexport const and = story.and;\nexport const but = story.but;\n\n// Re-export types from local types module\nexport type {\n StoryMeta,\n StoryStep,\n DocEntry,\n StepKeyword,\n StoryDocs,\n StoryOptions,\n TicketInput,\n NormalizedTicket,\n KvOptions,\n JsonOptions,\n CodeOptions,\n TableOptions,\n LinkOptions,\n SectionOptions,\n MermaidOptions,\n ScreenshotOptions,\n VideoOptions,\n CustomOptions,\n ConsoleOptions,\n ObservePageErrorsOptions,\n AttachmentOptions,\n} from './types';\n\n// TestStepInfo re-exported from @playwright/test for consumer convenience\nexport type { TestStepInfo } from './step-runner';\n"],"mappings":"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","names":["fn"]}
1
+ {"version":3,"sources":["../src/story-api.ts","../src/step-runner.ts","../src/index.ts"],"sourcesContent":["/**\n * Playwright story.* API for executable-stories.\n *\n * Uses native Playwright test() with opt-in documentation:\n *\n * @example\n * ```ts\n * import { test, expect } from '@playwright/test';\n * import { story } from 'executable-stories-playwright';\n *\n * test.describe('Calculator', () => {\n * test('adds two numbers', async ({ page }, testInfo) => {\n * story.init(testInfo);\n *\n * story.given('two numbers 5 and 3');\n * const a = 5;\n * const b = 3;\n *\n * story.when('I add them together');\n * const result = a + b;\n *\n * story.then('the result is 8');\n * expect(result).toBe(8);\n * });\n * });\n * ```\n */\n\nimport { createRequire } from 'node:module';\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { test, expect } from '@playwright/test';\nimport type { TestInfo, PlaywrightTestArgs, PlaywrightTestOptions, Page } from '@playwright/test';\nimport { tryGetActiveOtelContext, resolveTraceUrl } from 'executable-stories-core/utils/otel-detect';\nimport { buildHtmlDocEntry } from 'executable-stories-core/utils/doc-builders';\nimport type {\n StepKeyword,\n StoryMeta,\n StoryStep,\n DocEntry,\n FeatureInput,\n NormalizedTicket,\n TicketInput,\n} from './types';\nimport type {\n StoryDocs,\n StoryOptions,\n AttachmentOptions,\n ScopedAttachment,\n KvOptions,\n JsonOptions,\n StateOptions,\n CodeOptions,\n TableOptions,\n LinkOptions,\n SectionOptions,\n MermaidOptions,\n ScreenshotOptions,\n VideoOptions,\n HtmlOptions,\n CustomOptions,\n ConsoleOptions,\n ObservePageErrorsOptions,\n} from './types';\nimport { runStep, isAsyncFunction } from './step-runner';\nimport type { TestStepInfo } from './step-runner';\n\n// Re-export types for consumers\nexport type {\n StoryMeta,\n StoryStep,\n DocEntry,\n StepKeyword,\n StoryDocs,\n StoryOptions,\n AttachmentOptions,\n} from './types';\n\n// ============================================================================\n// Internal types\n// ============================================================================\n\n/** Fixture type for step callbacks: Playwright test args + options; custom extend() fixtures as unknown. */\ntype PlaywrightFixtures = PlaywrightTestArgs & PlaywrightTestOptions & Record<string, unknown>;\n\ninterface TimerEntry {\n start: number;\n stepIndex?: number;\n stepId?: string;\n consumed: boolean;\n}\n\ninterface StoryContext {\n meta: StoryMeta;\n currentStep: StoryStep | null;\n stepCounter: number;\n attachments: ScopedAttachment[];\n activeTimers: Map<number, TimerEntry>;\n timerCounter: number;\n fixtures?: Record<string, unknown>;\n /** Trace-link URL template, captured at init for later use by attachSpans. */\n traceUrlTemplate?: string;\n /**\n * Marker step awaiting its assertion count.\n *\n * A marker states the claim and the assertion follows it, so the count is\n * only known once the next step starts or the test ends.\n */\n pendingStep: StoryStep | null;\n /** Assertion counter reading taken when {@link pendingStep} was created. */\n pendingFrom?: number;\n}\n\n/**\n * Playwright's live per-test assertion counter, or undefined when unreadable.\n *\n * Undefined is not zero: it means we could not observe, and the report must not\n * report an unobserved step as one that asserted nothing.\n */\nfunction readAssertionCount(): number | undefined {\n try {\n const state = (expect as unknown as {\n getState?: () => { assertionCalls?: number };\n }).getState?.();\n return typeof state?.assertionCalls === 'number' ? state.assertionCalls : undefined;\n } catch {\n return undefined;\n }\n}\n\n/** Record how many assertions ran between `before` and now. */\nfunction recordAssertions(step: StoryStep, before: number | undefined): void {\n if (before === undefined) return;\n const after = readAssertionCount();\n if (after === undefined || after < before) return;\n step.assertions = after - before;\n}\n\n/**\n * Close off the marker step waiting on a count.\n *\n * Registered as a `test.afterEach` below so the last marker in a test still\n * collects the assertions written after it, before the reporter reads the\n * annotation in onTestEnd.\n */\nfunction flushPendingAssertions(): void {\n const ctx = activeContext;\n if (!ctx) return;\n const step = ctx.pendingStep;\n const before = ctx.pendingFrom;\n ctx.pendingStep = null;\n ctx.pendingFrom = undefined;\n if (!step) return;\n recordAssertions(step, before);\n syncAnnotationToTest();\n}\n\ntry {\n // Registering from this module keeps the hook and the context together; a\n // spec importing the story API is enough to install it.\n test.afterEach(() => {\n flushPendingAssertions();\n });\n} catch {\n // Imported outside a Playwright test file (tooling, the reporter): there is\n // no suite to register against and nothing to flush.\n}\n\n// ============================================================================\n// Playwright-specific context\n// ============================================================================\n\n/** Active story context - set by story.init() */\nlet activeContext: StoryContext | null = null;\n\n/** Reference to testInfo for attaching metadata */\nlet activeTestInfo: TestInfo | null = null;\n\n/** Counter to track source order of stories (increments on each story.init call) */\nlet sourceOrderCounter = 0;\n\n/**\n * Get the current story context. Throws if story.init() wasn't called.\n */\nfunction getContext(): StoryContext {\n if (!activeContext) {\n throw new Error(\n \"story.init(testInfo) must be called first. Use: test('name', async ({ page }, testInfo) => { story.init(testInfo); ... });\",\n );\n }\n return activeContext;\n}\n\n// ============================================================================\n// Helper functions (inlined from core)\n// ============================================================================\n\nfunction normalizeTickets(\n ticket: TicketInput | TicketInput[] | undefined,\n): NormalizedTicket[] | undefined {\n if (!ticket) return undefined;\n const arr = Array.isArray(ticket) ? ticket : [ticket];\n return arr.map((t) => (typeof t === 'string' ? { id: t } : t));\n}\n\nfunction convertStoryDocsToEntries(docs: StoryDocs): DocEntry[] {\n const entries: DocEntry[] = [];\n\n if (docs.note) {\n entries.push({ kind: 'note', text: docs.note, phase: 'runtime' });\n }\n if (docs.tag) {\n const names = Array.isArray(docs.tag) ? docs.tag : [docs.tag];\n entries.push({ kind: 'tag', names, phase: 'runtime' });\n }\n if (docs.kv) {\n for (const [label, value] of Object.entries(docs.kv)) {\n entries.push({ kind: 'kv', label, value, phase: 'runtime' });\n }\n }\n if (docs.code) {\n entries.push({\n kind: 'code',\n label: docs.code.label,\n content: docs.code.content,\n lang: docs.code.lang,\n phase: 'runtime',\n });\n }\n if (docs.json) {\n entries.push({\n kind: 'code',\n label: docs.json.label,\n content: JSON.stringify(docs.json.value, null, 2),\n lang: 'json',\n phase: 'runtime',\n });\n }\n if (docs.state) {\n warnIfStateLarge(docs.state.label, docs.state.value);\n entries.push({\n kind: 'state',\n label: docs.state.label,\n value: docs.state.value,\n phase: 'runtime',\n });\n }\n if (docs.table) {\n entries.push({\n kind: 'table',\n label: docs.table.label,\n columns: docs.table.columns,\n rows: docs.table.rows,\n phase: 'runtime',\n });\n }\n if (docs.link) {\n entries.push({\n kind: 'link',\n label: docs.link.label,\n url: docs.link.url,\n phase: 'runtime',\n });\n }\n if (docs.section) {\n entries.push({\n kind: 'section',\n title: docs.section.title,\n markdown: docs.section.markdown,\n phase: 'runtime',\n });\n }\n if (docs.mermaid) {\n entries.push({\n kind: 'mermaid',\n code: docs.mermaid.code,\n title: docs.mermaid.title,\n phase: 'runtime',\n });\n }\n if (docs.screenshot) {\n if (docs.screenshot.page) {\n throw new Error(\n 'story.screenshot({ page }) is not supported inside inline step docs ' +\n '(e.g. story.then(text, { screenshot: { page } })) because capturing a ' +\n 'screenshot is asynchronous and step markers run synchronously. Call ' +\n 'story.screenshot({ page, alt }) as its own statement instead.',\n );\n }\n if (!docs.screenshot.path) {\n throw new Error('story docs screenshot requires a `path` (or use story.screenshot({ page }) instead).');\n }\n entries.push({\n kind: 'screenshot',\n // Inline file bytes as a `data:` URI here too — this is the same\n // capture-vs-cleanup race that story.screenshot() guards against, and\n // this path used to skip it, so a screenshot passed via inline step\n // docs would silently never embed no matter how the file was captured.\n path: inlineScreenshotIfPossible(docs.screenshot.path),\n alt: docs.screenshot.alt,\n phase: 'runtime',\n });\n }\n if (docs.video) {\n entries.push({\n kind: 'video',\n path: docs.video.path,\n caption: docs.video.caption,\n poster: docs.video.poster,\n phase: 'runtime',\n });\n }\n if (docs.html) {\n entries.push(buildHtmlEntry(docs.html));\n }\n if (docs.custom) {\n entries.push({\n kind: 'custom',\n type: docs.custom.type,\n data: docs.custom.data,\n phase: 'runtime',\n });\n }\n\n return entries;\n}\n\n/**\n * Capture-time inlining for the html doc kind. Playwright is the only adapter\n * that pre-resolves a source before building the entry; the exactly-one-of rule\n * and the entry shape come from the canonical `buildHtmlDocEntry`.\n *\n * An *absolute* local path (e.g. `/home/runner/work/.../x.html`, or anything\n * under `testInfo.outputPath()`) is read now and inlined as iframe srcdoc: that\n * path 404s once the HTML report is downloaded as a CI artifact and opened on\n * another machine, so we capture the bytes while they exist — the same\n * reasoning as screenshot data-URI inlining.\n *\n * A *relative* path is left as-is so it behaves exactly like the Vitest adapter:\n * the formatter reads it at format time (default), or copies it as a hashed\n * asset under `assetMode: \"copy\"`. Inlining relative paths early would needlessly\n * foreclose copy mode. The embedded HTML must be self-contained (single-file):\n * relative references to sibling CSS/JS are not rewritten (directory bundling is planned).\n */\nfunction buildHtmlEntry(options: HtmlOptions): DocEntry {\n let { path: htmlPath, content } = options;\n // Only inline when `path` is the lone source: that keeps an invalid\n // multi-source call (e.g. path + content) reaching the canonical validator\n // instead of being silently \"fixed\" by overwriting content from disk. Listing\n // `htmlPath !== undefined` first in the chain narrows it for the rest.\n if (\n htmlPath !== undefined &&\n options.url === undefined &&\n content === undefined &&\n !/^https?:/i.test(htmlPath) &&\n path.isAbsolute(htmlPath)\n ) {\n try {\n if (fs.existsSync(htmlPath)) {\n content = fs.readFileSync(htmlPath, 'utf8');\n htmlPath = undefined;\n }\n } catch {\n // keep the path — the formatter retries at format time\n }\n }\n return buildHtmlDocEntry({ ...options, path: htmlPath, content });\n}\n\nconst SCREENSHOT_MIME_BY_EXT: Record<string, string> = {\n png: 'image/png',\n jpg: 'image/jpeg',\n jpeg: 'image/jpeg',\n gif: 'image/gif',\n webp: 'image/webp',\n svg: 'image/svg+xml',\n avif: 'image/avif',\n bmp: 'image/bmp',\n};\n\n/**\n * Read a screenshot file and return a `data:` URI; fall back to the original\n * path on any failure (remote URL, missing file, unknown extension).\n *\n * A missing/unreadable local path almost always means the screenshot was\n * never taken at that path — e.g. `story.screenshot({ path })` called without\n * a preceding `page.screenshot({ path })`, or the two paths drifted apart.\n * That failure otherwise surfaces minutes later as a broken image in a PR\n * comment or a \"Screenshot unavailable\" placeholder in the HTML report, far\n * from the line that caused it — so warn immediately, at the call site.\n */\nfunction inlineScreenshotIfPossible(filePath: string): string {\n if (/^(?:https?:|data:)/i.test(filePath)) return filePath;\n const ext = path.extname(filePath).slice(1).toLowerCase();\n const mime = SCREENSHOT_MIME_BY_EXT[ext];\n if (!mime) return filePath;\n try {\n if (!fs.existsSync(filePath)) {\n warnScreenshotUnavailable(filePath);\n return filePath;\n }\n const buf = fs.readFileSync(filePath);\n return `data:${mime};base64,${buf.toString('base64')}`;\n } catch (err) {\n warnScreenshotUnavailable(filePath, err);\n return filePath;\n }\n}\n\nfunction warnScreenshotUnavailable(filePath: string, cause?: unknown): void {\n const causeMessage = cause instanceof Error ? cause.message : cause ? String(cause) : undefined;\n console.warn(\n `[executable-stories-playwright] story.screenshot(): could not read \"${filePath}\" — ` +\n 'the report will show a \"Screenshot unavailable\" placeholder instead of the image. ' +\n 'Make sure a screenshot is written to this exact path before story.screenshot() runs ' +\n '(e.g. `await page.screenshot({ path })`), or capture it directly with ' +\n '`story.screenshot({ page, alt })`.' +\n (causeMessage ? ` Cause: ${causeMessage}` : ''),\n );\n}\n\n/**\n * story.video() never reads/inlines the file (video bytes are too large for\n * a `data:` URI) — it always defers to the formatter's asset bundler, which\n * runs later, at report-generation time. A *relative* path is legitimately\n * unresolvable yet (the bundler resolves it against the report output), so\n * this only warns for an *absolute* local path missing right now — that can\n * never resolve later either, since it's not relative to anything the\n * bundler controls. The most common cause: passing a path nothing has\n * written to (e.g. a typo, or the video hasn't finished flushing to disk —\n * see Playwright's `page.video()?.path()`, which resolves only after\n * `page.close()`).\n */\nfunction warnIfAbsoluteVideoPathMissing(filePath: string): void {\n if (/^(?:https?:|data:)/i.test(filePath)) return;\n if (!path.isAbsolute(filePath)) return;\n if (fs.existsSync(filePath)) return;\n console.warn(\n `[executable-stories-playwright] story.video(): \"${filePath}\" does not exist yet. ` +\n 'Video bytes are never inlined, so this exact path must resolve to a real file by the time the ' +\n 'report is built, or the report will show a \"Video unavailable\" placeholder instead of the clip.',\n );\n}\n\n/**\n * Warn (non-fatal) when a state snapshot serializes past 100KB — the entry is\n * still recorded, but a projection is usually what the storyboard needs.\n */\nfunction warnIfStateLarge(label: string | undefined, value: unknown): void {\n try {\n const len = JSON.stringify(value)?.length ?? 0;\n if (len > 100_000) {\n console.warn(\n `[executable-stories] state \"${label ?? ''}\" is ${Math.round(len / 1024)}KB — consider capturing a projection`,\n );\n }\n } catch {\n // Non-serializable value: recorded as-is, size unknown.\n }\n}\n\nfunction attachDoc(entry: DocEntry, children?: DocEntry[]): DocEntry {\n const ctx = getContext();\n if (children && children.length > 0) {\n entry.children = children;\n const childSet = new Set<DocEntry>(children);\n const filterDocs = (docs: DocEntry[]) => docs.filter((d) => !childSet.has(d));\n // Remove children from ALL containers (story-level + every step)\n ctx.meta.docs = filterDocs(ctx.meta.docs ?? []);\n for (const step of ctx.meta.steps) {\n if (step.docs) step.docs = filterDocs(step.docs);\n }\n }\n if (ctx.currentStep) {\n ctx.currentStep.docs ??= [];\n ctx.currentStep.docs.push(entry);\n } else {\n ctx.meta.docs ??= [];\n ctx.meta.docs.push(entry);\n }\n syncAnnotationToTest();\n return entry;\n}\n\n// ============================================================================\n// Suite path extraction\n// ============================================================================\n\n/**\n * Extract the suite path from testInfo.titlePath.\n * Playwright's titlePath includes: [projectName, ...describeTitles, testTitle]\n * We want just the describe titles (excluding project and test name).\n */\nfunction extractSuitePath(testInfo: TestInfo): string[] | undefined {\n const titlePath = testInfo.titlePath;\n if (titlePath.length <= 2) {\n return undefined;\n }\n const suitePath = titlePath.slice(1, -1);\n return suitePath.length > 0 ? suitePath : undefined;\n}\n\n// ============================================================================\n// Step markers\n// ============================================================================\n\nfunction createStepMarker(keyword: StepKeyword) {\n function stepMarker(text: string, docs?: StoryDocs): void;\n function stepMarker(text: string, children: DocEntry[]): void;\n function stepMarker<T>(text: string, body: (fixtures: PlaywrightFixtures, step?: TestStepInfo) => T): T;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n function stepMarker<T>(text: string, docsOrBody?: StoryDocs | DocEntry[] | ((...args: any[]) => T)): T | void {\n const ctx = getContext();\n const isCallback = typeof docsOrBody === 'function';\n const isChildrenArray = Array.isArray(docsOrBody);\n\n const resolvedKeyword: StepKeyword =\n (keyword === 'Given' || keyword === 'When' || keyword === 'Then') &&\n ctx.meta.steps.some((s) => s.keyword === keyword)\n ? 'And'\n : keyword;\n\n let stepDocs: DocEntry[] = [];\n if (!isCallback && !isChildrenArray && docsOrBody) {\n stepDocs = convertStoryDocsToEntries(docsOrBody as StoryDocs);\n }\n\n const step: StoryStep = {\n id: `step-${ctx.stepCounter++}`,\n keyword: resolvedKeyword,\n text,\n docs: stepDocs,\n ...(isCallback ? { wrapped: true } : {}),\n };\n\n // Close the previous marker before this step's own assertions begin.\n flushPendingAssertions();\n\n ctx.meta.steps.push(step);\n ctx.currentStep = step;\n if (!isCallback) {\n ctx.pendingStep = step;\n ctx.pendingFrom = readAssertionCount();\n }\n syncAnnotationToTest();\n\n // Handle DocEntry[] children: attach as step docs and deduplicate from story-level\n if (isChildrenArray) {\n const children = docsOrBody as DocEntry[];\n if (children.length > 0) {\n const childSet = new Set<DocEntry>(children);\n // Deduplicate from story-level docs\n ctx.meta.docs = (ctx.meta.docs ?? []).filter((d) => !childSet.has(d));\n // Deduplicate from step docs of earlier steps\n for (const prevStep of ctx.meta.steps) {\n if (prevStep !== step && prevStep.docs) {\n prevStep.docs = prevStep.docs.filter((d) => !childSet.has(d));\n }\n }\n step.docs = [...(step.docs ?? []), ...children];\n }\n syncAnnotationToTest();\n return;\n }\n\n if (!isCallback) return;\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const body = docsOrBody as (fixtures?: PlaywrightFixtures, stepInfo?: TestStepInfo) => T;\n const label = `${step.keyword}: ${text}`;\n const start = performance.now();\n const assertionsBefore = readAssertionCount();\n\n // ── Async or stepInfo-aware callbacks: route through runStep() for Playwright-native integrations ──\n // Integrations: screencast chapters (v1.59), test.step/TestStepInfo (v1.51),\n // tracing.group (v1.49). Activated when fixtures are available AND either:\n // 1. callback is an async function, OR\n // 2. callback expects TestStepInfo (arity >= 2)\n if (ctx.fixtures !== undefined && (isAsyncFunction(body) || body.length >= 2)) {\n const fixtures = ctx.fixtures as Record<string, unknown>;\n const result = runStep(\n label,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n body as unknown as (fixtures: Record<string, unknown>, step?: TestStepInfo) => Promise<any>,\n fixtures,\n );\n return result.then(\n (val: T) => { step.durationMs = performance.now() - start; recordAssertions(step, assertionsBefore); syncAnnotationToTest(); return val; },\n (err: unknown) => { step.durationMs = performance.now() - start; recordAssertions(step, assertionsBefore); syncAnnotationToTest(); throw err; },\n ) as T;\n }\n\n // ── Sync callbacks or no-fixture context: existing behaviour ─────────────\n try {\n const result = ctx.fixtures !== undefined ? body(ctx.fixtures as PlaywrightFixtures) : body();\n if (result instanceof Promise) {\n return result.then(\n (val) => { step.durationMs = performance.now() - start; recordAssertions(step, assertionsBefore); syncAnnotationToTest(); return val; },\n (err) => { step.durationMs = performance.now() - start; recordAssertions(step, assertionsBefore); syncAnnotationToTest(); throw err; },\n ) as T;\n }\n step.durationMs = performance.now() - start;\n syncAnnotationToTest();\n return result;\n } catch (err) {\n step.durationMs = performance.now() - start;\n syncAnnotationToTest();\n throw err;\n }\n }\n return stepMarker;\n}\n\n// ============================================================================\n// story.init() - Playwright-specific\n// ============================================================================\n\nfunction isTestInfo(x: unknown): x is TestInfo {\n return (\n typeof x === 'object' &&\n x !== null &&\n 'title' in x &&\n 'annotations' in x &&\n Array.isArray((x as TestInfo).annotations)\n );\n}\n\n/** init(testInfo) or init(fixtures, testInfo) or init(testInfo, { fixtures }). */\nfunction init(\n first: TestInfo | unknown,\n second?: StoryOptions | TestInfo,\n third?: StoryOptions,\n): void {\n let testInfo: TestInfo;\n let options: StoryOptions | undefined;\n let fixtures: unknown;\n\n if (second !== undefined && isTestInfo(second)) {\n fixtures = first;\n testInfo = second;\n options = third;\n } else {\n testInfo = first as TestInfo;\n options = second;\n fixtures = options?.fixtures;\n }\n\n const meta: StoryMeta = {\n scenario: testInfo.title,\n steps: [],\n suitePath: extractSuitePath(testInfo),\n tags: options?.tags,\n tickets: normalizeTickets(options?.ticket),\n covers: options?.covers,\n meta: options?.meta,\n sourceOrder: sourceOrderCounter++,\n };\n\n // Flag the scenario so the reporter promotes the Playwright screen recording\n // into a featured inline video doc entry (see reporter onTestEnd).\n if (options?.featureVideo) {\n meta.meta = { ...meta.meta, featureVideo: true };\n }\n\n // OTel bridge: detect active span, flow data bidirectionally\n const traceUrlTemplate =\n options?.traceUrlTemplate ?? process.env.OTEL_TRACE_URL_TEMPLATE;\n const otelCtx = tryGetActiveOtelContext();\n if (otelCtx) {\n // OTel -> Story: capture traceId + docs from the active span.\n applyTraceToMeta(meta, otelCtx.traceId, otelCtx.spanId, traceUrlTemplate);\n\n // Story -> OTel: enrich active span with story attributes\n try {\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n const reqUrl = import.meta.url\n ?? (typeof __filename !== 'undefined' ? `file://${__filename}` : undefined);\n const req = createRequire(reqUrl!);\n const api = req('@opentelemetry/api');\n const span = api.trace?.getActiveSpan?.();\n if (span) {\n span.setAttribute('story.scenario', testInfo.title);\n if (options?.tags?.length) span.setAttribute('story.tags', options.tags);\n if (options?.ticket) {\n const tickets = Array.isArray(options.ticket) ? options.ticket : [options.ticket];\n span.setAttribute('story.tickets', tickets.map((t) => typeof t === 'string' ? t : t.id));\n }\n }\n } catch { /* OTel not available */ }\n }\n\n testInfo.annotations.push({\n type: 'story-meta',\n description: JSON.stringify(meta),\n });\n\n const declaredFeature =\n declaredFeatures.get(fileKey(testInfo.file)) ?? declaredFeatures.get(UNRESOLVED_FILE);\n if (declaredFeature) {\n testInfo.annotations.push({\n type: 'story-feature',\n description: JSON.stringify(declaredFeature),\n });\n }\n\n // ── Feature: Tag sync (v1.43) ─────────────────────────────────────────────\n // Sync story tags to Playwright's native annotation system so they appear in\n // UI Mode tag filters and the HTML reporter's tag display.\n for (const tag of options?.tags ?? []) {\n testInfo.annotations.push({ type: 'tag', description: tag });\n }\n\n activeContext = {\n meta,\n currentStep: null,\n stepCounter: 0,\n attachments: [],\n activeTimers: new Map(),\n timerCounter: 0,\n fixtures: fixtures as Record<string, unknown> | undefined,\n traceUrlTemplate,\n pendingStep: null,\n };\n activeTestInfo = testInfo;\n}\n\n/**\n * Update the story-meta annotation on testInfo with the current meta (including steps).\n * Called after each step/doc so the reporter sees the full story in onTestEnd.\n */\nfunction syncAnnotationToTest(): void {\n if (!activeTestInfo || !activeContext) return;\n const annotation = activeTestInfo.annotations.find(\n (a) => a.type === 'story-meta',\n );\n if (annotation) {\n annotation.description = JSON.stringify(activeContext.meta);\n }\n}\n\n/**\n * Bridge an OTel trace into the story's meta + docs: structured `otel` meta, a\n * \"Trace ID\" key-value, and a \"View Trace\" link when the template resolves.\n * Idempotent — once a traceId is recorded it is not overwritten, so the\n * active-span path in init() and the explicit path in attachSpans() compose\n * without duplicating entries.\n */\nfunction applyTraceToMeta(\n meta: StoryMeta,\n traceId: string,\n spanId: string | undefined,\n template: string | undefined,\n): void {\n const existing = (meta.meta as { otel?: { traceId?: string } } | undefined)\n ?.otel;\n if (existing?.traceId) return;\n\n meta.meta = { ...meta.meta, otel: { traceId, spanId } };\n meta.docs = meta.docs ?? [];\n meta.docs.push({\n kind: 'kv',\n label: 'Trace ID',\n value: traceId,\n phase: 'runtime',\n });\n const url = resolveTraceUrl(template, traceId);\n if (url) {\n meta.docs.push({ kind: 'link', label: 'View Trace', url, phase: 'runtime' });\n }\n}\n\n// ============================================================================\n// story.fn() and story.expect()\n// ============================================================================\n\n/**\n * Wrap a function as a step with timing and error capture.\n * Records the step with `wrapped: true` and `durationMs`.\n */\nfunction fn<T>(keyword: StepKeyword, text: string, body: (fixtures: PlaywrightFixtures) => T): T;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction fn<T>(keyword: StepKeyword, text: string, body: (...args: any[]) => T): T {\n const ctx = getContext();\n const resolvedKeyword: StepKeyword =\n (keyword === 'Given' || keyword === 'When' || keyword === 'Then') &&\n ctx.meta.steps.some((s) => s.keyword === keyword)\n ? 'And'\n : keyword;\n const step: StoryStep = {\n id: `step-${ctx.stepCounter++}`,\n keyword: resolvedKeyword,\n text,\n docs: [],\n wrapped: true,\n };\n ctx.meta.steps.push(step);\n ctx.currentStep = step;\n syncAnnotationToTest();\n\n const start = performance.now();\n try {\n const result = ctx.fixtures !== undefined ? body(ctx.fixtures as PlaywrightFixtures) : body();\n if (result instanceof Promise) {\n return result.then(\n (val) => {\n step.durationMs = performance.now() - start;\n syncAnnotationToTest();\n return val;\n },\n (err) => {\n step.durationMs = performance.now() - start;\n syncAnnotationToTest();\n throw err;\n },\n ) as T;\n }\n step.durationMs = performance.now() - start;\n syncAnnotationToTest();\n return result;\n } catch (err) {\n step.durationMs = performance.now() - start;\n syncAnnotationToTest();\n throw err;\n }\n}\n\n/**\n * Wrap an assertion as a Then step with timing and error capture.\n * Shorthand for `story.fn('Then', text, body)`.\n */\nfunction storyExpect<T>(text: string, body: () => T): T {\n return fn('Then', text, body);\n}\n\n// ============================================================================\n// Playwright-specific attach\n// ============================================================================\n\nfunction playwrightAttach(options: AttachmentOptions): void {\n const ctx = getContext();\n const stepIndex = ctx.currentStep\n ? ctx.meta.steps.indexOf(ctx.currentStep)\n : undefined;\n ctx.attachments.push({\n ...options,\n stepId: ctx.currentStep?.id,\n });\n syncAnnotationToTest();\n\n if (activeTestInfo) {\n const attachOptions: { name: string; contentType: string; path?: string; body?: string | Buffer } = {\n name: options.name,\n contentType: options.mediaType,\n };\n if (options.path) attachOptions.path = options.path;\n if (options.body) attachOptions.body = options.body;\n activeTestInfo.attach(options.name, attachOptions);\n }\n}\n\n// ============================================================================\n// story.screenshot() — capture-and-attach, or attach-an-existing-file\n// ============================================================================\n\n/**\n * Two ways to attach a screenshot:\n * - `story.screenshot({ page, alt })` captures a fresh screenshot and inlines\n * it directly from the in-memory buffer — no filesystem round-trip, so\n * there's no path to fall out of sync and nothing for Playwright's output\n * cleanup to delete before the report is built. Prefer this form.\n * - `story.screenshot({ path, alt })` attaches a screenshot that already\n * exists on disk (e.g. one taken earlier for another purpose). The caller\n * is responsible for making sure something wrote a file to `path` first.\n */\nfunction screenshotImpl(options: ScreenshotOptions & { page: Page }, children?: DocEntry[]): Promise<DocEntry>;\nfunction screenshotImpl(\n options: ScreenshotOptions & { path: string; page?: undefined },\n children?: DocEntry[],\n): DocEntry;\nfunction screenshotImpl(options: ScreenshotOptions, children?: DocEntry[]): DocEntry | Promise<DocEntry> {\n if (options.page) {\n return options.page\n .screenshot(options.path ? { path: options.path, fullPage: options.fullPage ?? true } : { fullPage: options.fullPage ?? true })\n .then((buffer) => {\n const dataUri = `data:image/png;base64,${buffer.toString('base64')}`;\n return attachDoc({ kind: 'screenshot', path: dataUri, alt: options.alt, phase: 'runtime' }, children);\n });\n }\n if (!options.path) {\n throw new Error('story.screenshot() requires either `path` (an existing file) or `page` (to capture one).');\n }\n // Inline file bytes as a `data:` URI so the screenshot survives Playwright's\n // per-test outputDir cleanup (passing tests have their `test-results/<test>/`\n // directory deleted before the formatter runs). Falls back to the original\n // path for remote URLs or unreadable files.\n const resolvedPath = inlineScreenshotIfPossible(options.path);\n return attachDoc({ kind: 'screenshot', path: resolvedPath, alt: options.alt, phase: 'runtime' }, children);\n}\n\n// ============================================================================\n// Export story object\n// ============================================================================\n\nexport const story = {\n init,\n feature,\n\n // BDD step markers\n given: createStepMarker('Given'),\n when: createStepMarker('When'),\n then: createStepMarker('Then'),\n and: createStepMarker('And'),\n but: createStepMarker('But'),\n\n // AAA pattern aliases\n arrange: createStepMarker('Given'),\n act: createStepMarker('When'),\n assert: createStepMarker('Then'),\n\n // Additional aliases\n setup: createStepMarker('Given'),\n context: createStepMarker('Given'),\n execute: createStepMarker('When'),\n action: createStepMarker('When'),\n verify: createStepMarker('Then'),\n\n // Standalone doc methods\n note(text: string, children?: DocEntry[]): DocEntry {\n return attachDoc({ kind: 'note', text, phase: 'runtime' }, children);\n },\n\n tag(name: string | string[], children?: DocEntry[]): DocEntry {\n const names = Array.isArray(name) ? name : [name];\n return attachDoc({ kind: 'tag', names, phase: 'runtime' }, children);\n },\n\n kv(options: KvOptions, children?: DocEntry[]): DocEntry {\n return attachDoc({ kind: 'kv', label: options.label, value: options.value, phase: 'runtime' }, children);\n },\n\n json(options: JsonOptions, children?: DocEntry[]): DocEntry {\n const content = JSON.stringify(options.value, null, 2);\n return attachDoc({ kind: 'code', label: options.label, content, lang: 'json', phase: 'runtime' }, children);\n },\n\n /**\n * Capture what the world looks like at this step: a JSON-serializable data\n * snapshot that becomes a storyboard frame. Consecutive states with the same\n * label are diffed downstream, so snapshot the same projection at each step.\n *\n * A step can carry BOTH a screenshot and a state — the screen and the\n * backend record for the same moment:\n *\n * @example\n * story.when('the user completes checkout');\n * await story.screenshot({ page, alt: 'Payment form' });\n * story.state({ label: 'order', value: { id: 1042, status: 'paid', total: '£25' } });\n */\n state(options: StateOptions, children?: DocEntry[]): DocEntry {\n warnIfStateLarge(options.label, options.value);\n return attachDoc({ kind: 'state', label: options.label, value: options.value, phase: 'runtime' }, children);\n },\n\n code(options: CodeOptions, children?: DocEntry[]): DocEntry {\n return attachDoc({ kind: 'code', label: options.label, content: options.content, lang: options.lang, phase: 'runtime' }, children);\n },\n\n table(options: TableOptions, children?: DocEntry[]): DocEntry {\n return attachDoc({ kind: 'table', label: options.label, columns: options.columns, rows: options.rows, phase: 'runtime' }, children);\n },\n\n link(options: LinkOptions, children?: DocEntry[]): DocEntry {\n return attachDoc({ kind: 'link', label: options.label, url: options.url, phase: 'runtime' }, children);\n },\n\n section(options: SectionOptions, children?: DocEntry[]): DocEntry {\n return attachDoc({ kind: 'section', title: options.title, markdown: options.markdown, phase: 'runtime' }, children);\n },\n\n mermaid(options: MermaidOptions, children?: DocEntry[]): DocEntry {\n return attachDoc({ kind: 'mermaid', code: options.code, title: options.title, phase: 'runtime' }, children);\n },\n\n screenshot: screenshotImpl,\n\n video(options: VideoOptions, children?: DocEntry[]): DocEntry {\n // Unlike screenshots, video bytes are never inlined as a data URI — they're\n // too large. The path is kept as-is so the formatter's asset bundler copies\n // the file into the report/docs site. Pass a path relative to the output, or\n // use `featureVideo: true` on story.init() to auto-promote the Playwright\n // recording instead.\n warnIfAbsoluteVideoPathMissing(options.path);\n return attachDoc(\n { kind: 'video', path: options.path, caption: options.caption, poster: options.poster, phase: 'runtime' },\n children,\n );\n },\n\n /**\n * Embed HTML in a sandboxed iframe in the current step or story-level docs.\n * Exactly one of path/url/content is required. Local files are inlined so\n * the report stays self-contained — the HTML must therefore be\n * self-contained too (use your tool's single-file mode); relative sub-asset\n * references won't resolve.\n * @example story.html({ path: './coverage/index.html', title: 'Coverage' })\n * @example story.html({ url: 'https://dash.example.com/run/42', height: 600 })\n * @example story.html({ content: chartHtml, title: 'Latency chart' })\n */\n html(options: HtmlOptions, children?: DocEntry[]): DocEntry {\n return attachDoc(buildHtmlEntry(options), children);\n },\n\n custom(options: CustomOptions, children?: DocEntry[]): DocEntry {\n return attachDoc({ kind: 'custom', type: options.type, data: options.data, phase: 'runtime' }, children);\n },\n\n // ── Feature: Console capture (v1.56) ────────────────────────────────────\n /**\n * Snapshot the current page console messages (and optionally page errors)\n * and attach them as a code doc entry.\n *\n * Uses page.consoleMessages() and page.pageErrors() introduced in Playwright v1.56.\n * Safe to call on any Playwright version – silently produces empty output if the\n * APIs are not present.\n *\n * @example\n * story.when('the form is submitted', async ({ page }) => {\n * await page.click('#submit');\n * story.console({ page, label: 'Submit console output' });\n * });\n */\n console(options: ConsoleOptions, children?: DocEntry[]): DocEntry {\n const p = options.page as {\n consoleMessages?: () => Array<{ type(): string; text(): string }>;\n pageErrors?: () => Error[];\n };\n\n const lines: string[] = [];\n\n if (typeof p?.consoleMessages === 'function') {\n for (const msg of p.consoleMessages()) {\n lines.push(`[${msg.type()}] ${msg.text()}`);\n }\n }\n\n if (options.includeErrors === true && typeof p?.pageErrors === 'function') {\n for (const err of p.pageErrors()) {\n lines.push(`[error] ${err.message}`);\n }\n }\n\n return attachDoc(\n {\n kind: 'code',\n label: options.label ?? 'Console',\n content: lines.length > 0 ? lines.join('\\n') : '(no console output)',\n lang: 'log',\n phase: 'runtime',\n },\n children,\n );\n },\n\n /**\n * Capture current page runtime errors and attach as a structured doc entry.\n *\n * Collects from Playwright v1.56+ page.pageErrors() and page.consoleMessages().\n * This is intentionally snapshot-based so tests can call it at critical points\n * (after submit, after navigation) and keep evidence near relevant steps.\n */\n observePageErrors(options: ObservePageErrorsOptions, children?: DocEntry[]): DocEntry {\n const p = options.page as {\n consoleMessages?: () => Array<{ type(): string; text(): string }>;\n pageErrors?: () => Error[];\n };\n const ignore = options.ignore ?? [];\n const lines: string[] = [];\n\n if (typeof p?.pageErrors === 'function') {\n for (const err of p.pageErrors()) {\n const msg = err?.message ?? String(err);\n if (!ignore.some((rx) => rx.test(msg))) lines.push(`[pageerror] ${msg}`);\n }\n }\n if (typeof p?.consoleMessages === 'function') {\n for (const msg of p.consoleMessages()) {\n if (msg.type() !== 'error') continue;\n const text = msg.text();\n if (!ignore.some((rx) => rx.test(text))) lines.push(`[console.error] ${text}`);\n }\n }\n\n return attachDoc(\n {\n kind: 'code',\n label: options.label ?? 'Browser Runtime Errors',\n content: lines.length > 0 ? lines.join('\\n') : '(no runtime errors observed)',\n lang: 'log',\n phase: 'runtime',\n },\n children,\n );\n },\n\n // Attachments\n attach: playwrightAttach,\n\n // OTel span attachment\n attachSpans(\n spans: ReadonlyArray<Record<string, unknown>>,\n options?: { traceId?: string; spanId?: string },\n ): void {\n if (!activeTestInfo) return;\n const existing = activeTestInfo.annotations.find(\n (a) => a.type === 'story-otel-spans',\n );\n const description = JSON.stringify(spans);\n if (existing) {\n existing.description = description;\n } else {\n activeTestInfo.annotations.push({\n type: 'story-otel-spans',\n description,\n });\n }\n // Capture-then-attach: wire the trace badge + \"View Trace\" link when the\n // trace was created after init() (so the init-time bridge couldn't see it).\n if (options?.traceId && activeContext) {\n applyTraceToMeta(\n activeContext.meta,\n options.traceId,\n options.spanId,\n activeContext.traceUrlTemplate,\n );\n syncAnnotationToTest();\n }\n },\n\n // Step timing\n startTimer(): number {\n const ctx = getContext();\n const token = ctx.timerCounter++;\n const stepIndex = ctx.currentStep\n ? ctx.meta.steps.indexOf(ctx.currentStep)\n : undefined;\n ctx.activeTimers.set(token, {\n start: performance.now(),\n stepIndex: stepIndex !== undefined && stepIndex >= 0 ? stepIndex : undefined,\n stepId: ctx.currentStep?.id,\n consumed: false,\n });\n syncAnnotationToTest();\n return token;\n },\n\n endTimer(token: number): void {\n const ctx = getContext();\n const entry = ctx.activeTimers.get(token);\n if (!entry || entry.consumed) return;\n\n entry.consumed = true;\n const durationMs = performance.now() - entry.start;\n\n let step: StoryStep | undefined;\n if (entry.stepId) {\n step = ctx.meta.steps.find((s) => s.id === entry.stepId);\n }\n if (!step && entry.stepIndex !== undefined) {\n step = ctx.meta.steps[entry.stepIndex];\n }\n\n if (step) {\n step.durationMs = durationMs;\n }\n syncAnnotationToTest();\n },\n\n // Step wrappers\n fn,\n expect: storyExpect,\n};\n\nexport type Story = typeof story;\n\n/**\n * Declarations by the spec file that made them.\n *\n * A worker loads one spec file at a time, but it is reused across files. A\n * single \"last declaration wins\" slot therefore hands file A's feature to file\n * B when B declares none of its own, and the reporter keys features by\n * `test.location.file`, so B ends up with a feature it never wrote. Keying on\n * the declaring file is what stops that.\n */\nconst declaredFeatures = new Map<string, FeatureInput>();\n\n/**\n * One canonical key for a spec file, whichever way it reached us.\n *\n * Under ESM a V8 stack frame carries a file: URL, so the declaring file\n * arrives as `file:///C:/app/spec.ts` while `testInfo.file` is `C:\\app\\spec.ts`.\n * Compared as text those never match and the declaration is silently dropped.\n * Windows paths are also case-insensitive, and the two sources do not always\n * agree on the drive letter's case.\n */\nfunction fileKey(raw: string): string {\n let file = raw;\n if (file.startsWith('file://')) {\n try {\n file = fileURLToPath(file);\n } catch {\n // Not a URL we can decode; fall through with the raw text.\n }\n }\n const resolved = path.resolve(file);\n return process.platform === 'win32' ? resolved.toLowerCase() : resolved;\n}\n\n/**\n * Best-effort source file of whoever called into this module.\n *\n * `story.feature(...)` runs at module scope, before any test is running, so\n * the stack is the only place the spec file is available at that point.\n */\nfunction callerFile(): string | undefined {\n const stack = new Error().stack;\n if (!stack) return undefined;\n\n const files = stack\n .split('\\n')\n .slice(1)\n .map((line) => {\n const match = /\\(((?:file:\\/\\/)?[^()]+?):\\d+:\\d+\\)/.exec(line)\n ?? /at ((?:file:\\/\\/)?[^()\\s]+?):\\d+:\\d+/.exec(line);\n const file = match?.[1];\n // node: builtins are not files and must not be normalized into one.\n return file && !file.startsWith('node:') ? fileKey(file) : undefined;\n });\n\n // The first frame is this function, so its file is this module — in a build\n // that could be dist/index.js or src/story-api.ts. Comparing against it\n // beats matching the package name, which would also exclude a spec file\n // living inside this package (its own tests) or any user path that happens\n // to contain it.\n const self = files[0];\n return files.find((file) => file !== undefined && file !== self);\n}\n\n/**\n * Declare what the file's scenarios are for, before any of them run.\n *\n * Scenarios say what the system does. This says why the feature exists and who\n * it serves, so a reader meets the intent before the examples. Call it once per\n * spec file, at module scope or at the top of the outermost `test.describe`.\n *\n * @example\n * ```ts\n * story.feature({\n * kind: 'ability',\n * title: 'Shoppers can check out without an account',\n * narrative: 'Forcing a signup before payment is where most carts are abandoned.',\n * });\n * ```\n */\nfunction feature(input: FeatureInput): void {\n const file = callerFile();\n // Unresolvable file: keep the declaration rather than lose it silently, and\n // let the next test claim it. That is the old behaviour, now the exception.\n declaredFeatures.set(file ?? UNRESOLVED_FILE, input);\n}\n\n/** Bucket for a declaration whose file the stack could not name. */\nconst UNRESOLVED_FILE = '\\0unresolved';\n","/**\n * Playwright-native step execution helper.\n *\n * Centralises the cross-cutting concerns for fixture-aware step callbacks:\n * 1. page.screencast.showChapter() – narrated chapter markers in video recordings (v1.59)\n * 2. test.step() – TestStepInfo access + trace/report visibility (v1.51)\n * 3. context.tracing.group() – BDD phase grouping in trace viewer (v1.49)\n *\n * Used for callbacks that are either async functions or expect TestStepInfo.\n * Sync callbacks that don't need TestStepInfo follow the faster sync path.\n * All three integrations degrade gracefully if the API is absent.\n */\n\nimport { test } from '@playwright/test';\nimport type { TestStepInfo } from '@playwright/test';\n\nexport type { TestStepInfo };\n\n/** Async step callback that optionally receives TestStepInfo as a second argument. */\nexport type AsyncStepCallback<T = unknown> = (\n fixtures: Record<string, unknown>,\n step?: TestStepInfo,\n) => Promise<T>;\n\n/**\n * Execute an async step callback with full Playwright-native integration.\n *\n * Call order:\n * 1. page.screencast.showChapter(label) – sets chapter in the recording before the step runs\n * 2. test.step(label, …) – wraps execution for trace/report visibility\n * 3. context.tracing.group(label, …) – groups trace actions under the step label\n * 4. body(fixtures, stepInfo) – user callback with injected TestStepInfo\n */\nexport async function runStep<T>(\n label: string,\n body: AsyncStepCallback<T>,\n fixtures: Record<string, unknown>,\n): Promise<T> {\n const page = fixtures.page as Record<string, unknown> | undefined;\n // Derive context from fixtures or from page.context() (Playwright sync method)\n const context =\n (fixtures.context as Record<string, unknown> | undefined) ??\n (typeof (page as { context?: () => Record<string, unknown> })?.context === 'function'\n ? (page as { context: () => Record<string, unknown> }).context()\n : undefined);\n\n // ── Feature 1: Screencast chapter (v1.59) ─────────────────────────────────\n // Show the chapter BEFORE the step body runs so the recording reflects the\n // BDD step title at the right moment. Silently skipped on older Playwright.\n const screencast = page?.screencast as\n | { showChapter?: (label: string) => Promise<void> }\n | undefined;\n if (screencast?.showChapter) {\n try {\n await screencast.showChapter(label);\n } catch {\n // Graceful degradation: screencast not started or API unavailable\n }\n }\n\n // ── Feature 2: test.step (v1.51) + Feature 3: tracing.group (v1.49) ──────\n // test.step provides TestStepInfo for the callback and makes the step visible\n // in the Playwright trace viewer and HTML report as a named action.\n // tracing.group inside it groups the step's child actions under the label.\n return test.step(label, async (stepInfo) => {\n const tracing = context?.tracing as\n | { group?: <R>(label: string, fn: () => Promise<R>) => Promise<R> }\n | undefined;\n\n if (tracing?.group) {\n // Track whether body was invoked to avoid double-execution\n // when body throws inside tracing.group\n let bodyInvoked = false;\n try {\n return await tracing.group(label, async () => {\n bodyInvoked = true;\n return body(fixtures, stepInfo);\n });\n } catch (e) {\n // If body was invoked, it threw - re-throw (don't retry)\n if (bodyInvoked) throw e;\n // Otherwise, tracing.group itself threw (e.g., tracing not recording)\n // Fall back to calling body without tracing.group\n return body(fixtures, stepInfo);\n }\n }\n\n return body(fixtures, stepInfo);\n });\n}\n\n/**\n * Returns true if fn is an async function (declared with `async`).\n * Used along with callback arity to decide whether to route a step callback through runStep().\n *\n * Callbacks are routed through runStep() if they are async functions OR if they\n * have arity >= 2 (meaning they expect TestStepInfo as the second argument).\n */\nexport function isAsyncFunction(fn: unknown): boolean {\n return (\n typeof fn === 'function' &&\n (fn as { constructor?: { name?: string } }).constructor?.name === 'AsyncFunction'\n );\n}\n","/**\n * Playwright Executable Stories\n *\n * BDD-style executable documentation for Playwright Test.\n *\n * @example\n * ```ts\n * import { test, expect } from '@playwright/test';\n * import { story } from 'executable-stories-playwright';\n *\n * test.describe('Calculator', () => {\n * test('adds two numbers', async ({ page }, testInfo) => {\n * story.init(testInfo);\n *\n * story.given('two numbers 5 and 3');\n * const a = 5;\n * const b = 3;\n *\n * story.when('I add them together');\n * const result = a + b;\n *\n * story.then('the result is 8');\n * expect(result).toBe(8);\n * });\n * });\n * ```\n */\n\n// Story API\nimport { story } from './story-api';\nexport { story };\nexport type { Story } from './story-api';\n\n// Top-level step helpers (framework contract)\nexport const given = story.given;\nexport const when = story.when;\nexport const then = story.then;\nexport const and = story.and;\nexport const but = story.but;\n\n// Re-export types from local types module\nexport type {\n StoryMeta,\n StoryStep,\n DocEntry,\n StepKeyword,\n StoryDocs,\n StoryOptions,\n TicketInput,\n NormalizedTicket,\n KvOptions,\n JsonOptions,\n CodeOptions,\n TableOptions,\n LinkOptions,\n SectionOptions,\n MermaidOptions,\n ScreenshotOptions,\n VideoOptions,\n CustomOptions,\n ConsoleOptions,\n ObservePageErrorsOptions,\n AttachmentOptions,\n} from './types';\n\n// TestStepInfo re-exported from @playwright/test for consumer convenience\nexport type { TestStepInfo } from './step-runner';\n"],"mappings":"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","names":["test","fn","test"]}
@@ -40,6 +40,23 @@ declare class StoryReporter implements Reporter {
40
40
  private packageVersion;
41
41
  private gitSha;
42
42
  private projectRoot;
43
+ /**
44
+ * Left unknown until onBegin sees a config. Claiming full coverage without
45
+ * having looked would let a later merge retire scenarios on a guess.
46
+ */
47
+ private runScope;
48
+ /**
49
+ * Every spec file this run executed, story-bearing or not. Collected from all
50
+ * tests rather than from the scenarios, so a file whose last story was
51
+ * deleted is still known to have run and can have its report emptied.
52
+ */
53
+ private coveredSourceFiles;
54
+ /**
55
+ * Files where a test ended badly without ever declaring its story — a hook
56
+ * that threw before `story.init()`, a timeout during collection. Those
57
+ * scenarios are missing because the run broke, not because they were deleted.
58
+ */
59
+ private incompleteSourceFiles;
43
60
  private autotel;
44
61
  private testSpans;
45
62
  private stepSpanStacks;
package/dist/reporter.js CHANGED
@@ -101,6 +101,13 @@ import {
101
101
  saveHistory,
102
102
  stripAnsi
103
103
  } from "executable-stories-formatters";
104
+ function isNameFiltered(config) {
105
+ if (!config) return false;
106
+ if (config.shard != null) return true;
107
+ if (config.grepInvert != null) return true;
108
+ const patterns = Array.isArray(config.grep) ? config.grep : config.grep ? [config.grep] : [];
109
+ return patterns.some((pattern) => pattern.source !== ".*");
110
+ }
104
111
  function toRelativePosix(absolutePath, projectRoot) {
105
112
  return path.relative(projectRoot, absolutePath).split(path.sep).join("/");
106
113
  }
@@ -171,6 +178,23 @@ var StoryReporter = class {
171
178
  packageVersion;
172
179
  gitSha;
173
180
  projectRoot = process.cwd();
181
+ /**
182
+ * Left unknown until onBegin sees a config. Claiming full coverage without
183
+ * having looked would let a later merge retire scenarios on a guess.
184
+ */
185
+ runScope;
186
+ /**
187
+ * Every spec file this run executed, story-bearing or not. Collected from all
188
+ * tests rather than from the scenarios, so a file whose last story was
189
+ * deleted is still known to have run and can have its report emptied.
190
+ */
191
+ coveredSourceFiles = /* @__PURE__ */ new Set();
192
+ /**
193
+ * Files where a test ended badly without ever declaring its story — a hook
194
+ * that threw before `story.init()`, a timeout during collection. Those
195
+ * scenarios are missing because the run broke, not because they were deleted.
196
+ */
197
+ incompleteSourceFiles = /* @__PURE__ */ new Set();
174
198
  autotel = null;
175
199
  testSpans = /* @__PURE__ */ new Map();
176
200
  stepSpanStacks = /* @__PURE__ */ new Map();
@@ -186,6 +210,7 @@ var StoryReporter = class {
186
210
  this.startTime = Date.now();
187
211
  this.rootSuite = suite;
188
212
  this.projectRoot = config.rootDir ?? process.cwd();
213
+ if (config) this.runScope = isNameFiltered(config) ? "filtered" : "full";
189
214
  const includeMetadata = this.options.markdown?.includeMetadata ?? true;
190
215
  if (includeMetadata) {
191
216
  this.packageVersion = readPackageVersion(this.projectRoot);
@@ -231,6 +256,14 @@ var StoryReporter = class {
231
256
  }
232
257
  }
233
258
  onTestEnd(test, result) {
259
+ const file = test.location?.file;
260
+ if (file) {
261
+ const relative2 = toRelativePosix(file, this.projectRoot);
262
+ this.coveredSourceFiles.add(relative2);
263
+ const declared = test.annotations?.some((a) => a.type === "story-meta");
264
+ const brokeEarly = result.status === "failed" || result.status === "timedOut" || result.status === "interrupted";
265
+ if (!declared && brokeEarly) this.incompleteSourceFiles.add(relative2);
266
+ }
234
267
  if (this.autotel) {
235
268
  const stack = this.stepSpanStacks.get(test.id);
236
269
  if (stack) {
@@ -398,7 +431,7 @@ var StoryReporter = class {
398
431
  return planned;
399
432
  }
400
433
  async onEnd(_result) {
401
- if (this.scenarios.length === 0) return;
434
+ if (this.scenarios.length === 0 && this.coveredSourceFiles.size === 0) return;
402
435
  if (this.scenarios.length > 0) {
403
436
  const sampleScenario = this.scenarios[0];
404
437
  if ("tags" in sampleScenario) {
@@ -448,6 +481,9 @@ var StoryReporter = class {
448
481
  startedAtMs: this.startTime,
449
482
  finishedAtMs: Date.now(),
450
483
  projectRoot: this.projectRoot,
484
+ ...this.runScope ? { runScope: this.runScope } : {},
485
+ ...this.coveredSourceFiles.size > 0 ? { coveredSourceFiles: [...this.coveredSourceFiles].sort() } : {},
486
+ ...this.incompleteSourceFiles.size > 0 ? { incompleteSourceFiles: [...this.incompleteSourceFiles].sort() } : {},
451
487
  packageVersion: this.packageVersion,
452
488
  gitSha: this.gitSha,
453
489
  ci: detectCI()
@@ -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 */\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 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 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 // 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 if (this.scenarios.length === 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 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":[]}
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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "executable-stories-playwright",
3
- "version": "8.9.0",
3
+ "version": "8.10.0",
4
4
  "license": "Apache-2.0",
5
5
  "description": "BDD-style executable stories for Playwright Test with documentation generation",
6
6
  "type": "module",
@@ -34,8 +34,8 @@
34
34
  }
35
35
  },
36
36
  "dependencies": {
37
- "executable-stories-core": "0.20.0",
38
- "executable-stories-formatters": "1.12.0"
37
+ "executable-stories-core": "0.21.0",
38
+ "executable-stories-formatters": "1.13.0"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@opentelemetry/api": "^1.9.1",