executable-stories-jest 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.cjs CHANGED
@@ -49,6 +49,7 @@ var import_executable_stories_formatters = require("executable-stories-formatter
49
49
  var import_meta = {};
50
50
  var storyRegistry = globalThis.__jestExecutableStoriesRegistry ??= /* @__PURE__ */ new Map();
51
51
  var attachmentRegistry = /* @__PURE__ */ new Map();
52
+ var otelSpansRegistry = /* @__PURE__ */ new Map();
52
53
  var exitHandlerRegistered = globalThis.__jestExecutableStoriesExitHandler ?? false;
53
54
  function getOutputDir() {
54
55
  const baseDir = process.env.JEST_STORY_DOCS_DIR ?? ".jest-executable-stories";
@@ -65,15 +66,18 @@ function flushStories() {
65
66
  const baseName = testFilePath === "unknown" ? "unknown" : path.basename(testFilePath);
66
67
  const outFile = path.join(outputDir, `${baseName}.${hash}.json`);
67
68
  const fileAttachments = attachmentRegistry.get(testFilePath);
68
- const scenariosWithAttachments = scenarios.map((s) => ({
69
+ const fileOtelSpans = otelSpansRegistry.get(testFilePath);
70
+ const scenariosWithAttachments = scenarios.map((s, i) => ({
69
71
  ...s,
70
- _attachments: fileAttachments?.get(s.scenario) ?? []
72
+ _attachments: fileAttachments?.get(i) ?? [],
73
+ ...fileOtelSpans?.get(i) ? { _otelSpans: fileOtelSpans.get(i) } : {}
71
74
  }));
72
75
  const payload = { testFilePath, scenarios: scenariosWithAttachments };
73
76
  fs.writeFileSync(outFile, JSON.stringify(payload, null, 2) + "\n", "utf8");
74
77
  }
75
78
  storyRegistry.clear();
76
79
  attachmentRegistry.clear();
80
+ otelSpansRegistry.clear();
77
81
  }
78
82
  function registerExitHandler() {
79
83
  if (exitHandlerRegistered) return;
@@ -237,9 +241,12 @@ function init(options) {
237
241
  }
238
242
  }
239
243
  const existing = storyRegistry.get(testPath);
244
+ let scenarioIndex;
240
245
  if (existing) {
246
+ scenarioIndex = existing.length;
241
247
  existing.push(meta);
242
248
  } else {
249
+ scenarioIndex = 0;
243
250
  storyRegistry.set(testPath, [meta]);
244
251
  }
245
252
  registerExitHandler();
@@ -249,12 +256,14 @@ function init(options) {
249
256
  stepCounter: 0,
250
257
  attachments: [],
251
258
  activeTimers: /* @__PURE__ */ new Map(),
252
- timerCounter: 0
259
+ timerCounter: 0,
260
+ testPath,
261
+ scenarioIndex
253
262
  };
254
263
  if (!attachmentRegistry.has(testPath)) {
255
264
  attachmentRegistry.set(testPath, /* @__PURE__ */ new Map());
256
265
  }
257
- attachmentRegistry.get(testPath).set(meta.scenario, activeContext.attachments);
266
+ attachmentRegistry.get(testPath).set(scenarioIndex, activeContext.attachments);
258
267
  }
259
268
  function fn(keyword, text, body) {
260
269
  const ctx = getContext();
@@ -358,6 +367,14 @@ var story = {
358
367
  stepId: ctx.currentStep?.id
359
368
  });
360
369
  },
370
+ // OTel span attachment
371
+ attachSpans(spans) {
372
+ const ctx = getContext();
373
+ if (!otelSpansRegistry.has(ctx.testPath)) {
374
+ otelSpansRegistry.set(ctx.testPath, /* @__PURE__ */ new Map());
375
+ }
376
+ otelSpansRegistry.get(ctx.testPath).set(ctx.scenarioIndex, spans);
377
+ },
361
378
  // Step wrappers
362
379
  fn,
363
380
  expect: storyExpect,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/story-api.ts","../src/types.ts"],"sourcesContent":["/**\n * Jest Executable Stories\n *\n * BDD-style executable documentation for Jest.\n *\n * @example\n * ```ts\n * import { story } from 'executable-stories-jest';\n *\n * describe('Calculator', () => {\n * it('adds two numbers', () => {\n * story.init();\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\nexport type {\n StoryMeta,\n StoryStep,\n DocEntry,\n StepKeyword,\n StepMode,\n DocPhase,\n StoryDocs,\n StoryOptions,\n AttachmentOptions,\n KvOptions,\n JsonOptions,\n CodeOptions,\n TableOptions,\n LinkOptions,\n SectionOptions,\n MermaidOptions,\n ScreenshotOptions,\n CustomOptions,\n} from \"./types\";\n\nexport { STORY_META_KEY } from \"./types\";\n","/**\n * Jest story.* API for executable-stories.\n *\n * Uses native Jest describe/it/test with opt-in documentation:\n *\n * @example\n * ```ts\n * import { story } from 'executable-stories-jest';\n *\n * describe('Calculator', () => {\n * it('adds two numbers', () => {\n * story.init();\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 * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport { createHash } from \"node:crypto\";\nimport { createRequire } from 'node:module';\nimport { tryGetActiveOtelContext, resolveTraceUrl } from 'executable-stories-formatters';\nimport type {\n DocEntry,\n StepKeyword,\n StoryDocs,\n StoryMeta,\n StoryOptions,\n StoryStep,\n ScopedAttachment,\n AttachmentOptions,\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// Story Context\n// ============================================================================\n\n/** Internal timer entry */\ninterface TimerEntry {\n start: number;\n stepIndex?: number;\n stepId?: string;\n consumed: boolean;\n}\n\ninterface StoryContext {\n /** The story metadata being built */\n meta: StoryMeta;\n /** The current step (for attaching docs) */\n currentStep: StoryStep | null;\n /** Deterministic step counter (resets per test case) */\n stepCounter: number;\n /** Collected attachments with step scope */\n attachments: ScopedAttachment[];\n /** Active timers keyed by token */\n activeTimers: Map<number, TimerEntry>;\n /** Monotonic timer token counter */\n timerCounter: number;\n}\n\n// ============================================================================\n// File-based story collection (works across Jest worker processes)\n// ============================================================================\n\n// Use globalThis to ensure the registry is shared across module instances\n// This is needed because Jest may load the setup file and test files as separate module instances\ndeclare global {\n // eslint-disable-next-line no-var\n var __jestExecutableStoriesRegistry: Map<string, StoryMeta[]> | undefined;\n // eslint-disable-next-line no-var\n var __jestExecutableStoriesExitHandler: boolean | undefined;\n}\n\n/** Stories collected during test execution, keyed by test file path */\nconst storyRegistry: Map<string, StoryMeta[]> = globalThis.__jestExecutableStoriesRegistry ??= new Map();\n\n/** Attachments collected per story, keyed by test file path → scenario name → attachments */\nconst attachmentRegistry = new Map<string, Map<string, ScopedAttachment[]>>();\n\n/** Track if we've registered the process exit handler */\nlet exitHandlerRegistered = globalThis.__jestExecutableStoriesExitHandler ?? false;\n\n/** Get the output directory for story JSON files */\nfunction getOutputDir(): string {\n const baseDir = process.env.JEST_STORY_DOCS_DIR ?? \".jest-executable-stories\";\n return path.resolve(process.cwd(), baseDir);\n}\n\n/** Flush all collected stories to JSON files */\nfunction flushStories(): void {\n if (storyRegistry.size === 0) return;\n\n const workerId = process.env.JEST_WORKER_ID ?? \"0\";\n const outputDir = path.join(getOutputDir(), `worker-${workerId}`);\n fs.mkdirSync(outputDir, { recursive: true });\n\n for (const [testFilePath, scenarios] of storyRegistry) {\n if (!scenarios.length) continue;\n const hash = createHash(\"sha1\").update(testFilePath).digest(\"hex\").slice(0, 12);\n const baseName = testFilePath === \"unknown\" ? \"unknown\" : path.basename(testFilePath);\n const outFile = path.join(outputDir, `${baseName}.${hash}.json`);\n\n // Include attachments per scenario\n const fileAttachments = attachmentRegistry.get(testFilePath);\n const scenariosWithAttachments = scenarios.map((s) => ({\n ...s,\n _attachments: fileAttachments?.get(s.scenario) ?? [],\n }));\n\n const payload = { testFilePath, scenarios: scenariosWithAttachments };\n fs.writeFileSync(outFile, JSON.stringify(payload, null, 2) + \"\\n\", \"utf8\");\n }\n storyRegistry.clear();\n attachmentRegistry.clear();\n}\n\n/** Register process exit handler to flush stories (once per worker) */\nfunction registerExitHandler(): void {\n if (exitHandlerRegistered) return;\n exitHandlerRegistered = true;\n globalThis.__jestExecutableStoriesExitHandler = true;\n // Use 'exit' event - always fired when Node.js is about to exit\n // Note: Only sync operations work here, which is fine for fs.writeFileSync\n process.on(\"exit\", () => {\n flushStories();\n });\n}\n\n// ============================================================================\n// Jest-specific context\n// ============================================================================\n\n/** Active story context - set by story.init() */\nlet activeContext: StoryContext | 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() must be called first. Use: it('name', () => { story.init(); ... });\"\n );\n }\n return activeContext;\n}\n\n// ============================================================================\n// Helper Functions\n// ============================================================================\n\n/**\n * Normalize ticket option to array format.\n */\nfunction normalizeTickets(ticket: string | string[] | undefined): string[] | undefined {\n if (!ticket) return undefined;\n return Array.isArray(ticket) ? ticket : [ticket];\n}\n\n/**\n * Extract the suite path from Jest's currentTestName.\n * Jest's currentTestName is formatted as: \"describe1 > describe2 > test name\"\n */\nfunction extractSuitePath(currentTestName: string): { suitePath?: string[]; testName: string } {\n const parts = currentTestName.split(\" > \");\n if (parts.length <= 1) {\n return { testName: currentTestName };\n }\n const testName = parts[parts.length - 1];\n const suitePath = parts.slice(0, -1);\n return { suitePath, testName };\n}\n\n/**\n * Convert StoryDocs inline options to DocEntry array.\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({ kind: \"code\", label: docs.code.label, content: docs.code.content, lang: docs.code.lang, phase: \"runtime\" });\n }\n if (docs.json) {\n entries.push({ kind: \"code\", label: docs.json.label, content: JSON.stringify(docs.json.value, null, 2), lang: \"json\", phase: \"runtime\" });\n }\n if (docs.table) {\n entries.push({ kind: \"table\", label: docs.table.label, columns: docs.table.columns, rows: docs.table.rows, phase: \"runtime\" });\n }\n if (docs.link) {\n entries.push({ kind: \"link\", label: docs.link.label, url: docs.link.url, phase: \"runtime\" });\n }\n if (docs.section) {\n entries.push({ kind: \"section\", title: docs.section.title, markdown: docs.section.markdown, phase: \"runtime\" });\n }\n if (docs.mermaid) {\n entries.push({ kind: \"mermaid\", code: docs.mermaid.code, title: docs.mermaid.title, phase: \"runtime\" });\n }\n if (docs.screenshot) {\n entries.push({ kind: \"screenshot\", path: docs.screenshot.path, alt: docs.screenshot.alt, phase: \"runtime\" });\n }\n if (docs.custom) {\n entries.push({ kind: \"custom\", type: docs.custom.type, data: docs.custom.data, phase: \"runtime\" });\n }\n\n return entries;\n}\n\n// ============================================================================\n// Helper to attach doc entry to current step or story-level\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// Step Markers\n// ============================================================================\n\nfunction createStepMarker(keyword: StepKeyword) {\n function stepMarker(text: string, docs?: StoryDocs): void;\n function stepMarker<T>(text: string, body: () => T): T;\n function stepMarker<T>(text: string, docsOrBody?: StoryDocs | (() => 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\n if (!isCallback) return;\n\n const body = docsOrBody as () => T;\n const start = performance.now();\n\n try {\n const result = body();\n if (result instanceof Promise) {\n return result.then(\n (val) => { step.durationMs = performance.now() - start; return val; },\n (err) => { step.durationMs = performance.now() - start; throw err; },\n ) as T;\n }\n step.durationMs = performance.now() - start;\n return result;\n } catch (err) {\n step.durationMs = performance.now() - start;\n throw err;\n }\n }\n return stepMarker;\n}\n\n// ============================================================================\n// story.init() - Jest-specific\n// ============================================================================\n\n/**\n * Initialize a story for the current test.\n * Must be called at the start of each test that wants documentation.\n *\n * @param options - Optional story configuration (tags, ticket, meta)\n *\n * @example\n * ```ts\n * it('adds two numbers', () => {\n * story.init();\n * // ... rest of test\n * });\n * ```\n */\nfunction init(options?: StoryOptions): void {\n // Get current test info from Jest globals\n const state = expect.getState();\n const currentTestName = state.currentTestName || \"Unknown test\";\n const testPath = state.testPath || \"unknown\";\n\n const { suitePath, testName } = extractSuitePath(currentTestName);\n\n const meta: StoryMeta = {\n scenario: testName,\n steps: [],\n suitePath,\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', testName);\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 // Store in registry for this file\n const existing = storyRegistry.get(testPath);\n if (existing) {\n existing.push(meta);\n } else {\n storyRegistry.set(testPath, [meta]);\n }\n\n // Register exit handler to flush stories when worker exits\n registerExitHandler();\n\n // Set active context\n activeContext = {\n meta,\n currentStep: null,\n stepCounter: 0,\n attachments: [],\n activeTimers: new Map(),\n timerCounter: 0,\n };\n\n // Link attachments to the registry for this test file + scenario\n if (!attachmentRegistry.has(testPath)) {\n attachmentRegistry.set(testPath, new Map());\n }\n attachmentRegistry.get(testPath)!.set(meta.scenario, activeContext.attachments);\n}\n\n// ============================================================================\n// Step Wrappers: story.fn() and story.expect()\n// ============================================================================\n\n/**\n * Wrap a function body as a step. Records the step with timing and `wrapped: true`.\n * Supports both sync and async functions. Returns whatever the function returns.\n *\n * @param keyword - The BDD keyword (Given, When, Then, And, But)\n * @param text - Step description\n * @param body - The function to execute\n * @returns The return value of body (or a Promise of it if body is async)\n *\n * @example\n * ```ts\n * const data = story.fn('Given', 'setup data', () => ({ a: 5, b: 3 }));\n * const result = await story.fn('When', 'call API', async () => fetch('/api'));\n * ```\n */\nfunction fn<T>(keyword: StepKeyword, text: string, body: () => 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\n const step: StoryStep = {\n id: `step-${ctx.stepCounter++}`,\n keyword: resolvedKeyword,\n text,\n docs: [],\n wrapped: true,\n };\n\n ctx.meta.steps.push(step);\n ctx.currentStep = step;\n\n const start = performance.now();\n\n try {\n const result = body();\n\n // Handle async functions\n if (result instanceof Promise) {\n return result.then(\n (val) => {\n step.durationMs = performance.now() - start;\n return val;\n },\n (err) => {\n step.durationMs = performance.now() - start;\n throw err;\n },\n ) as T;\n }\n\n step.durationMs = performance.now() - start;\n return result;\n } catch (err) {\n step.durationMs = performance.now() - start;\n throw err;\n }\n}\n\n/**\n * Wrap an assertion as a Then step. Shorthand for `story.fn('Then', text, body)`.\n *\n * @param text - Step description\n * @param body - The assertion function to execute\n *\n * @example\n * ```ts\n * story.expect('the result is 8', () => { expect(result).toBe(8); });\n * ```\n */\nfunction storyExpect<T>(text: string, body: () => T): T {\n return fn('Then', text, body);\n}\n\n// ============================================================================\n// Export story object\n// ============================================================================\n\n/**\n * The main story API object for Jest.\n *\n * @example\n * ```ts\n * import { story } from 'executable-stories-jest';\n *\n * describe('Calculator', () => {\n * it('adds two numbers', () => {\n * story.init();\n *\n * story.given('two numbers 5 and 3');\n * const a = 5, 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 */\nexport const story = {\n // Jest-specific init\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 },\n\n tag(name: string | string[]): void {\n const names = Array.isArray(name) ? name : [name];\n attachDoc({ kind: \"tag\", names, phase: \"runtime\" });\n },\n\n kv(options: KvOptions): void {\n attachDoc({ kind: \"kv\", label: options.label, value: options.value, phase: \"runtime\" });\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 },\n\n code(options: CodeOptions): void {\n attachDoc({ kind: \"code\", label: options.label, content: options.content, lang: options.lang, phase: \"runtime\" });\n },\n\n table(options: TableOptions): void {\n attachDoc({ kind: \"table\", label: options.label, columns: options.columns, rows: options.rows, phase: \"runtime\" });\n },\n\n link(options: LinkOptions): void {\n attachDoc({ kind: \"link\", label: options.label, url: options.url, phase: \"runtime\" });\n },\n\n section(options: SectionOptions): void {\n attachDoc({ kind: \"section\", title: options.title, markdown: options.markdown, phase: \"runtime\" });\n },\n\n mermaid(options: MermaidOptions): void {\n attachDoc({ kind: \"mermaid\", code: options.code, title: options.title, phase: \"runtime\" });\n },\n\n screenshot(options: ScreenshotOptions): void {\n attachDoc({ kind: \"screenshot\", path: options.path, alt: options.alt, phase: \"runtime\" });\n },\n\n custom(options: CustomOptions): void {\n attachDoc({ kind: \"custom\", type: options.type, data: options.data, phase: \"runtime\" });\n },\n\n // Attachments\n attach(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 stepIndex: stepIndex !== undefined && stepIndex >= 0 ? stepIndex : undefined,\n stepId: ctx.currentStep?.id,\n });\n },\n\n // Step wrappers\n fn,\n expect: storyExpect,\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 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 },\n};\n\nexport type Story = typeof story;\n\n// ============================================================================\n// Internal exports for setup file\n// ============================================================================\n\n/**\n * Internal API for the setup file and tests. Not for public use.\n * @internal\n */\nexport const _internal = {\n flushStories,\n /** Clear active context (for tests that assert getContext() throws). */\n clearContext(): void {\n activeContext = null;\n },\n};\n","/**\n * Type definitions for executable-stories-jest.\n *\n * Shared story types are imported from executable-stories-formatters.\n * This module re-exports them and adds Jest-specific types.\n */\n\n// Re-export shared story types from formatters\nexport type {\n StepKeyword,\n StepMode,\n DocPhase,\n DocEntry,\n StoryStep,\n StoryMeta,\n} from 'executable-stories-formatters';\n\nexport { STORY_META_KEY } from 'executable-stories-formatters';\n\n// ============================================================================\n// Doc Options (for inline docs and standalone methods)\n// ============================================================================\n\n/** Options for kv() - key-value pair */\nexport interface KvOptions {\n label: string;\n value: unknown;\n}\n\n/** Options for json() - JSON code block */\nexport interface JsonOptions {\n label: string;\n value: unknown;\n}\n\n/** Options for code() - code block with optional language */\nexport interface CodeOptions {\n label: string;\n content: string;\n lang?: string;\n}\n\n/** Options for table() - markdown table */\nexport interface TableOptions {\n label: string;\n columns: string[];\n rows: string[][];\n}\n\n/** Options for link() - hyperlink */\nexport interface LinkOptions {\n label: string;\n url: string;\n}\n\n/** Options for section() - titled markdown section */\nexport interface SectionOptions {\n title: string;\n markdown: string;\n}\n\n/** Options for mermaid() - Mermaid diagram */\nexport interface MermaidOptions {\n code: string;\n title?: string;\n}\n\n/** Options for screenshot() - screenshot reference */\nexport interface ScreenshotOptions {\n path: string;\n alt?: string;\n}\n\n/** Options for custom() - custom doc entry */\nexport interface CustomOptions {\n type: string;\n data: unknown;\n}\n\n// ============================================================================\n// Inline Docs for Steps\n// ============================================================================\n\n/**\n * Inline documentation options for step markers.\n * Pass to story.given(), story.when(), story.then() as second argument.\n */\nexport interface StoryDocs {\n note?: string;\n tag?: string | string[];\n kv?: Record<string, unknown>;\n code?: CodeOptions;\n json?: JsonOptions;\n table?: TableOptions;\n link?: LinkOptions;\n section?: SectionOptions;\n mermaid?: MermaidOptions;\n screenshot?: ScreenshotOptions;\n custom?: CustomOptions;\n}\n\n// ============================================================================\n// Attachment Types\n// ============================================================================\n\n/** Options for attaching files or inline content to a test */\nexport interface AttachmentOptions {\n name: string;\n mediaType: string;\n path?: string;\n body?: string;\n encoding?: \"BASE64\" | \"IDENTITY\";\n charset?: string;\n fileName?: string;\n}\n\n/** Internal: attachment with step scope info */\nexport interface ScopedAttachment extends AttachmentOptions {\n stepIndex?: number;\n stepId?: string;\n}\n\n// ============================================================================\n// Story Options\n// ============================================================================\n\n/**\n * Options for configuring a story via story.init().\n */\nexport interface StoryOptions {\n tags?: string[];\n ticket?: string | string[];\n meta?: Record<string, unknown>;\n /** URL template for OTel trace links. Uses {traceId} placeholder. Also settable via OTEL_TRACE_URL_TEMPLATE env var. */\n traceUrlTemplate?: string;\n}\n"],"mappings":"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","names":["import_executable_stories_formatters"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/story-api.ts","../src/types.ts"],"sourcesContent":["/**\n * Jest Executable Stories\n *\n * BDD-style executable documentation for Jest.\n *\n * @example\n * ```ts\n * import { story } from 'executable-stories-jest';\n *\n * describe('Calculator', () => {\n * it('adds two numbers', () => {\n * story.init();\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\nexport type {\n StoryMeta,\n StoryStep,\n DocEntry,\n StepKeyword,\n StepMode,\n DocPhase,\n StoryDocs,\n StoryOptions,\n AttachmentOptions,\n KvOptions,\n JsonOptions,\n CodeOptions,\n TableOptions,\n LinkOptions,\n SectionOptions,\n MermaidOptions,\n ScreenshotOptions,\n CustomOptions,\n} from \"./types\";\n\nexport { STORY_META_KEY } from \"./types\";\n","/**\n * Jest story.* API for executable-stories.\n *\n * Uses native Jest describe/it/test with opt-in documentation:\n *\n * @example\n * ```ts\n * import { story } from 'executable-stories-jest';\n *\n * describe('Calculator', () => {\n * it('adds two numbers', () => {\n * story.init();\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 * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport { createHash } from \"node:crypto\";\nimport { createRequire } from 'node:module';\nimport { tryGetActiveOtelContext, resolveTraceUrl } from 'executable-stories-formatters';\nimport type {\n DocEntry,\n StepKeyword,\n StoryDocs,\n StoryMeta,\n StoryOptions,\n StoryStep,\n ScopedAttachment,\n AttachmentOptions,\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// Story Context\n// ============================================================================\n\n/** Internal timer entry */\ninterface TimerEntry {\n start: number;\n stepIndex?: number;\n stepId?: string;\n consumed: boolean;\n}\n\ninterface StoryContext {\n /** The story metadata being built */\n meta: StoryMeta;\n /** The current step (for attaching docs) */\n currentStep: StoryStep | null;\n /** Deterministic step counter (resets per test case) */\n stepCounter: number;\n /** Collected attachments with step scope */\n attachments: ScopedAttachment[];\n /** Active timers keyed by token */\n activeTimers: Map<number, TimerEntry>;\n /** Monotonic timer token counter */\n timerCounter: number;\n /** Test file path for registry lookups */\n testPath: string;\n /** Index into the storyRegistry array for this scenario */\n scenarioIndex: number;\n}\n\n// ============================================================================\n// File-based story collection (works across Jest worker processes)\n// ============================================================================\n\n// Use globalThis to ensure the registry is shared across module instances\n// This is needed because Jest may load the setup file and test files as separate module instances\ndeclare global {\n // eslint-disable-next-line no-var\n var __jestExecutableStoriesRegistry: Map<string, StoryMeta[]> | undefined;\n // eslint-disable-next-line no-var\n var __jestExecutableStoriesExitHandler: boolean | undefined;\n}\n\n/** Stories collected during test execution, keyed by test file path */\nconst storyRegistry: Map<string, StoryMeta[]> = globalThis.__jestExecutableStoriesRegistry ??= new Map();\n\n/** Attachments collected per story, keyed by test file path → scenario index → attachments */\nconst attachmentRegistry = new Map<string, Map<number, ScopedAttachment[]>>();\n\n/** OTel spans collected per story, keyed by test file path → scenario index → spans */\nconst otelSpansRegistry = new Map<string, Map<number, ReadonlyArray<Record<string, unknown>>>>();\n\n/** Track if we've registered the process exit handler */\nlet exitHandlerRegistered = globalThis.__jestExecutableStoriesExitHandler ?? false;\n\n/** Get the output directory for story JSON files */\nfunction getOutputDir(): string {\n const baseDir = process.env.JEST_STORY_DOCS_DIR ?? \".jest-executable-stories\";\n return path.resolve(process.cwd(), baseDir);\n}\n\n/** Flush all collected stories to JSON files */\nfunction flushStories(): void {\n if (storyRegistry.size === 0) return;\n\n const workerId = process.env.JEST_WORKER_ID ?? \"0\";\n const outputDir = path.join(getOutputDir(), `worker-${workerId}`);\n fs.mkdirSync(outputDir, { recursive: true });\n\n for (const [testFilePath, scenarios] of storyRegistry) {\n if (!scenarios.length) continue;\n const hash = createHash(\"sha1\").update(testFilePath).digest(\"hex\").slice(0, 12);\n const baseName = testFilePath === \"unknown\" ? \"unknown\" : path.basename(testFilePath);\n const outFile = path.join(outputDir, `${baseName}.${hash}.json`);\n\n // Include attachments and otelSpans per scenario (keyed by index, not name)\n const fileAttachments = attachmentRegistry.get(testFilePath);\n const fileOtelSpans = otelSpansRegistry.get(testFilePath);\n const scenariosWithAttachments = scenarios.map((s, i) => ({\n ...s,\n _attachments: fileAttachments?.get(i) ?? [],\n ...(fileOtelSpans?.get(i) ? { _otelSpans: fileOtelSpans.get(i) } : {}),\n }));\n\n const payload = { testFilePath, scenarios: scenariosWithAttachments };\n fs.writeFileSync(outFile, JSON.stringify(payload, null, 2) + \"\\n\", \"utf8\");\n }\n storyRegistry.clear();\n attachmentRegistry.clear();\n otelSpansRegistry.clear();\n}\n\n/** Register process exit handler to flush stories (once per worker) */\nfunction registerExitHandler(): void {\n if (exitHandlerRegistered) return;\n exitHandlerRegistered = true;\n globalThis.__jestExecutableStoriesExitHandler = true;\n // Use 'exit' event - always fired when Node.js is about to exit\n // Note: Only sync operations work here, which is fine for fs.writeFileSync\n process.on(\"exit\", () => {\n flushStories();\n });\n}\n\n// ============================================================================\n// Jest-specific context\n// ============================================================================\n\n/** Active story context - set by story.init() */\nlet activeContext: StoryContext | 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() must be called first. Use: it('name', () => { story.init(); ... });\"\n );\n }\n return activeContext;\n}\n\n// ============================================================================\n// Helper Functions\n// ============================================================================\n\n/**\n * Normalize ticket option to array format.\n */\nfunction normalizeTickets(ticket: string | string[] | undefined): string[] | undefined {\n if (!ticket) return undefined;\n return Array.isArray(ticket) ? ticket : [ticket];\n}\n\n/**\n * Extract the suite path from Jest's currentTestName.\n * Jest's currentTestName is formatted as: \"describe1 > describe2 > test name\"\n */\nfunction extractSuitePath(currentTestName: string): { suitePath?: string[]; testName: string } {\n const parts = currentTestName.split(\" > \");\n if (parts.length <= 1) {\n return { testName: currentTestName };\n }\n const testName = parts[parts.length - 1];\n const suitePath = parts.slice(0, -1);\n return { suitePath, testName };\n}\n\n/**\n * Convert StoryDocs inline options to DocEntry array.\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({ kind: \"code\", label: docs.code.label, content: docs.code.content, lang: docs.code.lang, phase: \"runtime\" });\n }\n if (docs.json) {\n entries.push({ kind: \"code\", label: docs.json.label, content: JSON.stringify(docs.json.value, null, 2), lang: \"json\", phase: \"runtime\" });\n }\n if (docs.table) {\n entries.push({ kind: \"table\", label: docs.table.label, columns: docs.table.columns, rows: docs.table.rows, phase: \"runtime\" });\n }\n if (docs.link) {\n entries.push({ kind: \"link\", label: docs.link.label, url: docs.link.url, phase: \"runtime\" });\n }\n if (docs.section) {\n entries.push({ kind: \"section\", title: docs.section.title, markdown: docs.section.markdown, phase: \"runtime\" });\n }\n if (docs.mermaid) {\n entries.push({ kind: \"mermaid\", code: docs.mermaid.code, title: docs.mermaid.title, phase: \"runtime\" });\n }\n if (docs.screenshot) {\n entries.push({ kind: \"screenshot\", path: docs.screenshot.path, alt: docs.screenshot.alt, phase: \"runtime\" });\n }\n if (docs.custom) {\n entries.push({ kind: \"custom\", type: docs.custom.type, data: docs.custom.data, phase: \"runtime\" });\n }\n\n return entries;\n}\n\n// ============================================================================\n// Helper to attach doc entry to current step or story-level\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// Step Markers\n// ============================================================================\n\nfunction createStepMarker(keyword: StepKeyword) {\n function stepMarker(text: string, docs?: StoryDocs): void;\n function stepMarker<T>(text: string, body: () => T): T;\n function stepMarker<T>(text: string, docsOrBody?: StoryDocs | (() => 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\n if (!isCallback) return;\n\n const body = docsOrBody as () => T;\n const start = performance.now();\n\n try {\n const result = body();\n if (result instanceof Promise) {\n return result.then(\n (val) => { step.durationMs = performance.now() - start; return val; },\n (err) => { step.durationMs = performance.now() - start; throw err; },\n ) as T;\n }\n step.durationMs = performance.now() - start;\n return result;\n } catch (err) {\n step.durationMs = performance.now() - start;\n throw err;\n }\n }\n return stepMarker;\n}\n\n// ============================================================================\n// story.init() - Jest-specific\n// ============================================================================\n\n/**\n * Initialize a story for the current test.\n * Must be called at the start of each test that wants documentation.\n *\n * @param options - Optional story configuration (tags, ticket, meta)\n *\n * @example\n * ```ts\n * it('adds two numbers', () => {\n * story.init();\n * // ... rest of test\n * });\n * ```\n */\nfunction init(options?: StoryOptions): void {\n // Get current test info from Jest globals\n const state = expect.getState();\n const currentTestName = state.currentTestName || \"Unknown test\";\n const testPath = state.testPath || \"unknown\";\n\n const { suitePath, testName } = extractSuitePath(currentTestName);\n\n const meta: StoryMeta = {\n scenario: testName,\n steps: [],\n suitePath,\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', testName);\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 // Store in registry for this file\n const existing = storyRegistry.get(testPath);\n let scenarioIndex: number;\n if (existing) {\n scenarioIndex = existing.length;\n existing.push(meta);\n } else {\n scenarioIndex = 0;\n storyRegistry.set(testPath, [meta]);\n }\n\n // Register exit handler to flush stories when worker exits\n registerExitHandler();\n\n // Set active context\n activeContext = {\n meta,\n currentStep: null,\n stepCounter: 0,\n attachments: [],\n activeTimers: new Map(),\n timerCounter: 0,\n testPath,\n scenarioIndex,\n };\n\n // Link attachments to the registry for this test file + scenario index\n if (!attachmentRegistry.has(testPath)) {\n attachmentRegistry.set(testPath, new Map());\n }\n attachmentRegistry.get(testPath)!.set(scenarioIndex, activeContext.attachments);\n}\n\n// ============================================================================\n// Step Wrappers: story.fn() and story.expect()\n// ============================================================================\n\n/**\n * Wrap a function body as a step. Records the step with timing and `wrapped: true`.\n * Supports both sync and async functions. Returns whatever the function returns.\n *\n * @param keyword - The BDD keyword (Given, When, Then, And, But)\n * @param text - Step description\n * @param body - The function to execute\n * @returns The return value of body (or a Promise of it if body is async)\n *\n * @example\n * ```ts\n * const data = story.fn('Given', 'setup data', () => ({ a: 5, b: 3 }));\n * const result = await story.fn('When', 'call API', async () => fetch('/api'));\n * ```\n */\nfunction fn<T>(keyword: StepKeyword, text: string, body: () => 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\n const step: StoryStep = {\n id: `step-${ctx.stepCounter++}`,\n keyword: resolvedKeyword,\n text,\n docs: [],\n wrapped: true,\n };\n\n ctx.meta.steps.push(step);\n ctx.currentStep = step;\n\n const start = performance.now();\n\n try {\n const result = body();\n\n // Handle async functions\n if (result instanceof Promise) {\n return result.then(\n (val) => {\n step.durationMs = performance.now() - start;\n return val;\n },\n (err) => {\n step.durationMs = performance.now() - start;\n throw err;\n },\n ) as T;\n }\n\n step.durationMs = performance.now() - start;\n return result;\n } catch (err) {\n step.durationMs = performance.now() - start;\n throw err;\n }\n}\n\n/**\n * Wrap an assertion as a Then step. Shorthand for `story.fn('Then', text, body)`.\n *\n * @param text - Step description\n * @param body - The assertion function to execute\n *\n * @example\n * ```ts\n * story.expect('the result is 8', () => { expect(result).toBe(8); });\n * ```\n */\nfunction storyExpect<T>(text: string, body: () => T): T {\n return fn('Then', text, body);\n}\n\n// ============================================================================\n// Export story object\n// ============================================================================\n\n/**\n * The main story API object for Jest.\n *\n * @example\n * ```ts\n * import { story } from 'executable-stories-jest';\n *\n * describe('Calculator', () => {\n * it('adds two numbers', () => {\n * story.init();\n *\n * story.given('two numbers 5 and 3');\n * const a = 5, 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 */\nexport const story = {\n // Jest-specific init\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 },\n\n tag(name: string | string[]): void {\n const names = Array.isArray(name) ? name : [name];\n attachDoc({ kind: \"tag\", names, phase: \"runtime\" });\n },\n\n kv(options: KvOptions): void {\n attachDoc({ kind: \"kv\", label: options.label, value: options.value, phase: \"runtime\" });\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 },\n\n code(options: CodeOptions): void {\n attachDoc({ kind: \"code\", label: options.label, content: options.content, lang: options.lang, phase: \"runtime\" });\n },\n\n table(options: TableOptions): void {\n attachDoc({ kind: \"table\", label: options.label, columns: options.columns, rows: options.rows, phase: \"runtime\" });\n },\n\n link(options: LinkOptions): void {\n attachDoc({ kind: \"link\", label: options.label, url: options.url, phase: \"runtime\" });\n },\n\n section(options: SectionOptions): void {\n attachDoc({ kind: \"section\", title: options.title, markdown: options.markdown, phase: \"runtime\" });\n },\n\n mermaid(options: MermaidOptions): void {\n attachDoc({ kind: \"mermaid\", code: options.code, title: options.title, phase: \"runtime\" });\n },\n\n screenshot(options: ScreenshotOptions): void {\n attachDoc({ kind: \"screenshot\", path: options.path, alt: options.alt, phase: \"runtime\" });\n },\n\n custom(options: CustomOptions): void {\n attachDoc({ kind: \"custom\", type: options.type, data: options.data, phase: \"runtime\" });\n },\n\n // Attachments\n attach(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 stepIndex: stepIndex !== undefined && stepIndex >= 0 ? stepIndex : undefined,\n stepId: ctx.currentStep?.id,\n });\n },\n\n // OTel span attachment\n attachSpans(spans: ReadonlyArray<Record<string, unknown>>): void {\n const ctx = getContext();\n if (!otelSpansRegistry.has(ctx.testPath)) {\n otelSpansRegistry.set(ctx.testPath, new Map());\n }\n otelSpansRegistry.get(ctx.testPath)!.set(ctx.scenarioIndex, spans);\n },\n\n // Step wrappers\n fn,\n expect: storyExpect,\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 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 },\n};\n\nexport type Story = typeof story;\n\n// ============================================================================\n// Internal exports for setup file\n// ============================================================================\n\n/**\n * Internal API for the setup file and tests. Not for public use.\n * @internal\n */\nexport const _internal = {\n flushStories,\n /** Clear active context (for tests that assert getContext() throws). */\n clearContext(): void {\n activeContext = null;\n },\n};\n","/**\n * Type definitions for executable-stories-jest.\n *\n * Shared story types are imported from executable-stories-formatters.\n * This module re-exports them and adds Jest-specific types.\n */\n\n// Re-export shared story types from formatters\nexport type {\n StepKeyword,\n StepMode,\n DocPhase,\n DocEntry,\n StoryStep,\n StoryMeta,\n} from 'executable-stories-formatters';\n\nexport { STORY_META_KEY } from 'executable-stories-formatters';\n\n// ============================================================================\n// Doc Options (for inline docs and standalone methods)\n// ============================================================================\n\n/** Options for kv() - key-value pair */\nexport interface KvOptions {\n label: string;\n value: unknown;\n}\n\n/** Options for json() - JSON code block */\nexport interface JsonOptions {\n label: string;\n value: unknown;\n}\n\n/** Options for code() - code block with optional language */\nexport interface CodeOptions {\n label: string;\n content: string;\n lang?: string;\n}\n\n/** Options for table() - markdown table */\nexport interface TableOptions {\n label: string;\n columns: string[];\n rows: string[][];\n}\n\n/** Options for link() - hyperlink */\nexport interface LinkOptions {\n label: string;\n url: string;\n}\n\n/** Options for section() - titled markdown section */\nexport interface SectionOptions {\n title: string;\n markdown: string;\n}\n\n/** Options for mermaid() - Mermaid diagram */\nexport interface MermaidOptions {\n code: string;\n title?: string;\n}\n\n/** Options for screenshot() - screenshot reference */\nexport interface ScreenshotOptions {\n path: string;\n alt?: string;\n}\n\n/** Options for custom() - custom doc entry */\nexport interface CustomOptions {\n type: string;\n data: unknown;\n}\n\n// ============================================================================\n// Inline Docs for Steps\n// ============================================================================\n\n/**\n * Inline documentation options for step markers.\n * Pass to story.given(), story.when(), story.then() as second argument.\n */\nexport interface StoryDocs {\n note?: string;\n tag?: string | string[];\n kv?: Record<string, unknown>;\n code?: CodeOptions;\n json?: JsonOptions;\n table?: TableOptions;\n link?: LinkOptions;\n section?: SectionOptions;\n mermaid?: MermaidOptions;\n screenshot?: ScreenshotOptions;\n custom?: CustomOptions;\n}\n\n// ============================================================================\n// Attachment Types\n// ============================================================================\n\n/** Options for attaching files or inline content to a test */\nexport interface AttachmentOptions {\n name: string;\n mediaType: string;\n path?: string;\n body?: string;\n encoding?: \"BASE64\" | \"IDENTITY\";\n charset?: string;\n fileName?: string;\n}\n\n/** Internal: attachment with step scope info */\nexport interface ScopedAttachment extends AttachmentOptions {\n stepIndex?: number;\n stepId?: string;\n}\n\n// ============================================================================\n// Story Options\n// ============================================================================\n\n/**\n * Options for configuring a story via story.init().\n */\nexport interface StoryOptions {\n tags?: string[];\n ticket?: string | string[];\n meta?: Record<string, unknown>;\n /** URL template for OTel trace links. Uses {traceId} placeholder. Also settable via OTEL_TRACE_URL_TEMPLATE env var. */\n traceUrlTemplate?: string;\n}\n"],"mappings":"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","names":["import_executable_stories_formatters"]}
package/dist/index.d.cts CHANGED
@@ -256,6 +256,7 @@ declare const story: {
256
256
  screenshot(options: ScreenshotOptions): void;
257
257
  custom(options: CustomOptions): void;
258
258
  attach(options: AttachmentOptions): void;
259
+ attachSpans(spans: ReadonlyArray<Record<string, unknown>>): void;
259
260
  fn: typeof fn;
260
261
  expect: typeof storyExpect;
261
262
  startTimer(): number;
package/dist/index.d.ts CHANGED
@@ -256,6 +256,7 @@ declare const story: {
256
256
  screenshot(options: ScreenshotOptions): void;
257
257
  custom(options: CustomOptions): void;
258
258
  attach(options: AttachmentOptions): void;
259
+ attachSpans(spans: ReadonlyArray<Record<string, unknown>>): void;
259
260
  fn: typeof fn;
260
261
  expect: typeof storyExpect;
261
262
  startTimer(): number;
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ import { createRequire } from "module";
6
6
  import { tryGetActiveOtelContext, resolveTraceUrl } from "executable-stories-formatters";
7
7
  var storyRegistry = globalThis.__jestExecutableStoriesRegistry ??= /* @__PURE__ */ new Map();
8
8
  var attachmentRegistry = /* @__PURE__ */ new Map();
9
+ var otelSpansRegistry = /* @__PURE__ */ new Map();
9
10
  var exitHandlerRegistered = globalThis.__jestExecutableStoriesExitHandler ?? false;
10
11
  function getOutputDir() {
11
12
  const baseDir = process.env.JEST_STORY_DOCS_DIR ?? ".jest-executable-stories";
@@ -22,15 +23,18 @@ function flushStories() {
22
23
  const baseName = testFilePath === "unknown" ? "unknown" : path.basename(testFilePath);
23
24
  const outFile = path.join(outputDir, `${baseName}.${hash}.json`);
24
25
  const fileAttachments = attachmentRegistry.get(testFilePath);
25
- const scenariosWithAttachments = scenarios.map((s) => ({
26
+ const fileOtelSpans = otelSpansRegistry.get(testFilePath);
27
+ const scenariosWithAttachments = scenarios.map((s, i) => ({
26
28
  ...s,
27
- _attachments: fileAttachments?.get(s.scenario) ?? []
29
+ _attachments: fileAttachments?.get(i) ?? [],
30
+ ...fileOtelSpans?.get(i) ? { _otelSpans: fileOtelSpans.get(i) } : {}
28
31
  }));
29
32
  const payload = { testFilePath, scenarios: scenariosWithAttachments };
30
33
  fs.writeFileSync(outFile, JSON.stringify(payload, null, 2) + "\n", "utf8");
31
34
  }
32
35
  storyRegistry.clear();
33
36
  attachmentRegistry.clear();
37
+ otelSpansRegistry.clear();
34
38
  }
35
39
  function registerExitHandler() {
36
40
  if (exitHandlerRegistered) return;
@@ -194,9 +198,12 @@ function init(options) {
194
198
  }
195
199
  }
196
200
  const existing = storyRegistry.get(testPath);
201
+ let scenarioIndex;
197
202
  if (existing) {
203
+ scenarioIndex = existing.length;
198
204
  existing.push(meta);
199
205
  } else {
206
+ scenarioIndex = 0;
200
207
  storyRegistry.set(testPath, [meta]);
201
208
  }
202
209
  registerExitHandler();
@@ -206,12 +213,14 @@ function init(options) {
206
213
  stepCounter: 0,
207
214
  attachments: [],
208
215
  activeTimers: /* @__PURE__ */ new Map(),
209
- timerCounter: 0
216
+ timerCounter: 0,
217
+ testPath,
218
+ scenarioIndex
210
219
  };
211
220
  if (!attachmentRegistry.has(testPath)) {
212
221
  attachmentRegistry.set(testPath, /* @__PURE__ */ new Map());
213
222
  }
214
- attachmentRegistry.get(testPath).set(meta.scenario, activeContext.attachments);
223
+ attachmentRegistry.get(testPath).set(scenarioIndex, activeContext.attachments);
215
224
  }
216
225
  function fn(keyword, text, body) {
217
226
  const ctx = getContext();
@@ -315,6 +324,14 @@ var story = {
315
324
  stepId: ctx.currentStep?.id
316
325
  });
317
326
  },
327
+ // OTel span attachment
328
+ attachSpans(spans) {
329
+ const ctx = getContext();
330
+ if (!otelSpansRegistry.has(ctx.testPath)) {
331
+ otelSpansRegistry.set(ctx.testPath, /* @__PURE__ */ new Map());
332
+ }
333
+ otelSpansRegistry.get(ctx.testPath).set(ctx.scenarioIndex, spans);
334
+ },
318
335
  // Step wrappers
319
336
  fn,
320
337
  expect: storyExpect,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/story-api.ts","../src/types.ts","../src/index.ts"],"sourcesContent":["/**\n * Jest story.* API for executable-stories.\n *\n * Uses native Jest describe/it/test with opt-in documentation:\n *\n * @example\n * ```ts\n * import { story } from 'executable-stories-jest';\n *\n * describe('Calculator', () => {\n * it('adds two numbers', () => {\n * story.init();\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 * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport { createHash } from \"node:crypto\";\nimport { createRequire } from 'node:module';\nimport { tryGetActiveOtelContext, resolveTraceUrl } from 'executable-stories-formatters';\nimport type {\n DocEntry,\n StepKeyword,\n StoryDocs,\n StoryMeta,\n StoryOptions,\n StoryStep,\n ScopedAttachment,\n AttachmentOptions,\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// Story Context\n// ============================================================================\n\n/** Internal timer entry */\ninterface TimerEntry {\n start: number;\n stepIndex?: number;\n stepId?: string;\n consumed: boolean;\n}\n\ninterface StoryContext {\n /** The story metadata being built */\n meta: StoryMeta;\n /** The current step (for attaching docs) */\n currentStep: StoryStep | null;\n /** Deterministic step counter (resets per test case) */\n stepCounter: number;\n /** Collected attachments with step scope */\n attachments: ScopedAttachment[];\n /** Active timers keyed by token */\n activeTimers: Map<number, TimerEntry>;\n /** Monotonic timer token counter */\n timerCounter: number;\n}\n\n// ============================================================================\n// File-based story collection (works across Jest worker processes)\n// ============================================================================\n\n// Use globalThis to ensure the registry is shared across module instances\n// This is needed because Jest may load the setup file and test files as separate module instances\ndeclare global {\n // eslint-disable-next-line no-var\n var __jestExecutableStoriesRegistry: Map<string, StoryMeta[]> | undefined;\n // eslint-disable-next-line no-var\n var __jestExecutableStoriesExitHandler: boolean | undefined;\n}\n\n/** Stories collected during test execution, keyed by test file path */\nconst storyRegistry: Map<string, StoryMeta[]> = globalThis.__jestExecutableStoriesRegistry ??= new Map();\n\n/** Attachments collected per story, keyed by test file path → scenario name → attachments */\nconst attachmentRegistry = new Map<string, Map<string, ScopedAttachment[]>>();\n\n/** Track if we've registered the process exit handler */\nlet exitHandlerRegistered = globalThis.__jestExecutableStoriesExitHandler ?? false;\n\n/** Get the output directory for story JSON files */\nfunction getOutputDir(): string {\n const baseDir = process.env.JEST_STORY_DOCS_DIR ?? \".jest-executable-stories\";\n return path.resolve(process.cwd(), baseDir);\n}\n\n/** Flush all collected stories to JSON files */\nfunction flushStories(): void {\n if (storyRegistry.size === 0) return;\n\n const workerId = process.env.JEST_WORKER_ID ?? \"0\";\n const outputDir = path.join(getOutputDir(), `worker-${workerId}`);\n fs.mkdirSync(outputDir, { recursive: true });\n\n for (const [testFilePath, scenarios] of storyRegistry) {\n if (!scenarios.length) continue;\n const hash = createHash(\"sha1\").update(testFilePath).digest(\"hex\").slice(0, 12);\n const baseName = testFilePath === \"unknown\" ? \"unknown\" : path.basename(testFilePath);\n const outFile = path.join(outputDir, `${baseName}.${hash}.json`);\n\n // Include attachments per scenario\n const fileAttachments = attachmentRegistry.get(testFilePath);\n const scenariosWithAttachments = scenarios.map((s) => ({\n ...s,\n _attachments: fileAttachments?.get(s.scenario) ?? [],\n }));\n\n const payload = { testFilePath, scenarios: scenariosWithAttachments };\n fs.writeFileSync(outFile, JSON.stringify(payload, null, 2) + \"\\n\", \"utf8\");\n }\n storyRegistry.clear();\n attachmentRegistry.clear();\n}\n\n/** Register process exit handler to flush stories (once per worker) */\nfunction registerExitHandler(): void {\n if (exitHandlerRegistered) return;\n exitHandlerRegistered = true;\n globalThis.__jestExecutableStoriesExitHandler = true;\n // Use 'exit' event - always fired when Node.js is about to exit\n // Note: Only sync operations work here, which is fine for fs.writeFileSync\n process.on(\"exit\", () => {\n flushStories();\n });\n}\n\n// ============================================================================\n// Jest-specific context\n// ============================================================================\n\n/** Active story context - set by story.init() */\nlet activeContext: StoryContext | 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() must be called first. Use: it('name', () => { story.init(); ... });\"\n );\n }\n return activeContext;\n}\n\n// ============================================================================\n// Helper Functions\n// ============================================================================\n\n/**\n * Normalize ticket option to array format.\n */\nfunction normalizeTickets(ticket: string | string[] | undefined): string[] | undefined {\n if (!ticket) return undefined;\n return Array.isArray(ticket) ? ticket : [ticket];\n}\n\n/**\n * Extract the suite path from Jest's currentTestName.\n * Jest's currentTestName is formatted as: \"describe1 > describe2 > test name\"\n */\nfunction extractSuitePath(currentTestName: string): { suitePath?: string[]; testName: string } {\n const parts = currentTestName.split(\" > \");\n if (parts.length <= 1) {\n return { testName: currentTestName };\n }\n const testName = parts[parts.length - 1];\n const suitePath = parts.slice(0, -1);\n return { suitePath, testName };\n}\n\n/**\n * Convert StoryDocs inline options to DocEntry array.\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({ kind: \"code\", label: docs.code.label, content: docs.code.content, lang: docs.code.lang, phase: \"runtime\" });\n }\n if (docs.json) {\n entries.push({ kind: \"code\", label: docs.json.label, content: JSON.stringify(docs.json.value, null, 2), lang: \"json\", phase: \"runtime\" });\n }\n if (docs.table) {\n entries.push({ kind: \"table\", label: docs.table.label, columns: docs.table.columns, rows: docs.table.rows, phase: \"runtime\" });\n }\n if (docs.link) {\n entries.push({ kind: \"link\", label: docs.link.label, url: docs.link.url, phase: \"runtime\" });\n }\n if (docs.section) {\n entries.push({ kind: \"section\", title: docs.section.title, markdown: docs.section.markdown, phase: \"runtime\" });\n }\n if (docs.mermaid) {\n entries.push({ kind: \"mermaid\", code: docs.mermaid.code, title: docs.mermaid.title, phase: \"runtime\" });\n }\n if (docs.screenshot) {\n entries.push({ kind: \"screenshot\", path: docs.screenshot.path, alt: docs.screenshot.alt, phase: \"runtime\" });\n }\n if (docs.custom) {\n entries.push({ kind: \"custom\", type: docs.custom.type, data: docs.custom.data, phase: \"runtime\" });\n }\n\n return entries;\n}\n\n// ============================================================================\n// Helper to attach doc entry to current step or story-level\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// Step Markers\n// ============================================================================\n\nfunction createStepMarker(keyword: StepKeyword) {\n function stepMarker(text: string, docs?: StoryDocs): void;\n function stepMarker<T>(text: string, body: () => T): T;\n function stepMarker<T>(text: string, docsOrBody?: StoryDocs | (() => 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\n if (!isCallback) return;\n\n const body = docsOrBody as () => T;\n const start = performance.now();\n\n try {\n const result = body();\n if (result instanceof Promise) {\n return result.then(\n (val) => { step.durationMs = performance.now() - start; return val; },\n (err) => { step.durationMs = performance.now() - start; throw err; },\n ) as T;\n }\n step.durationMs = performance.now() - start;\n return result;\n } catch (err) {\n step.durationMs = performance.now() - start;\n throw err;\n }\n }\n return stepMarker;\n}\n\n// ============================================================================\n// story.init() - Jest-specific\n// ============================================================================\n\n/**\n * Initialize a story for the current test.\n * Must be called at the start of each test that wants documentation.\n *\n * @param options - Optional story configuration (tags, ticket, meta)\n *\n * @example\n * ```ts\n * it('adds two numbers', () => {\n * story.init();\n * // ... rest of test\n * });\n * ```\n */\nfunction init(options?: StoryOptions): void {\n // Get current test info from Jest globals\n const state = expect.getState();\n const currentTestName = state.currentTestName || \"Unknown test\";\n const testPath = state.testPath || \"unknown\";\n\n const { suitePath, testName } = extractSuitePath(currentTestName);\n\n const meta: StoryMeta = {\n scenario: testName,\n steps: [],\n suitePath,\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', testName);\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 // Store in registry for this file\n const existing = storyRegistry.get(testPath);\n if (existing) {\n existing.push(meta);\n } else {\n storyRegistry.set(testPath, [meta]);\n }\n\n // Register exit handler to flush stories when worker exits\n registerExitHandler();\n\n // Set active context\n activeContext = {\n meta,\n currentStep: null,\n stepCounter: 0,\n attachments: [],\n activeTimers: new Map(),\n timerCounter: 0,\n };\n\n // Link attachments to the registry for this test file + scenario\n if (!attachmentRegistry.has(testPath)) {\n attachmentRegistry.set(testPath, new Map());\n }\n attachmentRegistry.get(testPath)!.set(meta.scenario, activeContext.attachments);\n}\n\n// ============================================================================\n// Step Wrappers: story.fn() and story.expect()\n// ============================================================================\n\n/**\n * Wrap a function body as a step. Records the step with timing and `wrapped: true`.\n * Supports both sync and async functions. Returns whatever the function returns.\n *\n * @param keyword - The BDD keyword (Given, When, Then, And, But)\n * @param text - Step description\n * @param body - The function to execute\n * @returns The return value of body (or a Promise of it if body is async)\n *\n * @example\n * ```ts\n * const data = story.fn('Given', 'setup data', () => ({ a: 5, b: 3 }));\n * const result = await story.fn('When', 'call API', async () => fetch('/api'));\n * ```\n */\nfunction fn<T>(keyword: StepKeyword, text: string, body: () => 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\n const step: StoryStep = {\n id: `step-${ctx.stepCounter++}`,\n keyword: resolvedKeyword,\n text,\n docs: [],\n wrapped: true,\n };\n\n ctx.meta.steps.push(step);\n ctx.currentStep = step;\n\n const start = performance.now();\n\n try {\n const result = body();\n\n // Handle async functions\n if (result instanceof Promise) {\n return result.then(\n (val) => {\n step.durationMs = performance.now() - start;\n return val;\n },\n (err) => {\n step.durationMs = performance.now() - start;\n throw err;\n },\n ) as T;\n }\n\n step.durationMs = performance.now() - start;\n return result;\n } catch (err) {\n step.durationMs = performance.now() - start;\n throw err;\n }\n}\n\n/**\n * Wrap an assertion as a Then step. Shorthand for `story.fn('Then', text, body)`.\n *\n * @param text - Step description\n * @param body - The assertion function to execute\n *\n * @example\n * ```ts\n * story.expect('the result is 8', () => { expect(result).toBe(8); });\n * ```\n */\nfunction storyExpect<T>(text: string, body: () => T): T {\n return fn('Then', text, body);\n}\n\n// ============================================================================\n// Export story object\n// ============================================================================\n\n/**\n * The main story API object for Jest.\n *\n * @example\n * ```ts\n * import { story } from 'executable-stories-jest';\n *\n * describe('Calculator', () => {\n * it('adds two numbers', () => {\n * story.init();\n *\n * story.given('two numbers 5 and 3');\n * const a = 5, 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 */\nexport const story = {\n // Jest-specific init\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 },\n\n tag(name: string | string[]): void {\n const names = Array.isArray(name) ? name : [name];\n attachDoc({ kind: \"tag\", names, phase: \"runtime\" });\n },\n\n kv(options: KvOptions): void {\n attachDoc({ kind: \"kv\", label: options.label, value: options.value, phase: \"runtime\" });\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 },\n\n code(options: CodeOptions): void {\n attachDoc({ kind: \"code\", label: options.label, content: options.content, lang: options.lang, phase: \"runtime\" });\n },\n\n table(options: TableOptions): void {\n attachDoc({ kind: \"table\", label: options.label, columns: options.columns, rows: options.rows, phase: \"runtime\" });\n },\n\n link(options: LinkOptions): void {\n attachDoc({ kind: \"link\", label: options.label, url: options.url, phase: \"runtime\" });\n },\n\n section(options: SectionOptions): void {\n attachDoc({ kind: \"section\", title: options.title, markdown: options.markdown, phase: \"runtime\" });\n },\n\n mermaid(options: MermaidOptions): void {\n attachDoc({ kind: \"mermaid\", code: options.code, title: options.title, phase: \"runtime\" });\n },\n\n screenshot(options: ScreenshotOptions): void {\n attachDoc({ kind: \"screenshot\", path: options.path, alt: options.alt, phase: \"runtime\" });\n },\n\n custom(options: CustomOptions): void {\n attachDoc({ kind: \"custom\", type: options.type, data: options.data, phase: \"runtime\" });\n },\n\n // Attachments\n attach(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 stepIndex: stepIndex !== undefined && stepIndex >= 0 ? stepIndex : undefined,\n stepId: ctx.currentStep?.id,\n });\n },\n\n // Step wrappers\n fn,\n expect: storyExpect,\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 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 },\n};\n\nexport type Story = typeof story;\n\n// ============================================================================\n// Internal exports for setup file\n// ============================================================================\n\n/**\n * Internal API for the setup file and tests. Not for public use.\n * @internal\n */\nexport const _internal = {\n flushStories,\n /** Clear active context (for tests that assert getContext() throws). */\n clearContext(): void {\n activeContext = null;\n },\n};\n","/**\n * Type definitions for executable-stories-jest.\n *\n * Shared story types are imported from executable-stories-formatters.\n * This module re-exports them and adds Jest-specific types.\n */\n\n// Re-export shared story types from formatters\nexport type {\n StepKeyword,\n StepMode,\n DocPhase,\n DocEntry,\n StoryStep,\n StoryMeta,\n} from 'executable-stories-formatters';\n\nexport { STORY_META_KEY } from 'executable-stories-formatters';\n\n// ============================================================================\n// Doc Options (for inline docs and standalone methods)\n// ============================================================================\n\n/** Options for kv() - key-value pair */\nexport interface KvOptions {\n label: string;\n value: unknown;\n}\n\n/** Options for json() - JSON code block */\nexport interface JsonOptions {\n label: string;\n value: unknown;\n}\n\n/** Options for code() - code block with optional language */\nexport interface CodeOptions {\n label: string;\n content: string;\n lang?: string;\n}\n\n/** Options for table() - markdown table */\nexport interface TableOptions {\n label: string;\n columns: string[];\n rows: string[][];\n}\n\n/** Options for link() - hyperlink */\nexport interface LinkOptions {\n label: string;\n url: string;\n}\n\n/** Options for section() - titled markdown section */\nexport interface SectionOptions {\n title: string;\n markdown: string;\n}\n\n/** Options for mermaid() - Mermaid diagram */\nexport interface MermaidOptions {\n code: string;\n title?: string;\n}\n\n/** Options for screenshot() - screenshot reference */\nexport interface ScreenshotOptions {\n path: string;\n alt?: string;\n}\n\n/** Options for custom() - custom doc entry */\nexport interface CustomOptions {\n type: string;\n data: unknown;\n}\n\n// ============================================================================\n// Inline Docs for Steps\n// ============================================================================\n\n/**\n * Inline documentation options for step markers.\n * Pass to story.given(), story.when(), story.then() as second argument.\n */\nexport interface StoryDocs {\n note?: string;\n tag?: string | string[];\n kv?: Record<string, unknown>;\n code?: CodeOptions;\n json?: JsonOptions;\n table?: TableOptions;\n link?: LinkOptions;\n section?: SectionOptions;\n mermaid?: MermaidOptions;\n screenshot?: ScreenshotOptions;\n custom?: CustomOptions;\n}\n\n// ============================================================================\n// Attachment Types\n// ============================================================================\n\n/** Options for attaching files or inline content to a test */\nexport interface AttachmentOptions {\n name: string;\n mediaType: string;\n path?: string;\n body?: string;\n encoding?: \"BASE64\" | \"IDENTITY\";\n charset?: string;\n fileName?: string;\n}\n\n/** Internal: attachment with step scope info */\nexport interface ScopedAttachment extends AttachmentOptions {\n stepIndex?: number;\n stepId?: string;\n}\n\n// ============================================================================\n// Story Options\n// ============================================================================\n\n/**\n * Options for configuring a story via story.init().\n */\nexport interface StoryOptions {\n tags?: string[];\n ticket?: string | string[];\n meta?: Record<string, unknown>;\n /** URL template for OTel trace links. Uses {traceId} placeholder. Also settable via OTEL_TRACE_URL_TEMPLATE env var. */\n traceUrlTemplate?: string;\n}\n","/**\n * Jest Executable Stories\n *\n * BDD-style executable documentation for Jest.\n *\n * @example\n * ```ts\n * import { story } from 'executable-stories-jest';\n *\n * describe('Calculator', () => {\n * it('adds two numbers', () => {\n * story.init();\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\nexport type {\n StoryMeta,\n StoryStep,\n DocEntry,\n StepKeyword,\n StepMode,\n DocPhase,\n StoryDocs,\n StoryOptions,\n AttachmentOptions,\n KvOptions,\n JsonOptions,\n CodeOptions,\n TableOptions,\n LinkOptions,\n SectionOptions,\n MermaidOptions,\n ScreenshotOptions,\n CustomOptions,\n} from \"./types\";\n\nexport { STORY_META_KEY } from \"./types\";\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/story-api.ts","../src/types.ts","../src/index.ts"],"sourcesContent":["/**\n * Jest story.* API for executable-stories.\n *\n * Uses native Jest describe/it/test with opt-in documentation:\n *\n * @example\n * ```ts\n * import { story } from 'executable-stories-jest';\n *\n * describe('Calculator', () => {\n * it('adds two numbers', () => {\n * story.init();\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 * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport { createHash } from \"node:crypto\";\nimport { createRequire } from 'node:module';\nimport { tryGetActiveOtelContext, resolveTraceUrl } from 'executable-stories-formatters';\nimport type {\n DocEntry,\n StepKeyword,\n StoryDocs,\n StoryMeta,\n StoryOptions,\n StoryStep,\n ScopedAttachment,\n AttachmentOptions,\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// Story Context\n// ============================================================================\n\n/** Internal timer entry */\ninterface TimerEntry {\n start: number;\n stepIndex?: number;\n stepId?: string;\n consumed: boolean;\n}\n\ninterface StoryContext {\n /** The story metadata being built */\n meta: StoryMeta;\n /** The current step (for attaching docs) */\n currentStep: StoryStep | null;\n /** Deterministic step counter (resets per test case) */\n stepCounter: number;\n /** Collected attachments with step scope */\n attachments: ScopedAttachment[];\n /** Active timers keyed by token */\n activeTimers: Map<number, TimerEntry>;\n /** Monotonic timer token counter */\n timerCounter: number;\n /** Test file path for registry lookups */\n testPath: string;\n /** Index into the storyRegistry array for this scenario */\n scenarioIndex: number;\n}\n\n// ============================================================================\n// File-based story collection (works across Jest worker processes)\n// ============================================================================\n\n// Use globalThis to ensure the registry is shared across module instances\n// This is needed because Jest may load the setup file and test files as separate module instances\ndeclare global {\n // eslint-disable-next-line no-var\n var __jestExecutableStoriesRegistry: Map<string, StoryMeta[]> | undefined;\n // eslint-disable-next-line no-var\n var __jestExecutableStoriesExitHandler: boolean | undefined;\n}\n\n/** Stories collected during test execution, keyed by test file path */\nconst storyRegistry: Map<string, StoryMeta[]> = globalThis.__jestExecutableStoriesRegistry ??= new Map();\n\n/** Attachments collected per story, keyed by test file path → scenario index → attachments */\nconst attachmentRegistry = new Map<string, Map<number, ScopedAttachment[]>>();\n\n/** OTel spans collected per story, keyed by test file path → scenario index → spans */\nconst otelSpansRegistry = new Map<string, Map<number, ReadonlyArray<Record<string, unknown>>>>();\n\n/** Track if we've registered the process exit handler */\nlet exitHandlerRegistered = globalThis.__jestExecutableStoriesExitHandler ?? false;\n\n/** Get the output directory for story JSON files */\nfunction getOutputDir(): string {\n const baseDir = process.env.JEST_STORY_DOCS_DIR ?? \".jest-executable-stories\";\n return path.resolve(process.cwd(), baseDir);\n}\n\n/** Flush all collected stories to JSON files */\nfunction flushStories(): void {\n if (storyRegistry.size === 0) return;\n\n const workerId = process.env.JEST_WORKER_ID ?? \"0\";\n const outputDir = path.join(getOutputDir(), `worker-${workerId}`);\n fs.mkdirSync(outputDir, { recursive: true });\n\n for (const [testFilePath, scenarios] of storyRegistry) {\n if (!scenarios.length) continue;\n const hash = createHash(\"sha1\").update(testFilePath).digest(\"hex\").slice(0, 12);\n const baseName = testFilePath === \"unknown\" ? \"unknown\" : path.basename(testFilePath);\n const outFile = path.join(outputDir, `${baseName}.${hash}.json`);\n\n // Include attachments and otelSpans per scenario (keyed by index, not name)\n const fileAttachments = attachmentRegistry.get(testFilePath);\n const fileOtelSpans = otelSpansRegistry.get(testFilePath);\n const scenariosWithAttachments = scenarios.map((s, i) => ({\n ...s,\n _attachments: fileAttachments?.get(i) ?? [],\n ...(fileOtelSpans?.get(i) ? { _otelSpans: fileOtelSpans.get(i) } : {}),\n }));\n\n const payload = { testFilePath, scenarios: scenariosWithAttachments };\n fs.writeFileSync(outFile, JSON.stringify(payload, null, 2) + \"\\n\", \"utf8\");\n }\n storyRegistry.clear();\n attachmentRegistry.clear();\n otelSpansRegistry.clear();\n}\n\n/** Register process exit handler to flush stories (once per worker) */\nfunction registerExitHandler(): void {\n if (exitHandlerRegistered) return;\n exitHandlerRegistered = true;\n globalThis.__jestExecutableStoriesExitHandler = true;\n // Use 'exit' event - always fired when Node.js is about to exit\n // Note: Only sync operations work here, which is fine for fs.writeFileSync\n process.on(\"exit\", () => {\n flushStories();\n });\n}\n\n// ============================================================================\n// Jest-specific context\n// ============================================================================\n\n/** Active story context - set by story.init() */\nlet activeContext: StoryContext | 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() must be called first. Use: it('name', () => { story.init(); ... });\"\n );\n }\n return activeContext;\n}\n\n// ============================================================================\n// Helper Functions\n// ============================================================================\n\n/**\n * Normalize ticket option to array format.\n */\nfunction normalizeTickets(ticket: string | string[] | undefined): string[] | undefined {\n if (!ticket) return undefined;\n return Array.isArray(ticket) ? ticket : [ticket];\n}\n\n/**\n * Extract the suite path from Jest's currentTestName.\n * Jest's currentTestName is formatted as: \"describe1 > describe2 > test name\"\n */\nfunction extractSuitePath(currentTestName: string): { suitePath?: string[]; testName: string } {\n const parts = currentTestName.split(\" > \");\n if (parts.length <= 1) {\n return { testName: currentTestName };\n }\n const testName = parts[parts.length - 1];\n const suitePath = parts.slice(0, -1);\n return { suitePath, testName };\n}\n\n/**\n * Convert StoryDocs inline options to DocEntry array.\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({ kind: \"code\", label: docs.code.label, content: docs.code.content, lang: docs.code.lang, phase: \"runtime\" });\n }\n if (docs.json) {\n entries.push({ kind: \"code\", label: docs.json.label, content: JSON.stringify(docs.json.value, null, 2), lang: \"json\", phase: \"runtime\" });\n }\n if (docs.table) {\n entries.push({ kind: \"table\", label: docs.table.label, columns: docs.table.columns, rows: docs.table.rows, phase: \"runtime\" });\n }\n if (docs.link) {\n entries.push({ kind: \"link\", label: docs.link.label, url: docs.link.url, phase: \"runtime\" });\n }\n if (docs.section) {\n entries.push({ kind: \"section\", title: docs.section.title, markdown: docs.section.markdown, phase: \"runtime\" });\n }\n if (docs.mermaid) {\n entries.push({ kind: \"mermaid\", code: docs.mermaid.code, title: docs.mermaid.title, phase: \"runtime\" });\n }\n if (docs.screenshot) {\n entries.push({ kind: \"screenshot\", path: docs.screenshot.path, alt: docs.screenshot.alt, phase: \"runtime\" });\n }\n if (docs.custom) {\n entries.push({ kind: \"custom\", type: docs.custom.type, data: docs.custom.data, phase: \"runtime\" });\n }\n\n return entries;\n}\n\n// ============================================================================\n// Helper to attach doc entry to current step or story-level\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// Step Markers\n// ============================================================================\n\nfunction createStepMarker(keyword: StepKeyword) {\n function stepMarker(text: string, docs?: StoryDocs): void;\n function stepMarker<T>(text: string, body: () => T): T;\n function stepMarker<T>(text: string, docsOrBody?: StoryDocs | (() => 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\n if (!isCallback) return;\n\n const body = docsOrBody as () => T;\n const start = performance.now();\n\n try {\n const result = body();\n if (result instanceof Promise) {\n return result.then(\n (val) => { step.durationMs = performance.now() - start; return val; },\n (err) => { step.durationMs = performance.now() - start; throw err; },\n ) as T;\n }\n step.durationMs = performance.now() - start;\n return result;\n } catch (err) {\n step.durationMs = performance.now() - start;\n throw err;\n }\n }\n return stepMarker;\n}\n\n// ============================================================================\n// story.init() - Jest-specific\n// ============================================================================\n\n/**\n * Initialize a story for the current test.\n * Must be called at the start of each test that wants documentation.\n *\n * @param options - Optional story configuration (tags, ticket, meta)\n *\n * @example\n * ```ts\n * it('adds two numbers', () => {\n * story.init();\n * // ... rest of test\n * });\n * ```\n */\nfunction init(options?: StoryOptions): void {\n // Get current test info from Jest globals\n const state = expect.getState();\n const currentTestName = state.currentTestName || \"Unknown test\";\n const testPath = state.testPath || \"unknown\";\n\n const { suitePath, testName } = extractSuitePath(currentTestName);\n\n const meta: StoryMeta = {\n scenario: testName,\n steps: [],\n suitePath,\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', testName);\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 // Store in registry for this file\n const existing = storyRegistry.get(testPath);\n let scenarioIndex: number;\n if (existing) {\n scenarioIndex = existing.length;\n existing.push(meta);\n } else {\n scenarioIndex = 0;\n storyRegistry.set(testPath, [meta]);\n }\n\n // Register exit handler to flush stories when worker exits\n registerExitHandler();\n\n // Set active context\n activeContext = {\n meta,\n currentStep: null,\n stepCounter: 0,\n attachments: [],\n activeTimers: new Map(),\n timerCounter: 0,\n testPath,\n scenarioIndex,\n };\n\n // Link attachments to the registry for this test file + scenario index\n if (!attachmentRegistry.has(testPath)) {\n attachmentRegistry.set(testPath, new Map());\n }\n attachmentRegistry.get(testPath)!.set(scenarioIndex, activeContext.attachments);\n}\n\n// ============================================================================\n// Step Wrappers: story.fn() and story.expect()\n// ============================================================================\n\n/**\n * Wrap a function body as a step. Records the step with timing and `wrapped: true`.\n * Supports both sync and async functions. Returns whatever the function returns.\n *\n * @param keyword - The BDD keyword (Given, When, Then, And, But)\n * @param text - Step description\n * @param body - The function to execute\n * @returns The return value of body (or a Promise of it if body is async)\n *\n * @example\n * ```ts\n * const data = story.fn('Given', 'setup data', () => ({ a: 5, b: 3 }));\n * const result = await story.fn('When', 'call API', async () => fetch('/api'));\n * ```\n */\nfunction fn<T>(keyword: StepKeyword, text: string, body: () => 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\n const step: StoryStep = {\n id: `step-${ctx.stepCounter++}`,\n keyword: resolvedKeyword,\n text,\n docs: [],\n wrapped: true,\n };\n\n ctx.meta.steps.push(step);\n ctx.currentStep = step;\n\n const start = performance.now();\n\n try {\n const result = body();\n\n // Handle async functions\n if (result instanceof Promise) {\n return result.then(\n (val) => {\n step.durationMs = performance.now() - start;\n return val;\n },\n (err) => {\n step.durationMs = performance.now() - start;\n throw err;\n },\n ) as T;\n }\n\n step.durationMs = performance.now() - start;\n return result;\n } catch (err) {\n step.durationMs = performance.now() - start;\n throw err;\n }\n}\n\n/**\n * Wrap an assertion as a Then step. Shorthand for `story.fn('Then', text, body)`.\n *\n * @param text - Step description\n * @param body - The assertion function to execute\n *\n * @example\n * ```ts\n * story.expect('the result is 8', () => { expect(result).toBe(8); });\n * ```\n */\nfunction storyExpect<T>(text: string, body: () => T): T {\n return fn('Then', text, body);\n}\n\n// ============================================================================\n// Export story object\n// ============================================================================\n\n/**\n * The main story API object for Jest.\n *\n * @example\n * ```ts\n * import { story } from 'executable-stories-jest';\n *\n * describe('Calculator', () => {\n * it('adds two numbers', () => {\n * story.init();\n *\n * story.given('two numbers 5 and 3');\n * const a = 5, 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 */\nexport const story = {\n // Jest-specific init\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 },\n\n tag(name: string | string[]): void {\n const names = Array.isArray(name) ? name : [name];\n attachDoc({ kind: \"tag\", names, phase: \"runtime\" });\n },\n\n kv(options: KvOptions): void {\n attachDoc({ kind: \"kv\", label: options.label, value: options.value, phase: \"runtime\" });\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 },\n\n code(options: CodeOptions): void {\n attachDoc({ kind: \"code\", label: options.label, content: options.content, lang: options.lang, phase: \"runtime\" });\n },\n\n table(options: TableOptions): void {\n attachDoc({ kind: \"table\", label: options.label, columns: options.columns, rows: options.rows, phase: \"runtime\" });\n },\n\n link(options: LinkOptions): void {\n attachDoc({ kind: \"link\", label: options.label, url: options.url, phase: \"runtime\" });\n },\n\n section(options: SectionOptions): void {\n attachDoc({ kind: \"section\", title: options.title, markdown: options.markdown, phase: \"runtime\" });\n },\n\n mermaid(options: MermaidOptions): void {\n attachDoc({ kind: \"mermaid\", code: options.code, title: options.title, phase: \"runtime\" });\n },\n\n screenshot(options: ScreenshotOptions): void {\n attachDoc({ kind: \"screenshot\", path: options.path, alt: options.alt, phase: \"runtime\" });\n },\n\n custom(options: CustomOptions): void {\n attachDoc({ kind: \"custom\", type: options.type, data: options.data, phase: \"runtime\" });\n },\n\n // Attachments\n attach(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 stepIndex: stepIndex !== undefined && stepIndex >= 0 ? stepIndex : undefined,\n stepId: ctx.currentStep?.id,\n });\n },\n\n // OTel span attachment\n attachSpans(spans: ReadonlyArray<Record<string, unknown>>): void {\n const ctx = getContext();\n if (!otelSpansRegistry.has(ctx.testPath)) {\n otelSpansRegistry.set(ctx.testPath, new Map());\n }\n otelSpansRegistry.get(ctx.testPath)!.set(ctx.scenarioIndex, spans);\n },\n\n // Step wrappers\n fn,\n expect: storyExpect,\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 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 },\n};\n\nexport type Story = typeof story;\n\n// ============================================================================\n// Internal exports for setup file\n// ============================================================================\n\n/**\n * Internal API for the setup file and tests. Not for public use.\n * @internal\n */\nexport const _internal = {\n flushStories,\n /** Clear active context (for tests that assert getContext() throws). */\n clearContext(): void {\n activeContext = null;\n },\n};\n","/**\n * Type definitions for executable-stories-jest.\n *\n * Shared story types are imported from executable-stories-formatters.\n * This module re-exports them and adds Jest-specific types.\n */\n\n// Re-export shared story types from formatters\nexport type {\n StepKeyword,\n StepMode,\n DocPhase,\n DocEntry,\n StoryStep,\n StoryMeta,\n} from 'executable-stories-formatters';\n\nexport { STORY_META_KEY } from 'executable-stories-formatters';\n\n// ============================================================================\n// Doc Options (for inline docs and standalone methods)\n// ============================================================================\n\n/** Options for kv() - key-value pair */\nexport interface KvOptions {\n label: string;\n value: unknown;\n}\n\n/** Options for json() - JSON code block */\nexport interface JsonOptions {\n label: string;\n value: unknown;\n}\n\n/** Options for code() - code block with optional language */\nexport interface CodeOptions {\n label: string;\n content: string;\n lang?: string;\n}\n\n/** Options for table() - markdown table */\nexport interface TableOptions {\n label: string;\n columns: string[];\n rows: string[][];\n}\n\n/** Options for link() - hyperlink */\nexport interface LinkOptions {\n label: string;\n url: string;\n}\n\n/** Options for section() - titled markdown section */\nexport interface SectionOptions {\n title: string;\n markdown: string;\n}\n\n/** Options for mermaid() - Mermaid diagram */\nexport interface MermaidOptions {\n code: string;\n title?: string;\n}\n\n/** Options for screenshot() - screenshot reference */\nexport interface ScreenshotOptions {\n path: string;\n alt?: string;\n}\n\n/** Options for custom() - custom doc entry */\nexport interface CustomOptions {\n type: string;\n data: unknown;\n}\n\n// ============================================================================\n// Inline Docs for Steps\n// ============================================================================\n\n/**\n * Inline documentation options for step markers.\n * Pass to story.given(), story.when(), story.then() as second argument.\n */\nexport interface StoryDocs {\n note?: string;\n tag?: string | string[];\n kv?: Record<string, unknown>;\n code?: CodeOptions;\n json?: JsonOptions;\n table?: TableOptions;\n link?: LinkOptions;\n section?: SectionOptions;\n mermaid?: MermaidOptions;\n screenshot?: ScreenshotOptions;\n custom?: CustomOptions;\n}\n\n// ============================================================================\n// Attachment Types\n// ============================================================================\n\n/** Options for attaching files or inline content to a test */\nexport interface AttachmentOptions {\n name: string;\n mediaType: string;\n path?: string;\n body?: string;\n encoding?: \"BASE64\" | \"IDENTITY\";\n charset?: string;\n fileName?: string;\n}\n\n/** Internal: attachment with step scope info */\nexport interface ScopedAttachment extends AttachmentOptions {\n stepIndex?: number;\n stepId?: string;\n}\n\n// ============================================================================\n// Story Options\n// ============================================================================\n\n/**\n * Options for configuring a story via story.init().\n */\nexport interface StoryOptions {\n tags?: string[];\n ticket?: string | string[];\n meta?: Record<string, unknown>;\n /** URL template for OTel trace links. Uses {traceId} placeholder. Also settable via OTEL_TRACE_URL_TEMPLATE env var. */\n traceUrlTemplate?: string;\n}\n","/**\n * Jest Executable Stories\n *\n * BDD-style executable documentation for Jest.\n *\n * @example\n * ```ts\n * import { story } from 'executable-stories-jest';\n *\n * describe('Calculator', () => {\n * it('adds two numbers', () => {\n * story.init();\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\nexport type {\n StoryMeta,\n StoryStep,\n DocEntry,\n StepKeyword,\n StepMode,\n DocPhase,\n StoryDocs,\n StoryOptions,\n AttachmentOptions,\n KvOptions,\n JsonOptions,\n CodeOptions,\n TableOptions,\n LinkOptions,\n SectionOptions,\n MermaidOptions,\n ScreenshotOptions,\n CustomOptions,\n} from \"./types\";\n\nexport { STORY_META_KEY } from \"./types\";\n"],"mappings":"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","names":[]}
package/dist/setup.cjs CHANGED
@@ -34,6 +34,7 @@ var import_executable_stories_formatters = require("executable-stories-formatter
34
34
  var import_meta = {};
35
35
  var storyRegistry = globalThis.__jestExecutableStoriesRegistry ??= /* @__PURE__ */ new Map();
36
36
  var attachmentRegistry = /* @__PURE__ */ new Map();
37
+ var otelSpansRegistry = /* @__PURE__ */ new Map();
37
38
  var exitHandlerRegistered = globalThis.__jestExecutableStoriesExitHandler ?? false;
38
39
  function getOutputDir() {
39
40
  const baseDir = process.env.JEST_STORY_DOCS_DIR ?? ".jest-executable-stories";
@@ -50,15 +51,18 @@ function flushStories() {
50
51
  const baseName = testFilePath === "unknown" ? "unknown" : path.basename(testFilePath);
51
52
  const outFile = path.join(outputDir, `${baseName}.${hash}.json`);
52
53
  const fileAttachments = attachmentRegistry.get(testFilePath);
53
- const scenariosWithAttachments = scenarios.map((s) => ({
54
+ const fileOtelSpans = otelSpansRegistry.get(testFilePath);
55
+ const scenariosWithAttachments = scenarios.map((s, i) => ({
54
56
  ...s,
55
- _attachments: fileAttachments?.get(s.scenario) ?? []
57
+ _attachments: fileAttachments?.get(i) ?? [],
58
+ ...fileOtelSpans?.get(i) ? { _otelSpans: fileOtelSpans.get(i) } : {}
56
59
  }));
57
60
  const payload = { testFilePath, scenarios: scenariosWithAttachments };
58
61
  fs.writeFileSync(outFile, JSON.stringify(payload, null, 2) + "\n", "utf8");
59
62
  }
60
63
  storyRegistry.clear();
61
64
  attachmentRegistry.clear();
65
+ otelSpansRegistry.clear();
62
66
  }
63
67
  function registerExitHandler() {
64
68
  if (exitHandlerRegistered) return;
@@ -222,9 +226,12 @@ function init(options) {
222
226
  }
223
227
  }
224
228
  const existing = storyRegistry.get(testPath);
229
+ let scenarioIndex;
225
230
  if (existing) {
231
+ scenarioIndex = existing.length;
226
232
  existing.push(meta);
227
233
  } else {
234
+ scenarioIndex = 0;
228
235
  storyRegistry.set(testPath, [meta]);
229
236
  }
230
237
  registerExitHandler();
@@ -234,12 +241,14 @@ function init(options) {
234
241
  stepCounter: 0,
235
242
  attachments: [],
236
243
  activeTimers: /* @__PURE__ */ new Map(),
237
- timerCounter: 0
244
+ timerCounter: 0,
245
+ testPath,
246
+ scenarioIndex
238
247
  };
239
248
  if (!attachmentRegistry.has(testPath)) {
240
249
  attachmentRegistry.set(testPath, /* @__PURE__ */ new Map());
241
250
  }
242
- attachmentRegistry.get(testPath).set(meta.scenario, activeContext.attachments);
251
+ attachmentRegistry.get(testPath).set(scenarioIndex, activeContext.attachments);
243
252
  }
244
253
  function fn(keyword, text, body) {
245
254
  const ctx = getContext();
@@ -343,6 +352,14 @@ var story = {
343
352
  stepId: ctx.currentStep?.id
344
353
  });
345
354
  },
355
+ // OTel span attachment
356
+ attachSpans(spans) {
357
+ const ctx = getContext();
358
+ if (!otelSpansRegistry.has(ctx.testPath)) {
359
+ otelSpansRegistry.set(ctx.testPath, /* @__PURE__ */ new Map());
360
+ }
361
+ otelSpansRegistry.get(ctx.testPath).set(ctx.scenarioIndex, spans);
362
+ },
346
363
  // Step wrappers
347
364
  fn,
348
365
  expect: storyExpect,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/setup.ts","../src/story-api.ts"],"sourcesContent":["/**\n * Jest setup file for executable-stories.\n *\n * Add this to your Jest config's setupFilesAfterEnv:\n *\n * @example\n * ```js\n * // jest.config.js\n * export default {\n * setupFilesAfterEnv: ['jest-executable-stories/setup'],\n * reporters: [\n * 'default',\n * ['jest-executable-stories/reporter', { outputFile: 'docs/user-stories.md' }]\n * ],\n * };\n * ```\n */\n\nimport { afterAll } from \"@jest/globals\";\nimport { _internal } from \"./story-api\";\n\n// Register afterAll hook to flush stories at the file level\nafterAll(() => {\n _internal.flushStories();\n});\n","/**\n * Jest story.* API for executable-stories.\n *\n * Uses native Jest describe/it/test with opt-in documentation:\n *\n * @example\n * ```ts\n * import { story } from 'executable-stories-jest';\n *\n * describe('Calculator', () => {\n * it('adds two numbers', () => {\n * story.init();\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 * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport { createHash } from \"node:crypto\";\nimport { createRequire } from 'node:module';\nimport { tryGetActiveOtelContext, resolveTraceUrl } from 'executable-stories-formatters';\nimport type {\n DocEntry,\n StepKeyword,\n StoryDocs,\n StoryMeta,\n StoryOptions,\n StoryStep,\n ScopedAttachment,\n AttachmentOptions,\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// Story Context\n// ============================================================================\n\n/** Internal timer entry */\ninterface TimerEntry {\n start: number;\n stepIndex?: number;\n stepId?: string;\n consumed: boolean;\n}\n\ninterface StoryContext {\n /** The story metadata being built */\n meta: StoryMeta;\n /** The current step (for attaching docs) */\n currentStep: StoryStep | null;\n /** Deterministic step counter (resets per test case) */\n stepCounter: number;\n /** Collected attachments with step scope */\n attachments: ScopedAttachment[];\n /** Active timers keyed by token */\n activeTimers: Map<number, TimerEntry>;\n /** Monotonic timer token counter */\n timerCounter: number;\n}\n\n// ============================================================================\n// File-based story collection (works across Jest worker processes)\n// ============================================================================\n\n// Use globalThis to ensure the registry is shared across module instances\n// This is needed because Jest may load the setup file and test files as separate module instances\ndeclare global {\n // eslint-disable-next-line no-var\n var __jestExecutableStoriesRegistry: Map<string, StoryMeta[]> | undefined;\n // eslint-disable-next-line no-var\n var __jestExecutableStoriesExitHandler: boolean | undefined;\n}\n\n/** Stories collected during test execution, keyed by test file path */\nconst storyRegistry: Map<string, StoryMeta[]> = globalThis.__jestExecutableStoriesRegistry ??= new Map();\n\n/** Attachments collected per story, keyed by test file path → scenario name → attachments */\nconst attachmentRegistry = new Map<string, Map<string, ScopedAttachment[]>>();\n\n/** Track if we've registered the process exit handler */\nlet exitHandlerRegistered = globalThis.__jestExecutableStoriesExitHandler ?? false;\n\n/** Get the output directory for story JSON files */\nfunction getOutputDir(): string {\n const baseDir = process.env.JEST_STORY_DOCS_DIR ?? \".jest-executable-stories\";\n return path.resolve(process.cwd(), baseDir);\n}\n\n/** Flush all collected stories to JSON files */\nfunction flushStories(): void {\n if (storyRegistry.size === 0) return;\n\n const workerId = process.env.JEST_WORKER_ID ?? \"0\";\n const outputDir = path.join(getOutputDir(), `worker-${workerId}`);\n fs.mkdirSync(outputDir, { recursive: true });\n\n for (const [testFilePath, scenarios] of storyRegistry) {\n if (!scenarios.length) continue;\n const hash = createHash(\"sha1\").update(testFilePath).digest(\"hex\").slice(0, 12);\n const baseName = testFilePath === \"unknown\" ? \"unknown\" : path.basename(testFilePath);\n const outFile = path.join(outputDir, `${baseName}.${hash}.json`);\n\n // Include attachments per scenario\n const fileAttachments = attachmentRegistry.get(testFilePath);\n const scenariosWithAttachments = scenarios.map((s) => ({\n ...s,\n _attachments: fileAttachments?.get(s.scenario) ?? [],\n }));\n\n const payload = { testFilePath, scenarios: scenariosWithAttachments };\n fs.writeFileSync(outFile, JSON.stringify(payload, null, 2) + \"\\n\", \"utf8\");\n }\n storyRegistry.clear();\n attachmentRegistry.clear();\n}\n\n/** Register process exit handler to flush stories (once per worker) */\nfunction registerExitHandler(): void {\n if (exitHandlerRegistered) return;\n exitHandlerRegistered = true;\n globalThis.__jestExecutableStoriesExitHandler = true;\n // Use 'exit' event - always fired when Node.js is about to exit\n // Note: Only sync operations work here, which is fine for fs.writeFileSync\n process.on(\"exit\", () => {\n flushStories();\n });\n}\n\n// ============================================================================\n// Jest-specific context\n// ============================================================================\n\n/** Active story context - set by story.init() */\nlet activeContext: StoryContext | 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() must be called first. Use: it('name', () => { story.init(); ... });\"\n );\n }\n return activeContext;\n}\n\n// ============================================================================\n// Helper Functions\n// ============================================================================\n\n/**\n * Normalize ticket option to array format.\n */\nfunction normalizeTickets(ticket: string | string[] | undefined): string[] | undefined {\n if (!ticket) return undefined;\n return Array.isArray(ticket) ? ticket : [ticket];\n}\n\n/**\n * Extract the suite path from Jest's currentTestName.\n * Jest's currentTestName is formatted as: \"describe1 > describe2 > test name\"\n */\nfunction extractSuitePath(currentTestName: string): { suitePath?: string[]; testName: string } {\n const parts = currentTestName.split(\" > \");\n if (parts.length <= 1) {\n return { testName: currentTestName };\n }\n const testName = parts[parts.length - 1];\n const suitePath = parts.slice(0, -1);\n return { suitePath, testName };\n}\n\n/**\n * Convert StoryDocs inline options to DocEntry array.\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({ kind: \"code\", label: docs.code.label, content: docs.code.content, lang: docs.code.lang, phase: \"runtime\" });\n }\n if (docs.json) {\n entries.push({ kind: \"code\", label: docs.json.label, content: JSON.stringify(docs.json.value, null, 2), lang: \"json\", phase: \"runtime\" });\n }\n if (docs.table) {\n entries.push({ kind: \"table\", label: docs.table.label, columns: docs.table.columns, rows: docs.table.rows, phase: \"runtime\" });\n }\n if (docs.link) {\n entries.push({ kind: \"link\", label: docs.link.label, url: docs.link.url, phase: \"runtime\" });\n }\n if (docs.section) {\n entries.push({ kind: \"section\", title: docs.section.title, markdown: docs.section.markdown, phase: \"runtime\" });\n }\n if (docs.mermaid) {\n entries.push({ kind: \"mermaid\", code: docs.mermaid.code, title: docs.mermaid.title, phase: \"runtime\" });\n }\n if (docs.screenshot) {\n entries.push({ kind: \"screenshot\", path: docs.screenshot.path, alt: docs.screenshot.alt, phase: \"runtime\" });\n }\n if (docs.custom) {\n entries.push({ kind: \"custom\", type: docs.custom.type, data: docs.custom.data, phase: \"runtime\" });\n }\n\n return entries;\n}\n\n// ============================================================================\n// Helper to attach doc entry to current step or story-level\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// Step Markers\n// ============================================================================\n\nfunction createStepMarker(keyword: StepKeyword) {\n function stepMarker(text: string, docs?: StoryDocs): void;\n function stepMarker<T>(text: string, body: () => T): T;\n function stepMarker<T>(text: string, docsOrBody?: StoryDocs | (() => 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\n if (!isCallback) return;\n\n const body = docsOrBody as () => T;\n const start = performance.now();\n\n try {\n const result = body();\n if (result instanceof Promise) {\n return result.then(\n (val) => { step.durationMs = performance.now() - start; return val; },\n (err) => { step.durationMs = performance.now() - start; throw err; },\n ) as T;\n }\n step.durationMs = performance.now() - start;\n return result;\n } catch (err) {\n step.durationMs = performance.now() - start;\n throw err;\n }\n }\n return stepMarker;\n}\n\n// ============================================================================\n// story.init() - Jest-specific\n// ============================================================================\n\n/**\n * Initialize a story for the current test.\n * Must be called at the start of each test that wants documentation.\n *\n * @param options - Optional story configuration (tags, ticket, meta)\n *\n * @example\n * ```ts\n * it('adds two numbers', () => {\n * story.init();\n * // ... rest of test\n * });\n * ```\n */\nfunction init(options?: StoryOptions): void {\n // Get current test info from Jest globals\n const state = expect.getState();\n const currentTestName = state.currentTestName || \"Unknown test\";\n const testPath = state.testPath || \"unknown\";\n\n const { suitePath, testName } = extractSuitePath(currentTestName);\n\n const meta: StoryMeta = {\n scenario: testName,\n steps: [],\n suitePath,\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', testName);\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 // Store in registry for this file\n const existing = storyRegistry.get(testPath);\n if (existing) {\n existing.push(meta);\n } else {\n storyRegistry.set(testPath, [meta]);\n }\n\n // Register exit handler to flush stories when worker exits\n registerExitHandler();\n\n // Set active context\n activeContext = {\n meta,\n currentStep: null,\n stepCounter: 0,\n attachments: [],\n activeTimers: new Map(),\n timerCounter: 0,\n };\n\n // Link attachments to the registry for this test file + scenario\n if (!attachmentRegistry.has(testPath)) {\n attachmentRegistry.set(testPath, new Map());\n }\n attachmentRegistry.get(testPath)!.set(meta.scenario, activeContext.attachments);\n}\n\n// ============================================================================\n// Step Wrappers: story.fn() and story.expect()\n// ============================================================================\n\n/**\n * Wrap a function body as a step. Records the step with timing and `wrapped: true`.\n * Supports both sync and async functions. Returns whatever the function returns.\n *\n * @param keyword - The BDD keyword (Given, When, Then, And, But)\n * @param text - Step description\n * @param body - The function to execute\n * @returns The return value of body (or a Promise of it if body is async)\n *\n * @example\n * ```ts\n * const data = story.fn('Given', 'setup data', () => ({ a: 5, b: 3 }));\n * const result = await story.fn('When', 'call API', async () => fetch('/api'));\n * ```\n */\nfunction fn<T>(keyword: StepKeyword, text: string, body: () => 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\n const step: StoryStep = {\n id: `step-${ctx.stepCounter++}`,\n keyword: resolvedKeyword,\n text,\n docs: [],\n wrapped: true,\n };\n\n ctx.meta.steps.push(step);\n ctx.currentStep = step;\n\n const start = performance.now();\n\n try {\n const result = body();\n\n // Handle async functions\n if (result instanceof Promise) {\n return result.then(\n (val) => {\n step.durationMs = performance.now() - start;\n return val;\n },\n (err) => {\n step.durationMs = performance.now() - start;\n throw err;\n },\n ) as T;\n }\n\n step.durationMs = performance.now() - start;\n return result;\n } catch (err) {\n step.durationMs = performance.now() - start;\n throw err;\n }\n}\n\n/**\n * Wrap an assertion as a Then step. Shorthand for `story.fn('Then', text, body)`.\n *\n * @param text - Step description\n * @param body - The assertion function to execute\n *\n * @example\n * ```ts\n * story.expect('the result is 8', () => { expect(result).toBe(8); });\n * ```\n */\nfunction storyExpect<T>(text: string, body: () => T): T {\n return fn('Then', text, body);\n}\n\n// ============================================================================\n// Export story object\n// ============================================================================\n\n/**\n * The main story API object for Jest.\n *\n * @example\n * ```ts\n * import { story } from 'executable-stories-jest';\n *\n * describe('Calculator', () => {\n * it('adds two numbers', () => {\n * story.init();\n *\n * story.given('two numbers 5 and 3');\n * const a = 5, 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 */\nexport const story = {\n // Jest-specific init\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 },\n\n tag(name: string | string[]): void {\n const names = Array.isArray(name) ? name : [name];\n attachDoc({ kind: \"tag\", names, phase: \"runtime\" });\n },\n\n kv(options: KvOptions): void {\n attachDoc({ kind: \"kv\", label: options.label, value: options.value, phase: \"runtime\" });\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 },\n\n code(options: CodeOptions): void {\n attachDoc({ kind: \"code\", label: options.label, content: options.content, lang: options.lang, phase: \"runtime\" });\n },\n\n table(options: TableOptions): void {\n attachDoc({ kind: \"table\", label: options.label, columns: options.columns, rows: options.rows, phase: \"runtime\" });\n },\n\n link(options: LinkOptions): void {\n attachDoc({ kind: \"link\", label: options.label, url: options.url, phase: \"runtime\" });\n },\n\n section(options: SectionOptions): void {\n attachDoc({ kind: \"section\", title: options.title, markdown: options.markdown, phase: \"runtime\" });\n },\n\n mermaid(options: MermaidOptions): void {\n attachDoc({ kind: \"mermaid\", code: options.code, title: options.title, phase: \"runtime\" });\n },\n\n screenshot(options: ScreenshotOptions): void {\n attachDoc({ kind: \"screenshot\", path: options.path, alt: options.alt, phase: \"runtime\" });\n },\n\n custom(options: CustomOptions): void {\n attachDoc({ kind: \"custom\", type: options.type, data: options.data, phase: \"runtime\" });\n },\n\n // Attachments\n attach(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 stepIndex: stepIndex !== undefined && stepIndex >= 0 ? stepIndex : undefined,\n stepId: ctx.currentStep?.id,\n });\n },\n\n // Step wrappers\n fn,\n expect: storyExpect,\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 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 },\n};\n\nexport type Story = typeof story;\n\n// ============================================================================\n// Internal exports for setup file\n// ============================================================================\n\n/**\n * Internal API for the setup file and tests. Not for public use.\n * @internal\n */\nexport const _internal = {\n flushStories,\n /** Clear active context (for tests that assert getContext() throws). */\n clearContext(): void {\n activeContext = null;\n },\n};\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/setup.ts","../src/story-api.ts"],"sourcesContent":["/**\n * Jest setup file for executable-stories.\n *\n * Add this to your Jest config's setupFilesAfterEnv:\n *\n * @example\n * ```js\n * // jest.config.js\n * export default {\n * setupFilesAfterEnv: ['jest-executable-stories/setup'],\n * reporters: [\n * 'default',\n * ['jest-executable-stories/reporter', { outputFile: 'docs/user-stories.md' }]\n * ],\n * };\n * ```\n */\n\nimport { afterAll } from \"@jest/globals\";\nimport { _internal } from \"./story-api\";\n\n// Register afterAll hook to flush stories at the file level\nafterAll(() => {\n _internal.flushStories();\n});\n","/**\n * Jest story.* API for executable-stories.\n *\n * Uses native Jest describe/it/test with opt-in documentation:\n *\n * @example\n * ```ts\n * import { story } from 'executable-stories-jest';\n *\n * describe('Calculator', () => {\n * it('adds two numbers', () => {\n * story.init();\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 * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport { createHash } from \"node:crypto\";\nimport { createRequire } from 'node:module';\nimport { tryGetActiveOtelContext, resolveTraceUrl } from 'executable-stories-formatters';\nimport type {\n DocEntry,\n StepKeyword,\n StoryDocs,\n StoryMeta,\n StoryOptions,\n StoryStep,\n ScopedAttachment,\n AttachmentOptions,\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// Story Context\n// ============================================================================\n\n/** Internal timer entry */\ninterface TimerEntry {\n start: number;\n stepIndex?: number;\n stepId?: string;\n consumed: boolean;\n}\n\ninterface StoryContext {\n /** The story metadata being built */\n meta: StoryMeta;\n /** The current step (for attaching docs) */\n currentStep: StoryStep | null;\n /** Deterministic step counter (resets per test case) */\n stepCounter: number;\n /** Collected attachments with step scope */\n attachments: ScopedAttachment[];\n /** Active timers keyed by token */\n activeTimers: Map<number, TimerEntry>;\n /** Monotonic timer token counter */\n timerCounter: number;\n /** Test file path for registry lookups */\n testPath: string;\n /** Index into the storyRegistry array for this scenario */\n scenarioIndex: number;\n}\n\n// ============================================================================\n// File-based story collection (works across Jest worker processes)\n// ============================================================================\n\n// Use globalThis to ensure the registry is shared across module instances\n// This is needed because Jest may load the setup file and test files as separate module instances\ndeclare global {\n // eslint-disable-next-line no-var\n var __jestExecutableStoriesRegistry: Map<string, StoryMeta[]> | undefined;\n // eslint-disable-next-line no-var\n var __jestExecutableStoriesExitHandler: boolean | undefined;\n}\n\n/** Stories collected during test execution, keyed by test file path */\nconst storyRegistry: Map<string, StoryMeta[]> = globalThis.__jestExecutableStoriesRegistry ??= new Map();\n\n/** Attachments collected per story, keyed by test file path → scenario index → attachments */\nconst attachmentRegistry = new Map<string, Map<number, ScopedAttachment[]>>();\n\n/** OTel spans collected per story, keyed by test file path → scenario index → spans */\nconst otelSpansRegistry = new Map<string, Map<number, ReadonlyArray<Record<string, unknown>>>>();\n\n/** Track if we've registered the process exit handler */\nlet exitHandlerRegistered = globalThis.__jestExecutableStoriesExitHandler ?? false;\n\n/** Get the output directory for story JSON files */\nfunction getOutputDir(): string {\n const baseDir = process.env.JEST_STORY_DOCS_DIR ?? \".jest-executable-stories\";\n return path.resolve(process.cwd(), baseDir);\n}\n\n/** Flush all collected stories to JSON files */\nfunction flushStories(): void {\n if (storyRegistry.size === 0) return;\n\n const workerId = process.env.JEST_WORKER_ID ?? \"0\";\n const outputDir = path.join(getOutputDir(), `worker-${workerId}`);\n fs.mkdirSync(outputDir, { recursive: true });\n\n for (const [testFilePath, scenarios] of storyRegistry) {\n if (!scenarios.length) continue;\n const hash = createHash(\"sha1\").update(testFilePath).digest(\"hex\").slice(0, 12);\n const baseName = testFilePath === \"unknown\" ? \"unknown\" : path.basename(testFilePath);\n const outFile = path.join(outputDir, `${baseName}.${hash}.json`);\n\n // Include attachments and otelSpans per scenario (keyed by index, not name)\n const fileAttachments = attachmentRegistry.get(testFilePath);\n const fileOtelSpans = otelSpansRegistry.get(testFilePath);\n const scenariosWithAttachments = scenarios.map((s, i) => ({\n ...s,\n _attachments: fileAttachments?.get(i) ?? [],\n ...(fileOtelSpans?.get(i) ? { _otelSpans: fileOtelSpans.get(i) } : {}),\n }));\n\n const payload = { testFilePath, scenarios: scenariosWithAttachments };\n fs.writeFileSync(outFile, JSON.stringify(payload, null, 2) + \"\\n\", \"utf8\");\n }\n storyRegistry.clear();\n attachmentRegistry.clear();\n otelSpansRegistry.clear();\n}\n\n/** Register process exit handler to flush stories (once per worker) */\nfunction registerExitHandler(): void {\n if (exitHandlerRegistered) return;\n exitHandlerRegistered = true;\n globalThis.__jestExecutableStoriesExitHandler = true;\n // Use 'exit' event - always fired when Node.js is about to exit\n // Note: Only sync operations work here, which is fine for fs.writeFileSync\n process.on(\"exit\", () => {\n flushStories();\n });\n}\n\n// ============================================================================\n// Jest-specific context\n// ============================================================================\n\n/** Active story context - set by story.init() */\nlet activeContext: StoryContext | 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() must be called first. Use: it('name', () => { story.init(); ... });\"\n );\n }\n return activeContext;\n}\n\n// ============================================================================\n// Helper Functions\n// ============================================================================\n\n/**\n * Normalize ticket option to array format.\n */\nfunction normalizeTickets(ticket: string | string[] | undefined): string[] | undefined {\n if (!ticket) return undefined;\n return Array.isArray(ticket) ? ticket : [ticket];\n}\n\n/**\n * Extract the suite path from Jest's currentTestName.\n * Jest's currentTestName is formatted as: \"describe1 > describe2 > test name\"\n */\nfunction extractSuitePath(currentTestName: string): { suitePath?: string[]; testName: string } {\n const parts = currentTestName.split(\" > \");\n if (parts.length <= 1) {\n return { testName: currentTestName };\n }\n const testName = parts[parts.length - 1];\n const suitePath = parts.slice(0, -1);\n return { suitePath, testName };\n}\n\n/**\n * Convert StoryDocs inline options to DocEntry array.\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({ kind: \"code\", label: docs.code.label, content: docs.code.content, lang: docs.code.lang, phase: \"runtime\" });\n }\n if (docs.json) {\n entries.push({ kind: \"code\", label: docs.json.label, content: JSON.stringify(docs.json.value, null, 2), lang: \"json\", phase: \"runtime\" });\n }\n if (docs.table) {\n entries.push({ kind: \"table\", label: docs.table.label, columns: docs.table.columns, rows: docs.table.rows, phase: \"runtime\" });\n }\n if (docs.link) {\n entries.push({ kind: \"link\", label: docs.link.label, url: docs.link.url, phase: \"runtime\" });\n }\n if (docs.section) {\n entries.push({ kind: \"section\", title: docs.section.title, markdown: docs.section.markdown, phase: \"runtime\" });\n }\n if (docs.mermaid) {\n entries.push({ kind: \"mermaid\", code: docs.mermaid.code, title: docs.mermaid.title, phase: \"runtime\" });\n }\n if (docs.screenshot) {\n entries.push({ kind: \"screenshot\", path: docs.screenshot.path, alt: docs.screenshot.alt, phase: \"runtime\" });\n }\n if (docs.custom) {\n entries.push({ kind: \"custom\", type: docs.custom.type, data: docs.custom.data, phase: \"runtime\" });\n }\n\n return entries;\n}\n\n// ============================================================================\n// Helper to attach doc entry to current step or story-level\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// Step Markers\n// ============================================================================\n\nfunction createStepMarker(keyword: StepKeyword) {\n function stepMarker(text: string, docs?: StoryDocs): void;\n function stepMarker<T>(text: string, body: () => T): T;\n function stepMarker<T>(text: string, docsOrBody?: StoryDocs | (() => 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\n if (!isCallback) return;\n\n const body = docsOrBody as () => T;\n const start = performance.now();\n\n try {\n const result = body();\n if (result instanceof Promise) {\n return result.then(\n (val) => { step.durationMs = performance.now() - start; return val; },\n (err) => { step.durationMs = performance.now() - start; throw err; },\n ) as T;\n }\n step.durationMs = performance.now() - start;\n return result;\n } catch (err) {\n step.durationMs = performance.now() - start;\n throw err;\n }\n }\n return stepMarker;\n}\n\n// ============================================================================\n// story.init() - Jest-specific\n// ============================================================================\n\n/**\n * Initialize a story for the current test.\n * Must be called at the start of each test that wants documentation.\n *\n * @param options - Optional story configuration (tags, ticket, meta)\n *\n * @example\n * ```ts\n * it('adds two numbers', () => {\n * story.init();\n * // ... rest of test\n * });\n * ```\n */\nfunction init(options?: StoryOptions): void {\n // Get current test info from Jest globals\n const state = expect.getState();\n const currentTestName = state.currentTestName || \"Unknown test\";\n const testPath = state.testPath || \"unknown\";\n\n const { suitePath, testName } = extractSuitePath(currentTestName);\n\n const meta: StoryMeta = {\n scenario: testName,\n steps: [],\n suitePath,\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', testName);\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 // Store in registry for this file\n const existing = storyRegistry.get(testPath);\n let scenarioIndex: number;\n if (existing) {\n scenarioIndex = existing.length;\n existing.push(meta);\n } else {\n scenarioIndex = 0;\n storyRegistry.set(testPath, [meta]);\n }\n\n // Register exit handler to flush stories when worker exits\n registerExitHandler();\n\n // Set active context\n activeContext = {\n meta,\n currentStep: null,\n stepCounter: 0,\n attachments: [],\n activeTimers: new Map(),\n timerCounter: 0,\n testPath,\n scenarioIndex,\n };\n\n // Link attachments to the registry for this test file + scenario index\n if (!attachmentRegistry.has(testPath)) {\n attachmentRegistry.set(testPath, new Map());\n }\n attachmentRegistry.get(testPath)!.set(scenarioIndex, activeContext.attachments);\n}\n\n// ============================================================================\n// Step Wrappers: story.fn() and story.expect()\n// ============================================================================\n\n/**\n * Wrap a function body as a step. Records the step with timing and `wrapped: true`.\n * Supports both sync and async functions. Returns whatever the function returns.\n *\n * @param keyword - The BDD keyword (Given, When, Then, And, But)\n * @param text - Step description\n * @param body - The function to execute\n * @returns The return value of body (or a Promise of it if body is async)\n *\n * @example\n * ```ts\n * const data = story.fn('Given', 'setup data', () => ({ a: 5, b: 3 }));\n * const result = await story.fn('When', 'call API', async () => fetch('/api'));\n * ```\n */\nfunction fn<T>(keyword: StepKeyword, text: string, body: () => 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\n const step: StoryStep = {\n id: `step-${ctx.stepCounter++}`,\n keyword: resolvedKeyword,\n text,\n docs: [],\n wrapped: true,\n };\n\n ctx.meta.steps.push(step);\n ctx.currentStep = step;\n\n const start = performance.now();\n\n try {\n const result = body();\n\n // Handle async functions\n if (result instanceof Promise) {\n return result.then(\n (val) => {\n step.durationMs = performance.now() - start;\n return val;\n },\n (err) => {\n step.durationMs = performance.now() - start;\n throw err;\n },\n ) as T;\n }\n\n step.durationMs = performance.now() - start;\n return result;\n } catch (err) {\n step.durationMs = performance.now() - start;\n throw err;\n }\n}\n\n/**\n * Wrap an assertion as a Then step. Shorthand for `story.fn('Then', text, body)`.\n *\n * @param text - Step description\n * @param body - The assertion function to execute\n *\n * @example\n * ```ts\n * story.expect('the result is 8', () => { expect(result).toBe(8); });\n * ```\n */\nfunction storyExpect<T>(text: string, body: () => T): T {\n return fn('Then', text, body);\n}\n\n// ============================================================================\n// Export story object\n// ============================================================================\n\n/**\n * The main story API object for Jest.\n *\n * @example\n * ```ts\n * import { story } from 'executable-stories-jest';\n *\n * describe('Calculator', () => {\n * it('adds two numbers', () => {\n * story.init();\n *\n * story.given('two numbers 5 and 3');\n * const a = 5, 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 */\nexport const story = {\n // Jest-specific init\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 },\n\n tag(name: string | string[]): void {\n const names = Array.isArray(name) ? name : [name];\n attachDoc({ kind: \"tag\", names, phase: \"runtime\" });\n },\n\n kv(options: KvOptions): void {\n attachDoc({ kind: \"kv\", label: options.label, value: options.value, phase: \"runtime\" });\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 },\n\n code(options: CodeOptions): void {\n attachDoc({ kind: \"code\", label: options.label, content: options.content, lang: options.lang, phase: \"runtime\" });\n },\n\n table(options: TableOptions): void {\n attachDoc({ kind: \"table\", label: options.label, columns: options.columns, rows: options.rows, phase: \"runtime\" });\n },\n\n link(options: LinkOptions): void {\n attachDoc({ kind: \"link\", label: options.label, url: options.url, phase: \"runtime\" });\n },\n\n section(options: SectionOptions): void {\n attachDoc({ kind: \"section\", title: options.title, markdown: options.markdown, phase: \"runtime\" });\n },\n\n mermaid(options: MermaidOptions): void {\n attachDoc({ kind: \"mermaid\", code: options.code, title: options.title, phase: \"runtime\" });\n },\n\n screenshot(options: ScreenshotOptions): void {\n attachDoc({ kind: \"screenshot\", path: options.path, alt: options.alt, phase: \"runtime\" });\n },\n\n custom(options: CustomOptions): void {\n attachDoc({ kind: \"custom\", type: options.type, data: options.data, phase: \"runtime\" });\n },\n\n // Attachments\n attach(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 stepIndex: stepIndex !== undefined && stepIndex >= 0 ? stepIndex : undefined,\n stepId: ctx.currentStep?.id,\n });\n },\n\n // OTel span attachment\n attachSpans(spans: ReadonlyArray<Record<string, unknown>>): void {\n const ctx = getContext();\n if (!otelSpansRegistry.has(ctx.testPath)) {\n otelSpansRegistry.set(ctx.testPath, new Map());\n }\n otelSpansRegistry.get(ctx.testPath)!.set(ctx.scenarioIndex, spans);\n },\n\n // Step wrappers\n fn,\n expect: storyExpect,\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 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 },\n};\n\nexport type Story = typeof story;\n\n// ============================================================================\n// Internal exports for setup file\n// ============================================================================\n\n/**\n * Internal API for the setup file and tests. Not for public use.\n * @internal\n */\nexport const _internal = {\n flushStories,\n /** Clear active context (for tests that assert getContext() throws). */\n clearContext(): void {\n activeContext = null;\n },\n};\n"],"mappings":"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","names":[]}
package/dist/setup.js CHANGED
@@ -9,6 +9,7 @@ import { createRequire } from "module";
9
9
  import { tryGetActiveOtelContext, resolveTraceUrl } from "executable-stories-formatters";
10
10
  var storyRegistry = globalThis.__jestExecutableStoriesRegistry ??= /* @__PURE__ */ new Map();
11
11
  var attachmentRegistry = /* @__PURE__ */ new Map();
12
+ var otelSpansRegistry = /* @__PURE__ */ new Map();
12
13
  var exitHandlerRegistered = globalThis.__jestExecutableStoriesExitHandler ?? false;
13
14
  function getOutputDir() {
14
15
  const baseDir = process.env.JEST_STORY_DOCS_DIR ?? ".jest-executable-stories";
@@ -25,15 +26,18 @@ function flushStories() {
25
26
  const baseName = testFilePath === "unknown" ? "unknown" : path.basename(testFilePath);
26
27
  const outFile = path.join(outputDir, `${baseName}.${hash}.json`);
27
28
  const fileAttachments = attachmentRegistry.get(testFilePath);
28
- const scenariosWithAttachments = scenarios.map((s) => ({
29
+ const fileOtelSpans = otelSpansRegistry.get(testFilePath);
30
+ const scenariosWithAttachments = scenarios.map((s, i) => ({
29
31
  ...s,
30
- _attachments: fileAttachments?.get(s.scenario) ?? []
32
+ _attachments: fileAttachments?.get(i) ?? [],
33
+ ...fileOtelSpans?.get(i) ? { _otelSpans: fileOtelSpans.get(i) } : {}
31
34
  }));
32
35
  const payload = { testFilePath, scenarios: scenariosWithAttachments };
33
36
  fs.writeFileSync(outFile, JSON.stringify(payload, null, 2) + "\n", "utf8");
34
37
  }
35
38
  storyRegistry.clear();
36
39
  attachmentRegistry.clear();
40
+ otelSpansRegistry.clear();
37
41
  }
38
42
  function registerExitHandler() {
39
43
  if (exitHandlerRegistered) return;
@@ -197,9 +201,12 @@ function init(options) {
197
201
  }
198
202
  }
199
203
  const existing = storyRegistry.get(testPath);
204
+ let scenarioIndex;
200
205
  if (existing) {
206
+ scenarioIndex = existing.length;
201
207
  existing.push(meta);
202
208
  } else {
209
+ scenarioIndex = 0;
203
210
  storyRegistry.set(testPath, [meta]);
204
211
  }
205
212
  registerExitHandler();
@@ -209,12 +216,14 @@ function init(options) {
209
216
  stepCounter: 0,
210
217
  attachments: [],
211
218
  activeTimers: /* @__PURE__ */ new Map(),
212
- timerCounter: 0
219
+ timerCounter: 0,
220
+ testPath,
221
+ scenarioIndex
213
222
  };
214
223
  if (!attachmentRegistry.has(testPath)) {
215
224
  attachmentRegistry.set(testPath, /* @__PURE__ */ new Map());
216
225
  }
217
- attachmentRegistry.get(testPath).set(meta.scenario, activeContext.attachments);
226
+ attachmentRegistry.get(testPath).set(scenarioIndex, activeContext.attachments);
218
227
  }
219
228
  function fn(keyword, text, body) {
220
229
  const ctx = getContext();
@@ -318,6 +327,14 @@ var story = {
318
327
  stepId: ctx.currentStep?.id
319
328
  });
320
329
  },
330
+ // OTel span attachment
331
+ attachSpans(spans) {
332
+ const ctx = getContext();
333
+ if (!otelSpansRegistry.has(ctx.testPath)) {
334
+ otelSpansRegistry.set(ctx.testPath, /* @__PURE__ */ new Map());
335
+ }
336
+ otelSpansRegistry.get(ctx.testPath).set(ctx.scenarioIndex, spans);
337
+ },
321
338
  // Step wrappers
322
339
  fn,
323
340
  expect: storyExpect,
package/dist/setup.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/setup.ts","../src/story-api.ts"],"sourcesContent":["/**\n * Jest setup file for executable-stories.\n *\n * Add this to your Jest config's setupFilesAfterEnv:\n *\n * @example\n * ```js\n * // jest.config.js\n * export default {\n * setupFilesAfterEnv: ['jest-executable-stories/setup'],\n * reporters: [\n * 'default',\n * ['jest-executable-stories/reporter', { outputFile: 'docs/user-stories.md' }]\n * ],\n * };\n * ```\n */\n\nimport { afterAll } from \"@jest/globals\";\nimport { _internal } from \"./story-api\";\n\n// Register afterAll hook to flush stories at the file level\nafterAll(() => {\n _internal.flushStories();\n});\n","/**\n * Jest story.* API for executable-stories.\n *\n * Uses native Jest describe/it/test with opt-in documentation:\n *\n * @example\n * ```ts\n * import { story } from 'executable-stories-jest';\n *\n * describe('Calculator', () => {\n * it('adds two numbers', () => {\n * story.init();\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 * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport { createHash } from \"node:crypto\";\nimport { createRequire } from 'node:module';\nimport { tryGetActiveOtelContext, resolveTraceUrl } from 'executable-stories-formatters';\nimport type {\n DocEntry,\n StepKeyword,\n StoryDocs,\n StoryMeta,\n StoryOptions,\n StoryStep,\n ScopedAttachment,\n AttachmentOptions,\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// Story Context\n// ============================================================================\n\n/** Internal timer entry */\ninterface TimerEntry {\n start: number;\n stepIndex?: number;\n stepId?: string;\n consumed: boolean;\n}\n\ninterface StoryContext {\n /** The story metadata being built */\n meta: StoryMeta;\n /** The current step (for attaching docs) */\n currentStep: StoryStep | null;\n /** Deterministic step counter (resets per test case) */\n stepCounter: number;\n /** Collected attachments with step scope */\n attachments: ScopedAttachment[];\n /** Active timers keyed by token */\n activeTimers: Map<number, TimerEntry>;\n /** Monotonic timer token counter */\n timerCounter: number;\n}\n\n// ============================================================================\n// File-based story collection (works across Jest worker processes)\n// ============================================================================\n\n// Use globalThis to ensure the registry is shared across module instances\n// This is needed because Jest may load the setup file and test files as separate module instances\ndeclare global {\n // eslint-disable-next-line no-var\n var __jestExecutableStoriesRegistry: Map<string, StoryMeta[]> | undefined;\n // eslint-disable-next-line no-var\n var __jestExecutableStoriesExitHandler: boolean | undefined;\n}\n\n/** Stories collected during test execution, keyed by test file path */\nconst storyRegistry: Map<string, StoryMeta[]> = globalThis.__jestExecutableStoriesRegistry ??= new Map();\n\n/** Attachments collected per story, keyed by test file path → scenario name → attachments */\nconst attachmentRegistry = new Map<string, Map<string, ScopedAttachment[]>>();\n\n/** Track if we've registered the process exit handler */\nlet exitHandlerRegistered = globalThis.__jestExecutableStoriesExitHandler ?? false;\n\n/** Get the output directory for story JSON files */\nfunction getOutputDir(): string {\n const baseDir = process.env.JEST_STORY_DOCS_DIR ?? \".jest-executable-stories\";\n return path.resolve(process.cwd(), baseDir);\n}\n\n/** Flush all collected stories to JSON files */\nfunction flushStories(): void {\n if (storyRegistry.size === 0) return;\n\n const workerId = process.env.JEST_WORKER_ID ?? \"0\";\n const outputDir = path.join(getOutputDir(), `worker-${workerId}`);\n fs.mkdirSync(outputDir, { recursive: true });\n\n for (const [testFilePath, scenarios] of storyRegistry) {\n if (!scenarios.length) continue;\n const hash = createHash(\"sha1\").update(testFilePath).digest(\"hex\").slice(0, 12);\n const baseName = testFilePath === \"unknown\" ? \"unknown\" : path.basename(testFilePath);\n const outFile = path.join(outputDir, `${baseName}.${hash}.json`);\n\n // Include attachments per scenario\n const fileAttachments = attachmentRegistry.get(testFilePath);\n const scenariosWithAttachments = scenarios.map((s) => ({\n ...s,\n _attachments: fileAttachments?.get(s.scenario) ?? [],\n }));\n\n const payload = { testFilePath, scenarios: scenariosWithAttachments };\n fs.writeFileSync(outFile, JSON.stringify(payload, null, 2) + \"\\n\", \"utf8\");\n }\n storyRegistry.clear();\n attachmentRegistry.clear();\n}\n\n/** Register process exit handler to flush stories (once per worker) */\nfunction registerExitHandler(): void {\n if (exitHandlerRegistered) return;\n exitHandlerRegistered = true;\n globalThis.__jestExecutableStoriesExitHandler = true;\n // Use 'exit' event - always fired when Node.js is about to exit\n // Note: Only sync operations work here, which is fine for fs.writeFileSync\n process.on(\"exit\", () => {\n flushStories();\n });\n}\n\n// ============================================================================\n// Jest-specific context\n// ============================================================================\n\n/** Active story context - set by story.init() */\nlet activeContext: StoryContext | 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() must be called first. Use: it('name', () => { story.init(); ... });\"\n );\n }\n return activeContext;\n}\n\n// ============================================================================\n// Helper Functions\n// ============================================================================\n\n/**\n * Normalize ticket option to array format.\n */\nfunction normalizeTickets(ticket: string | string[] | undefined): string[] | undefined {\n if (!ticket) return undefined;\n return Array.isArray(ticket) ? ticket : [ticket];\n}\n\n/**\n * Extract the suite path from Jest's currentTestName.\n * Jest's currentTestName is formatted as: \"describe1 > describe2 > test name\"\n */\nfunction extractSuitePath(currentTestName: string): { suitePath?: string[]; testName: string } {\n const parts = currentTestName.split(\" > \");\n if (parts.length <= 1) {\n return { testName: currentTestName };\n }\n const testName = parts[parts.length - 1];\n const suitePath = parts.slice(0, -1);\n return { suitePath, testName };\n}\n\n/**\n * Convert StoryDocs inline options to DocEntry array.\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({ kind: \"code\", label: docs.code.label, content: docs.code.content, lang: docs.code.lang, phase: \"runtime\" });\n }\n if (docs.json) {\n entries.push({ kind: \"code\", label: docs.json.label, content: JSON.stringify(docs.json.value, null, 2), lang: \"json\", phase: \"runtime\" });\n }\n if (docs.table) {\n entries.push({ kind: \"table\", label: docs.table.label, columns: docs.table.columns, rows: docs.table.rows, phase: \"runtime\" });\n }\n if (docs.link) {\n entries.push({ kind: \"link\", label: docs.link.label, url: docs.link.url, phase: \"runtime\" });\n }\n if (docs.section) {\n entries.push({ kind: \"section\", title: docs.section.title, markdown: docs.section.markdown, phase: \"runtime\" });\n }\n if (docs.mermaid) {\n entries.push({ kind: \"mermaid\", code: docs.mermaid.code, title: docs.mermaid.title, phase: \"runtime\" });\n }\n if (docs.screenshot) {\n entries.push({ kind: \"screenshot\", path: docs.screenshot.path, alt: docs.screenshot.alt, phase: \"runtime\" });\n }\n if (docs.custom) {\n entries.push({ kind: \"custom\", type: docs.custom.type, data: docs.custom.data, phase: \"runtime\" });\n }\n\n return entries;\n}\n\n// ============================================================================\n// Helper to attach doc entry to current step or story-level\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// Step Markers\n// ============================================================================\n\nfunction createStepMarker(keyword: StepKeyword) {\n function stepMarker(text: string, docs?: StoryDocs): void;\n function stepMarker<T>(text: string, body: () => T): T;\n function stepMarker<T>(text: string, docsOrBody?: StoryDocs | (() => 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\n if (!isCallback) return;\n\n const body = docsOrBody as () => T;\n const start = performance.now();\n\n try {\n const result = body();\n if (result instanceof Promise) {\n return result.then(\n (val) => { step.durationMs = performance.now() - start; return val; },\n (err) => { step.durationMs = performance.now() - start; throw err; },\n ) as T;\n }\n step.durationMs = performance.now() - start;\n return result;\n } catch (err) {\n step.durationMs = performance.now() - start;\n throw err;\n }\n }\n return stepMarker;\n}\n\n// ============================================================================\n// story.init() - Jest-specific\n// ============================================================================\n\n/**\n * Initialize a story for the current test.\n * Must be called at the start of each test that wants documentation.\n *\n * @param options - Optional story configuration (tags, ticket, meta)\n *\n * @example\n * ```ts\n * it('adds two numbers', () => {\n * story.init();\n * // ... rest of test\n * });\n * ```\n */\nfunction init(options?: StoryOptions): void {\n // Get current test info from Jest globals\n const state = expect.getState();\n const currentTestName = state.currentTestName || \"Unknown test\";\n const testPath = state.testPath || \"unknown\";\n\n const { suitePath, testName } = extractSuitePath(currentTestName);\n\n const meta: StoryMeta = {\n scenario: testName,\n steps: [],\n suitePath,\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', testName);\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 // Store in registry for this file\n const existing = storyRegistry.get(testPath);\n if (existing) {\n existing.push(meta);\n } else {\n storyRegistry.set(testPath, [meta]);\n }\n\n // Register exit handler to flush stories when worker exits\n registerExitHandler();\n\n // Set active context\n activeContext = {\n meta,\n currentStep: null,\n stepCounter: 0,\n attachments: [],\n activeTimers: new Map(),\n timerCounter: 0,\n };\n\n // Link attachments to the registry for this test file + scenario\n if (!attachmentRegistry.has(testPath)) {\n attachmentRegistry.set(testPath, new Map());\n }\n attachmentRegistry.get(testPath)!.set(meta.scenario, activeContext.attachments);\n}\n\n// ============================================================================\n// Step Wrappers: story.fn() and story.expect()\n// ============================================================================\n\n/**\n * Wrap a function body as a step. Records the step with timing and `wrapped: true`.\n * Supports both sync and async functions. Returns whatever the function returns.\n *\n * @param keyword - The BDD keyword (Given, When, Then, And, But)\n * @param text - Step description\n * @param body - The function to execute\n * @returns The return value of body (or a Promise of it if body is async)\n *\n * @example\n * ```ts\n * const data = story.fn('Given', 'setup data', () => ({ a: 5, b: 3 }));\n * const result = await story.fn('When', 'call API', async () => fetch('/api'));\n * ```\n */\nfunction fn<T>(keyword: StepKeyword, text: string, body: () => 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\n const step: StoryStep = {\n id: `step-${ctx.stepCounter++}`,\n keyword: resolvedKeyword,\n text,\n docs: [],\n wrapped: true,\n };\n\n ctx.meta.steps.push(step);\n ctx.currentStep = step;\n\n const start = performance.now();\n\n try {\n const result = body();\n\n // Handle async functions\n if (result instanceof Promise) {\n return result.then(\n (val) => {\n step.durationMs = performance.now() - start;\n return val;\n },\n (err) => {\n step.durationMs = performance.now() - start;\n throw err;\n },\n ) as T;\n }\n\n step.durationMs = performance.now() - start;\n return result;\n } catch (err) {\n step.durationMs = performance.now() - start;\n throw err;\n }\n}\n\n/**\n * Wrap an assertion as a Then step. Shorthand for `story.fn('Then', text, body)`.\n *\n * @param text - Step description\n * @param body - The assertion function to execute\n *\n * @example\n * ```ts\n * story.expect('the result is 8', () => { expect(result).toBe(8); });\n * ```\n */\nfunction storyExpect<T>(text: string, body: () => T): T {\n return fn('Then', text, body);\n}\n\n// ============================================================================\n// Export story object\n// ============================================================================\n\n/**\n * The main story API object for Jest.\n *\n * @example\n * ```ts\n * import { story } from 'executable-stories-jest';\n *\n * describe('Calculator', () => {\n * it('adds two numbers', () => {\n * story.init();\n *\n * story.given('two numbers 5 and 3');\n * const a = 5, 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 */\nexport const story = {\n // Jest-specific init\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 },\n\n tag(name: string | string[]): void {\n const names = Array.isArray(name) ? name : [name];\n attachDoc({ kind: \"tag\", names, phase: \"runtime\" });\n },\n\n kv(options: KvOptions): void {\n attachDoc({ kind: \"kv\", label: options.label, value: options.value, phase: \"runtime\" });\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 },\n\n code(options: CodeOptions): void {\n attachDoc({ kind: \"code\", label: options.label, content: options.content, lang: options.lang, phase: \"runtime\" });\n },\n\n table(options: TableOptions): void {\n attachDoc({ kind: \"table\", label: options.label, columns: options.columns, rows: options.rows, phase: \"runtime\" });\n },\n\n link(options: LinkOptions): void {\n attachDoc({ kind: \"link\", label: options.label, url: options.url, phase: \"runtime\" });\n },\n\n section(options: SectionOptions): void {\n attachDoc({ kind: \"section\", title: options.title, markdown: options.markdown, phase: \"runtime\" });\n },\n\n mermaid(options: MermaidOptions): void {\n attachDoc({ kind: \"mermaid\", code: options.code, title: options.title, phase: \"runtime\" });\n },\n\n screenshot(options: ScreenshotOptions): void {\n attachDoc({ kind: \"screenshot\", path: options.path, alt: options.alt, phase: \"runtime\" });\n },\n\n custom(options: CustomOptions): void {\n attachDoc({ kind: \"custom\", type: options.type, data: options.data, phase: \"runtime\" });\n },\n\n // Attachments\n attach(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 stepIndex: stepIndex !== undefined && stepIndex >= 0 ? stepIndex : undefined,\n stepId: ctx.currentStep?.id,\n });\n },\n\n // Step wrappers\n fn,\n expect: storyExpect,\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 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 },\n};\n\nexport type Story = typeof story;\n\n// ============================================================================\n// Internal exports for setup file\n// ============================================================================\n\n/**\n * Internal API for the setup file and tests. Not for public use.\n * @internal\n */\nexport const _internal = {\n flushStories,\n /** Clear active context (for tests that assert getContext() throws). */\n clearContext(): void {\n activeContext = null;\n },\n};\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/setup.ts","../src/story-api.ts"],"sourcesContent":["/**\n * Jest setup file for executable-stories.\n *\n * Add this to your Jest config's setupFilesAfterEnv:\n *\n * @example\n * ```js\n * // jest.config.js\n * export default {\n * setupFilesAfterEnv: ['jest-executable-stories/setup'],\n * reporters: [\n * 'default',\n * ['jest-executable-stories/reporter', { outputFile: 'docs/user-stories.md' }]\n * ],\n * };\n * ```\n */\n\nimport { afterAll } from \"@jest/globals\";\nimport { _internal } from \"./story-api\";\n\n// Register afterAll hook to flush stories at the file level\nafterAll(() => {\n _internal.flushStories();\n});\n","/**\n * Jest story.* API for executable-stories.\n *\n * Uses native Jest describe/it/test with opt-in documentation:\n *\n * @example\n * ```ts\n * import { story } from 'executable-stories-jest';\n *\n * describe('Calculator', () => {\n * it('adds two numbers', () => {\n * story.init();\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 * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport { createHash } from \"node:crypto\";\nimport { createRequire } from 'node:module';\nimport { tryGetActiveOtelContext, resolveTraceUrl } from 'executable-stories-formatters';\nimport type {\n DocEntry,\n StepKeyword,\n StoryDocs,\n StoryMeta,\n StoryOptions,\n StoryStep,\n ScopedAttachment,\n AttachmentOptions,\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// Story Context\n// ============================================================================\n\n/** Internal timer entry */\ninterface TimerEntry {\n start: number;\n stepIndex?: number;\n stepId?: string;\n consumed: boolean;\n}\n\ninterface StoryContext {\n /** The story metadata being built */\n meta: StoryMeta;\n /** The current step (for attaching docs) */\n currentStep: StoryStep | null;\n /** Deterministic step counter (resets per test case) */\n stepCounter: number;\n /** Collected attachments with step scope */\n attachments: ScopedAttachment[];\n /** Active timers keyed by token */\n activeTimers: Map<number, TimerEntry>;\n /** Monotonic timer token counter */\n timerCounter: number;\n /** Test file path for registry lookups */\n testPath: string;\n /** Index into the storyRegistry array for this scenario */\n scenarioIndex: number;\n}\n\n// ============================================================================\n// File-based story collection (works across Jest worker processes)\n// ============================================================================\n\n// Use globalThis to ensure the registry is shared across module instances\n// This is needed because Jest may load the setup file and test files as separate module instances\ndeclare global {\n // eslint-disable-next-line no-var\n var __jestExecutableStoriesRegistry: Map<string, StoryMeta[]> | undefined;\n // eslint-disable-next-line no-var\n var __jestExecutableStoriesExitHandler: boolean | undefined;\n}\n\n/** Stories collected during test execution, keyed by test file path */\nconst storyRegistry: Map<string, StoryMeta[]> = globalThis.__jestExecutableStoriesRegistry ??= new Map();\n\n/** Attachments collected per story, keyed by test file path → scenario index → attachments */\nconst attachmentRegistry = new Map<string, Map<number, ScopedAttachment[]>>();\n\n/** OTel spans collected per story, keyed by test file path → scenario index → spans */\nconst otelSpansRegistry = new Map<string, Map<number, ReadonlyArray<Record<string, unknown>>>>();\n\n/** Track if we've registered the process exit handler */\nlet exitHandlerRegistered = globalThis.__jestExecutableStoriesExitHandler ?? false;\n\n/** Get the output directory for story JSON files */\nfunction getOutputDir(): string {\n const baseDir = process.env.JEST_STORY_DOCS_DIR ?? \".jest-executable-stories\";\n return path.resolve(process.cwd(), baseDir);\n}\n\n/** Flush all collected stories to JSON files */\nfunction flushStories(): void {\n if (storyRegistry.size === 0) return;\n\n const workerId = process.env.JEST_WORKER_ID ?? \"0\";\n const outputDir = path.join(getOutputDir(), `worker-${workerId}`);\n fs.mkdirSync(outputDir, { recursive: true });\n\n for (const [testFilePath, scenarios] of storyRegistry) {\n if (!scenarios.length) continue;\n const hash = createHash(\"sha1\").update(testFilePath).digest(\"hex\").slice(0, 12);\n const baseName = testFilePath === \"unknown\" ? \"unknown\" : path.basename(testFilePath);\n const outFile = path.join(outputDir, `${baseName}.${hash}.json`);\n\n // Include attachments and otelSpans per scenario (keyed by index, not name)\n const fileAttachments = attachmentRegistry.get(testFilePath);\n const fileOtelSpans = otelSpansRegistry.get(testFilePath);\n const scenariosWithAttachments = scenarios.map((s, i) => ({\n ...s,\n _attachments: fileAttachments?.get(i) ?? [],\n ...(fileOtelSpans?.get(i) ? { _otelSpans: fileOtelSpans.get(i) } : {}),\n }));\n\n const payload = { testFilePath, scenarios: scenariosWithAttachments };\n fs.writeFileSync(outFile, JSON.stringify(payload, null, 2) + \"\\n\", \"utf8\");\n }\n storyRegistry.clear();\n attachmentRegistry.clear();\n otelSpansRegistry.clear();\n}\n\n/** Register process exit handler to flush stories (once per worker) */\nfunction registerExitHandler(): void {\n if (exitHandlerRegistered) return;\n exitHandlerRegistered = true;\n globalThis.__jestExecutableStoriesExitHandler = true;\n // Use 'exit' event - always fired when Node.js is about to exit\n // Note: Only sync operations work here, which is fine for fs.writeFileSync\n process.on(\"exit\", () => {\n flushStories();\n });\n}\n\n// ============================================================================\n// Jest-specific context\n// ============================================================================\n\n/** Active story context - set by story.init() */\nlet activeContext: StoryContext | 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() must be called first. Use: it('name', () => { story.init(); ... });\"\n );\n }\n return activeContext;\n}\n\n// ============================================================================\n// Helper Functions\n// ============================================================================\n\n/**\n * Normalize ticket option to array format.\n */\nfunction normalizeTickets(ticket: string | string[] | undefined): string[] | undefined {\n if (!ticket) return undefined;\n return Array.isArray(ticket) ? ticket : [ticket];\n}\n\n/**\n * Extract the suite path from Jest's currentTestName.\n * Jest's currentTestName is formatted as: \"describe1 > describe2 > test name\"\n */\nfunction extractSuitePath(currentTestName: string): { suitePath?: string[]; testName: string } {\n const parts = currentTestName.split(\" > \");\n if (parts.length <= 1) {\n return { testName: currentTestName };\n }\n const testName = parts[parts.length - 1];\n const suitePath = parts.slice(0, -1);\n return { suitePath, testName };\n}\n\n/**\n * Convert StoryDocs inline options to DocEntry array.\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({ kind: \"code\", label: docs.code.label, content: docs.code.content, lang: docs.code.lang, phase: \"runtime\" });\n }\n if (docs.json) {\n entries.push({ kind: \"code\", label: docs.json.label, content: JSON.stringify(docs.json.value, null, 2), lang: \"json\", phase: \"runtime\" });\n }\n if (docs.table) {\n entries.push({ kind: \"table\", label: docs.table.label, columns: docs.table.columns, rows: docs.table.rows, phase: \"runtime\" });\n }\n if (docs.link) {\n entries.push({ kind: \"link\", label: docs.link.label, url: docs.link.url, phase: \"runtime\" });\n }\n if (docs.section) {\n entries.push({ kind: \"section\", title: docs.section.title, markdown: docs.section.markdown, phase: \"runtime\" });\n }\n if (docs.mermaid) {\n entries.push({ kind: \"mermaid\", code: docs.mermaid.code, title: docs.mermaid.title, phase: \"runtime\" });\n }\n if (docs.screenshot) {\n entries.push({ kind: \"screenshot\", path: docs.screenshot.path, alt: docs.screenshot.alt, phase: \"runtime\" });\n }\n if (docs.custom) {\n entries.push({ kind: \"custom\", type: docs.custom.type, data: docs.custom.data, phase: \"runtime\" });\n }\n\n return entries;\n}\n\n// ============================================================================\n// Helper to attach doc entry to current step or story-level\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// Step Markers\n// ============================================================================\n\nfunction createStepMarker(keyword: StepKeyword) {\n function stepMarker(text: string, docs?: StoryDocs): void;\n function stepMarker<T>(text: string, body: () => T): T;\n function stepMarker<T>(text: string, docsOrBody?: StoryDocs | (() => 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\n if (!isCallback) return;\n\n const body = docsOrBody as () => T;\n const start = performance.now();\n\n try {\n const result = body();\n if (result instanceof Promise) {\n return result.then(\n (val) => { step.durationMs = performance.now() - start; return val; },\n (err) => { step.durationMs = performance.now() - start; throw err; },\n ) as T;\n }\n step.durationMs = performance.now() - start;\n return result;\n } catch (err) {\n step.durationMs = performance.now() - start;\n throw err;\n }\n }\n return stepMarker;\n}\n\n// ============================================================================\n// story.init() - Jest-specific\n// ============================================================================\n\n/**\n * Initialize a story for the current test.\n * Must be called at the start of each test that wants documentation.\n *\n * @param options - Optional story configuration (tags, ticket, meta)\n *\n * @example\n * ```ts\n * it('adds two numbers', () => {\n * story.init();\n * // ... rest of test\n * });\n * ```\n */\nfunction init(options?: StoryOptions): void {\n // Get current test info from Jest globals\n const state = expect.getState();\n const currentTestName = state.currentTestName || \"Unknown test\";\n const testPath = state.testPath || \"unknown\";\n\n const { suitePath, testName } = extractSuitePath(currentTestName);\n\n const meta: StoryMeta = {\n scenario: testName,\n steps: [],\n suitePath,\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', testName);\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 // Store in registry for this file\n const existing = storyRegistry.get(testPath);\n let scenarioIndex: number;\n if (existing) {\n scenarioIndex = existing.length;\n existing.push(meta);\n } else {\n scenarioIndex = 0;\n storyRegistry.set(testPath, [meta]);\n }\n\n // Register exit handler to flush stories when worker exits\n registerExitHandler();\n\n // Set active context\n activeContext = {\n meta,\n currentStep: null,\n stepCounter: 0,\n attachments: [],\n activeTimers: new Map(),\n timerCounter: 0,\n testPath,\n scenarioIndex,\n };\n\n // Link attachments to the registry for this test file + scenario index\n if (!attachmentRegistry.has(testPath)) {\n attachmentRegistry.set(testPath, new Map());\n }\n attachmentRegistry.get(testPath)!.set(scenarioIndex, activeContext.attachments);\n}\n\n// ============================================================================\n// Step Wrappers: story.fn() and story.expect()\n// ============================================================================\n\n/**\n * Wrap a function body as a step. Records the step with timing and `wrapped: true`.\n * Supports both sync and async functions. Returns whatever the function returns.\n *\n * @param keyword - The BDD keyword (Given, When, Then, And, But)\n * @param text - Step description\n * @param body - The function to execute\n * @returns The return value of body (or a Promise of it if body is async)\n *\n * @example\n * ```ts\n * const data = story.fn('Given', 'setup data', () => ({ a: 5, b: 3 }));\n * const result = await story.fn('When', 'call API', async () => fetch('/api'));\n * ```\n */\nfunction fn<T>(keyword: StepKeyword, text: string, body: () => 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\n const step: StoryStep = {\n id: `step-${ctx.stepCounter++}`,\n keyword: resolvedKeyword,\n text,\n docs: [],\n wrapped: true,\n };\n\n ctx.meta.steps.push(step);\n ctx.currentStep = step;\n\n const start = performance.now();\n\n try {\n const result = body();\n\n // Handle async functions\n if (result instanceof Promise) {\n return result.then(\n (val) => {\n step.durationMs = performance.now() - start;\n return val;\n },\n (err) => {\n step.durationMs = performance.now() - start;\n throw err;\n },\n ) as T;\n }\n\n step.durationMs = performance.now() - start;\n return result;\n } catch (err) {\n step.durationMs = performance.now() - start;\n throw err;\n }\n}\n\n/**\n * Wrap an assertion as a Then step. Shorthand for `story.fn('Then', text, body)`.\n *\n * @param text - Step description\n * @param body - The assertion function to execute\n *\n * @example\n * ```ts\n * story.expect('the result is 8', () => { expect(result).toBe(8); });\n * ```\n */\nfunction storyExpect<T>(text: string, body: () => T): T {\n return fn('Then', text, body);\n}\n\n// ============================================================================\n// Export story object\n// ============================================================================\n\n/**\n * The main story API object for Jest.\n *\n * @example\n * ```ts\n * import { story } from 'executable-stories-jest';\n *\n * describe('Calculator', () => {\n * it('adds two numbers', () => {\n * story.init();\n *\n * story.given('two numbers 5 and 3');\n * const a = 5, 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 */\nexport const story = {\n // Jest-specific init\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 },\n\n tag(name: string | string[]): void {\n const names = Array.isArray(name) ? name : [name];\n attachDoc({ kind: \"tag\", names, phase: \"runtime\" });\n },\n\n kv(options: KvOptions): void {\n attachDoc({ kind: \"kv\", label: options.label, value: options.value, phase: \"runtime\" });\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 },\n\n code(options: CodeOptions): void {\n attachDoc({ kind: \"code\", label: options.label, content: options.content, lang: options.lang, phase: \"runtime\" });\n },\n\n table(options: TableOptions): void {\n attachDoc({ kind: \"table\", label: options.label, columns: options.columns, rows: options.rows, phase: \"runtime\" });\n },\n\n link(options: LinkOptions): void {\n attachDoc({ kind: \"link\", label: options.label, url: options.url, phase: \"runtime\" });\n },\n\n section(options: SectionOptions): void {\n attachDoc({ kind: \"section\", title: options.title, markdown: options.markdown, phase: \"runtime\" });\n },\n\n mermaid(options: MermaidOptions): void {\n attachDoc({ kind: \"mermaid\", code: options.code, title: options.title, phase: \"runtime\" });\n },\n\n screenshot(options: ScreenshotOptions): void {\n attachDoc({ kind: \"screenshot\", path: options.path, alt: options.alt, phase: \"runtime\" });\n },\n\n custom(options: CustomOptions): void {\n attachDoc({ kind: \"custom\", type: options.type, data: options.data, phase: \"runtime\" });\n },\n\n // Attachments\n attach(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 stepIndex: stepIndex !== undefined && stepIndex >= 0 ? stepIndex : undefined,\n stepId: ctx.currentStep?.id,\n });\n },\n\n // OTel span attachment\n attachSpans(spans: ReadonlyArray<Record<string, unknown>>): void {\n const ctx = getContext();\n if (!otelSpansRegistry.has(ctx.testPath)) {\n otelSpansRegistry.set(ctx.testPath, new Map());\n }\n otelSpansRegistry.get(ctx.testPath)!.set(ctx.scenarioIndex, spans);\n },\n\n // Step wrappers\n fn,\n expect: storyExpect,\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 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 },\n};\n\nexport type Story = typeof story;\n\n// ============================================================================\n// Internal exports for setup file\n// ============================================================================\n\n/**\n * Internal API for the setup file and tests. Not for public use.\n * @internal\n */\nexport const _internal = {\n flushStories,\n /** Clear active context (for tests that assert getContext() throws). */\n clearContext(): void {\n activeContext = null;\n },\n};\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "executable-stories-jest",
3
- "version": "8.0.0",
3
+ "version": "8.1.0",
4
4
  "description": "BDD-style executable stories for Jest with documentation generation",
5
5
  "type": "module",
6
6
  "sideEffects": false,