executable-stories-vitest 8.0.0 → 8.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +8 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +19 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -430,6 +430,12 @@ function fn(keyword, text, body) {
|
|
|
430
430
|
function storyExpect(text, body) {
|
|
431
431
|
return fn("Then", text, body);
|
|
432
432
|
}
|
|
433
|
+
function attachSpans(spans) {
|
|
434
|
+
const ctx = getContext();
|
|
435
|
+
if (ctx.taskMeta) {
|
|
436
|
+
ctx.taskMeta.otelSpans = spans;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
433
439
|
var story = {
|
|
434
440
|
// Core
|
|
435
441
|
init,
|
|
@@ -463,6 +469,8 @@ var story = {
|
|
|
463
469
|
custom,
|
|
464
470
|
// Attachments
|
|
465
471
|
attach,
|
|
472
|
+
// OTel span attachment
|
|
473
|
+
attachSpans,
|
|
466
474
|
// Step wrappers
|
|
467
475
|
fn,
|
|
468
476
|
expect: storyExpect,
|
package/dist/index.cjs.map
CHANGED
|
@@ -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// 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 // 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 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"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -339,6 +339,24 @@ declare function fn<T>(keyword: StepKeyword, text: string, body: () => T): T;
|
|
|
339
339
|
* ```
|
|
340
340
|
*/
|
|
341
341
|
declare function storyExpect<T>(text: string, body: () => T): T;
|
|
342
|
+
/**
|
|
343
|
+
* Attach OTel spans to the current test so the StoryReporter renders them
|
|
344
|
+
* as a trace waterfall in HTML reports.
|
|
345
|
+
*
|
|
346
|
+
* Accepts any array of objects with at least `spanId` and `name` fields.
|
|
347
|
+
* Structurally compatible with autotel's `SerializedSpan` and the
|
|
348
|
+
* `OtelSpan` type from executable-stories-formatters.
|
|
349
|
+
*
|
|
350
|
+
* @example
|
|
351
|
+
* ```ts
|
|
352
|
+
* import { serializeSpan } from 'autotel/test-span-collector';
|
|
353
|
+
*
|
|
354
|
+
* // After running code that creates spans:
|
|
355
|
+
* const spans = exporter.getFinishedSpans().map(serializeSpan);
|
|
356
|
+
* story.attachSpans(spans);
|
|
357
|
+
* ```
|
|
358
|
+
*/
|
|
359
|
+
declare function attachSpans(spans: ReadonlyArray<Record<string, unknown>>): void;
|
|
342
360
|
/**
|
|
343
361
|
* The main story API object.
|
|
344
362
|
*
|
|
@@ -431,6 +449,7 @@ declare const story: {
|
|
|
431
449
|
tag: typeof tag;
|
|
432
450
|
custom: typeof custom;
|
|
433
451
|
attach: typeof attach;
|
|
452
|
+
attachSpans: typeof attachSpans;
|
|
434
453
|
fn: typeof fn;
|
|
435
454
|
expect: typeof storyExpect;
|
|
436
455
|
startTimer: typeof startTimer;
|
package/dist/index.d.ts
CHANGED
|
@@ -339,6 +339,24 @@ declare function fn<T>(keyword: StepKeyword, text: string, body: () => T): T;
|
|
|
339
339
|
* ```
|
|
340
340
|
*/
|
|
341
341
|
declare function storyExpect<T>(text: string, body: () => T): T;
|
|
342
|
+
/**
|
|
343
|
+
* Attach OTel spans to the current test so the StoryReporter renders them
|
|
344
|
+
* as a trace waterfall in HTML reports.
|
|
345
|
+
*
|
|
346
|
+
* Accepts any array of objects with at least `spanId` and `name` fields.
|
|
347
|
+
* Structurally compatible with autotel's `SerializedSpan` and the
|
|
348
|
+
* `OtelSpan` type from executable-stories-formatters.
|
|
349
|
+
*
|
|
350
|
+
* @example
|
|
351
|
+
* ```ts
|
|
352
|
+
* import { serializeSpan } from 'autotel/test-span-collector';
|
|
353
|
+
*
|
|
354
|
+
* // After running code that creates spans:
|
|
355
|
+
* const spans = exporter.getFinishedSpans().map(serializeSpan);
|
|
356
|
+
* story.attachSpans(spans);
|
|
357
|
+
* ```
|
|
358
|
+
*/
|
|
359
|
+
declare function attachSpans(spans: ReadonlyArray<Record<string, unknown>>): void;
|
|
342
360
|
/**
|
|
343
361
|
* The main story API object.
|
|
344
362
|
*
|
|
@@ -431,6 +449,7 @@ declare const story: {
|
|
|
431
449
|
tag: typeof tag;
|
|
432
450
|
custom: typeof custom;
|
|
433
451
|
attach: typeof attach;
|
|
452
|
+
attachSpans: typeof attachSpans;
|
|
434
453
|
fn: typeof fn;
|
|
435
454
|
expect: typeof storyExpect;
|
|
436
455
|
startTimer: typeof startTimer;
|
package/dist/index.js
CHANGED
|
@@ -404,6 +404,12 @@ function fn(keyword, text, body) {
|
|
|
404
404
|
function storyExpect(text, body) {
|
|
405
405
|
return fn("Then", text, body);
|
|
406
406
|
}
|
|
407
|
+
function attachSpans(spans) {
|
|
408
|
+
const ctx = getContext();
|
|
409
|
+
if (ctx.taskMeta) {
|
|
410
|
+
ctx.taskMeta.otelSpans = spans;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
407
413
|
var story = {
|
|
408
414
|
// Core
|
|
409
415
|
init,
|
|
@@ -437,6 +443,8 @@ var story = {
|
|
|
437
443
|
custom,
|
|
438
444
|
// Attachments
|
|
439
445
|
attach,
|
|
446
|
+
// OTel span attachment
|
|
447
|
+
attachSpans,
|
|
440
448
|
// Step wrappers
|
|
441
449
|
fn,
|
|
442
450
|
expect: storyExpect,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/story-api.ts","../src/types.ts","../src/index.ts"],"sourcesContent":["/**\n * 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// 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 // 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 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":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "executable-stories-vitest",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.1.0",
|
|
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",
|