executable-stories-jest 8.1.0 → 8.1.1

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.js CHANGED
@@ -56,7 +56,8 @@ function getContext() {
56
56
  }
57
57
  function normalizeTickets(ticket) {
58
58
  if (!ticket) return void 0;
59
- return Array.isArray(ticket) ? ticket : [ticket];
59
+ const arr = Array.isArray(ticket) ? ticket : [ticket];
60
+ return arr.map((t) => typeof t === "string" ? { id: t } : t);
60
61
  }
61
62
  function extractSuitePath(currentTestName) {
62
63
  const parts = currentTestName.split(" > ");
@@ -107,8 +108,17 @@ function convertStoryDocsToEntries(docs) {
107
108
  }
108
109
  return entries;
109
110
  }
110
- function attachDoc(entry) {
111
+ function attachDoc(entry, children) {
111
112
  const ctx = getContext();
113
+ if (children && children.length > 0) {
114
+ entry.children = children;
115
+ const childSet = new Set(children);
116
+ const filterDocs = (docs) => docs.filter((d) => !childSet.has(d));
117
+ ctx.meta.docs = filterDocs(ctx.meta.docs ?? []);
118
+ for (const step of ctx.meta.steps) {
119
+ if (step.docs) step.docs = filterDocs(step.docs);
120
+ }
121
+ }
112
122
  if (ctx.currentStep) {
113
123
  ctx.currentStep.docs ??= [];
114
124
  ctx.currentStep.docs.push(entry);
@@ -116,21 +126,41 @@ function attachDoc(entry) {
116
126
  ctx.meta.docs ??= [];
117
127
  ctx.meta.docs.push(entry);
118
128
  }
129
+ return entry;
119
130
  }
120
131
  function createStepMarker(keyword) {
121
132
  function stepMarker(text, docsOrBody) {
122
133
  const ctx = getContext();
123
134
  const isCallback = typeof docsOrBody === "function";
135
+ const isChildrenArray = Array.isArray(docsOrBody);
124
136
  const resolvedKeyword = (keyword === "Given" || keyword === "When" || keyword === "Then") && ctx.meta.steps.some((s) => s.keyword === keyword) ? "And" : keyword;
137
+ let stepDocs = [];
138
+ if (!isCallback && !isChildrenArray && docsOrBody) {
139
+ stepDocs = convertStoryDocsToEntries(docsOrBody);
140
+ }
125
141
  const step = {
126
142
  id: `step-${ctx.stepCounter++}`,
127
143
  keyword: resolvedKeyword,
128
144
  text,
129
- docs: !isCallback && docsOrBody ? convertStoryDocsToEntries(docsOrBody) : [],
145
+ docs: stepDocs,
130
146
  ...isCallback ? { wrapped: true } : {}
131
147
  };
132
148
  ctx.meta.steps.push(step);
133
149
  ctx.currentStep = step;
150
+ if (isChildrenArray) {
151
+ const children = docsOrBody;
152
+ if (children.length > 0) {
153
+ const childSet = new Set(children);
154
+ ctx.meta.docs = (ctx.meta.docs ?? []).filter((d) => !childSet.has(d));
155
+ for (const prevStep of ctx.meta.steps) {
156
+ if (prevStep !== step && prevStep.docs) {
157
+ prevStep.docs = prevStep.docs.filter((d) => !childSet.has(d));
158
+ }
159
+ }
160
+ step.docs = [...step.docs ?? [], ...children];
161
+ }
162
+ return;
163
+ }
134
164
  if (!isCallback) return;
135
165
  const body = docsOrBody;
136
166
  const start = performance.now();
@@ -191,7 +221,7 @@ function init(options) {
191
221
  if (options?.tags?.length) span.setAttribute("story.tags", options.tags);
192
222
  if (options?.ticket) {
193
223
  const tickets = Array.isArray(options.ticket) ? options.ticket : [options.ticket];
194
- span.setAttribute("story.tickets", tickets);
224
+ span.setAttribute("story.tickets", tickets.map((t) => typeof t === "string" ? t : t.id));
195
225
  }
196
226
  }
197
227
  } catch {
@@ -279,40 +309,40 @@ var story = {
279
309
  action: createStepMarker("When"),
280
310
  verify: createStepMarker("Then"),
281
311
  // Standalone doc methods
282
- note(text) {
283
- attachDoc({ kind: "note", text, phase: "runtime" });
312
+ note(text, children) {
313
+ return attachDoc({ kind: "note", text, phase: "runtime" }, children);
284
314
  },
285
- tag(name) {
315
+ tag(name, children) {
286
316
  const names = Array.isArray(name) ? name : [name];
287
- attachDoc({ kind: "tag", names, phase: "runtime" });
317
+ return attachDoc({ kind: "tag", names, phase: "runtime" }, children);
288
318
  },
289
- kv(options) {
290
- attachDoc({ kind: "kv", label: options.label, value: options.value, phase: "runtime" });
319
+ kv(options, children) {
320
+ return attachDoc({ kind: "kv", label: options.label, value: options.value, phase: "runtime" }, children);
291
321
  },
292
- json(options) {
322
+ json(options, children) {
293
323
  const content = JSON.stringify(options.value, null, 2);
294
- attachDoc({ kind: "code", label: options.label, content, lang: "json", phase: "runtime" });
324
+ return attachDoc({ kind: "code", label: options.label, content, lang: "json", phase: "runtime" }, children);
295
325
  },
296
- code(options) {
297
- attachDoc({ kind: "code", label: options.label, content: options.content, lang: options.lang, phase: "runtime" });
326
+ code(options, children) {
327
+ return attachDoc({ kind: "code", label: options.label, content: options.content, lang: options.lang, phase: "runtime" }, children);
298
328
  },
299
- table(options) {
300
- attachDoc({ kind: "table", label: options.label, columns: options.columns, rows: options.rows, phase: "runtime" });
329
+ table(options, children) {
330
+ return attachDoc({ kind: "table", label: options.label, columns: options.columns, rows: options.rows, phase: "runtime" }, children);
301
331
  },
302
- link(options) {
303
- attachDoc({ kind: "link", label: options.label, url: options.url, phase: "runtime" });
332
+ link(options, children) {
333
+ return attachDoc({ kind: "link", label: options.label, url: options.url, phase: "runtime" }, children);
304
334
  },
305
- section(options) {
306
- attachDoc({ kind: "section", title: options.title, markdown: options.markdown, phase: "runtime" });
335
+ section(options, children) {
336
+ return attachDoc({ kind: "section", title: options.title, markdown: options.markdown, phase: "runtime" }, children);
307
337
  },
308
- mermaid(options) {
309
- attachDoc({ kind: "mermaid", code: options.code, title: options.title, phase: "runtime" });
338
+ mermaid(options, children) {
339
+ return attachDoc({ kind: "mermaid", code: options.code, title: options.title, phase: "runtime" }, children);
310
340
  },
311
- screenshot(options) {
312
- attachDoc({ kind: "screenshot", path: options.path, alt: options.alt, phase: "runtime" });
341
+ screenshot(options, children) {
342
+ return attachDoc({ kind: "screenshot", path: options.path, alt: options.alt, phase: "runtime" }, children);
313
343
  },
314
- custom(options) {
315
- attachDoc({ kind: "custom", type: options.type, data: options.data, phase: "runtime" });
344
+ custom(options, children) {
345
+ return attachDoc({ kind: "custom", type: options.type, data: options.data, phase: "runtime" }, children);
316
346
  },
317
347
  // Attachments
318
348
  attach(options) {
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 /** 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":[]}
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 NormalizedTicket,\n StepKeyword,\n StoryDocs,\n StoryMeta,\n StoryOptions,\n StoryStep,\n ScopedAttachment,\n AttachmentOptions,\n TicketInput,\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 TicketInput,\n NormalizedTicket,\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 of NormalizedTicket objects.\n */\nfunction normalizeTickets(\n ticket: TicketInput | TicketInput[] | undefined,\n): NormalizedTicket[] | undefined {\n if (!ticket) return undefined;\n const arr = Array.isArray(ticket) ? ticket : [ticket];\n return arr.map((t) => (typeof t === 'string' ? { id: t } : t));\n}\n\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, children?: DocEntry[]): DocEntry {\n const ctx = getContext();\n if (children && children.length > 0) {\n entry.children = children;\n const childSet = new Set<DocEntry>(children);\n const filterDocs = (docs: DocEntry[]) => docs.filter((d) => !childSet.has(d));\n // Remove children from ALL containers (story-level + every step)\n ctx.meta.docs = filterDocs(ctx.meta.docs ?? []);\n for (const step of ctx.meta.steps) {\n if (step.docs) step.docs = filterDocs(step.docs);\n }\n }\n if (ctx.currentStep) {\n ctx.currentStep.docs ??= [];\n ctx.currentStep.docs.push(entry);\n } else {\n ctx.meta.docs ??= [];\n ctx.meta.docs.push(entry);\n }\n return entry;\n}\n\n// ============================================================================\n// Step Markers\n// ============================================================================\n\nfunction createStepMarker(keyword: StepKeyword) {\n function stepMarker(text: string, docs?: StoryDocs): void;\n function stepMarker(text: string, children: DocEntry[]): void;\n function stepMarker<T>(text: string, body: () => T): T;\n function stepMarker<T>(text: string, docsOrBody?: StoryDocs | DocEntry[] | (() => T)): T | void {\n const ctx = getContext();\n const isCallback = typeof docsOrBody === 'function';\n const isChildrenArray = Array.isArray(docsOrBody);\n\n const resolvedKeyword: StepKeyword =\n (keyword === 'Given' || keyword === 'When' || keyword === 'Then') &&\n ctx.meta.steps.some((s) => s.keyword === keyword)\n ? 'And'\n : keyword;\n\n let stepDocs: DocEntry[] = [];\n if (!isCallback && !isChildrenArray && docsOrBody) {\n stepDocs = convertStoryDocsToEntries(docsOrBody as StoryDocs);\n }\n\n const step: StoryStep = {\n id: `step-${ctx.stepCounter++}`,\n keyword: resolvedKeyword,\n text,\n docs: stepDocs,\n ...(isCallback ? { wrapped: true } : {}),\n };\n\n ctx.meta.steps.push(step);\n ctx.currentStep = step;\n\n // Handle DocEntry[] children: attach as step docs and deduplicate from story-level\n if (isChildrenArray) {\n const children = docsOrBody as DocEntry[];\n if (children.length > 0) {\n const childSet = new Set<DocEntry>(children);\n // Deduplicate from story-level docs\n ctx.meta.docs = (ctx.meta.docs ?? []).filter((d) => !childSet.has(d));\n // Deduplicate from step docs of earlier steps\n for (const prevStep of ctx.meta.steps) {\n if (prevStep !== step && prevStep.docs) {\n prevStep.docs = prevStep.docs.filter((d) => !childSet.has(d));\n }\n }\n step.docs = [...(step.docs ?? []), ...children];\n }\n return;\n }\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.map((t) => typeof t === 'string' ? t : t.id));\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, children?: DocEntry[]): DocEntry {\n return attachDoc({ kind: \"note\", text, phase: \"runtime\" }, children);\n },\n\n tag(name: string | string[], children?: DocEntry[]): DocEntry {\n const names = Array.isArray(name) ? name : [name];\n return attachDoc({ kind: \"tag\", names, phase: \"runtime\" }, children);\n },\n\n kv(options: KvOptions, children?: DocEntry[]): DocEntry {\n return attachDoc({ kind: \"kv\", label: options.label, value: options.value, phase: \"runtime\" }, children);\n },\n\n json(options: JsonOptions, children?: DocEntry[]): DocEntry {\n const content = JSON.stringify(options.value, null, 2);\n return attachDoc({ kind: \"code\", label: options.label, content, lang: \"json\", phase: \"runtime\" }, children);\n },\n\n code(options: CodeOptions, children?: DocEntry[]): DocEntry {\n return attachDoc({ kind: \"code\", label: options.label, content: options.content, lang: options.lang, phase: \"runtime\" }, children);\n },\n\n table(options: TableOptions, children?: DocEntry[]): DocEntry {\n return attachDoc({ kind: \"table\", label: options.label, columns: options.columns, rows: options.rows, phase: \"runtime\" }, children);\n },\n\n link(options: LinkOptions, children?: DocEntry[]): DocEntry {\n return attachDoc({ kind: \"link\", label: options.label, url: options.url, phase: \"runtime\" }, children);\n },\n\n section(options: SectionOptions, children?: DocEntry[]): DocEntry {\n return attachDoc({ kind: \"section\", title: options.title, markdown: options.markdown, phase: \"runtime\" }, children);\n },\n\n mermaid(options: MermaidOptions, children?: DocEntry[]): DocEntry {\n return attachDoc({ kind: \"mermaid\", code: options.code, title: options.title, phase: \"runtime\" }, children);\n },\n\n screenshot(options: ScreenshotOptions, children?: DocEntry[]): DocEntry {\n return attachDoc({ kind: \"screenshot\", path: options.path, alt: options.alt, phase: \"runtime\" }, children);\n },\n\n custom(options: CustomOptions, children?: DocEntry[]): DocEntry {\n return attachDoc({ kind: \"custom\", type: options.type, data: options.data, phase: \"runtime\" }, children);\n },\n\n // 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 NormalizedTicket,\n} from 'executable-stories-formatters';\n\nexport { STORY_META_KEY } from 'executable-stories-formatters';\n\n/** A ticket reference: either a plain string ID or an object with id and optional url */\nexport type TicketInput = string | { id: string; url?: string };\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?: TicketInput | TicketInput[];\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 NormalizedTicket,\n TicketInput,\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
@@ -84,7 +84,8 @@ function getContext() {
84
84
  }
85
85
  function normalizeTickets(ticket) {
86
86
  if (!ticket) return void 0;
87
- return Array.isArray(ticket) ? ticket : [ticket];
87
+ const arr = Array.isArray(ticket) ? ticket : [ticket];
88
+ return arr.map((t) => typeof t === "string" ? { id: t } : t);
88
89
  }
89
90
  function extractSuitePath(currentTestName) {
90
91
  const parts = currentTestName.split(" > ");
@@ -135,8 +136,17 @@ function convertStoryDocsToEntries(docs) {
135
136
  }
136
137
  return entries;
137
138
  }
138
- function attachDoc(entry) {
139
+ function attachDoc(entry, children) {
139
140
  const ctx = getContext();
141
+ if (children && children.length > 0) {
142
+ entry.children = children;
143
+ const childSet = new Set(children);
144
+ const filterDocs = (docs) => docs.filter((d) => !childSet.has(d));
145
+ ctx.meta.docs = filterDocs(ctx.meta.docs ?? []);
146
+ for (const step of ctx.meta.steps) {
147
+ if (step.docs) step.docs = filterDocs(step.docs);
148
+ }
149
+ }
140
150
  if (ctx.currentStep) {
141
151
  ctx.currentStep.docs ??= [];
142
152
  ctx.currentStep.docs.push(entry);
@@ -144,21 +154,41 @@ function attachDoc(entry) {
144
154
  ctx.meta.docs ??= [];
145
155
  ctx.meta.docs.push(entry);
146
156
  }
157
+ return entry;
147
158
  }
148
159
  function createStepMarker(keyword) {
149
160
  function stepMarker(text, docsOrBody) {
150
161
  const ctx = getContext();
151
162
  const isCallback = typeof docsOrBody === "function";
163
+ const isChildrenArray = Array.isArray(docsOrBody);
152
164
  const resolvedKeyword = (keyword === "Given" || keyword === "When" || keyword === "Then") && ctx.meta.steps.some((s) => s.keyword === keyword) ? "And" : keyword;
165
+ let stepDocs = [];
166
+ if (!isCallback && !isChildrenArray && docsOrBody) {
167
+ stepDocs = convertStoryDocsToEntries(docsOrBody);
168
+ }
153
169
  const step = {
154
170
  id: `step-${ctx.stepCounter++}`,
155
171
  keyword: resolvedKeyword,
156
172
  text,
157
- docs: !isCallback && docsOrBody ? convertStoryDocsToEntries(docsOrBody) : [],
173
+ docs: stepDocs,
158
174
  ...isCallback ? { wrapped: true } : {}
159
175
  };
160
176
  ctx.meta.steps.push(step);
161
177
  ctx.currentStep = step;
178
+ if (isChildrenArray) {
179
+ const children = docsOrBody;
180
+ if (children.length > 0) {
181
+ const childSet = new Set(children);
182
+ ctx.meta.docs = (ctx.meta.docs ?? []).filter((d) => !childSet.has(d));
183
+ for (const prevStep of ctx.meta.steps) {
184
+ if (prevStep !== step && prevStep.docs) {
185
+ prevStep.docs = prevStep.docs.filter((d) => !childSet.has(d));
186
+ }
187
+ }
188
+ step.docs = [...step.docs ?? [], ...children];
189
+ }
190
+ return;
191
+ }
162
192
  if (!isCallback) return;
163
193
  const body = docsOrBody;
164
194
  const start = performance.now();
@@ -219,7 +249,7 @@ function init(options) {
219
249
  if (options?.tags?.length) span.setAttribute("story.tags", options.tags);
220
250
  if (options?.ticket) {
221
251
  const tickets = Array.isArray(options.ticket) ? options.ticket : [options.ticket];
222
- span.setAttribute("story.tickets", tickets);
252
+ span.setAttribute("story.tickets", tickets.map((t) => typeof t === "string" ? t : t.id));
223
253
  }
224
254
  }
225
255
  } catch {
@@ -307,40 +337,40 @@ var story = {
307
337
  action: createStepMarker("When"),
308
338
  verify: createStepMarker("Then"),
309
339
  // Standalone doc methods
310
- note(text) {
311
- attachDoc({ kind: "note", text, phase: "runtime" });
340
+ note(text, children) {
341
+ return attachDoc({ kind: "note", text, phase: "runtime" }, children);
312
342
  },
313
- tag(name) {
343
+ tag(name, children) {
314
344
  const names = Array.isArray(name) ? name : [name];
315
- attachDoc({ kind: "tag", names, phase: "runtime" });
345
+ return attachDoc({ kind: "tag", names, phase: "runtime" }, children);
316
346
  },
317
- kv(options) {
318
- attachDoc({ kind: "kv", label: options.label, value: options.value, phase: "runtime" });
347
+ kv(options, children) {
348
+ return attachDoc({ kind: "kv", label: options.label, value: options.value, phase: "runtime" }, children);
319
349
  },
320
- json(options) {
350
+ json(options, children) {
321
351
  const content = JSON.stringify(options.value, null, 2);
322
- attachDoc({ kind: "code", label: options.label, content, lang: "json", phase: "runtime" });
352
+ return attachDoc({ kind: "code", label: options.label, content, lang: "json", phase: "runtime" }, children);
323
353
  },
324
- code(options) {
325
- attachDoc({ kind: "code", label: options.label, content: options.content, lang: options.lang, phase: "runtime" });
354
+ code(options, children) {
355
+ return attachDoc({ kind: "code", label: options.label, content: options.content, lang: options.lang, phase: "runtime" }, children);
326
356
  },
327
- table(options) {
328
- attachDoc({ kind: "table", label: options.label, columns: options.columns, rows: options.rows, phase: "runtime" });
357
+ table(options, children) {
358
+ return attachDoc({ kind: "table", label: options.label, columns: options.columns, rows: options.rows, phase: "runtime" }, children);
329
359
  },
330
- link(options) {
331
- attachDoc({ kind: "link", label: options.label, url: options.url, phase: "runtime" });
360
+ link(options, children) {
361
+ return attachDoc({ kind: "link", label: options.label, url: options.url, phase: "runtime" }, children);
332
362
  },
333
- section(options) {
334
- attachDoc({ kind: "section", title: options.title, markdown: options.markdown, phase: "runtime" });
363
+ section(options, children) {
364
+ return attachDoc({ kind: "section", title: options.title, markdown: options.markdown, phase: "runtime" }, children);
335
365
  },
336
- mermaid(options) {
337
- attachDoc({ kind: "mermaid", code: options.code, title: options.title, phase: "runtime" });
366
+ mermaid(options, children) {
367
+ return attachDoc({ kind: "mermaid", code: options.code, title: options.title, phase: "runtime" }, children);
338
368
  },
339
- screenshot(options) {
340
- attachDoc({ kind: "screenshot", path: options.path, alt: options.alt, phase: "runtime" });
369
+ screenshot(options, children) {
370
+ return attachDoc({ kind: "screenshot", path: options.path, alt: options.alt, phase: "runtime" }, children);
341
371
  },
342
- custom(options) {
343
- attachDoc({ kind: "custom", type: options.type, data: options.data, phase: "runtime" });
372
+ custom(options, children) {
373
+ return attachDoc({ kind: "custom", type: options.type, data: options.data, phase: "runtime" }, children);
344
374
  },
345
375
  // Attachments
346
376
  attach(options) {