executable-stories-vitest 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.cjs CHANGED
@@ -67,7 +67,8 @@ function extractSuitePath(task) {
67
67
  }
68
68
  function normalizeTickets(ticket) {
69
69
  if (!ticket) return void 0;
70
- return Array.isArray(ticket) ? ticket : [ticket];
70
+ const arr = Array.isArray(ticket) ? ticket : [ticket];
71
+ return arr.map((t) => typeof t === "string" ? { id: t } : t);
71
72
  }
72
73
  function convertStoryDocsToEntries(docs) {
73
74
  const entries = [];
@@ -182,7 +183,7 @@ function init(task, options) {
182
183
  if (options?.tags?.length) span.setAttribute("story.tags", options.tags);
183
184
  if (options?.ticket) {
184
185
  const tickets = Array.isArray(options.ticket) ? options.ticket : [options.ticket];
185
- span.setAttribute("story.tickets", tickets);
186
+ span.setAttribute("story.tickets", tickets.map((t) => typeof t === "string" ? t : t.id));
186
187
  }
187
188
  }
188
189
  } catch {
@@ -203,17 +204,37 @@ function createStepMarker(keyword) {
203
204
  function stepMarker(text, docsOrBody) {
204
205
  const ctx = getContext();
205
206
  const isCallback = typeof docsOrBody === "function";
207
+ const isChildrenArray = Array.isArray(docsOrBody);
206
208
  const resolvedKeyword = (keyword === "Given" || keyword === "When" || keyword === "Then") && ctx.meta.steps.some((s) => s.keyword === keyword) ? "And" : keyword;
209
+ let stepDocs = [];
210
+ if (!isCallback && !isChildrenArray && docsOrBody) {
211
+ stepDocs = convertStoryDocsToEntries(docsOrBody);
212
+ }
207
213
  const step = {
208
214
  id: `step-${ctx.stepCounter++}`,
209
215
  keyword: resolvedKeyword,
210
216
  text,
211
- docs: !isCallback && docsOrBody ? convertStoryDocsToEntries(docsOrBody) : [],
217
+ docs: stepDocs,
212
218
  ...isCallback ? { wrapped: true } : {}
213
219
  };
214
220
  ctx.meta.steps.push(step);
215
221
  ctx.currentStep = step;
216
222
  syncMetaToTask();
223
+ if (isChildrenArray) {
224
+ const children = docsOrBody;
225
+ if (children.length > 0) {
226
+ const childSet = new Set(children);
227
+ ctx.meta.docs = (ctx.meta.docs ?? []).filter((d) => !childSet.has(d));
228
+ for (const prevStep of ctx.meta.steps) {
229
+ if (prevStep !== step && prevStep.docs) {
230
+ prevStep.docs = prevStep.docs.filter((d) => !childSet.has(d));
231
+ }
232
+ }
233
+ step.docs = [...step.docs ?? [], ...children];
234
+ }
235
+ syncMetaToTask();
236
+ return;
237
+ }
217
238
  if (!isCallback) return;
218
239
  const body = docsOrBody;
219
240
  const start = performance.now();
@@ -244,19 +265,20 @@ function createStepMarker(keyword) {
244
265
  }
245
266
  return stepMarker;
246
267
  }
247
- function note(text) {
248
- const ctx = getContext();
249
- const entry = { kind: "note", text, phase: "runtime" };
250
- if (ctx.currentStep) {
251
- ctx.currentStep.docs ??= [];
252
- ctx.currentStep.docs.push(entry);
253
- } else {
254
- ctx.meta.docs ??= [];
255
- ctx.meta.docs.push(entry);
256
- }
268
+ function note(text, children) {
269
+ return attachDoc({ kind: "note", text, phase: "runtime" }, children);
257
270
  }
258
- function attachDoc(entry) {
271
+ function attachDoc(entry, children) {
259
272
  const ctx = getContext();
273
+ if (children && children.length > 0) {
274
+ entry.children = children;
275
+ const childSet = new Set(children);
276
+ const filterDocs = (docs) => docs.filter((d) => !childSet.has(d));
277
+ ctx.meta.docs = filterDocs(ctx.meta.docs ?? []);
278
+ for (const step of ctx.meta.steps) {
279
+ if (step.docs) step.docs = filterDocs(step.docs);
280
+ }
281
+ }
260
282
  if (ctx.currentStep) {
261
283
  ctx.currentStep.docs ??= [];
262
284
  ctx.currentStep.docs.push(entry);
@@ -265,86 +287,87 @@ function attachDoc(entry) {
265
287
  ctx.meta.docs.push(entry);
266
288
  }
267
289
  syncMetaToTask();
290
+ return entry;
268
291
  }
269
- function kv(options) {
270
- attachDoc({
292
+ function kv(options, children) {
293
+ return attachDoc({
271
294
  kind: "kv",
272
295
  label: options.label,
273
296
  value: options.value,
274
297
  phase: "runtime"
275
- });
298
+ }, children);
276
299
  }
277
- function json(options) {
300
+ function json(options, children) {
278
301
  const content = JSON.stringify(options.value, null, 2);
279
- attachDoc({
302
+ return attachDoc({
280
303
  kind: "code",
281
304
  label: options.label,
282
305
  content,
283
306
  lang: "json",
284
307
  phase: "runtime"
285
- });
308
+ }, children);
286
309
  }
287
- function code(options) {
288
- attachDoc({
310
+ function code(options, children) {
311
+ return attachDoc({
289
312
  kind: "code",
290
313
  label: options.label,
291
314
  content: options.content,
292
315
  lang: options.lang,
293
316
  phase: "runtime"
294
- });
317
+ }, children);
295
318
  }
296
- function table(options) {
297
- attachDoc({
319
+ function table(options, children) {
320
+ return attachDoc({
298
321
  kind: "table",
299
322
  label: options.label,
300
323
  columns: options.columns,
301
324
  rows: options.rows,
302
325
  phase: "runtime"
303
- });
326
+ }, children);
304
327
  }
305
- function link(options) {
306
- attachDoc({
328
+ function link(options, children) {
329
+ return attachDoc({
307
330
  kind: "link",
308
331
  label: options.label,
309
332
  url: options.url,
310
333
  phase: "runtime"
311
- });
334
+ }, children);
312
335
  }
313
- function section(options) {
314
- attachDoc({
336
+ function section(options, children) {
337
+ return attachDoc({
315
338
  kind: "section",
316
339
  title: options.title,
317
340
  markdown: options.markdown,
318
341
  phase: "runtime"
319
- });
342
+ }, children);
320
343
  }
321
- function mermaid(options) {
322
- attachDoc({
344
+ function mermaid(options, children) {
345
+ return attachDoc({
323
346
  kind: "mermaid",
324
347
  code: options.code,
325
348
  title: options.title,
326
349
  phase: "runtime"
327
- });
350
+ }, children);
328
351
  }
329
- function screenshot(options) {
330
- attachDoc({
352
+ function screenshot(options, children) {
353
+ return attachDoc({
331
354
  kind: "screenshot",
332
355
  path: options.path,
333
356
  alt: options.alt,
334
357
  phase: "runtime"
335
- });
358
+ }, children);
336
359
  }
337
- function tag(name) {
360
+ function tag(name, children) {
338
361
  const names = Array.isArray(name) ? name : [name];
339
- attachDoc({ kind: "tag", names, phase: "runtime" });
362
+ return attachDoc({ kind: "tag", names, phase: "runtime" }, children);
340
363
  }
341
- function custom(options) {
342
- attachDoc({
364
+ function custom(options, children) {
365
+ return attachDoc({
343
366
  kind: "custom",
344
367
  type: options.type,
345
368
  data: options.data,
346
369
  phase: "runtime"
347
- });
370
+ }, children);
348
371
  }
349
372
  function attach(options) {
350
373
  const ctx = getContext();
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/story-api.ts","../src/types.ts"],"sourcesContent":["/**\n * executable-stories-vitest: Native Vitest story/given/when/then with Markdown doc generation.\n *\n * Uses native Vitest describe/it/test for full IDE support:\n *\n * @example\n * ```ts\n * import { describe, it, expect } from 'vitest';\n * import { story } from 'executable-stories-vitest';\n *\n * describe('Calculator', () => {\n * it('adds two numbers', ({ task }) => {\n * story.init(task);\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 *\n * In vitest.config, import StoryReporter from \"executable-stories-vitest/reporter\":\n *\n * @example\n * ```ts\n * import { defineConfig } from \"vitest/config\";\n * import { StoryReporter } from \"executable-stories-vitest/reporter\";\n *\n * export default defineConfig({\n * test: {\n * reporters: [\"default\", new StoryReporter()],\n * },\n * });\n * ```\n */\n\n// Core API\nexport { story, type Story } from './story-api';\n\n// Types for consumers\nexport type {\n StoryMeta,\n StoryStep,\n DocEntry,\n StepKeyword,\n StepMode,\n DocPhase,\n StoryDocs,\n StoryOptions,\n VitestTask,\n VitestSuite,\n} from './types';\n\nexport { STORY_META_KEY } from './types';\n\n// Reporter types (actual reporter is in /reporter subpath)\nexport type {\n StoryReporterOptions,\n OutputFormat,\n OutputMode,\n ColocatedStyle,\n OutputRule,\n FormatterOptions,\n} from './reporter';\n\nconst STORY_REPORTER_GUARD_MSG =\n 'Do not import StoryReporter from \"executable-stories-vitest\". In vitest.config, import it from \"executable-stories-vitest/reporter\".';\n\n/** @internal Guard: throws if used. Import StoryReporter from \"executable-stories-vitest/reporter\" in vitest.config. */\nexport class StoryReporter {\n static __isGuard = true;\n constructor() {\n throw new Error(STORY_REPORTER_GUARD_MSG);\n }\n}\n","/**\n * story.* API for executable-stories-vitest.\n *\n * Uses native Vitest describe/it/test with opt-in documentation:\n *\n * @example\n * ```ts\n * import { describe, it, expect } from 'vitest';\n * import { story } from 'executable-stories-vitest';\n *\n * describe('Calculator', () => {\n * it('adds two numbers', ({ task }) => {\n * story.init(task);\n *\n * story.given('two numbers 5 and 3');\n * const a = 5;\n * const b = 3;\n *\n * story.when('I add them together');\n * const result = a + b;\n *\n * story.then('the result is 8');\n * expect(result).toBe(8);\n * });\n * });\n * ```\n */\n\nimport { createRequire } from 'node:module';\nimport {\n tryGetActiveOtelContext,\n resolveTraceUrl,\n} from 'executable-stories-formatters';\nimport type {\n DocEntry,\n StepKeyword,\n StoryDocs,\n StoryMeta,\n StoryOptions,\n StoryStep,\n VitestSuite,\n} from './types';\n\n// ============================================================================\n// Task Interface (compatible with Vitest's actual task type)\n// ============================================================================\n\n/**\n * Minimal task interface compatible with Vitest's Test type.\n * The meta property accepts any object type to be compatible with Vitest's TaskMeta.\n */\ninterface TaskLike {\n name: string;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n meta: any;\n suite?: VitestSuite;\n file?: { name?: string };\n}\n\n// ============================================================================\n// Story Context\n// ============================================================================\n\n/** Attachment options for story.attach() */\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 */\ninterface ScopedAttachment extends AttachmentOptions {\n stepIndex?: number;\n stepId?: string;\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 /** Reference to task.meta for updates */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n taskMeta: any;\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/** 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(task) must be called first. Use: it('name', ({ task }) => { story.init(task); ... });\",\n );\n }\n return activeContext;\n}\n\n/** Re-attach current meta to task.meta.story so reporter sees steps and docs (e.g. story.note). */\nfunction syncMetaToTask(): void {\n if (activeContext?.taskMeta) {\n activeContext.taskMeta.story = activeContext.meta;\n }\n}\n\n// ============================================================================\n// Helper Functions\n// ============================================================================\n\n/**\n * Check if a name looks like a file path (to filter out from suite paths).\n */\nfunction looksLikeFilePath(name: string): boolean {\n if (name.includes('/') || name.includes('\\\\')) return true;\n if (name.includes('.spec.') || name.includes('.test.')) return true;\n if (/\\.(spec|test)\\.(ts|js|mjs|cjs)$/.test(name)) return true;\n if (/\\.(ts|js|mjs|cjs)$/.test(name)) return true;\n return false;\n}\n\n/**\n * Extract the suite path (parent describe names) from a Vitest task object.\n */\nfunction extractSuitePath(task: TaskLike): string[] | undefined {\n const path: string[] = [];\n const fileName = task.file?.name;\n let current: VitestSuite | undefined = task.suite;\n\n while (current) {\n const name = current.name;\n if (\n name &&\n name.trim() !== '' &&\n name !== '<root>' &&\n name !== fileName &&\n !looksLikeFilePath(name)\n ) {\n path.unshift(name);\n }\n current = current.suite;\n }\n\n return path.length > 0 ? path : undefined;\n}\n\n/**\n * Normalize ticket option to array format.\n */\nfunction normalizeTickets(\n ticket: string | string[] | undefined,\n): string[] | undefined {\n if (!ticket) return undefined;\n return Array.isArray(ticket) ? ticket : [ticket];\n}\n\n/**\n * Convert StoryDocs inline options to DocEntry array.\n * Matches the standalone DocApi method signatures.\n */\nfunction convertStoryDocsToEntries(docs: StoryDocs): DocEntry[] {\n const entries: DocEntry[] = [];\n\n // note(text)\n if (docs.note) {\n entries.push({ kind: 'note', text: docs.note, phase: 'runtime' });\n }\n\n // tag(name | names)\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\n // kv(label, value) - multiple pairs via Record\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\n // code(label, content, lang?)\n if (docs.code) {\n entries.push({\n kind: 'code',\n label: docs.code.label,\n content: docs.code.content,\n lang: docs.code.lang,\n phase: 'runtime',\n });\n }\n\n // json(label, value)\n if (docs.json) {\n entries.push({\n kind: 'code',\n label: docs.json.label,\n content: JSON.stringify(docs.json.value, null, 2),\n lang: 'json',\n phase: 'runtime',\n });\n }\n\n // table(label, columns, rows)\n if (docs.table) {\n entries.push({\n kind: 'table',\n label: docs.table.label,\n columns: docs.table.columns,\n rows: docs.table.rows,\n phase: 'runtime',\n });\n }\n\n // link(label, url)\n if (docs.link) {\n entries.push({\n kind: 'link',\n label: docs.link.label,\n url: docs.link.url,\n phase: 'runtime',\n });\n }\n\n // section(title, markdown)\n if (docs.section) {\n entries.push({\n kind: 'section',\n title: docs.section.title,\n markdown: docs.section.markdown,\n phase: 'runtime',\n });\n }\n\n // mermaid(code, title?)\n if (docs.mermaid) {\n entries.push({\n kind: 'mermaid',\n code: docs.mermaid.code,\n title: docs.mermaid.title,\n phase: 'runtime',\n });\n }\n\n // screenshot(path, alt?)\n if (docs.screenshot) {\n entries.push({\n kind: 'screenshot',\n path: docs.screenshot.path,\n alt: docs.screenshot.alt,\n phase: 'runtime',\n });\n }\n\n // custom(type, data)\n if (docs.custom) {\n entries.push({\n kind: 'custom',\n type: docs.custom.type,\n data: docs.custom.data,\n phase: 'runtime',\n });\n }\n\n return entries;\n}\n\n// ============================================================================\n// story.init()\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 task - The Vitest task object from ({ task }) => { ... }\n * @param options - Optional story configuration (tags, ticket, meta)\n *\n * @example\n * ```ts\n * it('adds two numbers', ({ task }) => {\n * story.init(task);\n * // ... rest of test\n * });\n *\n * // With options:\n * it('admin deletes user', ({ task }) => {\n * story.init(task, {\n * tags: ['admin', 'destructive'],\n * ticket: 'JIRA-456'\n * });\n * });\n * ```\n */\nfunction init(task: TaskLike, options?: StoryOptions): void {\n const meta: StoryMeta = {\n scenario: task.name,\n steps: [],\n suitePath: extractSuitePath(task),\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', task.name);\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 // Attach to task.meta so reporter can find it\n task.meta.story = meta;\n\n // Set active context\n activeContext = {\n meta,\n currentStep: null,\n taskMeta: task.meta,\n stepCounter: 0,\n attachments: [],\n activeTimers: new Map(),\n timerCounter: 0,\n };\n}\n\n// ============================================================================\n// Step Markers\n// ============================================================================\n\n/**\n * Create a step marker function for a given keyword.\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 syncMetaToTask();\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; syncMetaToTask(); return val; },\n (err) => { step.durationMs = performance.now() - start; syncMetaToTask(); throw err; },\n ) as T;\n }\n step.durationMs = performance.now() - start;\n syncMetaToTask();\n return result;\n } catch (err) {\n step.durationMs = performance.now() - start;\n syncMetaToTask();\n throw err;\n }\n }\n return stepMarker;\n}\n\n// ============================================================================\n// Doc Methods (Standalone)\n// ============================================================================\n\n/**\n * Add a free-text note to the current step or story-level if before any step.\n */\nfunction note(text: string): void {\n const ctx = getContext();\n const entry: DocEntry = { kind: 'note', text, phase: 'runtime' };\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}\n\n// ============================================================================\n// Doc Method Types (shared between standalone and inline)\n// ============================================================================\n\n/** Options for kv() - key-value pair */\ninterface KvOptions {\n label: string;\n value: unknown;\n}\n\n/** Options for json() - JSON code block */\ninterface JsonOptions {\n label: string;\n value: unknown;\n}\n\n/** Options for code() - code block with optional language */\ninterface CodeOptions {\n label: string;\n content: string;\n lang?: string;\n}\n\n/** Options for table() - markdown table */\ninterface TableOptions {\n label: string;\n columns: string[];\n rows: string[][];\n}\n\n/** Options for link() - hyperlink */\ninterface LinkOptions {\n label: string;\n url: string;\n}\n\n/** Options for section() - titled markdown section */\ninterface SectionOptions {\n title: string;\n markdown: string;\n}\n\n/** Options for mermaid() - Mermaid diagram */\ninterface MermaidOptions {\n code: string;\n title?: string;\n}\n\n/** Options for screenshot() - screenshot reference */\ninterface ScreenshotOptions {\n path: string;\n alt?: string;\n}\n\n/** Options for custom() - custom doc entry */\ninterface CustomOptions {\n type: string;\n data: unknown;\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 syncMetaToTask();\n}\n\n// ============================================================================\n// Doc Methods (Standalone) - same shape as inline docs\n// ============================================================================\n\n/**\n * Add a key-value pair to the current step or story-level.\n * @example story.kv({ label: 'Payment ID', value: 'pay_123' })\n */\nfunction kv(options: KvOptions): void {\n attachDoc({\n kind: 'kv',\n label: options.label,\n value: options.value,\n phase: 'runtime',\n });\n}\n\n/**\n * Add a JSON code block to the current step or story-level.\n * @example story.json({ label: 'Order', value: { id: 123 } })\n */\nfunction json(options: JsonOptions): void {\n const content = JSON.stringify(options.value, null, 2);\n attachDoc({\n kind: 'code',\n label: options.label,\n content,\n lang: 'json',\n phase: 'runtime',\n });\n}\n\n/**\n * Add a code block with optional language to the current step or story-level.\n * @example story.code({ label: 'Config', content: 'port: 3000', lang: 'yaml' })\n */\nfunction code(options: CodeOptions): void {\n attachDoc({\n kind: 'code',\n label: options.label,\n content: options.content,\n lang: options.lang,\n phase: 'runtime',\n });\n}\n\n/**\n * Add a markdown table to the current step or story-level.\n * @example story.table({ label: 'Users', columns: ['Name', 'Role'], rows: [['Alice', 'Admin']] })\n */\nfunction table(options: TableOptions): void {\n attachDoc({\n kind: 'table',\n label: options.label,\n columns: options.columns,\n rows: options.rows,\n phase: 'runtime',\n });\n}\n\n/**\n * Add a hyperlink to the current step or story-level.\n * @example story.link({ label: 'API Docs', url: 'https://docs.example.com' })\n */\nfunction link(options: LinkOptions): void {\n attachDoc({\n kind: 'link',\n label: options.label,\n url: options.url,\n phase: 'runtime',\n });\n}\n\n/**\n * Add a titled section with markdown content to the current step or story-level.\n * @example story.section({ title: 'Details', markdown: 'This is **important**' })\n */\nfunction section(options: SectionOptions): void {\n attachDoc({\n kind: 'section',\n title: options.title,\n markdown: options.markdown,\n phase: 'runtime',\n });\n}\n\n/**\n * Add a Mermaid diagram to the current step or story-level.\n * @example story.mermaid({ code: 'graph LR; A-->B', title: 'Flow' })\n */\nfunction mermaid(options: MermaidOptions): void {\n attachDoc({\n kind: 'mermaid',\n code: options.code,\n title: options.title,\n phase: 'runtime',\n });\n}\n\n/**\n * Add a screenshot reference to the current step or story-level.\n * @example story.screenshot({ path: '/screenshots/result.png', alt: 'Final result' })\n */\nfunction screenshot(options: ScreenshotOptions): void {\n attachDoc({\n kind: 'screenshot',\n path: options.path,\n alt: options.alt,\n phase: 'runtime',\n });\n}\n\n/**\n * Add tag(s) to the current step or story-level.\n * @example story.tag('admin') or story.tag(['admin', 'security'])\n */\nfunction tag(name: string | string[]): void {\n const names = Array.isArray(name) ? name : [name];\n attachDoc({ kind: 'tag', names, phase: 'runtime' });\n}\n\n/**\n * Add a custom documentation entry for use with custom renderers.\n * @example story.custom({ type: 'myType', data: { foo: 'bar' } })\n */\nfunction custom(options: CustomOptions): void {\n attachDoc({\n kind: 'custom',\n type: options.type,\n data: options.data,\n phase: 'runtime',\n });\n}\n\n// ============================================================================\n// Attachments\n// ============================================================================\n\n/**\n * Attach a file or inline content to the current step or test case.\n * @example story.attach({ name: 'screenshot', mediaType: 'image/png', path: '/tmp/screenshot.png' })\n */\nfunction 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 // Store attachments on task.meta so reporter can read them\n if (ctx.taskMeta) {\n ctx.taskMeta.storyAttachments = ctx.attachments;\n }\n}\n\n// ============================================================================\n// Step Timing\n// ============================================================================\n\n/**\n * Start a timer for the current step. Returns a token to pass to endTimer().\n */\nfunction 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/**\n * End a timer and record duration on the step that was active when startTimer() was called.\n */\nfunction 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 syncMetaToTask();\n }\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 syncMetaToTask();\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 syncMetaToTask();\n return val;\n },\n (err) => {\n step.durationMs = performance.now() - start;\n syncMetaToTask();\n throw err;\n },\n ) as T;\n }\n\n step.durationMs = performance.now() - start;\n syncMetaToTask();\n return result;\n } catch (err) {\n step.durationMs = performance.now() - start;\n syncMetaToTask();\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 * await story.expect('async check', async () => { ... });\n * ```\n */\nfunction storyExpect<T>(text: string, body: () => T): T {\n return fn('Then', text, body);\n}\n\n// ============================================================================\n// Span attachment: story.attachSpans()\n// ============================================================================\n\n/**\n * Attach OTel spans to the current test so the StoryReporter renders them\n * as a trace waterfall in HTML reports.\n *\n * Accepts any array of objects with at least `spanId` and `name` fields.\n * Structurally compatible with autotel's `SerializedSpan` and the\n * `OtelSpan` type from executable-stories-formatters.\n *\n * @example\n * ```ts\n * import { serializeSpan } from 'autotel/test-span-collector';\n *\n * // After running code that creates spans:\n * const spans = exporter.getFinishedSpans().map(serializeSpan);\n * story.attachSpans(spans);\n * ```\n */\nfunction attachSpans(spans: ReadonlyArray<Record<string, unknown>>): void {\n const ctx = getContext();\n if (ctx.taskMeta) {\n ctx.taskMeta.otelSpans = spans;\n }\n}\n\n// ============================================================================\n// Export story object\n// ============================================================================\n\n/**\n * The main story API object.\n *\n * Use with native Vitest describe/it/test for full IDE support:\n *\n * @example\n * ```ts\n * import { describe, it, expect } from 'vitest';\n * import { story } from 'executable-stories-vitest';\n *\n * describe('Calculator', () => {\n * it('adds two numbers', ({ task }) => {\n * story.init(task);\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 // Core\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,\n kv,\n json,\n code,\n table,\n link,\n section,\n mermaid,\n screenshot,\n tag,\n custom,\n\n // Attachments\n attach,\n\n // OTel span attachment\n attachSpans,\n\n // Step wrappers\n fn,\n expect: storyExpect,\n\n // Step timing\n startTimer,\n endTimer,\n};\n\nexport type Story = typeof story;\n","/**\n * Type definitions for executable-stories-vitest.\n *\n * Shared story types (StepKeyword, DocEntry, StoryStep, StoryMeta, etc.)\n * are imported from executable-stories-formatters — the single source of truth.\n * This module re-exports them and adds Vitest-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// Vitest-specific Types\n// ============================================================================\n\n/**\n * Inline documentation options for step markers.\n * Pass to story.given(), story.when(), story.then() as second argument.\n *\n * @example\n * ```ts\n * story.given('valid credentials', {\n * json: { label: 'Credentials', value: { email: 'test@example.com', password: '***' } },\n * note: 'Password is masked for security'\n * });\n * ```\n */\nexport interface StoryDocs {\n /** Add a free-text note */\n note?: string;\n /** Add tag(s) for categorization */\n tag?: string | string[];\n /** Add key-value pairs */\n kv?: Record<string, unknown>;\n /** Add a code block with label and optional language */\n code?: { label: string; content: string; lang?: string };\n /** Add a JSON data block with label */\n json?: { label: string; value: unknown };\n /** Add a markdown table with label */\n table?: { label: string; columns: string[]; rows: string[][] };\n /** Add a hyperlink */\n link?: { label: string; url: string };\n /** Add a titled section with markdown content */\n section?: { title: string; markdown: string };\n /** Add a Mermaid diagram with optional title */\n mermaid?: { code: string; title?: string };\n /** Add a screenshot reference */\n screenshot?: { path: string; alt?: string };\n /** Add a custom documentation entry */\n custom?: { type: string; data: unknown };\n}\n\n/**\n * Options for configuring a story via story.init().\n *\n * @example\n * ```ts\n * it('admin deletes user', ({ task }) => {\n * story.init(task, {\n * tags: ['admin', 'destructive'],\n * ticket: 'JIRA-456'\n * });\n * });\n * ```\n */\nexport interface StoryOptions {\n /** Tags for filtering and categorizing stories */\n tags?: string[];\n /** Ticket/issue reference(s) for requirements traceability */\n ticket?: string | string[];\n /** Arbitrary user-defined metadata */\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// ============================================================================\n// Vitest Task Type (minimal interface)\n// ============================================================================\n\n/** Minimal Vitest suite interface for suite path extraction */\nexport interface VitestSuite {\n /** Suite name */\n name?: string;\n /** Parent suite (optional) */\n suite?: VitestSuite;\n}\n\n/**\n * Minimal Vitest task interface for story.init().\n * This is the { task } from it('name', ({ task }) => { ... }).\n *\n * Uses generic type parameter to be compatible with Vitest's actual TaskMeta type.\n */\nexport interface VitestTask<TMeta = Record<string, unknown>> {\n /** The test/task name */\n name: string;\n /** Task metadata object where we store story data */\n meta: TMeta;\n /** Parent suite (optional) */\n suite?: VitestSuite;\n /** The test file (optional) */\n file?: { name?: 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 * executable-stories-vitest: Native Vitest story/given/when/then with Markdown doc generation.\n *\n * Uses native Vitest describe/it/test for full IDE support:\n *\n * @example\n * ```ts\n * import { describe, it, expect } from 'vitest';\n * import { story } from 'executable-stories-vitest';\n *\n * describe('Calculator', () => {\n * it('adds two numbers', ({ task }) => {\n * story.init(task);\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 *\n * In vitest.config, import StoryReporter from \"executable-stories-vitest/reporter\":\n *\n * @example\n * ```ts\n * import { defineConfig } from \"vitest/config\";\n * import { StoryReporter } from \"executable-stories-vitest/reporter\";\n *\n * export default defineConfig({\n * test: {\n * reporters: [\"default\", new StoryReporter()],\n * },\n * });\n * ```\n */\n\n// Core API\nexport { story, type Story } from './story-api';\n\n// Types for consumers\nexport type {\n StoryMeta,\n StoryStep,\n DocEntry,\n StepKeyword,\n StepMode,\n DocPhase,\n NormalizedTicket,\n TicketInput,\n StoryDocs,\n StoryOptions,\n VitestTask,\n VitestSuite,\n} from './types';\n\nexport { STORY_META_KEY } from './types';\n\n// Reporter types (actual reporter is in /reporter subpath)\nexport type {\n StoryReporterOptions,\n OutputFormat,\n OutputMode,\n ColocatedStyle,\n OutputRule,\n FormatterOptions,\n} from './reporter';\n\nconst STORY_REPORTER_GUARD_MSG =\n 'Do not import StoryReporter from \"executable-stories-vitest\". In vitest.config, import it from \"executable-stories-vitest/reporter\".';\n\n/** @internal Guard: throws if used. Import StoryReporter from \"executable-stories-vitest/reporter\" in vitest.config. */\nexport class StoryReporter {\n static __isGuard = true;\n constructor() {\n throw new Error(STORY_REPORTER_GUARD_MSG);\n }\n}\n","/**\n * story.* API for executable-stories-vitest.\n *\n * Uses native Vitest describe/it/test with opt-in documentation:\n *\n * @example\n * ```ts\n * import { describe, it, expect } from 'vitest';\n * import { story } from 'executable-stories-vitest';\n *\n * describe('Calculator', () => {\n * it('adds two numbers', ({ task }) => {\n * story.init(task);\n *\n * story.given('two numbers 5 and 3');\n * const a = 5;\n * const b = 3;\n *\n * story.when('I add them together');\n * const result = a + b;\n *\n * story.then('the result is 8');\n * expect(result).toBe(8);\n * });\n * });\n * ```\n */\n\nimport { createRequire } from 'node:module';\nimport {\n tryGetActiveOtelContext,\n resolveTraceUrl,\n} from 'executable-stories-formatters';\nimport type {\n DocEntry,\n NormalizedTicket,\n StepKeyword,\n StoryDocs,\n StoryMeta,\n StoryOptions,\n StoryStep,\n TicketInput,\n VitestSuite,\n} from './types';\n\n// ============================================================================\n// Task Interface (compatible with Vitest's actual task type)\n// ============================================================================\n\n/**\n * Minimal task interface compatible with Vitest's Test type.\n * The meta property accepts any object type to be compatible with Vitest's TaskMeta.\n */\ninterface TaskLike {\n name: string;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n meta: any;\n suite?: VitestSuite;\n file?: { name?: string };\n}\n\n// ============================================================================\n// Story Context\n// ============================================================================\n\n/** Attachment options for story.attach() */\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 */\ninterface ScopedAttachment extends AttachmentOptions {\n stepIndex?: number;\n stepId?: string;\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 /** Reference to task.meta for updates */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n taskMeta: any;\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/** 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(task) must be called first. Use: it('name', ({ task }) => { story.init(task); ... });\",\n );\n }\n return activeContext;\n}\n\n/** Re-attach current meta to task.meta.story so reporter sees steps and docs (e.g. story.note). */\nfunction syncMetaToTask(): void {\n if (activeContext?.taskMeta) {\n activeContext.taskMeta.story = activeContext.meta;\n }\n}\n\n// ============================================================================\n// Helper Functions\n// ============================================================================\n\n/**\n * Check if a name looks like a file path (to filter out from suite paths).\n */\nfunction looksLikeFilePath(name: string): boolean {\n if (name.includes('/') || name.includes('\\\\')) return true;\n if (name.includes('.spec.') || name.includes('.test.')) return true;\n if (/\\.(spec|test)\\.(ts|js|mjs|cjs)$/.test(name)) return true;\n if (/\\.(ts|js|mjs|cjs)$/.test(name)) return true;\n return false;\n}\n\n/**\n * Extract the suite path (parent describe names) from a Vitest task object.\n */\nfunction extractSuitePath(task: TaskLike): string[] | undefined {\n const path: string[] = [];\n const fileName = task.file?.name;\n let current: VitestSuite | undefined = task.suite;\n\n while (current) {\n const name = current.name;\n if (\n name &&\n name.trim() !== '' &&\n name !== '<root>' &&\n name !== fileName &&\n !looksLikeFilePath(name)\n ) {\n path.unshift(name);\n }\n current = current.suite;\n }\n\n return path.length > 0 ? path : undefined;\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 * Convert StoryDocs inline options to DocEntry array.\n * Matches the standalone DocApi method signatures.\n */\nfunction convertStoryDocsToEntries(docs: StoryDocs): DocEntry[] {\n const entries: DocEntry[] = [];\n\n // note(text)\n if (docs.note) {\n entries.push({ kind: 'note', text: docs.note, phase: 'runtime' });\n }\n\n // tag(name | names)\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\n // kv(label, value) - multiple pairs via Record\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\n // code(label, content, lang?)\n if (docs.code) {\n entries.push({\n kind: 'code',\n label: docs.code.label,\n content: docs.code.content,\n lang: docs.code.lang,\n phase: 'runtime',\n });\n }\n\n // json(label, value)\n if (docs.json) {\n entries.push({\n kind: 'code',\n label: docs.json.label,\n content: JSON.stringify(docs.json.value, null, 2),\n lang: 'json',\n phase: 'runtime',\n });\n }\n\n // table(label, columns, rows)\n if (docs.table) {\n entries.push({\n kind: 'table',\n label: docs.table.label,\n columns: docs.table.columns,\n rows: docs.table.rows,\n phase: 'runtime',\n });\n }\n\n // link(label, url)\n if (docs.link) {\n entries.push({\n kind: 'link',\n label: docs.link.label,\n url: docs.link.url,\n phase: 'runtime',\n });\n }\n\n // section(title, markdown)\n if (docs.section) {\n entries.push({\n kind: 'section',\n title: docs.section.title,\n markdown: docs.section.markdown,\n phase: 'runtime',\n });\n }\n\n // mermaid(code, title?)\n if (docs.mermaid) {\n entries.push({\n kind: 'mermaid',\n code: docs.mermaid.code,\n title: docs.mermaid.title,\n phase: 'runtime',\n });\n }\n\n // screenshot(path, alt?)\n if (docs.screenshot) {\n entries.push({\n kind: 'screenshot',\n path: docs.screenshot.path,\n alt: docs.screenshot.alt,\n phase: 'runtime',\n });\n }\n\n // custom(type, data)\n if (docs.custom) {\n entries.push({\n kind: 'custom',\n type: docs.custom.type,\n data: docs.custom.data,\n phase: 'runtime',\n });\n }\n\n return entries;\n}\n\n// ============================================================================\n// story.init()\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 task - The Vitest task object from ({ task }) => { ... }\n * @param options - Optional story configuration (tags, ticket, meta)\n *\n * @example\n * ```ts\n * it('adds two numbers', ({ task }) => {\n * story.init(task);\n * // ... rest of test\n * });\n *\n * // With options:\n * it('admin deletes user', ({ task }) => {\n * story.init(task, {\n * tags: ['admin', 'destructive'],\n * ticket: 'JIRA-456'\n * });\n * });\n * ```\n */\nfunction init(task: TaskLike, options?: StoryOptions): void {\n const meta: StoryMeta = {\n scenario: task.name,\n steps: [],\n suitePath: extractSuitePath(task),\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 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', task.name);\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 // Attach to task.meta so reporter can find it\n task.meta.story = meta;\n\n // Set active context\n activeContext = {\n meta,\n currentStep: null,\n taskMeta: task.meta,\n stepCounter: 0,\n attachments: [],\n activeTimers: new Map(),\n timerCounter: 0,\n };\n}\n\n// ============================================================================\n// Step Markers\n// ============================================================================\n\n/**\n * Create a step marker function for a given keyword.\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 syncMetaToTask();\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 syncMetaToTask();\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; syncMetaToTask(); return val; },\n (err) => { step.durationMs = performance.now() - start; syncMetaToTask(); throw err; },\n ) as T;\n }\n step.durationMs = performance.now() - start;\n syncMetaToTask();\n return result;\n } catch (err) {\n step.durationMs = performance.now() - start;\n syncMetaToTask();\n throw err;\n }\n }\n return stepMarker;\n}\n\n// ============================================================================\n// Doc Methods (Standalone)\n// ============================================================================\n\n/**\n * Add a free-text note to the current step or story-level if before any step.\n */\nfunction note(text: string, children?: DocEntry[]): DocEntry {\n return attachDoc({ kind: 'note', text, phase: 'runtime' }, children);\n}\n\n// ============================================================================\n// Doc Method Types (shared between standalone and inline)\n// ============================================================================\n\n/** Options for kv() - key-value pair */\ninterface KvOptions {\n label: string;\n value: unknown;\n}\n\n/** Options for json() - JSON code block */\ninterface JsonOptions {\n label: string;\n value: unknown;\n}\n\n/** Options for code() - code block with optional language */\ninterface CodeOptions {\n label: string;\n content: string;\n lang?: string;\n}\n\n/** Options for table() - markdown table */\ninterface TableOptions {\n label: string;\n columns: string[];\n rows: string[][];\n}\n\n/** Options for link() - hyperlink */\ninterface LinkOptions {\n label: string;\n url: string;\n}\n\n/** Options for section() - titled markdown section */\ninterface SectionOptions {\n title: string;\n markdown: string;\n}\n\n/** Options for mermaid() - Mermaid diagram */\ninterface MermaidOptions {\n code: string;\n title?: string;\n}\n\n/** Options for screenshot() - screenshot reference */\ninterface ScreenshotOptions {\n path: string;\n alt?: string;\n}\n\n/** Options for custom() - custom doc entry */\ninterface CustomOptions {\n type: string;\n data: unknown;\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 syncMetaToTask();\n return entry;\n}\n\n// ============================================================================\n// Doc Methods (Standalone) - same shape as inline docs\n// ============================================================================\n\n/**\n * Add a key-value pair to the current step or story-level.\n * @example story.kv({ label: 'Payment ID', value: 'pay_123' })\n */\nfunction kv(options: KvOptions, children?: DocEntry[]): DocEntry {\n return attachDoc({\n kind: 'kv',\n label: options.label,\n value: options.value,\n phase: 'runtime',\n }, children);\n}\n\n/**\n * Add a JSON code block to the current step or story-level.\n * @example story.json({ label: 'Order', value: { id: 123 } })\n */\nfunction json(options: JsonOptions, children?: DocEntry[]): DocEntry {\n const content = JSON.stringify(options.value, null, 2);\n return attachDoc({\n kind: 'code',\n label: options.label,\n content,\n lang: 'json',\n phase: 'runtime',\n }, children);\n}\n\n/**\n * Add a code block with optional language to the current step or story-level.\n * @example story.code({ label: 'Config', content: 'port: 3000', lang: 'yaml' })\n */\nfunction code(options: CodeOptions, children?: DocEntry[]): DocEntry {\n return attachDoc({\n kind: 'code',\n label: options.label,\n content: options.content,\n lang: options.lang,\n phase: 'runtime',\n }, children);\n}\n\n/**\n * Add a markdown table to the current step or story-level.\n * @example story.table({ label: 'Users', columns: ['Name', 'Role'], rows: [['Alice', 'Admin']] })\n */\nfunction table(options: TableOptions, children?: DocEntry[]): DocEntry {\n return attachDoc({\n kind: 'table',\n label: options.label,\n columns: options.columns,\n rows: options.rows,\n phase: 'runtime',\n }, children);\n}\n\n/**\n * Add a hyperlink to the current step or story-level.\n * @example story.link({ label: 'API Docs', url: 'https://docs.example.com' })\n */\nfunction link(options: LinkOptions, children?: DocEntry[]): DocEntry {\n return attachDoc({\n kind: 'link',\n label: options.label,\n url: options.url,\n phase: 'runtime',\n }, children);\n}\n\n/**\n * Add a titled section with markdown content to the current step or story-level.\n * @example story.section({ title: 'Details', markdown: 'This is **important**' })\n */\nfunction section(options: SectionOptions, children?: DocEntry[]): DocEntry {\n return attachDoc({\n kind: 'section',\n title: options.title,\n markdown: options.markdown,\n phase: 'runtime',\n }, children);\n}\n\n/**\n * Add a Mermaid diagram to the current step or story-level.\n * @example story.mermaid({ code: 'graph LR; A-->B', title: 'Flow' })\n */\nfunction mermaid(options: MermaidOptions, children?: DocEntry[]): DocEntry {\n return attachDoc({\n kind: 'mermaid',\n code: options.code,\n title: options.title,\n phase: 'runtime',\n }, children);\n}\n\n/**\n * Add a screenshot reference to the current step or story-level.\n * @example story.screenshot({ path: '/screenshots/result.png', alt: 'Final result' })\n */\nfunction screenshot(options: ScreenshotOptions, children?: DocEntry[]): DocEntry {\n return attachDoc({\n kind: 'screenshot',\n path: options.path,\n alt: options.alt,\n phase: 'runtime',\n }, children);\n}\n\n/**\n * Add tag(s) to the current step or story-level.\n * @example story.tag('admin') or story.tag(['admin', 'security'])\n */\nfunction 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/**\n * Add a custom documentation entry for use with custom renderers.\n * @example story.custom({ type: 'myType', data: { foo: 'bar' } })\n */\nfunction custom(options: CustomOptions, children?: DocEntry[]): DocEntry {\n return attachDoc({\n kind: 'custom',\n type: options.type,\n data: options.data,\n phase: 'runtime',\n }, children);\n}\n\n// ============================================================================\n// Attachments\n// ============================================================================\n\n/**\n * Attach a file or inline content to the current step or test case.\n * @example story.attach({ name: 'screenshot', mediaType: 'image/png', path: '/tmp/screenshot.png' })\n */\nfunction 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 // Store attachments on task.meta so reporter can read them\n if (ctx.taskMeta) {\n ctx.taskMeta.storyAttachments = ctx.attachments;\n }\n}\n\n// ============================================================================\n// Step Timing\n// ============================================================================\n\n/**\n * Start a timer for the current step. Returns a token to pass to endTimer().\n */\nfunction 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/**\n * End a timer and record duration on the step that was active when startTimer() was called.\n */\nfunction 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 syncMetaToTask();\n }\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 syncMetaToTask();\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 syncMetaToTask();\n return val;\n },\n (err) => {\n step.durationMs = performance.now() - start;\n syncMetaToTask();\n throw err;\n },\n ) as T;\n }\n\n step.durationMs = performance.now() - start;\n syncMetaToTask();\n return result;\n } catch (err) {\n step.durationMs = performance.now() - start;\n syncMetaToTask();\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 * await story.expect('async check', async () => { ... });\n * ```\n */\nfunction storyExpect<T>(text: string, body: () => T): T {\n return fn('Then', text, body);\n}\n\n// ============================================================================\n// Span attachment: story.attachSpans()\n// ============================================================================\n\n/**\n * Attach OTel spans to the current test so the StoryReporter renders them\n * as a trace waterfall in HTML reports.\n *\n * Accepts any array of objects with at least `spanId` and `name` fields.\n * Structurally compatible with autotel's `SerializedSpan` and the\n * `OtelSpan` type from executable-stories-formatters.\n *\n * @example\n * ```ts\n * import { serializeSpan } from 'autotel/test-span-collector';\n *\n * // After running code that creates spans:\n * const spans = exporter.getFinishedSpans().map(serializeSpan);\n * story.attachSpans(spans);\n * ```\n */\nfunction attachSpans(spans: ReadonlyArray<Record<string, unknown>>): void {\n const ctx = getContext();\n if (ctx.taskMeta) {\n ctx.taskMeta.otelSpans = spans;\n }\n}\n\n// ============================================================================\n// Export story object\n// ============================================================================\n\n/**\n * The main story API object.\n *\n * Use with native Vitest describe/it/test for full IDE support:\n *\n * @example\n * ```ts\n * import { describe, it, expect } from 'vitest';\n * import { story } from 'executable-stories-vitest';\n *\n * describe('Calculator', () => {\n * it('adds two numbers', ({ task }) => {\n * story.init(task);\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 // Core\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,\n kv,\n json,\n code,\n table,\n link,\n section,\n mermaid,\n screenshot,\n tag,\n custom,\n\n // Attachments\n attach,\n\n // OTel span attachment\n attachSpans,\n\n // Step wrappers\n fn,\n expect: storyExpect,\n\n // Step timing\n startTimer,\n endTimer,\n};\n\nexport type Story = typeof story;\n","/**\n * Type definitions for executable-stories-vitest.\n *\n * Shared story types (StepKeyword, DocEntry, StoryStep, StoryMeta, etc.)\n * are imported from executable-stories-formatters — the single source of truth.\n * This module re-exports them and adds Vitest-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// ============================================================================\n// Ticket Input Types\n// ============================================================================\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// Vitest-specific Types\n// ============================================================================\n\n/**\n * Inline documentation options for step markers.\n * Pass to story.given(), story.when(), story.then() as second argument.\n *\n * @example\n * ```ts\n * story.given('valid credentials', {\n * json: { label: 'Credentials', value: { email: 'test@example.com', password: '***' } },\n * note: 'Password is masked for security'\n * });\n * ```\n */\nexport interface StoryDocs {\n /** Add a free-text note */\n note?: string;\n /** Add tag(s) for categorization */\n tag?: string | string[];\n /** Add key-value pairs */\n kv?: Record<string, unknown>;\n /** Add a code block with label and optional language */\n code?: { label: string; content: string; lang?: string };\n /** Add a JSON data block with label */\n json?: { label: string; value: unknown };\n /** Add a markdown table with label */\n table?: { label: string; columns: string[]; rows: string[][] };\n /** Add a hyperlink */\n link?: { label: string; url: string };\n /** Add a titled section with markdown content */\n section?: { title: string; markdown: string };\n /** Add a Mermaid diagram with optional title */\n mermaid?: { code: string; title?: string };\n /** Add a screenshot reference */\n screenshot?: { path: string; alt?: string };\n /** Add a custom documentation entry */\n custom?: { type: string; data: unknown };\n}\n\n/**\n * Options for configuring a story via story.init().\n *\n * @example\n * ```ts\n * it('admin deletes user', ({ task }) => {\n * story.init(task, {\n * tags: ['admin', 'destructive'],\n * ticket: 'JIRA-456'\n * });\n * });\n * ```\n */\nexport interface StoryOptions {\n /** Tags for filtering and categorizing stories */\n tags?: string[];\n /** Ticket/issue reference(s) for requirements traceability */\n ticket?: TicketInput | TicketInput[];\n /** Arbitrary user-defined metadata */\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// ============================================================================\n// Vitest Task Type (minimal interface)\n// ============================================================================\n\n/** Minimal Vitest suite interface for suite path extraction */\nexport interface VitestSuite {\n /** Suite name */\n name?: string;\n /** Parent suite (optional) */\n suite?: VitestSuite;\n}\n\n/**\n * Minimal Vitest task interface for story.init().\n * This is the { task } from it('name', ({ task }) => { ... }).\n *\n * Uses generic type parameter to be compatible with Vitest's actual TaskMeta type.\n */\nexport interface VitestTask<TMeta = Record<string, unknown>> {\n /** The test/task name */\n name: string;\n /** Task metadata object where we store story data */\n meta: TMeta;\n /** Parent suite (optional) */\n suite?: VitestSuite;\n /** The test file (optional) */\n file?: { name?: string };\n}\n"],"mappings":"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","names":["import_executable_stories_formatters"]}
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { StepKeyword } from 'executable-stories-formatters';
2
- export { ColocatedStyle, DocEntry, DocPhase, FormatterOptions, OutputFormat, OutputMode, OutputRule, STORY_META_KEY, StepKeyword, StepMode, StoryMeta, StoryStep } from 'executable-stories-formatters';
1
+ import { DocEntry, StepKeyword } from 'executable-stories-formatters';
2
+ export { ColocatedStyle, DocEntry, DocPhase, FormatterOptions, NormalizedTicket, OutputFormat, OutputMode, OutputRule, STORY_META_KEY, StepKeyword, StepMode, StoryMeta, StoryStep } from 'executable-stories-formatters';
3
3
  export { StoryReporterOptions } from './reporter.cjs';
4
4
  import 'vitest/node';
5
5
 
@@ -11,6 +11,11 @@ import 'vitest/node';
11
11
  * This module re-exports them and adds Vitest-specific types.
12
12
  */
13
13
 
14
+ /** A ticket reference: either a plain string ID or an object with id and optional url */
15
+ type TicketInput = string | {
16
+ id: string;
17
+ url?: string;
18
+ };
14
19
  /**
15
20
  * Inline documentation options for step markers.
16
21
  * Pass to story.given(), story.when(), story.then() as second argument.
@@ -90,7 +95,7 @@ interface StoryOptions {
90
95
  /** Tags for filtering and categorizing stories */
91
96
  tags?: string[];
92
97
  /** Ticket/issue reference(s) for requirements traceability */
93
- ticket?: string | string[];
98
+ ticket?: TicketInput | TicketInput[];
94
99
  /** Arbitrary user-defined metadata */
95
100
  meta?: Record<string, unknown>;
96
101
  /** URL template for OTel trace links. Uses {traceId} placeholder. Also settable via OTEL_TRACE_URL_TEMPLATE env var. */
@@ -199,7 +204,7 @@ declare function init(task: TaskLike, options?: StoryOptions): void;
199
204
  /**
200
205
  * Add a free-text note to the current step or story-level if before any step.
201
206
  */
202
- declare function note(text: string): void;
207
+ declare function note(text: string, children?: DocEntry[]): DocEntry;
203
208
  /** Options for kv() - key-value pair */
204
209
  interface KvOptions {
205
210
  label: string;
@@ -251,52 +256,52 @@ interface CustomOptions {
251
256
  * Add a key-value pair to the current step or story-level.
252
257
  * @example story.kv({ label: 'Payment ID', value: 'pay_123' })
253
258
  */
254
- declare function kv(options: KvOptions): void;
259
+ declare function kv(options: KvOptions, children?: DocEntry[]): DocEntry;
255
260
  /**
256
261
  * Add a JSON code block to the current step or story-level.
257
262
  * @example story.json({ label: 'Order', value: { id: 123 } })
258
263
  */
259
- declare function json(options: JsonOptions): void;
264
+ declare function json(options: JsonOptions, children?: DocEntry[]): DocEntry;
260
265
  /**
261
266
  * Add a code block with optional language to the current step or story-level.
262
267
  * @example story.code({ label: 'Config', content: 'port: 3000', lang: 'yaml' })
263
268
  */
264
- declare function code(options: CodeOptions): void;
269
+ declare function code(options: CodeOptions, children?: DocEntry[]): DocEntry;
265
270
  /**
266
271
  * Add a markdown table to the current step or story-level.
267
272
  * @example story.table({ label: 'Users', columns: ['Name', 'Role'], rows: [['Alice', 'Admin']] })
268
273
  */
269
- declare function table(options: TableOptions): void;
274
+ declare function table(options: TableOptions, children?: DocEntry[]): DocEntry;
270
275
  /**
271
276
  * Add a hyperlink to the current step or story-level.
272
277
  * @example story.link({ label: 'API Docs', url: 'https://docs.example.com' })
273
278
  */
274
- declare function link(options: LinkOptions): void;
279
+ declare function link(options: LinkOptions, children?: DocEntry[]): DocEntry;
275
280
  /**
276
281
  * Add a titled section with markdown content to the current step or story-level.
277
282
  * @example story.section({ title: 'Details', markdown: 'This is **important**' })
278
283
  */
279
- declare function section(options: SectionOptions): void;
284
+ declare function section(options: SectionOptions, children?: DocEntry[]): DocEntry;
280
285
  /**
281
286
  * Add a Mermaid diagram to the current step or story-level.
282
287
  * @example story.mermaid({ code: 'graph LR; A-->B', title: 'Flow' })
283
288
  */
284
- declare function mermaid(options: MermaidOptions): void;
289
+ declare function mermaid(options: MermaidOptions, children?: DocEntry[]): DocEntry;
285
290
  /**
286
291
  * Add a screenshot reference to the current step or story-level.
287
292
  * @example story.screenshot({ path: '/screenshots/result.png', alt: 'Final result' })
288
293
  */
289
- declare function screenshot(options: ScreenshotOptions): void;
294
+ declare function screenshot(options: ScreenshotOptions, children?: DocEntry[]): DocEntry;
290
295
  /**
291
296
  * Add tag(s) to the current step or story-level.
292
297
  * @example story.tag('admin') or story.tag(['admin', 'security'])
293
298
  */
294
- declare function tag(name: string | string[]): void;
299
+ declare function tag(name: string | string[], children?: DocEntry[]): DocEntry;
295
300
  /**
296
301
  * Add a custom documentation entry for use with custom renderers.
297
302
  * @example story.custom({ type: 'myType', data: { foo: 'bar' } })
298
303
  */
299
- declare function custom(options: CustomOptions): void;
304
+ declare function custom(options: CustomOptions, children?: DocEntry[]): DocEntry;
300
305
  /**
301
306
  * Attach a file or inline content to the current step or test case.
302
307
  * @example story.attach({ name: 'screenshot', mediaType: 'image/png', path: '/tmp/screenshot.png' })
@@ -387,54 +392,67 @@ declare const story: {
387
392
  init: typeof init;
388
393
  given: {
389
394
  (text: string, docs?: StoryDocs): void;
395
+ (text: string, children: DocEntry[]): void;
390
396
  <T>(text: string, body: () => T): T;
391
397
  };
392
398
  when: {
393
399
  (text: string, docs?: StoryDocs): void;
400
+ (text: string, children: DocEntry[]): void;
394
401
  <T>(text: string, body: () => T): T;
395
402
  };
396
403
  then: {
397
404
  (text: string, docs?: StoryDocs): void;
405
+ (text: string, children: DocEntry[]): void;
398
406
  <T>(text: string, body: () => T): T;
399
407
  };
400
408
  and: {
401
409
  (text: string, docs?: StoryDocs): void;
410
+ (text: string, children: DocEntry[]): void;
402
411
  <T>(text: string, body: () => T): T;
403
412
  };
404
413
  but: {
405
414
  (text: string, docs?: StoryDocs): void;
415
+ (text: string, children: DocEntry[]): void;
406
416
  <T>(text: string, body: () => T): T;
407
417
  };
408
418
  arrange: {
409
419
  (text: string, docs?: StoryDocs): void;
420
+ (text: string, children: DocEntry[]): void;
410
421
  <T>(text: string, body: () => T): T;
411
422
  };
412
423
  act: {
413
424
  (text: string, docs?: StoryDocs): void;
425
+ (text: string, children: DocEntry[]): void;
414
426
  <T>(text: string, body: () => T): T;
415
427
  };
416
428
  assert: {
417
429
  (text: string, docs?: StoryDocs): void;
430
+ (text: string, children: DocEntry[]): void;
418
431
  <T>(text: string, body: () => T): T;
419
432
  };
420
433
  setup: {
421
434
  (text: string, docs?: StoryDocs): void;
435
+ (text: string, children: DocEntry[]): void;
422
436
  <T>(text: string, body: () => T): T;
423
437
  };
424
438
  context: {
425
439
  (text: string, docs?: StoryDocs): void;
440
+ (text: string, children: DocEntry[]): void;
426
441
  <T>(text: string, body: () => T): T;
427
442
  };
428
443
  execute: {
429
444
  (text: string, docs?: StoryDocs): void;
445
+ (text: string, children: DocEntry[]): void;
430
446
  <T>(text: string, body: () => T): T;
431
447
  };
432
448
  action: {
433
449
  (text: string, docs?: StoryDocs): void;
450
+ (text: string, children: DocEntry[]): void;
434
451
  <T>(text: string, body: () => T): T;
435
452
  };
436
453
  verify: {
437
454
  (text: string, docs?: StoryDocs): void;
455
+ (text: string, children: DocEntry[]): void;
438
456
  <T>(text: string, body: () => T): T;
439
457
  };
440
458
  note: typeof note;
@@ -504,4 +522,4 @@ declare class StoryReporter {
504
522
  constructor();
505
523
  }
506
524
 
507
- export { type Story, type StoryDocs, type StoryOptions, StoryReporter, type VitestSuite, type VitestTask, story };
525
+ export { type Story, type StoryDocs, type StoryOptions, StoryReporter, type TicketInput, type VitestSuite, type VitestTask, story };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { StepKeyword } from 'executable-stories-formatters';
2
- export { ColocatedStyle, DocEntry, DocPhase, FormatterOptions, OutputFormat, OutputMode, OutputRule, STORY_META_KEY, StepKeyword, StepMode, StoryMeta, StoryStep } from 'executable-stories-formatters';
1
+ import { DocEntry, StepKeyword } from 'executable-stories-formatters';
2
+ export { ColocatedStyle, DocEntry, DocPhase, FormatterOptions, NormalizedTicket, OutputFormat, OutputMode, OutputRule, STORY_META_KEY, StepKeyword, StepMode, StoryMeta, StoryStep } from 'executable-stories-formatters';
3
3
  export { StoryReporterOptions } from './reporter.js';
4
4
  import 'vitest/node';
5
5
 
@@ -11,6 +11,11 @@ import 'vitest/node';
11
11
  * This module re-exports them and adds Vitest-specific types.
12
12
  */
13
13
 
14
+ /** A ticket reference: either a plain string ID or an object with id and optional url */
15
+ type TicketInput = string | {
16
+ id: string;
17
+ url?: string;
18
+ };
14
19
  /**
15
20
  * Inline documentation options for step markers.
16
21
  * Pass to story.given(), story.when(), story.then() as second argument.
@@ -90,7 +95,7 @@ interface StoryOptions {
90
95
  /** Tags for filtering and categorizing stories */
91
96
  tags?: string[];
92
97
  /** Ticket/issue reference(s) for requirements traceability */
93
- ticket?: string | string[];
98
+ ticket?: TicketInput | TicketInput[];
94
99
  /** Arbitrary user-defined metadata */
95
100
  meta?: Record<string, unknown>;
96
101
  /** URL template for OTel trace links. Uses {traceId} placeholder. Also settable via OTEL_TRACE_URL_TEMPLATE env var. */
@@ -199,7 +204,7 @@ declare function init(task: TaskLike, options?: StoryOptions): void;
199
204
  /**
200
205
  * Add a free-text note to the current step or story-level if before any step.
201
206
  */
202
- declare function note(text: string): void;
207
+ declare function note(text: string, children?: DocEntry[]): DocEntry;
203
208
  /** Options for kv() - key-value pair */
204
209
  interface KvOptions {
205
210
  label: string;
@@ -251,52 +256,52 @@ interface CustomOptions {
251
256
  * Add a key-value pair to the current step or story-level.
252
257
  * @example story.kv({ label: 'Payment ID', value: 'pay_123' })
253
258
  */
254
- declare function kv(options: KvOptions): void;
259
+ declare function kv(options: KvOptions, children?: DocEntry[]): DocEntry;
255
260
  /**
256
261
  * Add a JSON code block to the current step or story-level.
257
262
  * @example story.json({ label: 'Order', value: { id: 123 } })
258
263
  */
259
- declare function json(options: JsonOptions): void;
264
+ declare function json(options: JsonOptions, children?: DocEntry[]): DocEntry;
260
265
  /**
261
266
  * Add a code block with optional language to the current step or story-level.
262
267
  * @example story.code({ label: 'Config', content: 'port: 3000', lang: 'yaml' })
263
268
  */
264
- declare function code(options: CodeOptions): void;
269
+ declare function code(options: CodeOptions, children?: DocEntry[]): DocEntry;
265
270
  /**
266
271
  * Add a markdown table to the current step or story-level.
267
272
  * @example story.table({ label: 'Users', columns: ['Name', 'Role'], rows: [['Alice', 'Admin']] })
268
273
  */
269
- declare function table(options: TableOptions): void;
274
+ declare function table(options: TableOptions, children?: DocEntry[]): DocEntry;
270
275
  /**
271
276
  * Add a hyperlink to the current step or story-level.
272
277
  * @example story.link({ label: 'API Docs', url: 'https://docs.example.com' })
273
278
  */
274
- declare function link(options: LinkOptions): void;
279
+ declare function link(options: LinkOptions, children?: DocEntry[]): DocEntry;
275
280
  /**
276
281
  * Add a titled section with markdown content to the current step or story-level.
277
282
  * @example story.section({ title: 'Details', markdown: 'This is **important**' })
278
283
  */
279
- declare function section(options: SectionOptions): void;
284
+ declare function section(options: SectionOptions, children?: DocEntry[]): DocEntry;
280
285
  /**
281
286
  * Add a Mermaid diagram to the current step or story-level.
282
287
  * @example story.mermaid({ code: 'graph LR; A-->B', title: 'Flow' })
283
288
  */
284
- declare function mermaid(options: MermaidOptions): void;
289
+ declare function mermaid(options: MermaidOptions, children?: DocEntry[]): DocEntry;
285
290
  /**
286
291
  * Add a screenshot reference to the current step or story-level.
287
292
  * @example story.screenshot({ path: '/screenshots/result.png', alt: 'Final result' })
288
293
  */
289
- declare function screenshot(options: ScreenshotOptions): void;
294
+ declare function screenshot(options: ScreenshotOptions, children?: DocEntry[]): DocEntry;
290
295
  /**
291
296
  * Add tag(s) to the current step or story-level.
292
297
  * @example story.tag('admin') or story.tag(['admin', 'security'])
293
298
  */
294
- declare function tag(name: string | string[]): void;
299
+ declare function tag(name: string | string[], children?: DocEntry[]): DocEntry;
295
300
  /**
296
301
  * Add a custom documentation entry for use with custom renderers.
297
302
  * @example story.custom({ type: 'myType', data: { foo: 'bar' } })
298
303
  */
299
- declare function custom(options: CustomOptions): void;
304
+ declare function custom(options: CustomOptions, children?: DocEntry[]): DocEntry;
300
305
  /**
301
306
  * Attach a file or inline content to the current step or test case.
302
307
  * @example story.attach({ name: 'screenshot', mediaType: 'image/png', path: '/tmp/screenshot.png' })
@@ -387,54 +392,67 @@ declare const story: {
387
392
  init: typeof init;
388
393
  given: {
389
394
  (text: string, docs?: StoryDocs): void;
395
+ (text: string, children: DocEntry[]): void;
390
396
  <T>(text: string, body: () => T): T;
391
397
  };
392
398
  when: {
393
399
  (text: string, docs?: StoryDocs): void;
400
+ (text: string, children: DocEntry[]): void;
394
401
  <T>(text: string, body: () => T): T;
395
402
  };
396
403
  then: {
397
404
  (text: string, docs?: StoryDocs): void;
405
+ (text: string, children: DocEntry[]): void;
398
406
  <T>(text: string, body: () => T): T;
399
407
  };
400
408
  and: {
401
409
  (text: string, docs?: StoryDocs): void;
410
+ (text: string, children: DocEntry[]): void;
402
411
  <T>(text: string, body: () => T): T;
403
412
  };
404
413
  but: {
405
414
  (text: string, docs?: StoryDocs): void;
415
+ (text: string, children: DocEntry[]): void;
406
416
  <T>(text: string, body: () => T): T;
407
417
  };
408
418
  arrange: {
409
419
  (text: string, docs?: StoryDocs): void;
420
+ (text: string, children: DocEntry[]): void;
410
421
  <T>(text: string, body: () => T): T;
411
422
  };
412
423
  act: {
413
424
  (text: string, docs?: StoryDocs): void;
425
+ (text: string, children: DocEntry[]): void;
414
426
  <T>(text: string, body: () => T): T;
415
427
  };
416
428
  assert: {
417
429
  (text: string, docs?: StoryDocs): void;
430
+ (text: string, children: DocEntry[]): void;
418
431
  <T>(text: string, body: () => T): T;
419
432
  };
420
433
  setup: {
421
434
  (text: string, docs?: StoryDocs): void;
435
+ (text: string, children: DocEntry[]): void;
422
436
  <T>(text: string, body: () => T): T;
423
437
  };
424
438
  context: {
425
439
  (text: string, docs?: StoryDocs): void;
440
+ (text: string, children: DocEntry[]): void;
426
441
  <T>(text: string, body: () => T): T;
427
442
  };
428
443
  execute: {
429
444
  (text: string, docs?: StoryDocs): void;
445
+ (text: string, children: DocEntry[]): void;
430
446
  <T>(text: string, body: () => T): T;
431
447
  };
432
448
  action: {
433
449
  (text: string, docs?: StoryDocs): void;
450
+ (text: string, children: DocEntry[]): void;
434
451
  <T>(text: string, body: () => T): T;
435
452
  };
436
453
  verify: {
437
454
  (text: string, docs?: StoryDocs): void;
455
+ (text: string, children: DocEntry[]): void;
438
456
  <T>(text: string, body: () => T): T;
439
457
  };
440
458
  note: typeof note;
@@ -504,4 +522,4 @@ declare class StoryReporter {
504
522
  constructor();
505
523
  }
506
524
 
507
- export { type Story, type StoryDocs, type StoryOptions, StoryReporter, type VitestSuite, type VitestTask, story };
525
+ export { type Story, type StoryDocs, type StoryOptions, StoryReporter, type TicketInput, type VitestSuite, type VitestTask, story };
package/dist/index.js CHANGED
@@ -41,7 +41,8 @@ function extractSuitePath(task) {
41
41
  }
42
42
  function normalizeTickets(ticket) {
43
43
  if (!ticket) return void 0;
44
- return Array.isArray(ticket) ? ticket : [ticket];
44
+ const arr = Array.isArray(ticket) ? ticket : [ticket];
45
+ return arr.map((t) => typeof t === "string" ? { id: t } : t);
45
46
  }
46
47
  function convertStoryDocsToEntries(docs) {
47
48
  const entries = [];
@@ -156,7 +157,7 @@ function init(task, options) {
156
157
  if (options?.tags?.length) span.setAttribute("story.tags", options.tags);
157
158
  if (options?.ticket) {
158
159
  const tickets = Array.isArray(options.ticket) ? options.ticket : [options.ticket];
159
- span.setAttribute("story.tickets", tickets);
160
+ span.setAttribute("story.tickets", tickets.map((t) => typeof t === "string" ? t : t.id));
160
161
  }
161
162
  }
162
163
  } catch {
@@ -177,17 +178,37 @@ function createStepMarker(keyword) {
177
178
  function stepMarker(text, docsOrBody) {
178
179
  const ctx = getContext();
179
180
  const isCallback = typeof docsOrBody === "function";
181
+ const isChildrenArray = Array.isArray(docsOrBody);
180
182
  const resolvedKeyword = (keyword === "Given" || keyword === "When" || keyword === "Then") && ctx.meta.steps.some((s) => s.keyword === keyword) ? "And" : keyword;
183
+ let stepDocs = [];
184
+ if (!isCallback && !isChildrenArray && docsOrBody) {
185
+ stepDocs = convertStoryDocsToEntries(docsOrBody);
186
+ }
181
187
  const step = {
182
188
  id: `step-${ctx.stepCounter++}`,
183
189
  keyword: resolvedKeyword,
184
190
  text,
185
- docs: !isCallback && docsOrBody ? convertStoryDocsToEntries(docsOrBody) : [],
191
+ docs: stepDocs,
186
192
  ...isCallback ? { wrapped: true } : {}
187
193
  };
188
194
  ctx.meta.steps.push(step);
189
195
  ctx.currentStep = step;
190
196
  syncMetaToTask();
197
+ if (isChildrenArray) {
198
+ const children = docsOrBody;
199
+ if (children.length > 0) {
200
+ const childSet = new Set(children);
201
+ ctx.meta.docs = (ctx.meta.docs ?? []).filter((d) => !childSet.has(d));
202
+ for (const prevStep of ctx.meta.steps) {
203
+ if (prevStep !== step && prevStep.docs) {
204
+ prevStep.docs = prevStep.docs.filter((d) => !childSet.has(d));
205
+ }
206
+ }
207
+ step.docs = [...step.docs ?? [], ...children];
208
+ }
209
+ syncMetaToTask();
210
+ return;
211
+ }
191
212
  if (!isCallback) return;
192
213
  const body = docsOrBody;
193
214
  const start = performance.now();
@@ -218,19 +239,20 @@ function createStepMarker(keyword) {
218
239
  }
219
240
  return stepMarker;
220
241
  }
221
- function note(text) {
222
- const ctx = getContext();
223
- const entry = { kind: "note", text, phase: "runtime" };
224
- if (ctx.currentStep) {
225
- ctx.currentStep.docs ??= [];
226
- ctx.currentStep.docs.push(entry);
227
- } else {
228
- ctx.meta.docs ??= [];
229
- ctx.meta.docs.push(entry);
230
- }
242
+ function note(text, children) {
243
+ return attachDoc({ kind: "note", text, phase: "runtime" }, children);
231
244
  }
232
- function attachDoc(entry) {
245
+ function attachDoc(entry, children) {
233
246
  const ctx = getContext();
247
+ if (children && children.length > 0) {
248
+ entry.children = children;
249
+ const childSet = new Set(children);
250
+ const filterDocs = (docs) => docs.filter((d) => !childSet.has(d));
251
+ ctx.meta.docs = filterDocs(ctx.meta.docs ?? []);
252
+ for (const step of ctx.meta.steps) {
253
+ if (step.docs) step.docs = filterDocs(step.docs);
254
+ }
255
+ }
234
256
  if (ctx.currentStep) {
235
257
  ctx.currentStep.docs ??= [];
236
258
  ctx.currentStep.docs.push(entry);
@@ -239,86 +261,87 @@ function attachDoc(entry) {
239
261
  ctx.meta.docs.push(entry);
240
262
  }
241
263
  syncMetaToTask();
264
+ return entry;
242
265
  }
243
- function kv(options) {
244
- attachDoc({
266
+ function kv(options, children) {
267
+ return attachDoc({
245
268
  kind: "kv",
246
269
  label: options.label,
247
270
  value: options.value,
248
271
  phase: "runtime"
249
- });
272
+ }, children);
250
273
  }
251
- function json(options) {
274
+ function json(options, children) {
252
275
  const content = JSON.stringify(options.value, null, 2);
253
- attachDoc({
276
+ return attachDoc({
254
277
  kind: "code",
255
278
  label: options.label,
256
279
  content,
257
280
  lang: "json",
258
281
  phase: "runtime"
259
- });
282
+ }, children);
260
283
  }
261
- function code(options) {
262
- attachDoc({
284
+ function code(options, children) {
285
+ return attachDoc({
263
286
  kind: "code",
264
287
  label: options.label,
265
288
  content: options.content,
266
289
  lang: options.lang,
267
290
  phase: "runtime"
268
- });
291
+ }, children);
269
292
  }
270
- function table(options) {
271
- attachDoc({
293
+ function table(options, children) {
294
+ return attachDoc({
272
295
  kind: "table",
273
296
  label: options.label,
274
297
  columns: options.columns,
275
298
  rows: options.rows,
276
299
  phase: "runtime"
277
- });
300
+ }, children);
278
301
  }
279
- function link(options) {
280
- attachDoc({
302
+ function link(options, children) {
303
+ return attachDoc({
281
304
  kind: "link",
282
305
  label: options.label,
283
306
  url: options.url,
284
307
  phase: "runtime"
285
- });
308
+ }, children);
286
309
  }
287
- function section(options) {
288
- attachDoc({
310
+ function section(options, children) {
311
+ return attachDoc({
289
312
  kind: "section",
290
313
  title: options.title,
291
314
  markdown: options.markdown,
292
315
  phase: "runtime"
293
- });
316
+ }, children);
294
317
  }
295
- function mermaid(options) {
296
- attachDoc({
318
+ function mermaid(options, children) {
319
+ return attachDoc({
297
320
  kind: "mermaid",
298
321
  code: options.code,
299
322
  title: options.title,
300
323
  phase: "runtime"
301
- });
324
+ }, children);
302
325
  }
303
- function screenshot(options) {
304
- attachDoc({
326
+ function screenshot(options, children) {
327
+ return attachDoc({
305
328
  kind: "screenshot",
306
329
  path: options.path,
307
330
  alt: options.alt,
308
331
  phase: "runtime"
309
- });
332
+ }, children);
310
333
  }
311
- function tag(name) {
334
+ function tag(name, children) {
312
335
  const names = Array.isArray(name) ? name : [name];
313
- attachDoc({ kind: "tag", names, phase: "runtime" });
336
+ return attachDoc({ kind: "tag", names, phase: "runtime" }, children);
314
337
  }
315
- function custom(options) {
316
- attachDoc({
338
+ function custom(options, children) {
339
+ return attachDoc({
317
340
  kind: "custom",
318
341
  type: options.type,
319
342
  data: options.data,
320
343
  phase: "runtime"
321
- });
344
+ }, children);
322
345
  }
323
346
  function attach(options) {
324
347
  const ctx = getContext();
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/story-api.ts","../src/types.ts","../src/index.ts"],"sourcesContent":["/**\n * story.* API for executable-stories-vitest.\n *\n * Uses native Vitest describe/it/test with opt-in documentation:\n *\n * @example\n * ```ts\n * import { describe, it, expect } from 'vitest';\n * import { story } from 'executable-stories-vitest';\n *\n * describe('Calculator', () => {\n * it('adds two numbers', ({ task }) => {\n * story.init(task);\n *\n * story.given('two numbers 5 and 3');\n * const a = 5;\n * const b = 3;\n *\n * story.when('I add them together');\n * const result = a + b;\n *\n * story.then('the result is 8');\n * expect(result).toBe(8);\n * });\n * });\n * ```\n */\n\nimport { createRequire } from 'node:module';\nimport {\n tryGetActiveOtelContext,\n resolveTraceUrl,\n} from 'executable-stories-formatters';\nimport type {\n DocEntry,\n StepKeyword,\n StoryDocs,\n StoryMeta,\n StoryOptions,\n StoryStep,\n VitestSuite,\n} from './types';\n\n// ============================================================================\n// Task Interface (compatible with Vitest's actual task type)\n// ============================================================================\n\n/**\n * Minimal task interface compatible with Vitest's Test type.\n * The meta property accepts any object type to be compatible with Vitest's TaskMeta.\n */\ninterface TaskLike {\n name: string;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n meta: any;\n suite?: VitestSuite;\n file?: { name?: string };\n}\n\n// ============================================================================\n// Story Context\n// ============================================================================\n\n/** Attachment options for story.attach() */\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 */\ninterface ScopedAttachment extends AttachmentOptions {\n stepIndex?: number;\n stepId?: string;\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 /** Reference to task.meta for updates */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n taskMeta: any;\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/** 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(task) must be called first. Use: it('name', ({ task }) => { story.init(task); ... });\",\n );\n }\n return activeContext;\n}\n\n/** Re-attach current meta to task.meta.story so reporter sees steps and docs (e.g. story.note). */\nfunction syncMetaToTask(): void {\n if (activeContext?.taskMeta) {\n activeContext.taskMeta.story = activeContext.meta;\n }\n}\n\n// ============================================================================\n// Helper Functions\n// ============================================================================\n\n/**\n * Check if a name looks like a file path (to filter out from suite paths).\n */\nfunction looksLikeFilePath(name: string): boolean {\n if (name.includes('/') || name.includes('\\\\')) return true;\n if (name.includes('.spec.') || name.includes('.test.')) return true;\n if (/\\.(spec|test)\\.(ts|js|mjs|cjs)$/.test(name)) return true;\n if (/\\.(ts|js|mjs|cjs)$/.test(name)) return true;\n return false;\n}\n\n/**\n * Extract the suite path (parent describe names) from a Vitest task object.\n */\nfunction extractSuitePath(task: TaskLike): string[] | undefined {\n const path: string[] = [];\n const fileName = task.file?.name;\n let current: VitestSuite | undefined = task.suite;\n\n while (current) {\n const name = current.name;\n if (\n name &&\n name.trim() !== '' &&\n name !== '<root>' &&\n name !== fileName &&\n !looksLikeFilePath(name)\n ) {\n path.unshift(name);\n }\n current = current.suite;\n }\n\n return path.length > 0 ? path : undefined;\n}\n\n/**\n * Normalize ticket option to array format.\n */\nfunction normalizeTickets(\n ticket: string | string[] | undefined,\n): string[] | undefined {\n if (!ticket) return undefined;\n return Array.isArray(ticket) ? ticket : [ticket];\n}\n\n/**\n * Convert StoryDocs inline options to DocEntry array.\n * Matches the standalone DocApi method signatures.\n */\nfunction convertStoryDocsToEntries(docs: StoryDocs): DocEntry[] {\n const entries: DocEntry[] = [];\n\n // note(text)\n if (docs.note) {\n entries.push({ kind: 'note', text: docs.note, phase: 'runtime' });\n }\n\n // tag(name | names)\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\n // kv(label, value) - multiple pairs via Record\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\n // code(label, content, lang?)\n if (docs.code) {\n entries.push({\n kind: 'code',\n label: docs.code.label,\n content: docs.code.content,\n lang: docs.code.lang,\n phase: 'runtime',\n });\n }\n\n // json(label, value)\n if (docs.json) {\n entries.push({\n kind: 'code',\n label: docs.json.label,\n content: JSON.stringify(docs.json.value, null, 2),\n lang: 'json',\n phase: 'runtime',\n });\n }\n\n // table(label, columns, rows)\n if (docs.table) {\n entries.push({\n kind: 'table',\n label: docs.table.label,\n columns: docs.table.columns,\n rows: docs.table.rows,\n phase: 'runtime',\n });\n }\n\n // link(label, url)\n if (docs.link) {\n entries.push({\n kind: 'link',\n label: docs.link.label,\n url: docs.link.url,\n phase: 'runtime',\n });\n }\n\n // section(title, markdown)\n if (docs.section) {\n entries.push({\n kind: 'section',\n title: docs.section.title,\n markdown: docs.section.markdown,\n phase: 'runtime',\n });\n }\n\n // mermaid(code, title?)\n if (docs.mermaid) {\n entries.push({\n kind: 'mermaid',\n code: docs.mermaid.code,\n title: docs.mermaid.title,\n phase: 'runtime',\n });\n }\n\n // screenshot(path, alt?)\n if (docs.screenshot) {\n entries.push({\n kind: 'screenshot',\n path: docs.screenshot.path,\n alt: docs.screenshot.alt,\n phase: 'runtime',\n });\n }\n\n // custom(type, data)\n if (docs.custom) {\n entries.push({\n kind: 'custom',\n type: docs.custom.type,\n data: docs.custom.data,\n phase: 'runtime',\n });\n }\n\n return entries;\n}\n\n// ============================================================================\n// story.init()\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 task - The Vitest task object from ({ task }) => { ... }\n * @param options - Optional story configuration (tags, ticket, meta)\n *\n * @example\n * ```ts\n * it('adds two numbers', ({ task }) => {\n * story.init(task);\n * // ... rest of test\n * });\n *\n * // With options:\n * it('admin deletes user', ({ task }) => {\n * story.init(task, {\n * tags: ['admin', 'destructive'],\n * ticket: 'JIRA-456'\n * });\n * });\n * ```\n */\nfunction init(task: TaskLike, options?: StoryOptions): void {\n const meta: StoryMeta = {\n scenario: task.name,\n steps: [],\n suitePath: extractSuitePath(task),\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', task.name);\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 // Attach to task.meta so reporter can find it\n task.meta.story = meta;\n\n // Set active context\n activeContext = {\n meta,\n currentStep: null,\n taskMeta: task.meta,\n stepCounter: 0,\n attachments: [],\n activeTimers: new Map(),\n timerCounter: 0,\n };\n}\n\n// ============================================================================\n// Step Markers\n// ============================================================================\n\n/**\n * Create a step marker function for a given keyword.\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 syncMetaToTask();\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; syncMetaToTask(); return val; },\n (err) => { step.durationMs = performance.now() - start; syncMetaToTask(); throw err; },\n ) as T;\n }\n step.durationMs = performance.now() - start;\n syncMetaToTask();\n return result;\n } catch (err) {\n step.durationMs = performance.now() - start;\n syncMetaToTask();\n throw err;\n }\n }\n return stepMarker;\n}\n\n// ============================================================================\n// Doc Methods (Standalone)\n// ============================================================================\n\n/**\n * Add a free-text note to the current step or story-level if before any step.\n */\nfunction note(text: string): void {\n const ctx = getContext();\n const entry: DocEntry = { kind: 'note', text, phase: 'runtime' };\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}\n\n// ============================================================================\n// Doc Method Types (shared between standalone and inline)\n// ============================================================================\n\n/** Options for kv() - key-value pair */\ninterface KvOptions {\n label: string;\n value: unknown;\n}\n\n/** Options for json() - JSON code block */\ninterface JsonOptions {\n label: string;\n value: unknown;\n}\n\n/** Options for code() - code block with optional language */\ninterface CodeOptions {\n label: string;\n content: string;\n lang?: string;\n}\n\n/** Options for table() - markdown table */\ninterface TableOptions {\n label: string;\n columns: string[];\n rows: string[][];\n}\n\n/** Options for link() - hyperlink */\ninterface LinkOptions {\n label: string;\n url: string;\n}\n\n/** Options for section() - titled markdown section */\ninterface SectionOptions {\n title: string;\n markdown: string;\n}\n\n/** Options for mermaid() - Mermaid diagram */\ninterface MermaidOptions {\n code: string;\n title?: string;\n}\n\n/** Options for screenshot() - screenshot reference */\ninterface ScreenshotOptions {\n path: string;\n alt?: string;\n}\n\n/** Options for custom() - custom doc entry */\ninterface CustomOptions {\n type: string;\n data: unknown;\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 syncMetaToTask();\n}\n\n// ============================================================================\n// Doc Methods (Standalone) - same shape as inline docs\n// ============================================================================\n\n/**\n * Add a key-value pair to the current step or story-level.\n * @example story.kv({ label: 'Payment ID', value: 'pay_123' })\n */\nfunction kv(options: KvOptions): void {\n attachDoc({\n kind: 'kv',\n label: options.label,\n value: options.value,\n phase: 'runtime',\n });\n}\n\n/**\n * Add a JSON code block to the current step or story-level.\n * @example story.json({ label: 'Order', value: { id: 123 } })\n */\nfunction json(options: JsonOptions): void {\n const content = JSON.stringify(options.value, null, 2);\n attachDoc({\n kind: 'code',\n label: options.label,\n content,\n lang: 'json',\n phase: 'runtime',\n });\n}\n\n/**\n * Add a code block with optional language to the current step or story-level.\n * @example story.code({ label: 'Config', content: 'port: 3000', lang: 'yaml' })\n */\nfunction code(options: CodeOptions): void {\n attachDoc({\n kind: 'code',\n label: options.label,\n content: options.content,\n lang: options.lang,\n phase: 'runtime',\n });\n}\n\n/**\n * Add a markdown table to the current step or story-level.\n * @example story.table({ label: 'Users', columns: ['Name', 'Role'], rows: [['Alice', 'Admin']] })\n */\nfunction table(options: TableOptions): void {\n attachDoc({\n kind: 'table',\n label: options.label,\n columns: options.columns,\n rows: options.rows,\n phase: 'runtime',\n });\n}\n\n/**\n * Add a hyperlink to the current step or story-level.\n * @example story.link({ label: 'API Docs', url: 'https://docs.example.com' })\n */\nfunction link(options: LinkOptions): void {\n attachDoc({\n kind: 'link',\n label: options.label,\n url: options.url,\n phase: 'runtime',\n });\n}\n\n/**\n * Add a titled section with markdown content to the current step or story-level.\n * @example story.section({ title: 'Details', markdown: 'This is **important**' })\n */\nfunction section(options: SectionOptions): void {\n attachDoc({\n kind: 'section',\n title: options.title,\n markdown: options.markdown,\n phase: 'runtime',\n });\n}\n\n/**\n * Add a Mermaid diagram to the current step or story-level.\n * @example story.mermaid({ code: 'graph LR; A-->B', title: 'Flow' })\n */\nfunction mermaid(options: MermaidOptions): void {\n attachDoc({\n kind: 'mermaid',\n code: options.code,\n title: options.title,\n phase: 'runtime',\n });\n}\n\n/**\n * Add a screenshot reference to the current step or story-level.\n * @example story.screenshot({ path: '/screenshots/result.png', alt: 'Final result' })\n */\nfunction screenshot(options: ScreenshotOptions): void {\n attachDoc({\n kind: 'screenshot',\n path: options.path,\n alt: options.alt,\n phase: 'runtime',\n });\n}\n\n/**\n * Add tag(s) to the current step or story-level.\n * @example story.tag('admin') or story.tag(['admin', 'security'])\n */\nfunction tag(name: string | string[]): void {\n const names = Array.isArray(name) ? name : [name];\n attachDoc({ kind: 'tag', names, phase: 'runtime' });\n}\n\n/**\n * Add a custom documentation entry for use with custom renderers.\n * @example story.custom({ type: 'myType', data: { foo: 'bar' } })\n */\nfunction custom(options: CustomOptions): void {\n attachDoc({\n kind: 'custom',\n type: options.type,\n data: options.data,\n phase: 'runtime',\n });\n}\n\n// ============================================================================\n// Attachments\n// ============================================================================\n\n/**\n * Attach a file or inline content to the current step or test case.\n * @example story.attach({ name: 'screenshot', mediaType: 'image/png', path: '/tmp/screenshot.png' })\n */\nfunction 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 // Store attachments on task.meta so reporter can read them\n if (ctx.taskMeta) {\n ctx.taskMeta.storyAttachments = ctx.attachments;\n }\n}\n\n// ============================================================================\n// Step Timing\n// ============================================================================\n\n/**\n * Start a timer for the current step. Returns a token to pass to endTimer().\n */\nfunction 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/**\n * End a timer and record duration on the step that was active when startTimer() was called.\n */\nfunction 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 syncMetaToTask();\n }\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 syncMetaToTask();\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 syncMetaToTask();\n return val;\n },\n (err) => {\n step.durationMs = performance.now() - start;\n syncMetaToTask();\n throw err;\n },\n ) as T;\n }\n\n step.durationMs = performance.now() - start;\n syncMetaToTask();\n return result;\n } catch (err) {\n step.durationMs = performance.now() - start;\n syncMetaToTask();\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 * await story.expect('async check', async () => { ... });\n * ```\n */\nfunction storyExpect<T>(text: string, body: () => T): T {\n return fn('Then', text, body);\n}\n\n// ============================================================================\n// Span attachment: story.attachSpans()\n// ============================================================================\n\n/**\n * Attach OTel spans to the current test so the StoryReporter renders them\n * as a trace waterfall in HTML reports.\n *\n * Accepts any array of objects with at least `spanId` and `name` fields.\n * Structurally compatible with autotel's `SerializedSpan` and the\n * `OtelSpan` type from executable-stories-formatters.\n *\n * @example\n * ```ts\n * import { serializeSpan } from 'autotel/test-span-collector';\n *\n * // After running code that creates spans:\n * const spans = exporter.getFinishedSpans().map(serializeSpan);\n * story.attachSpans(spans);\n * ```\n */\nfunction attachSpans(spans: ReadonlyArray<Record<string, unknown>>): void {\n const ctx = getContext();\n if (ctx.taskMeta) {\n ctx.taskMeta.otelSpans = spans;\n }\n}\n\n// ============================================================================\n// Export story object\n// ============================================================================\n\n/**\n * The main story API object.\n *\n * Use with native Vitest describe/it/test for full IDE support:\n *\n * @example\n * ```ts\n * import { describe, it, expect } from 'vitest';\n * import { story } from 'executable-stories-vitest';\n *\n * describe('Calculator', () => {\n * it('adds two numbers', ({ task }) => {\n * story.init(task);\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 // Core\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,\n kv,\n json,\n code,\n table,\n link,\n section,\n mermaid,\n screenshot,\n tag,\n custom,\n\n // Attachments\n attach,\n\n // OTel span attachment\n attachSpans,\n\n // Step wrappers\n fn,\n expect: storyExpect,\n\n // Step timing\n startTimer,\n endTimer,\n};\n\nexport type Story = typeof story;\n","/**\n * Type definitions for executable-stories-vitest.\n *\n * Shared story types (StepKeyword, DocEntry, StoryStep, StoryMeta, etc.)\n * are imported from executable-stories-formatters — the single source of truth.\n * This module re-exports them and adds Vitest-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// Vitest-specific Types\n// ============================================================================\n\n/**\n * Inline documentation options for step markers.\n * Pass to story.given(), story.when(), story.then() as second argument.\n *\n * @example\n * ```ts\n * story.given('valid credentials', {\n * json: { label: 'Credentials', value: { email: 'test@example.com', password: '***' } },\n * note: 'Password is masked for security'\n * });\n * ```\n */\nexport interface StoryDocs {\n /** Add a free-text note */\n note?: string;\n /** Add tag(s) for categorization */\n tag?: string | string[];\n /** Add key-value pairs */\n kv?: Record<string, unknown>;\n /** Add a code block with label and optional language */\n code?: { label: string; content: string; lang?: string };\n /** Add a JSON data block with label */\n json?: { label: string; value: unknown };\n /** Add a markdown table with label */\n table?: { label: string; columns: string[]; rows: string[][] };\n /** Add a hyperlink */\n link?: { label: string; url: string };\n /** Add a titled section with markdown content */\n section?: { title: string; markdown: string };\n /** Add a Mermaid diagram with optional title */\n mermaid?: { code: string; title?: string };\n /** Add a screenshot reference */\n screenshot?: { path: string; alt?: string };\n /** Add a custom documentation entry */\n custom?: { type: string; data: unknown };\n}\n\n/**\n * Options for configuring a story via story.init().\n *\n * @example\n * ```ts\n * it('admin deletes user', ({ task }) => {\n * story.init(task, {\n * tags: ['admin', 'destructive'],\n * ticket: 'JIRA-456'\n * });\n * });\n * ```\n */\nexport interface StoryOptions {\n /** Tags for filtering and categorizing stories */\n tags?: string[];\n /** Ticket/issue reference(s) for requirements traceability */\n ticket?: string | string[];\n /** Arbitrary user-defined metadata */\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// ============================================================================\n// Vitest Task Type (minimal interface)\n// ============================================================================\n\n/** Minimal Vitest suite interface for suite path extraction */\nexport interface VitestSuite {\n /** Suite name */\n name?: string;\n /** Parent suite (optional) */\n suite?: VitestSuite;\n}\n\n/**\n * Minimal Vitest task interface for story.init().\n * This is the { task } from it('name', ({ task }) => { ... }).\n *\n * Uses generic type parameter to be compatible with Vitest's actual TaskMeta type.\n */\nexport interface VitestTask<TMeta = Record<string, unknown>> {\n /** The test/task name */\n name: string;\n /** Task metadata object where we store story data */\n meta: TMeta;\n /** Parent suite (optional) */\n suite?: VitestSuite;\n /** The test file (optional) */\n file?: { name?: string };\n}\n","/**\n * executable-stories-vitest: Native Vitest story/given/when/then with Markdown doc generation.\n *\n * Uses native Vitest describe/it/test for full IDE support:\n *\n * @example\n * ```ts\n * import { describe, it, expect } from 'vitest';\n * import { story } from 'executable-stories-vitest';\n *\n * describe('Calculator', () => {\n * it('adds two numbers', ({ task }) => {\n * story.init(task);\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 *\n * In vitest.config, import StoryReporter from \"executable-stories-vitest/reporter\":\n *\n * @example\n * ```ts\n * import { defineConfig } from \"vitest/config\";\n * import { StoryReporter } from \"executable-stories-vitest/reporter\";\n *\n * export default defineConfig({\n * test: {\n * reporters: [\"default\", new StoryReporter()],\n * },\n * });\n * ```\n */\n\n// Core API\nexport { story, type Story } from './story-api';\n\n// Types for consumers\nexport type {\n StoryMeta,\n StoryStep,\n DocEntry,\n StepKeyword,\n StepMode,\n DocPhase,\n StoryDocs,\n StoryOptions,\n VitestTask,\n VitestSuite,\n} from './types';\n\nexport { STORY_META_KEY } from './types';\n\n// Reporter types (actual reporter is in /reporter subpath)\nexport type {\n StoryReporterOptions,\n OutputFormat,\n OutputMode,\n ColocatedStyle,\n OutputRule,\n FormatterOptions,\n} from './reporter';\n\nconst STORY_REPORTER_GUARD_MSG =\n 'Do not import StoryReporter from \"executable-stories-vitest\". In vitest.config, import it from \"executable-stories-vitest/reporter\".';\n\n/** @internal Guard: throws if used. Import StoryReporter from \"executable-stories-vitest/reporter\" in vitest.config. */\nexport class StoryReporter {\n static __isGuard = true;\n constructor() {\n throw new Error(STORY_REPORTER_GUARD_MSG);\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/story-api.ts","../src/types.ts","../src/index.ts"],"sourcesContent":["/**\n * story.* API for executable-stories-vitest.\n *\n * Uses native Vitest describe/it/test with opt-in documentation:\n *\n * @example\n * ```ts\n * import { describe, it, expect } from 'vitest';\n * import { story } from 'executable-stories-vitest';\n *\n * describe('Calculator', () => {\n * it('adds two numbers', ({ task }) => {\n * story.init(task);\n *\n * story.given('two numbers 5 and 3');\n * const a = 5;\n * const b = 3;\n *\n * story.when('I add them together');\n * const result = a + b;\n *\n * story.then('the result is 8');\n * expect(result).toBe(8);\n * });\n * });\n * ```\n */\n\nimport { createRequire } from 'node:module';\nimport {\n tryGetActiveOtelContext,\n resolveTraceUrl,\n} from 'executable-stories-formatters';\nimport type {\n DocEntry,\n NormalizedTicket,\n StepKeyword,\n StoryDocs,\n StoryMeta,\n StoryOptions,\n StoryStep,\n TicketInput,\n VitestSuite,\n} from './types';\n\n// ============================================================================\n// Task Interface (compatible with Vitest's actual task type)\n// ============================================================================\n\n/**\n * Minimal task interface compatible with Vitest's Test type.\n * The meta property accepts any object type to be compatible with Vitest's TaskMeta.\n */\ninterface TaskLike {\n name: string;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n meta: any;\n suite?: VitestSuite;\n file?: { name?: string };\n}\n\n// ============================================================================\n// Story Context\n// ============================================================================\n\n/** Attachment options for story.attach() */\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 */\ninterface ScopedAttachment extends AttachmentOptions {\n stepIndex?: number;\n stepId?: string;\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 /** Reference to task.meta for updates */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n taskMeta: any;\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/** 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(task) must be called first. Use: it('name', ({ task }) => { story.init(task); ... });\",\n );\n }\n return activeContext;\n}\n\n/** Re-attach current meta to task.meta.story so reporter sees steps and docs (e.g. story.note). */\nfunction syncMetaToTask(): void {\n if (activeContext?.taskMeta) {\n activeContext.taskMeta.story = activeContext.meta;\n }\n}\n\n// ============================================================================\n// Helper Functions\n// ============================================================================\n\n/**\n * Check if a name looks like a file path (to filter out from suite paths).\n */\nfunction looksLikeFilePath(name: string): boolean {\n if (name.includes('/') || name.includes('\\\\')) return true;\n if (name.includes('.spec.') || name.includes('.test.')) return true;\n if (/\\.(spec|test)\\.(ts|js|mjs|cjs)$/.test(name)) return true;\n if (/\\.(ts|js|mjs|cjs)$/.test(name)) return true;\n return false;\n}\n\n/**\n * Extract the suite path (parent describe names) from a Vitest task object.\n */\nfunction extractSuitePath(task: TaskLike): string[] | undefined {\n const path: string[] = [];\n const fileName = task.file?.name;\n let current: VitestSuite | undefined = task.suite;\n\n while (current) {\n const name = current.name;\n if (\n name &&\n name.trim() !== '' &&\n name !== '<root>' &&\n name !== fileName &&\n !looksLikeFilePath(name)\n ) {\n path.unshift(name);\n }\n current = current.suite;\n }\n\n return path.length > 0 ? path : undefined;\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 * Convert StoryDocs inline options to DocEntry array.\n * Matches the standalone DocApi method signatures.\n */\nfunction convertStoryDocsToEntries(docs: StoryDocs): DocEntry[] {\n const entries: DocEntry[] = [];\n\n // note(text)\n if (docs.note) {\n entries.push({ kind: 'note', text: docs.note, phase: 'runtime' });\n }\n\n // tag(name | names)\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\n // kv(label, value) - multiple pairs via Record\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\n // code(label, content, lang?)\n if (docs.code) {\n entries.push({\n kind: 'code',\n label: docs.code.label,\n content: docs.code.content,\n lang: docs.code.lang,\n phase: 'runtime',\n });\n }\n\n // json(label, value)\n if (docs.json) {\n entries.push({\n kind: 'code',\n label: docs.json.label,\n content: JSON.stringify(docs.json.value, null, 2),\n lang: 'json',\n phase: 'runtime',\n });\n }\n\n // table(label, columns, rows)\n if (docs.table) {\n entries.push({\n kind: 'table',\n label: docs.table.label,\n columns: docs.table.columns,\n rows: docs.table.rows,\n phase: 'runtime',\n });\n }\n\n // link(label, url)\n if (docs.link) {\n entries.push({\n kind: 'link',\n label: docs.link.label,\n url: docs.link.url,\n phase: 'runtime',\n });\n }\n\n // section(title, markdown)\n if (docs.section) {\n entries.push({\n kind: 'section',\n title: docs.section.title,\n markdown: docs.section.markdown,\n phase: 'runtime',\n });\n }\n\n // mermaid(code, title?)\n if (docs.mermaid) {\n entries.push({\n kind: 'mermaid',\n code: docs.mermaid.code,\n title: docs.mermaid.title,\n phase: 'runtime',\n });\n }\n\n // screenshot(path, alt?)\n if (docs.screenshot) {\n entries.push({\n kind: 'screenshot',\n path: docs.screenshot.path,\n alt: docs.screenshot.alt,\n phase: 'runtime',\n });\n }\n\n // custom(type, data)\n if (docs.custom) {\n entries.push({\n kind: 'custom',\n type: docs.custom.type,\n data: docs.custom.data,\n phase: 'runtime',\n });\n }\n\n return entries;\n}\n\n// ============================================================================\n// story.init()\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 task - The Vitest task object from ({ task }) => { ... }\n * @param options - Optional story configuration (tags, ticket, meta)\n *\n * @example\n * ```ts\n * it('adds two numbers', ({ task }) => {\n * story.init(task);\n * // ... rest of test\n * });\n *\n * // With options:\n * it('admin deletes user', ({ task }) => {\n * story.init(task, {\n * tags: ['admin', 'destructive'],\n * ticket: 'JIRA-456'\n * });\n * });\n * ```\n */\nfunction init(task: TaskLike, options?: StoryOptions): void {\n const meta: StoryMeta = {\n scenario: task.name,\n steps: [],\n suitePath: extractSuitePath(task),\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 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', task.name);\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 // Attach to task.meta so reporter can find it\n task.meta.story = meta;\n\n // Set active context\n activeContext = {\n meta,\n currentStep: null,\n taskMeta: task.meta,\n stepCounter: 0,\n attachments: [],\n activeTimers: new Map(),\n timerCounter: 0,\n };\n}\n\n// ============================================================================\n// Step Markers\n// ============================================================================\n\n/**\n * Create a step marker function for a given keyword.\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 syncMetaToTask();\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 syncMetaToTask();\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; syncMetaToTask(); return val; },\n (err) => { step.durationMs = performance.now() - start; syncMetaToTask(); throw err; },\n ) as T;\n }\n step.durationMs = performance.now() - start;\n syncMetaToTask();\n return result;\n } catch (err) {\n step.durationMs = performance.now() - start;\n syncMetaToTask();\n throw err;\n }\n }\n return stepMarker;\n}\n\n// ============================================================================\n// Doc Methods (Standalone)\n// ============================================================================\n\n/**\n * Add a free-text note to the current step or story-level if before any step.\n */\nfunction note(text: string, children?: DocEntry[]): DocEntry {\n return attachDoc({ kind: 'note', text, phase: 'runtime' }, children);\n}\n\n// ============================================================================\n// Doc Method Types (shared between standalone and inline)\n// ============================================================================\n\n/** Options for kv() - key-value pair */\ninterface KvOptions {\n label: string;\n value: unknown;\n}\n\n/** Options for json() - JSON code block */\ninterface JsonOptions {\n label: string;\n value: unknown;\n}\n\n/** Options for code() - code block with optional language */\ninterface CodeOptions {\n label: string;\n content: string;\n lang?: string;\n}\n\n/** Options for table() - markdown table */\ninterface TableOptions {\n label: string;\n columns: string[];\n rows: string[][];\n}\n\n/** Options for link() - hyperlink */\ninterface LinkOptions {\n label: string;\n url: string;\n}\n\n/** Options for section() - titled markdown section */\ninterface SectionOptions {\n title: string;\n markdown: string;\n}\n\n/** Options for mermaid() - Mermaid diagram */\ninterface MermaidOptions {\n code: string;\n title?: string;\n}\n\n/** Options for screenshot() - screenshot reference */\ninterface ScreenshotOptions {\n path: string;\n alt?: string;\n}\n\n/** Options for custom() - custom doc entry */\ninterface CustomOptions {\n type: string;\n data: unknown;\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 syncMetaToTask();\n return entry;\n}\n\n// ============================================================================\n// Doc Methods (Standalone) - same shape as inline docs\n// ============================================================================\n\n/**\n * Add a key-value pair to the current step or story-level.\n * @example story.kv({ label: 'Payment ID', value: 'pay_123' })\n */\nfunction kv(options: KvOptions, children?: DocEntry[]): DocEntry {\n return attachDoc({\n kind: 'kv',\n label: options.label,\n value: options.value,\n phase: 'runtime',\n }, children);\n}\n\n/**\n * Add a JSON code block to the current step or story-level.\n * @example story.json({ label: 'Order', value: { id: 123 } })\n */\nfunction json(options: JsonOptions, children?: DocEntry[]): DocEntry {\n const content = JSON.stringify(options.value, null, 2);\n return attachDoc({\n kind: 'code',\n label: options.label,\n content,\n lang: 'json',\n phase: 'runtime',\n }, children);\n}\n\n/**\n * Add a code block with optional language to the current step or story-level.\n * @example story.code({ label: 'Config', content: 'port: 3000', lang: 'yaml' })\n */\nfunction code(options: CodeOptions, children?: DocEntry[]): DocEntry {\n return attachDoc({\n kind: 'code',\n label: options.label,\n content: options.content,\n lang: options.lang,\n phase: 'runtime',\n }, children);\n}\n\n/**\n * Add a markdown table to the current step or story-level.\n * @example story.table({ label: 'Users', columns: ['Name', 'Role'], rows: [['Alice', 'Admin']] })\n */\nfunction table(options: TableOptions, children?: DocEntry[]): DocEntry {\n return attachDoc({\n kind: 'table',\n label: options.label,\n columns: options.columns,\n rows: options.rows,\n phase: 'runtime',\n }, children);\n}\n\n/**\n * Add a hyperlink to the current step or story-level.\n * @example story.link({ label: 'API Docs', url: 'https://docs.example.com' })\n */\nfunction link(options: LinkOptions, children?: DocEntry[]): DocEntry {\n return attachDoc({\n kind: 'link',\n label: options.label,\n url: options.url,\n phase: 'runtime',\n }, children);\n}\n\n/**\n * Add a titled section with markdown content to the current step or story-level.\n * @example story.section({ title: 'Details', markdown: 'This is **important**' })\n */\nfunction section(options: SectionOptions, children?: DocEntry[]): DocEntry {\n return attachDoc({\n kind: 'section',\n title: options.title,\n markdown: options.markdown,\n phase: 'runtime',\n }, children);\n}\n\n/**\n * Add a Mermaid diagram to the current step or story-level.\n * @example story.mermaid({ code: 'graph LR; A-->B', title: 'Flow' })\n */\nfunction mermaid(options: MermaidOptions, children?: DocEntry[]): DocEntry {\n return attachDoc({\n kind: 'mermaid',\n code: options.code,\n title: options.title,\n phase: 'runtime',\n }, children);\n}\n\n/**\n * Add a screenshot reference to the current step or story-level.\n * @example story.screenshot({ path: '/screenshots/result.png', alt: 'Final result' })\n */\nfunction screenshot(options: ScreenshotOptions, children?: DocEntry[]): DocEntry {\n return attachDoc({\n kind: 'screenshot',\n path: options.path,\n alt: options.alt,\n phase: 'runtime',\n }, children);\n}\n\n/**\n * Add tag(s) to the current step or story-level.\n * @example story.tag('admin') or story.tag(['admin', 'security'])\n */\nfunction 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/**\n * Add a custom documentation entry for use with custom renderers.\n * @example story.custom({ type: 'myType', data: { foo: 'bar' } })\n */\nfunction custom(options: CustomOptions, children?: DocEntry[]): DocEntry {\n return attachDoc({\n kind: 'custom',\n type: options.type,\n data: options.data,\n phase: 'runtime',\n }, children);\n}\n\n// ============================================================================\n// Attachments\n// ============================================================================\n\n/**\n * Attach a file or inline content to the current step or test case.\n * @example story.attach({ name: 'screenshot', mediaType: 'image/png', path: '/tmp/screenshot.png' })\n */\nfunction 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 // Store attachments on task.meta so reporter can read them\n if (ctx.taskMeta) {\n ctx.taskMeta.storyAttachments = ctx.attachments;\n }\n}\n\n// ============================================================================\n// Step Timing\n// ============================================================================\n\n/**\n * Start a timer for the current step. Returns a token to pass to endTimer().\n */\nfunction 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/**\n * End a timer and record duration on the step that was active when startTimer() was called.\n */\nfunction 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 syncMetaToTask();\n }\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 syncMetaToTask();\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 syncMetaToTask();\n return val;\n },\n (err) => {\n step.durationMs = performance.now() - start;\n syncMetaToTask();\n throw err;\n },\n ) as T;\n }\n\n step.durationMs = performance.now() - start;\n syncMetaToTask();\n return result;\n } catch (err) {\n step.durationMs = performance.now() - start;\n syncMetaToTask();\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 * await story.expect('async check', async () => { ... });\n * ```\n */\nfunction storyExpect<T>(text: string, body: () => T): T {\n return fn('Then', text, body);\n}\n\n// ============================================================================\n// Span attachment: story.attachSpans()\n// ============================================================================\n\n/**\n * Attach OTel spans to the current test so the StoryReporter renders them\n * as a trace waterfall in HTML reports.\n *\n * Accepts any array of objects with at least `spanId` and `name` fields.\n * Structurally compatible with autotel's `SerializedSpan` and the\n * `OtelSpan` type from executable-stories-formatters.\n *\n * @example\n * ```ts\n * import { serializeSpan } from 'autotel/test-span-collector';\n *\n * // After running code that creates spans:\n * const spans = exporter.getFinishedSpans().map(serializeSpan);\n * story.attachSpans(spans);\n * ```\n */\nfunction attachSpans(spans: ReadonlyArray<Record<string, unknown>>): void {\n const ctx = getContext();\n if (ctx.taskMeta) {\n ctx.taskMeta.otelSpans = spans;\n }\n}\n\n// ============================================================================\n// Export story object\n// ============================================================================\n\n/**\n * The main story API object.\n *\n * Use with native Vitest describe/it/test for full IDE support:\n *\n * @example\n * ```ts\n * import { describe, it, expect } from 'vitest';\n * import { story } from 'executable-stories-vitest';\n *\n * describe('Calculator', () => {\n * it('adds two numbers', ({ task }) => {\n * story.init(task);\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 // Core\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,\n kv,\n json,\n code,\n table,\n link,\n section,\n mermaid,\n screenshot,\n tag,\n custom,\n\n // Attachments\n attach,\n\n // OTel span attachment\n attachSpans,\n\n // Step wrappers\n fn,\n expect: storyExpect,\n\n // Step timing\n startTimer,\n endTimer,\n};\n\nexport type Story = typeof story;\n","/**\n * Type definitions for executable-stories-vitest.\n *\n * Shared story types (StepKeyword, DocEntry, StoryStep, StoryMeta, etc.)\n * are imported from executable-stories-formatters — the single source of truth.\n * This module re-exports them and adds Vitest-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// ============================================================================\n// Ticket Input Types\n// ============================================================================\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// Vitest-specific Types\n// ============================================================================\n\n/**\n * Inline documentation options for step markers.\n * Pass to story.given(), story.when(), story.then() as second argument.\n *\n * @example\n * ```ts\n * story.given('valid credentials', {\n * json: { label: 'Credentials', value: { email: 'test@example.com', password: '***' } },\n * note: 'Password is masked for security'\n * });\n * ```\n */\nexport interface StoryDocs {\n /** Add a free-text note */\n note?: string;\n /** Add tag(s) for categorization */\n tag?: string | string[];\n /** Add key-value pairs */\n kv?: Record<string, unknown>;\n /** Add a code block with label and optional language */\n code?: { label: string; content: string; lang?: string };\n /** Add a JSON data block with label */\n json?: { label: string; value: unknown };\n /** Add a markdown table with label */\n table?: { label: string; columns: string[]; rows: string[][] };\n /** Add a hyperlink */\n link?: { label: string; url: string };\n /** Add a titled section with markdown content */\n section?: { title: string; markdown: string };\n /** Add a Mermaid diagram with optional title */\n mermaid?: { code: string; title?: string };\n /** Add a screenshot reference */\n screenshot?: { path: string; alt?: string };\n /** Add a custom documentation entry */\n custom?: { type: string; data: unknown };\n}\n\n/**\n * Options for configuring a story via story.init().\n *\n * @example\n * ```ts\n * it('admin deletes user', ({ task }) => {\n * story.init(task, {\n * tags: ['admin', 'destructive'],\n * ticket: 'JIRA-456'\n * });\n * });\n * ```\n */\nexport interface StoryOptions {\n /** Tags for filtering and categorizing stories */\n tags?: string[];\n /** Ticket/issue reference(s) for requirements traceability */\n ticket?: TicketInput | TicketInput[];\n /** Arbitrary user-defined metadata */\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// ============================================================================\n// Vitest Task Type (minimal interface)\n// ============================================================================\n\n/** Minimal Vitest suite interface for suite path extraction */\nexport interface VitestSuite {\n /** Suite name */\n name?: string;\n /** Parent suite (optional) */\n suite?: VitestSuite;\n}\n\n/**\n * Minimal Vitest task interface for story.init().\n * This is the { task } from it('name', ({ task }) => { ... }).\n *\n * Uses generic type parameter to be compatible with Vitest's actual TaskMeta type.\n */\nexport interface VitestTask<TMeta = Record<string, unknown>> {\n /** The test/task name */\n name: string;\n /** Task metadata object where we store story data */\n meta: TMeta;\n /** Parent suite (optional) */\n suite?: VitestSuite;\n /** The test file (optional) */\n file?: { name?: string };\n}\n","/**\n * executable-stories-vitest: Native Vitest story/given/when/then with Markdown doc generation.\n *\n * Uses native Vitest describe/it/test for full IDE support:\n *\n * @example\n * ```ts\n * import { describe, it, expect } from 'vitest';\n * import { story } from 'executable-stories-vitest';\n *\n * describe('Calculator', () => {\n * it('adds two numbers', ({ task }) => {\n * story.init(task);\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 *\n * In vitest.config, import StoryReporter from \"executable-stories-vitest/reporter\":\n *\n * @example\n * ```ts\n * import { defineConfig } from \"vitest/config\";\n * import { StoryReporter } from \"executable-stories-vitest/reporter\";\n *\n * export default defineConfig({\n * test: {\n * reporters: [\"default\", new StoryReporter()],\n * },\n * });\n * ```\n */\n\n// Core API\nexport { story, type Story } from './story-api';\n\n// Types for consumers\nexport type {\n StoryMeta,\n StoryStep,\n DocEntry,\n StepKeyword,\n StepMode,\n DocPhase,\n NormalizedTicket,\n TicketInput,\n StoryDocs,\n StoryOptions,\n VitestTask,\n VitestSuite,\n} from './types';\n\nexport { STORY_META_KEY } from './types';\n\n// Reporter types (actual reporter is in /reporter subpath)\nexport type {\n StoryReporterOptions,\n OutputFormat,\n OutputMode,\n ColocatedStyle,\n OutputRule,\n FormatterOptions,\n} from './reporter';\n\nconst STORY_REPORTER_GUARD_MSG =\n 'Do not import StoryReporter from \"executable-stories-vitest\". In vitest.config, import it from \"executable-stories-vitest/reporter\".';\n\n/** @internal Guard: throws if used. Import StoryReporter from \"executable-stories-vitest/reporter\" in vitest.config. */\nexport class StoryReporter {\n static __isGuard = true;\n constructor() {\n throw new Error(STORY_REPORTER_GUARD_MSG);\n }\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "executable-stories-vitest",
3
- "version": "8.1.0",
3
+ "version": "8.1.1",
4
4
  "description": "TS-first story/given/when/then helpers for Vitest with Markdown user-story doc generation.",
5
5
  "author": "Jag Reehal <jag@jagreehal.com>",
6
6
  "homepage": "https://github.com/jagreehal/executable-stories#readme",
@@ -38,7 +38,7 @@
38
38
  ],
39
39
  "peerDependencies": {
40
40
  "vitest": ">=4.1.0",
41
- "executable-stories-formatters": "^0.7.0"
41
+ "executable-stories-formatters": "^0.7.1"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@opentelemetry/api": "^1.9.0",
@@ -48,7 +48,7 @@
48
48
  "typescript": "^5.9.3",
49
49
  "vitest": "^4.0.18",
50
50
  "eslint-config-executable-stories": "0.2.0",
51
- "executable-stories-formatters": "0.7.0"
51
+ "executable-stories-formatters": "0.7.1"
52
52
  },
53
53
  "keywords": [
54
54
  "vitest",