executable-stories-playwright 8.0.0 → 8.1.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/dist/index.d.ts +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -1
- package/dist/reporter.d.ts +14 -2
- package/dist/reporter.js +15 -1
- package/dist/reporter.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -189,6 +189,7 @@ declare const story: {
|
|
|
189
189
|
screenshot(options: ScreenshotOptions): void;
|
|
190
190
|
custom(options: CustomOptions): void;
|
|
191
191
|
attach: typeof playwrightAttach;
|
|
192
|
+
attachSpans(spans: ReadonlyArray<Record<string, unknown>>): void;
|
|
192
193
|
startTimer(): number;
|
|
193
194
|
endTimer(token: number): void;
|
|
194
195
|
fn: typeof fn;
|
package/dist/index.js
CHANGED
|
@@ -367,6 +367,22 @@ var story = {
|
|
|
367
367
|
},
|
|
368
368
|
// Attachments
|
|
369
369
|
attach: playwrightAttach,
|
|
370
|
+
// OTel span attachment
|
|
371
|
+
attachSpans(spans) {
|
|
372
|
+
if (!activeTestInfo) return;
|
|
373
|
+
const existing = activeTestInfo.annotations.find(
|
|
374
|
+
(a) => a.type === "story-otel-spans"
|
|
375
|
+
);
|
|
376
|
+
const description = JSON.stringify(spans);
|
|
377
|
+
if (existing) {
|
|
378
|
+
existing.description = description;
|
|
379
|
+
} else {
|
|
380
|
+
activeTestInfo.annotations.push({
|
|
381
|
+
type: "story-otel-spans",
|
|
382
|
+
description
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
},
|
|
370
386
|
// Step timing
|
|
371
387
|
startTimer() {
|
|
372
388
|
const ctx = getContext();
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/story-api.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 type { TestInfo, PlaywrightTestArgs, PlaywrightTestOptions } from '@playwright/test';\nimport {\n tryGetActiveOtelContext,\n resolveTraceUrl,\n} from 'executable-stories-formatters';\nimport type {\n StepKeyword,\n StoryMeta,\n StoryStep,\n DocEntry,\n} from './types';\nimport type {\n StoryDocs,\n StoryOptions,\n AttachmentOptions,\n ScopedAttachment,\n KvOptions,\n JsonOptions,\n CodeOptions,\n TableOptions,\n LinkOptions,\n SectionOptions,\n MermaidOptions,\n ScreenshotOptions,\n CustomOptions,\n} from './types';\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}\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(ticket: string | string[] | undefined): string[] | undefined {\n if (!ticket) return undefined;\n return Array.isArray(ticket) ? ticket : [ticket];\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.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 entries.push({\n kind: 'screenshot',\n path: docs.screenshot.path,\n alt: docs.screenshot.alt,\n phase: 'runtime',\n });\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\nfunction attachDoc(entry: DocEntry): void {\n const ctx = getContext();\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}\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<T>(text: string, body: (fixtures: PlaywrightFixtures) => T): T;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n function stepMarker<T>(text: string, docsOrBody?: StoryDocs | ((...args: any[]) => T)): T | void {\n const ctx = getContext();\n const isCallback = typeof docsOrBody === 'function';\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 const step: StoryStep = {\n id: `step-${ctx.stepCounter++}`,\n keyword: resolvedKeyword,\n text,\n docs: (!isCallback && docsOrBody) ? convertStoryDocsToEntries(docsOrBody) : [],\n ...(isCallback ? { wrapped: true } : {}),\n };\n\n ctx.meta.steps.push(step);\n ctx.currentStep = step;\n syncAnnotationToTest();\n\n if (!isCallback) return;\n\n const body = docsOrBody as (fixtures?: PlaywrightFixtures) => T;\n const start = performance.now();\n\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 meta: options?.meta,\n sourceOrder: sourceOrderCounter++,\n };\n\n // OTel bridge: detect active span, flow data bidirectionally\n const otelCtx = tryGetActiveOtelContext();\n if (otelCtx) {\n // OTel -> Story: capture traceId in structured meta\n meta.meta = { ...meta.meta, otel: { traceId: otelCtx.traceId, spanId: otelCtx.spanId } };\n\n // OTel -> Story: inject human-readable doc entries\n meta.docs = meta.docs ?? [];\n meta.docs.push({ kind: 'kv', label: 'Trace ID', value: otelCtx.traceId, phase: 'runtime' });\n\n const template = options?.traceUrlTemplate ?? process.env.OTEL_TRACE_URL_TEMPLATE;\n const url = resolveTraceUrl(template, otelCtx.traceId);\n if (url) {\n meta.docs.push({ kind: 'link', label: 'View Trace', url, phase: 'runtime' });\n }\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);\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 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 };\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// 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// Export story object\n// ============================================================================\n\nexport const story = {\n init,\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): void {\n attachDoc({ kind: 'note', text, phase: 'runtime' });\n syncAnnotationToTest();\n },\n\n tag(name: string | string[]): void {\n const names = Array.isArray(name) ? name : [name];\n attachDoc({ kind: 'tag', names, phase: 'runtime' });\n syncAnnotationToTest();\n },\n\n kv(options: KvOptions): void {\n attachDoc({ kind: 'kv', label: options.label, value: options.value, phase: 'runtime' });\n syncAnnotationToTest();\n },\n\n json(options: JsonOptions): void {\n const content = JSON.stringify(options.value, null, 2);\n attachDoc({ kind: 'code', label: options.label, content, lang: 'json', phase: 'runtime' });\n syncAnnotationToTest();\n },\n\n code(options: CodeOptions): void {\n attachDoc({ kind: 'code', label: options.label, content: options.content, lang: options.lang, phase: 'runtime' });\n syncAnnotationToTest();\n },\n\n table(options: TableOptions): void {\n attachDoc({ kind: 'table', label: options.label, columns: options.columns, rows: options.rows, phase: 'runtime' });\n syncAnnotationToTest();\n },\n\n link(options: LinkOptions): void {\n attachDoc({ kind: 'link', label: options.label, url: options.url, phase: 'runtime' });\n syncAnnotationToTest();\n },\n\n section(options: SectionOptions): void {\n attachDoc({ kind: 'section', title: options.title, markdown: options.markdown, phase: 'runtime' });\n syncAnnotationToTest();\n },\n\n mermaid(options: MermaidOptions): void {\n attachDoc({ kind: 'mermaid', code: options.code, title: options.title, phase: 'runtime' });\n syncAnnotationToTest();\n },\n\n screenshot(options: ScreenshotOptions): void {\n attachDoc({ kind: 'screenshot', path: options.path, alt: options.alt, phase: 'runtime' });\n syncAnnotationToTest();\n },\n\n custom(options: CustomOptions): void {\n attachDoc({ kind: 'custom', type: options.type, data: options.data, phase: 'runtime' });\n syncAnnotationToTest();\n },\n\n // Attachments\n attach: playwrightAttach,\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 * 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 KvOptions,\n JsonOptions,\n CodeOptions,\n TableOptions,\n LinkOptions,\n SectionOptions,\n MermaidOptions,\n ScreenshotOptions,\n CustomOptions,\n} from './types';\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/story-api.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 type { TestInfo, PlaywrightTestArgs, PlaywrightTestOptions } from '@playwright/test';\nimport {\n tryGetActiveOtelContext,\n resolveTraceUrl,\n} from 'executable-stories-formatters';\nimport type {\n StepKeyword,\n StoryMeta,\n StoryStep,\n DocEntry,\n} from './types';\nimport type {\n StoryDocs,\n StoryOptions,\n AttachmentOptions,\n ScopedAttachment,\n KvOptions,\n JsonOptions,\n CodeOptions,\n TableOptions,\n LinkOptions,\n SectionOptions,\n MermaidOptions,\n ScreenshotOptions,\n CustomOptions,\n} from './types';\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}\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(ticket: string | string[] | undefined): string[] | undefined {\n if (!ticket) return undefined;\n return Array.isArray(ticket) ? ticket : [ticket];\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.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 entries.push({\n kind: 'screenshot',\n path: docs.screenshot.path,\n alt: docs.screenshot.alt,\n phase: 'runtime',\n });\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\nfunction attachDoc(entry: DocEntry): void {\n const ctx = getContext();\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}\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<T>(text: string, body: (fixtures: PlaywrightFixtures) => T): T;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n function stepMarker<T>(text: string, docsOrBody?: StoryDocs | ((...args: any[]) => T)): T | void {\n const ctx = getContext();\n const isCallback = typeof docsOrBody === 'function';\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 const step: StoryStep = {\n id: `step-${ctx.stepCounter++}`,\n keyword: resolvedKeyword,\n text,\n docs: (!isCallback && docsOrBody) ? convertStoryDocsToEntries(docsOrBody) : [],\n ...(isCallback ? { wrapped: true } : {}),\n };\n\n ctx.meta.steps.push(step);\n ctx.currentStep = step;\n syncAnnotationToTest();\n\n if (!isCallback) return;\n\n const body = docsOrBody as (fixtures?: PlaywrightFixtures) => T;\n const start = performance.now();\n\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 meta: options?.meta,\n sourceOrder: sourceOrderCounter++,\n };\n\n // OTel bridge: detect active span, flow data bidirectionally\n const otelCtx = tryGetActiveOtelContext();\n if (otelCtx) {\n // OTel -> Story: capture traceId in structured meta\n meta.meta = { ...meta.meta, otel: { traceId: otelCtx.traceId, spanId: otelCtx.spanId } };\n\n // OTel -> Story: inject human-readable doc entries\n meta.docs = meta.docs ?? [];\n meta.docs.push({ kind: 'kv', label: 'Trace ID', value: otelCtx.traceId, phase: 'runtime' });\n\n const template = options?.traceUrlTemplate ?? process.env.OTEL_TRACE_URL_TEMPLATE;\n const url = resolveTraceUrl(template, otelCtx.traceId);\n if (url) {\n meta.docs.push({ kind: 'link', label: 'View Trace', url, phase: 'runtime' });\n }\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);\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 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 };\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// 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// Export story object\n// ============================================================================\n\nexport const story = {\n init,\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): void {\n attachDoc({ kind: 'note', text, phase: 'runtime' });\n syncAnnotationToTest();\n },\n\n tag(name: string | string[]): void {\n const names = Array.isArray(name) ? name : [name];\n attachDoc({ kind: 'tag', names, phase: 'runtime' });\n syncAnnotationToTest();\n },\n\n kv(options: KvOptions): void {\n attachDoc({ kind: 'kv', label: options.label, value: options.value, phase: 'runtime' });\n syncAnnotationToTest();\n },\n\n json(options: JsonOptions): void {\n const content = JSON.stringify(options.value, null, 2);\n attachDoc({ kind: 'code', label: options.label, content, lang: 'json', phase: 'runtime' });\n syncAnnotationToTest();\n },\n\n code(options: CodeOptions): void {\n attachDoc({ kind: 'code', label: options.label, content: options.content, lang: options.lang, phase: 'runtime' });\n syncAnnotationToTest();\n },\n\n table(options: TableOptions): void {\n attachDoc({ kind: 'table', label: options.label, columns: options.columns, rows: options.rows, phase: 'runtime' });\n syncAnnotationToTest();\n },\n\n link(options: LinkOptions): void {\n attachDoc({ kind: 'link', label: options.label, url: options.url, phase: 'runtime' });\n syncAnnotationToTest();\n },\n\n section(options: SectionOptions): void {\n attachDoc({ kind: 'section', title: options.title, markdown: options.markdown, phase: 'runtime' });\n syncAnnotationToTest();\n },\n\n mermaid(options: MermaidOptions): void {\n attachDoc({ kind: 'mermaid', code: options.code, title: options.title, phase: 'runtime' });\n syncAnnotationToTest();\n },\n\n screenshot(options: ScreenshotOptions): void {\n attachDoc({ kind: 'screenshot', path: options.path, alt: options.alt, phase: 'runtime' });\n syncAnnotationToTest();\n },\n\n custom(options: CustomOptions): void {\n attachDoc({ kind: 'custom', type: options.type, data: options.data, phase: 'runtime' });\n syncAnnotationToTest();\n },\n\n // Attachments\n attach: playwrightAttach,\n\n // OTel span attachment\n attachSpans(spans: ReadonlyArray<Record<string, unknown>>): 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 },\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 * 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 KvOptions,\n JsonOptions,\n CodeOptions,\n TableOptions,\n LinkOptions,\n SectionOptions,\n MermaidOptions,\n ScreenshotOptions,\n CustomOptions,\n} from './types';\n"],"mappings":"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","names":[]}
|
package/dist/reporter.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Reporter, FullConfig, TestCase, TestResult, TestStep, FullResult } from '@playwright/test/reporter';
|
|
2
|
-
import { FormatterOptions } from 'executable-stories-formatters';
|
|
2
|
+
import { FormatterOptions, RawAttachment } from 'executable-stories-formatters';
|
|
3
3
|
export { ColocatedStyle, FormatterOptions, OutputFormat, OutputMode, OutputRule } from 'executable-stories-formatters';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -29,5 +29,17 @@ declare class StoryReporter implements Reporter {
|
|
|
29
29
|
onTestEnd(test: TestCase, result: TestResult): void;
|
|
30
30
|
onEnd(_result: FullResult): Promise<void>;
|
|
31
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Deduplicate video attachments by name.
|
|
34
|
+
*
|
|
35
|
+
* Playwright with `video: "on"` may produce multiple video files per test
|
|
36
|
+
* in the output directory (e.g. `video.webm` and `video-1.webm`), attaching
|
|
37
|
+
* all of them to the test result. This leads to duplicate videos in reports.
|
|
38
|
+
*
|
|
39
|
+
* For each unique video attachment name, keep only the last occurrence —
|
|
40
|
+
* Playwright appends the real recording after any stubs.
|
|
41
|
+
* Non-video attachments are always preserved.
|
|
42
|
+
*/
|
|
43
|
+
declare function deduplicateVideoAttachments(attachments: RawAttachment[]): RawAttachment[];
|
|
32
44
|
|
|
33
|
-
export { type StoryReporterOptions, StoryReporter as default };
|
|
45
|
+
export { type StoryReporterOptions, deduplicateVideoAttachments, StoryReporter as default };
|
package/dist/reporter.js
CHANGED
|
@@ -209,7 +209,7 @@ var StoryReporter = class {
|
|
|
209
209
|
error = err.message || String(err);
|
|
210
210
|
errorStack = err.stack;
|
|
211
211
|
}
|
|
212
|
-
const
|
|
212
|
+
const allAttachments = (result.attachments ?? []).map((a) => {
|
|
213
213
|
let body;
|
|
214
214
|
let encoding;
|
|
215
215
|
if (a.body !== void 0) {
|
|
@@ -229,6 +229,7 @@ var StoryReporter = class {
|
|
|
229
229
|
encoding
|
|
230
230
|
};
|
|
231
231
|
});
|
|
232
|
+
const attachments = deduplicateVideoAttachments(allAttachments);
|
|
232
233
|
const stepEvents = meta.steps.filter((s) => s.durationMs !== void 0).map((s, i) => ({
|
|
233
234
|
index: i,
|
|
234
235
|
title: s.text,
|
|
@@ -341,7 +342,20 @@ var StoryReporter = class {
|
|
|
341
342
|
}
|
|
342
343
|
}
|
|
343
344
|
};
|
|
345
|
+
function deduplicateVideoAttachments(attachments) {
|
|
346
|
+
const lastVideoIndex = /* @__PURE__ */ new Map();
|
|
347
|
+
for (let i = 0; i < attachments.length; i++) {
|
|
348
|
+
if (attachments[i].mediaType.startsWith("video/")) {
|
|
349
|
+
lastVideoIndex.set(attachments[i].name, i);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return attachments.filter((att, i) => {
|
|
353
|
+
if (!att.mediaType.startsWith("video/")) return true;
|
|
354
|
+
return lastVideoIndex.get(att.name) === i;
|
|
355
|
+
});
|
|
356
|
+
}
|
|
344
357
|
export {
|
|
358
|
+
deduplicateVideoAttachments,
|
|
345
359
|
StoryReporter as default
|
|
346
360
|
};
|
|
347
361
|
//# sourceMappingURL=reporter.js.map
|
package/dist/reporter.js.map
CHANGED
|
@@ -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 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 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\n// ============================================================================\n// Internal Types\n// ============================================================================\n\ninterface CollectedScenario {\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\n// ============================================================================\n// Reporter Implementation\n// ============================================================================\n\nexport default class StoryReporter implements Reporter {\n private options: StoryReporterOptions;\n private scenarios: CollectedScenario[] = [];\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 onBegin(config: FullConfig): void {\n this.startTime = Date.now();\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\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 = err.message || String(err);\n errorStack = err.stack;\n }\n\n // Map Playwright result.attachments → RawAttachment[]\n const attachments: RawAttachment[] = (result.attachments ?? []).map((a) => {\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\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 }, i: number) => ({\n index: i,\n title: s.text,\n durationMs: s.durationMs,\n }));\n\n this.scenarios.push({\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 async onEnd(_result: FullResult): Promise<void> {\n if (this.scenarios.length === 0) return;\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: \"fail\",\n interrupted: \"fail\",\n };\n\n return {\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\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 * 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 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 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\n// ============================================================================\n// Internal Types\n// ============================================================================\n\ninterface CollectedScenario {\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\n// ============================================================================\n// Reporter Implementation\n// ============================================================================\n\nexport default class StoryReporter implements Reporter {\n private options: StoryReporterOptions;\n private scenarios: CollectedScenario[] = [];\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 onBegin(config: FullConfig): void {\n this.startTime = Date.now();\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\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 = err.message || String(err);\n errorStack = err.stack;\n }\n\n // Map Playwright result.attachments → RawAttachment[]\n const allAttachments: RawAttachment[] = (result.attachments ?? []).map((a) => {\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\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 // 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 }, i: number) => ({\n index: i,\n title: s.text,\n durationMs: s.durationMs,\n }));\n\n this.scenarios.push({\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 async onEnd(_result: FullResult): Promise<void> {\n if (this.scenarios.length === 0) return;\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: \"fail\",\n interrupted: \"fail\",\n };\n\n return {\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\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":[]}
|