executable-stories-jest 8.7.0 → 8.8.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.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/story-api.ts","../src/types.ts","../src/index.ts"],"sourcesContent":["/**\n * Jest story.* API for executable-stories.\n *\n * Uses native Jest describe/it/test with opt-in documentation:\n *\n * @example\n * ```ts\n * import { story } from 'executable-stories-jest';\n *\n * describe('Calculator', () => {\n * it('adds two numbers', () => {\n * story.init();\n *\n * story.given('two numbers 5 and 3');\n * const a = 5;\n * const b = 3;\n *\n * story.when('I add them together');\n * const result = a + b;\n *\n * story.then('the result is 8');\n * expect(result).toBe(8);\n * });\n * });\n * ```\n */\n\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport { createHash } from \"node:crypto\";\nimport { createRequire } from 'node:module';\nimport { tryGetActiveOtelContext, resolveTraceUrl } from 'executable-stories-core/utils/otel-detect';\nimport { buildHtmlDocEntry } from 'executable-stories-core/utils/doc-builders';\nimport type {\n DocEntry,\n FeatureInput,\n NormalizedTicket,\n StepKeyword,\n StoryDocs,\n StoryMeta,\n StoryOptions,\n StoryStep,\n ScopedAttachment,\n AttachmentOptions,\n TicketInput,\n KvOptions,\n JsonOptions,\n StateOptions,\n CodeOptions,\n TableOptions,\n LinkOptions,\n SectionOptions,\n MermaidOptions,\n ScreenshotOptions,\n VideoOptions,\n HtmlOptions,\n CustomOptions,\n} from './types';\n\n// Re-export types for consumers\nexport type {\n StoryMeta,\n StoryStep,\n DocEntry,\n StepKeyword,\n StoryDocs,\n StoryOptions,\n AttachmentOptions,\n TicketInput,\n NormalizedTicket,\n} from './types';\n\n// ============================================================================\n// Story Context\n// ============================================================================\n\n/** Internal timer entry */\ninterface TimerEntry {\n start: number;\n stepIndex?: number;\n stepId?: string;\n consumed: boolean;\n}\n\ninterface StoryContext {\n /** The story metadata being built */\n meta: StoryMeta;\n /** The current step (for attaching docs) */\n currentStep: StoryStep | null;\n /** Deterministic step counter (resets per test case) */\n stepCounter: number;\n /** Collected attachments with step scope */\n attachments: ScopedAttachment[];\n /** Active timers keyed by token */\n activeTimers: Map<number, TimerEntry>;\n /** Monotonic timer token counter */\n timerCounter: number;\n /** Test file path for registry lookups */\n testPath: string;\n /** Index into the storyRegistry array for this scenario */\n scenarioIndex: number;\n}\n\n// ============================================================================\n// File-based story collection (works across Jest worker processes)\n// ============================================================================\n\n// Use globalThis to ensure the registry is shared across module instances\n// This is needed because Jest may load the setup file and test files as separate module instances\ndeclare global {\n // eslint-disable-next-line no-var\n var __jestExecutableStoriesRegistry: Map<string, StoryMeta[]> | undefined;\n // eslint-disable-next-line no-var\n var __jestExecutableStoriesExitHandler: boolean | undefined;\n}\n\n/** Feature declarations, keyed by test file path */\nconst featureRegistry = new Map<string, FeatureInput>();\n\n/** Stories collected during test execution, keyed by test file path */\nconst storyRegistry: Map<string, StoryMeta[]> = globalThis.__jestExecutableStoriesRegistry ??= new Map();\n\n/** Attachments collected per story, keyed by test file path → scenario index → attachments */\nconst attachmentRegistry = new Map<string, Map<number, ScopedAttachment[]>>();\n\n/** OTel spans collected per story, keyed by test file path → scenario index → spans */\nconst otelSpansRegistry = new Map<string, Map<number, ReadonlyArray<Record<string, unknown>>>>();\n\n/** Track if we've registered the process exit handler */\nlet exitHandlerRegistered = globalThis.__jestExecutableStoriesExitHandler ?? false;\n\n/** Get the output directory for story JSON files */\nfunction getOutputDir(): string {\n const baseDir = process.env.JEST_STORY_DOCS_DIR ?? \".jest-executable-stories\";\n return path.resolve(process.cwd(), baseDir);\n}\n\n/** Flush all collected stories to JSON files */\nfunction flushStories(): void {\n if (storyRegistry.size === 0) return;\n\n const workerId = process.env.JEST_WORKER_ID ?? \"0\";\n const outputDir = path.join(getOutputDir(), `worker-${workerId}`);\n fs.mkdirSync(outputDir, { recursive: true });\n\n for (const [testFilePath, scenarios] of storyRegistry) {\n if (!scenarios.length) continue;\n const hash = createHash(\"sha1\").update(testFilePath).digest(\"hex\").slice(0, 12);\n const baseName = testFilePath === \"unknown\" ? \"unknown\" : path.basename(testFilePath);\n const outFile = path.join(outputDir, `${baseName}.${hash}.json`);\n\n // Include attachments and otelSpans per scenario (keyed by index, not name)\n const fileAttachments = attachmentRegistry.get(testFilePath);\n const fileOtelSpans = otelSpansRegistry.get(testFilePath);\n const scenariosWithAttachments = scenarios.map((s, i) => ({\n ...s,\n _attachments: fileAttachments?.get(i) ?? [],\n ...(fileOtelSpans?.get(i) ? { _otelSpans: fileOtelSpans.get(i) } : {}),\n }));\n\n const declared = featureRegistry.get(testFilePath);\n const payload = {\n testFilePath,\n ...(declared ? { feature: declared } : {}),\n scenarios: scenariosWithAttachments,\n };\n fs.writeFileSync(outFile, JSON.stringify(payload, null, 2) + \"\\n\", \"utf8\");\n }\n storyRegistry.clear();\n attachmentRegistry.clear();\n otelSpansRegistry.clear();\n featureRegistry.clear();\n}\n\n/** Register process exit handler to flush stories (once per worker) */\nfunction registerExitHandler(): void {\n if (exitHandlerRegistered) return;\n exitHandlerRegistered = true;\n globalThis.__jestExecutableStoriesExitHandler = true;\n // Use 'exit' event - always fired when Node.js is about to exit\n // Note: Only sync operations work here, which is fine for fs.writeFileSync\n process.on(\"exit\", () => {\n flushStories();\n });\n}\n\n// ============================================================================\n// Jest-specific context\n// ============================================================================\n\n/** Active story context - set by story.init() */\nlet activeContext: StoryContext | null = null;\n\n/** Counter to track source order of stories (increments on each story.init call) */\nlet sourceOrderCounter = 0;\n\n/**\n * Get the current story context. Throws if story.init() wasn't called.\n */\nfunction getContext(): StoryContext {\n if (!activeContext) {\n throw new Error(\n \"story.init() must be called first. Use: it('name', () => { story.init(); ... });\"\n );\n }\n return activeContext;\n}\n\n// ============================================================================\n// Helper Functions\n// ============================================================================\n\n/**\n * Normalize ticket option to array of NormalizedTicket objects.\n */\nfunction normalizeTickets(\n ticket: TicketInput | TicketInput[] | undefined,\n): NormalizedTicket[] | undefined {\n if (!ticket) return undefined;\n const arr = Array.isArray(ticket) ? ticket : [ticket];\n return arr.map((t) => (typeof t === 'string' ? { id: t } : t));\n}\n\n/**\n * Extract the suite path from Jest's currentTestName.\n * Jest's currentTestName is formatted as: \"describe1 > describe2 > test name\"\n */\nfunction extractSuitePath(currentTestName: string): { suitePath?: string[]; testName: string } {\n const parts = currentTestName.split(\" > \");\n if (parts.length <= 1) {\n return { testName: currentTestName };\n }\n const testName = parts[parts.length - 1];\n const suitePath = parts.slice(0, -1);\n return { suitePath, testName };\n}\n\n/**\n * Convert StoryDocs inline options to DocEntry array.\n */\nfunction convertStoryDocsToEntries(docs: StoryDocs): DocEntry[] {\n const entries: DocEntry[] = [];\n\n if (docs.note) {\n entries.push({ kind: \"note\", text: docs.note, phase: \"runtime\" });\n }\n if (docs.tag) {\n const names = Array.isArray(docs.tag) ? docs.tag : [docs.tag];\n entries.push({ kind: \"tag\", names, phase: \"runtime\" });\n }\n if (docs.kv) {\n for (const [label, value] of Object.entries(docs.kv)) {\n entries.push({ kind: \"kv\", label, value, phase: \"runtime\" });\n }\n }\n if (docs.code) {\n entries.push({ kind: \"code\", label: docs.code.label, content: docs.code.content, lang: docs.code.lang, phase: \"runtime\" });\n }\n if (docs.json) {\n entries.push({ kind: \"code\", label: docs.json.label, content: JSON.stringify(docs.json.value, null, 2), lang: \"json\", phase: \"runtime\" });\n }\n if (docs.state) {\n warnIfLargeState(docs.state.label, docs.state.value);\n entries.push({ kind: \"state\", label: docs.state.label, value: docs.state.value, phase: \"runtime\" });\n }\n if (docs.table) {\n entries.push({ kind: \"table\", label: docs.table.label, columns: docs.table.columns, rows: docs.table.rows, phase: \"runtime\" });\n }\n if (docs.link) {\n entries.push({ kind: \"link\", label: docs.link.label, url: docs.link.url, phase: \"runtime\" });\n }\n if (docs.section) {\n entries.push({ kind: \"section\", title: docs.section.title, markdown: docs.section.markdown, phase: \"runtime\" });\n }\n if (docs.mermaid) {\n entries.push({ kind: \"mermaid\", code: docs.mermaid.code, title: docs.mermaid.title, phase: \"runtime\" });\n }\n if (docs.screenshot) {\n entries.push({ kind: \"screenshot\", path: docs.screenshot.path, alt: docs.screenshot.alt, phase: \"runtime\" });\n }\n if (docs.video) {\n entries.push({ kind: \"video\", path: docs.video.path, caption: docs.video.caption, poster: docs.video.poster, phase: \"runtime\" });\n }\n if (docs.html) {\n entries.push(buildHtmlDocEntry(docs.html));\n }\n if (docs.custom) {\n entries.push({ kind: \"custom\", type: docs.custom.type, data: docs.custom.data, phase: \"runtime\" });\n }\n\n return entries;\n}\n\n/** Warn (non-fatal) when a state snapshot is suspiciously large. */\nfunction warnIfLargeState(label: string | undefined, value: unknown): void {\n try {\n const length = JSON.stringify(value)?.length ?? 0;\n if (length > 100_000) {\n console.warn(\n `[executable-stories] state \"${label ?? 'state'}\" is ${Math.round(length / 1024)}KB — consider capturing a projection of the entity instead of the whole thing`,\n );\n }\n } catch {\n /* non-serializable value; still recorded */\n }\n}\n\n// ============================================================================\n// Helper to attach doc entry to current step or story-level\n// ============================================================================\n\nfunction attachDoc(entry: DocEntry, children?: DocEntry[]): DocEntry {\n const ctx = getContext();\n if (children && children.length > 0) {\n entry.children = children;\n const childSet = new Set<DocEntry>(children);\n const filterDocs = (docs: DocEntry[]) => docs.filter((d) => !childSet.has(d));\n // Remove children from ALL containers (story-level + every step)\n ctx.meta.docs = filterDocs(ctx.meta.docs ?? []);\n for (const step of ctx.meta.steps) {\n if (step.docs) step.docs = filterDocs(step.docs);\n }\n }\n if (ctx.currentStep) {\n ctx.currentStep.docs ??= [];\n ctx.currentStep.docs.push(entry);\n } else {\n ctx.meta.docs ??= [];\n ctx.meta.docs.push(entry);\n }\n return entry;\n}\n\n// ============================================================================\n// Step Markers\n// ============================================================================\n\nfunction createStepMarker(keyword: StepKeyword) {\n function stepMarker(text: string, docs?: StoryDocs): void;\n function stepMarker(text: string, children: DocEntry[]): void;\n function stepMarker<T>(text: string, body: () => T): T;\n function stepMarker<T>(text: string, docsOrBody?: StoryDocs | DocEntry[] | (() => T)): T | void {\n const ctx = getContext();\n const isCallback = typeof docsOrBody === 'function';\n const isChildrenArray = Array.isArray(docsOrBody);\n\n const resolvedKeyword: StepKeyword =\n (keyword === 'Given' || keyword === 'When' || keyword === 'Then') &&\n ctx.meta.steps.some((s) => s.keyword === keyword)\n ? 'And'\n : keyword;\n\n let stepDocs: DocEntry[] = [];\n if (!isCallback && !isChildrenArray && docsOrBody) {\n stepDocs = convertStoryDocsToEntries(docsOrBody as StoryDocs);\n }\n\n const step: StoryStep = {\n id: `step-${ctx.stepCounter++}`,\n keyword: resolvedKeyword,\n text,\n docs: stepDocs,\n ...(isCallback ? { wrapped: true } : {}),\n };\n\n ctx.meta.steps.push(step);\n ctx.currentStep = step;\n\n // Handle DocEntry[] children: attach as step docs and deduplicate from story-level\n if (isChildrenArray) {\n const children = docsOrBody as DocEntry[];\n if (children.length > 0) {\n const childSet = new Set<DocEntry>(children);\n // Deduplicate from story-level docs\n ctx.meta.docs = (ctx.meta.docs ?? []).filter((d) => !childSet.has(d));\n // Deduplicate from step docs of earlier steps\n for (const prevStep of ctx.meta.steps) {\n if (prevStep !== step && prevStep.docs) {\n prevStep.docs = prevStep.docs.filter((d) => !childSet.has(d));\n }\n }\n step.docs = [...(step.docs ?? []), ...children];\n }\n return;\n }\n\n if (!isCallback) return;\n\n const body = docsOrBody as () => T;\n const start = performance.now();\n\n try {\n const result = body();\n if (result instanceof Promise) {\n return result.then(\n (val) => { step.durationMs = performance.now() - start; return val; },\n (err) => { step.durationMs = performance.now() - start; throw err; },\n ) as T;\n }\n step.durationMs = performance.now() - start;\n return result;\n } catch (err) {\n step.durationMs = performance.now() - start;\n throw err;\n }\n }\n return stepMarker;\n}\n\n// ============================================================================\n// story.init() - Jest-specific\n// ============================================================================\n\n/**\n * Initialize a story for the current test.\n * Must be called at the start of each test that wants documentation.\n *\n * @param options - Optional story configuration (tags, ticket, meta)\n *\n * @example\n * ```ts\n * it('adds two numbers', () => {\n * story.init();\n * // ... rest of test\n * });\n * ```\n */\n/**\n * Declare what the file's scenarios are for, before any of them run.\n *\n * Scenarios say what the system does. This says why the feature exists and who\n * it serves, so a reader meets the intent before the examples. Call it once per\n * test file, at module scope or at the top of the outermost `describe`.\n *\n * @example\n * ```ts\n * story.feature({\n * kind: 'ability',\n * title: 'Employees can secure their passwords',\n * narrative: 'Weak passwords are how most accounts get taken over.',\n * });\n * ```\n */\nfunction feature(input: FeatureInput): void {\n // Jest only knows the current file once a test is running, so resolve it the\n // same way `init` does and fall back to the module that called us.\n const testPath = expect.getState().testPath || callerFile() || \"unknown\";\n featureRegistry.set(testPath, input);\n registerExitHandler();\n}\n\n/**\n * Best-effort source file of whoever called into this module.\n *\n * `story.feature(...)` usually runs at module scope, where Jest has not yet set\n * `testPath`. The stack is the only place the file name is available then.\n */\nfunction callerFile(): string | undefined {\n const stack = new Error().stack;\n if (!stack) return undefined;\n\n for (const line of stack.split(\"\\n\").slice(1)) {\n const match = /\\((?:file:\\/\\/)?([^()]+?):\\d+:\\d+\\)/.exec(line)\n ?? /at (?:file:\\/\\/)?([^()\\s]+?):\\d+:\\d+/.exec(line);\n const file = match?.[1];\n if (file && !file.includes(\"executable-stories-jest\") && !file.startsWith(\"node:\")) {\n return file;\n }\n }\n\n return undefined;\n}\n\nfunction init(options?: StoryOptions): void {\n // Get current test info from Jest globals\n const state = expect.getState();\n const currentTestName = state.currentTestName || \"Unknown test\";\n const testPath = state.testPath || \"unknown\";\n\n const { suitePath, testName } = extractSuitePath(currentTestName);\n\n const meta: StoryMeta = {\n scenario: testName,\n steps: [],\n suitePath,\n tags: options?.tags,\n tickets: normalizeTickets(options?.ticket),\n covers: options?.covers,\n meta: options?.meta,\n sourceOrder: sourceOrderCounter++,\n };\n\n // OTel bridge: detect active span, flow data bidirectionally\n const otelCtx = tryGetActiveOtelContext();\n if (otelCtx) {\n // OTel -> Story: capture traceId in structured meta\n meta.meta = { ...meta.meta, otel: { traceId: otelCtx.traceId, spanId: otelCtx.spanId } };\n\n // OTel -> Story: inject human-readable doc entries\n meta.docs = meta.docs ?? [];\n meta.docs.push({ kind: 'kv', label: 'Trace ID', value: otelCtx.traceId, phase: 'runtime' });\n\n const template = options?.traceUrlTemplate ?? process.env.OTEL_TRACE_URL_TEMPLATE;\n const url = resolveTraceUrl(template, otelCtx.traceId);\n if (url) {\n meta.docs.push({ kind: 'link', label: 'View Trace', url, phase: 'runtime' });\n }\n\n // Story -> OTel: enrich active span with story attributes\n try {\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n const reqUrl = import.meta.url\n ?? (typeof __filename !== 'undefined' ? `file://${__filename}` : undefined);\n const req = createRequire(reqUrl!);\n const api = req('@opentelemetry/api');\n const span = api.trace?.getActiveSpan?.();\n if (span) {\n span.setAttribute('story.scenario', testName);\n if (options?.tags?.length) span.setAttribute('story.tags', options.tags);\n if (options?.ticket) {\n const tickets = Array.isArray(options.ticket) ? options.ticket : [options.ticket];\n span.setAttribute('story.tickets', tickets.map((t) => typeof t === 'string' ? t : t.id));\n }\n }\n } catch { /* OTel not available */ }\n }\n\n // Store in registry for this file\n const existing = storyRegistry.get(testPath);\n let scenarioIndex: number;\n if (existing) {\n scenarioIndex = existing.length;\n existing.push(meta);\n } else {\n scenarioIndex = 0;\n storyRegistry.set(testPath, [meta]);\n }\n\n // Register exit handler to flush stories when worker exits\n registerExitHandler();\n\n // Set active context\n activeContext = {\n meta,\n currentStep: null,\n stepCounter: 0,\n attachments: [],\n activeTimers: new Map(),\n timerCounter: 0,\n testPath,\n scenarioIndex,\n };\n\n // Link attachments to the registry for this test file + scenario index\n if (!attachmentRegistry.has(testPath)) {\n attachmentRegistry.set(testPath, new Map());\n }\n attachmentRegistry.get(testPath)!.set(scenarioIndex, activeContext.attachments);\n}\n\n// ============================================================================\n// Step Wrappers: story.fn() and story.expect()\n// ============================================================================\n\n/**\n * Wrap a function body as a step. Records the step with timing and `wrapped: true`.\n * Supports both sync and async functions. Returns whatever the function returns.\n *\n * @param keyword - The BDD keyword (Given, When, Then, And, But)\n * @param text - Step description\n * @param body - The function to execute\n * @returns The return value of body (or a Promise of it if body is async)\n *\n * @example\n * ```ts\n * const data = story.fn('Given', 'setup data', () => ({ a: 5, b: 3 }));\n * const result = await story.fn('When', 'call API', async () => fetch('/api'));\n * ```\n */\nfunction fn<T>(keyword: StepKeyword, text: string, body: () => T): T {\n const ctx = getContext();\n const resolvedKeyword: StepKeyword =\n (keyword === 'Given' || keyword === 'When' || keyword === 'Then') &&\n ctx.meta.steps.some((s) => s.keyword === keyword)\n ? 'And'\n : keyword;\n\n const step: StoryStep = {\n id: `step-${ctx.stepCounter++}`,\n keyword: resolvedKeyword,\n text,\n docs: [],\n wrapped: true,\n };\n\n ctx.meta.steps.push(step);\n ctx.currentStep = step;\n\n const start = performance.now();\n\n try {\n const result = body();\n\n // Handle async functions\n if (result instanceof Promise) {\n return result.then(\n (val) => {\n step.durationMs = performance.now() - start;\n return val;\n },\n (err) => {\n step.durationMs = performance.now() - start;\n throw err;\n },\n ) as T;\n }\n\n step.durationMs = performance.now() - start;\n return result;\n } catch (err) {\n step.durationMs = performance.now() - start;\n throw err;\n }\n}\n\n/**\n * Wrap an assertion as a Then step. Shorthand for `story.fn('Then', text, body)`.\n *\n * @param text - Step description\n * @param body - The assertion function to execute\n *\n * @example\n * ```ts\n * story.expect('the result is 8', () => { expect(result).toBe(8); });\n * ```\n */\nfunction storyExpect<T>(text: string, body: () => T): T {\n return fn('Then', text, body);\n}\n\n// ============================================================================\n// Export story object\n// ============================================================================\n\n/**\n * The main story API object for Jest.\n *\n * @example\n * ```ts\n * import { story } from 'executable-stories-jest';\n *\n * describe('Calculator', () => {\n * it('adds two numbers', () => {\n * story.init();\n *\n * story.given('two numbers 5 and 3');\n * const a = 5, b = 3;\n *\n * story.when('I add them together');\n * const result = a + b;\n *\n * story.then('the result is 8');\n * expect(result).toBe(8);\n * });\n * });\n * ```\n */\nexport const story = {\n // Jest-specific init\n init,\n feature,\n\n // BDD step markers\n given: createStepMarker(\"Given\"),\n when: createStepMarker(\"When\"),\n then: createStepMarker(\"Then\"),\n and: createStepMarker(\"And\"),\n but: createStepMarker(\"But\"),\n\n // AAA pattern aliases\n arrange: createStepMarker(\"Given\"),\n act: createStepMarker(\"When\"),\n assert: createStepMarker(\"Then\"),\n\n // Additional aliases\n setup: createStepMarker(\"Given\"),\n context: createStepMarker(\"Given\"),\n execute: createStepMarker(\"When\"),\n action: createStepMarker(\"When\"),\n verify: createStepMarker(\"Then\"),\n\n // Standalone doc methods\n note(text: string, children?: DocEntry[]): DocEntry {\n return attachDoc({ kind: \"note\", text, phase: \"runtime\" }, children);\n },\n\n tag(name: string | string[], children?: DocEntry[]): DocEntry {\n const names = Array.isArray(name) ? name : [name];\n return attachDoc({ kind: \"tag\", names, phase: \"runtime\" }, children);\n },\n\n kv(options: KvOptions, children?: DocEntry[]): DocEntry {\n return attachDoc({ kind: \"kv\", label: options.label, value: options.value, phase: \"runtime\" }, children);\n },\n\n json(options: JsonOptions, children?: DocEntry[]): DocEntry {\n const content = JSON.stringify(options.value, null, 2);\n return attachDoc({ kind: \"code\", label: options.label, content, lang: \"json\", phase: \"runtime\" }, children);\n },\n\n /**\n * Capture what the world looks like at this step — a data snapshot that\n * becomes a storyboard frame in generated docs. Consecutive snapshots with\n * the same label get diffed downstream. The value must be JSON-serializable.\n * @example\n * ```ts\n * story.given('an empty basket');\n * story.state({ label: 'Basket', value: { items: [], total: 0 } });\n * story.when('a widget is added');\n * story.state({ label: 'Basket', value: { items: ['widget'], total: 49.99 } });\n * ```\n */\n state(options: StateOptions, children?: DocEntry[]): DocEntry {\n warnIfLargeState(options.label, options.value);\n return attachDoc({ kind: \"state\", label: options.label, value: options.value, phase: \"runtime\" }, children);\n },\n\n code(options: CodeOptions, children?: DocEntry[]): DocEntry {\n return attachDoc({ kind: \"code\", label: options.label, content: options.content, lang: options.lang, phase: \"runtime\" }, children);\n },\n\n table(options: TableOptions, children?: DocEntry[]): DocEntry {\n return attachDoc({ kind: \"table\", label: options.label, columns: options.columns, rows: options.rows, phase: \"runtime\" }, children);\n },\n\n link(options: LinkOptions, children?: DocEntry[]): DocEntry {\n return attachDoc({ kind: \"link\", label: options.label, url: options.url, phase: \"runtime\" }, children);\n },\n\n section(options: SectionOptions, children?: DocEntry[]): DocEntry {\n return attachDoc({ kind: \"section\", title: options.title, markdown: options.markdown, phase: \"runtime\" }, children);\n },\n\n mermaid(options: MermaidOptions, children?: DocEntry[]): DocEntry {\n return attachDoc({ kind: \"mermaid\", code: options.code, title: options.title, phase: \"runtime\" }, children);\n },\n\n screenshot(options: ScreenshotOptions, children?: DocEntry[]): DocEntry {\n return attachDoc({ kind: \"screenshot\", path: options.path, alt: options.alt, phase: \"runtime\" }, children);\n },\n\n video(options: VideoOptions, children?: DocEntry[]): DocEntry {\n return attachDoc({ kind: \"video\", path: options.path, caption: options.caption, poster: options.poster, phase: \"runtime\" }, children);\n },\n\n html(options: HtmlOptions, children?: DocEntry[]): DocEntry {\n return attachDoc(buildHtmlDocEntry(options), children);\n },\n\n custom(options: CustomOptions, children?: DocEntry[]): DocEntry {\n return attachDoc({ kind: \"custom\", type: options.type, data: options.data, phase: \"runtime\" }, children);\n },\n\n // Attachments\n attach(options: AttachmentOptions): void {\n const ctx = getContext();\n const stepIndex = ctx.currentStep\n ? ctx.meta.steps.indexOf(ctx.currentStep)\n : undefined;\n ctx.attachments.push({\n ...options,\n stepIndex: stepIndex !== undefined && stepIndex >= 0 ? stepIndex : undefined,\n stepId: ctx.currentStep?.id,\n });\n },\n\n // OTel span attachment\n attachSpans(spans: ReadonlyArray<Record<string, unknown>>): void {\n const ctx = getContext();\n if (!otelSpansRegistry.has(ctx.testPath)) {\n otelSpansRegistry.set(ctx.testPath, new Map());\n }\n otelSpansRegistry.get(ctx.testPath)!.set(ctx.scenarioIndex, spans);\n },\n\n // Step wrappers\n fn,\n expect: storyExpect,\n\n // Step timing\n startTimer(): number {\n const ctx = getContext();\n const token = ctx.timerCounter++;\n const stepIndex = ctx.currentStep\n ? ctx.meta.steps.indexOf(ctx.currentStep)\n : undefined;\n ctx.activeTimers.set(token, {\n start: performance.now(),\n stepIndex: stepIndex !== undefined && stepIndex >= 0 ? stepIndex : undefined,\n stepId: ctx.currentStep?.id,\n consumed: false,\n });\n return token;\n },\n\n endTimer(token: number): void {\n const ctx = getContext();\n const entry = ctx.activeTimers.get(token);\n if (!entry || entry.consumed) return;\n\n entry.consumed = true;\n const durationMs = performance.now() - entry.start;\n\n let step: StoryStep | undefined;\n if (entry.stepId) {\n step = ctx.meta.steps.find((s) => s.id === entry.stepId);\n }\n if (!step && entry.stepIndex !== undefined) {\n step = ctx.meta.steps[entry.stepIndex];\n }\n\n if (step) {\n step.durationMs = durationMs;\n }\n },\n};\n\nexport type Story = typeof story;\n\n// ============================================================================\n// Internal exports for setup file\n// ============================================================================\n\n/**\n * Internal API for the setup file and tests. Not for public use.\n * @internal\n */\nexport const _internal = {\n flushStories,\n /** Clear active context (for tests that assert getContext() throws). */\n clearContext(): void {\n activeContext = null;\n },\n};\n","/**\n * Type definitions for executable-stories-jest.\n *\n * Shared story types are imported from executable-stories-core.\n * This module re-exports them and adds Jest-specific types.\n */\n\n// Re-export shared story types from core\nexport type {\n StepKeyword,\n StepMode,\n DocPhase,\n DocEntry,\n StoryStep,\n StoryMeta,\n NormalizedTicket,\n} from 'executable-stories-core/types/story';\n\nexport { STORY_META_KEY } from 'executable-stories-core/types/story';\n\n/** A ticket reference: either a plain string ID or an object with id and optional url */\nexport type TicketInput = string | { id: string; url?: string };\n\n// ============================================================================\n// Doc Options (for inline docs and standalone methods)\n// ============================================================================\n\n/** Options for kv() - key-value pair */\nexport interface KvOptions {\n label: string;\n value: unknown;\n}\n\n/** Options for json() - JSON code block */\nexport interface JsonOptions {\n label: string;\n value: unknown;\n}\n\n/** Options for state() - data snapshot of the world at this step */\nexport interface StateOptions {\n label?: string;\n value: unknown;\n}\n\n/** Options for code() - code block with optional language */\nexport interface CodeOptions {\n label: string;\n content: string;\n lang?: string;\n}\n\n/** Options for table() - markdown table */\nexport interface TableOptions {\n label: string;\n columns: string[];\n rows: string[][];\n}\n\n/** Options for link() - hyperlink */\nexport interface LinkOptions {\n label: string;\n url: string;\n}\n\n/** Options for section() - titled markdown section */\nexport interface SectionOptions {\n title: string;\n markdown: string;\n}\n\n/** Options for mermaid() - Mermaid diagram */\nexport interface MermaidOptions {\n code: string;\n title?: string;\n}\n\n/** Options for screenshot() - screenshot reference */\nexport interface ScreenshotOptions {\n path: string;\n alt?: string;\n}\n\n/** Options for video() - video reference */\nexport interface VideoOptions {\n path: string;\n caption?: string;\n poster?: string;\n}\n\n/** Options for html() - HTML embedded in a sandboxed iframe. Exactly one of path/url/content. */\nexport interface HtmlOptions {\n /** Local HTML file path (inlined into the report by default) */\n path?: string;\n /** Remote URL rendered via iframe src */\n url?: string;\n /** Inline HTML content rendered via iframe srcdoc */\n content?: string;\n title?: string;\n /** Iframe height: number → px, string passed through (e.g. '60vh'). Default 400px. */\n height?: number | string;\n}\n\n/** Options for custom() - custom doc entry */\nexport interface CustomOptions {\n type: string;\n data: unknown;\n}\n\n// ============================================================================\n// Inline Docs for Steps\n// ============================================================================\n\n/**\n * Inline documentation options for step markers.\n * Pass to story.given(), story.when(), story.then() as second argument.\n */\nexport interface StoryDocs {\n note?: string;\n tag?: string | string[];\n kv?: Record<string, unknown>;\n code?: CodeOptions;\n json?: JsonOptions;\n state?: StateOptions;\n table?: TableOptions;\n link?: LinkOptions;\n section?: SectionOptions;\n mermaid?: MermaidOptions;\n screenshot?: ScreenshotOptions;\n video?: VideoOptions;\n html?: HtmlOptions;\n custom?: CustomOptions;\n}\n\n// ============================================================================\n// Attachment Types\n// ============================================================================\n\n/** Options for attaching files or inline content to a test */\nexport interface AttachmentOptions {\n name: string;\n mediaType: string;\n path?: string;\n body?: string;\n encoding?: \"BASE64\" | \"IDENTITY\";\n charset?: string;\n fileName?: string;\n}\n\n/** Internal: attachment with step scope info */\nexport interface ScopedAttachment extends AttachmentOptions {\n stepIndex?: number;\n stepId?: string;\n}\n\n// ============================================================================\n// Story Options\n// ============================================================================\n\n/**\n * Options for configuring a story via story.init().\n */\n/** One entry in a feature's glossary. */\nexport interface GlossaryTerm {\n /** The term as it appears in scenario and step text. */\n term: string;\n /** What it means, in one or two sentences. */\n definition: string;\n}\n\n/** What `story.feature(...)` accepts. */\nexport interface FeatureInput {\n /** Heading for the feature. */\n title: string;\n /**\n * How to introduce it. `ability` frames the feature as something a person can\n * now do; `business-need` covers cross-cutting concerns like security or\n * performance that no single user asks for. Defaults to `feature`.\n */\n kind?: \"feature\" | \"ability\" | \"business-need\";\n /** Markdown explaining why the feature exists and who it serves. */\n narrative?: string;\n /** Tags applied to every scenario in the file. */\n tags?: string[];\n /** Terms this feature defines, linked wherever they appear in the report. */\n glossary?: GlossaryTerm[];\n}\n\nexport interface StoryOptions {\n tags?: string[];\n ticket?: TicketInput | TicketInput[];\n /** Product-code paths/globs this scenario exercises (project-root-relative). */\n covers?: string[];\n meta?: Record<string, unknown>;\n /** URL template for OTel trace links. Uses {traceId} placeholder. Also settable via OTEL_TRACE_URL_TEMPLATE env var. */\n traceUrlTemplate?: string;\n}\n","/**\n * Jest Executable Stories\n *\n * BDD-style executable documentation for Jest.\n *\n * @example\n * ```ts\n * import { story } from 'executable-stories-jest';\n *\n * describe('Calculator', () => {\n * it('adds two numbers', () => {\n * story.init();\n *\n * story.given('two numbers 5 and 3');\n * const a = 5;\n * const b = 3;\n *\n * story.when('I add them together');\n * const result = a + b;\n *\n * story.then('the result is 8');\n * expect(result).toBe(8);\n * });\n * });\n * ```\n */\n\n// Story API\nimport { story } from \"./story-api\";\nexport { story };\nexport type { Story } from \"./story-api\";\n\n// Top-level step helpers (framework contract)\nexport const given = story.given;\nexport const when = story.when;\nexport const then = story.then;\nexport const and = story.and;\nexport const but = story.but;\n\n// Re-export types\nexport type {\n StoryMeta,\n StoryStep,\n DocEntry,\n StepKeyword,\n StepMode,\n DocPhase,\n NormalizedTicket,\n TicketInput,\n StoryDocs,\n StoryOptions,\n AttachmentOptions,\n KvOptions,\n JsonOptions,\n StateOptions,\n CodeOptions,\n TableOptions,\n LinkOptions,\n SectionOptions,\n MermaidOptions,\n ScreenshotOptions,\n VideoOptions,\n HtmlOptions,\n CustomOptions,\n} from \"./types\";\n\nexport { STORY_META_KEY } from \"./types\";\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/story-api.ts","../src/types.ts","../src/index.ts"],"sourcesContent":["/**\n * Jest story.* API for executable-stories.\n *\n * Uses native Jest describe/it/test with opt-in documentation:\n *\n * @example\n * ```ts\n * import { story } from 'executable-stories-jest';\n *\n * describe('Calculator', () => {\n * it('adds two numbers', () => {\n * story.init();\n *\n * story.given('two numbers 5 and 3');\n * const a = 5;\n * const b = 3;\n *\n * story.when('I add them together');\n * const result = a + b;\n *\n * story.then('the result is 8');\n * expect(result).toBe(8);\n * });\n * });\n * ```\n */\n\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport { createHash } from \"node:crypto\";\nimport { createRequire } from 'node:module';\nimport { tryGetActiveOtelContext, resolveTraceUrl } from 'executable-stories-core/utils/otel-detect';\nimport { buildHtmlDocEntry } from 'executable-stories-core/utils/doc-builders';\nimport type {\n DocEntry,\n FeatureInput,\n NormalizedTicket,\n StepKeyword,\n StoryDocs,\n StoryMeta,\n StoryOptions,\n StoryStep,\n ScopedAttachment,\n AttachmentOptions,\n TicketInput,\n KvOptions,\n JsonOptions,\n StateOptions,\n CodeOptions,\n TableOptions,\n LinkOptions,\n SectionOptions,\n MermaidOptions,\n ScreenshotOptions,\n VideoOptions,\n HtmlOptions,\n CustomOptions,\n} from './types';\n\n// Re-export types for consumers\nexport type {\n StoryMeta,\n StoryStep,\n DocEntry,\n StepKeyword,\n StoryDocs,\n StoryOptions,\n AttachmentOptions,\n TicketInput,\n NormalizedTicket,\n} from './types';\n\n// ============================================================================\n// Story Context\n// ============================================================================\n\n/** Internal timer entry */\ninterface TimerEntry {\n start: number;\n stepIndex?: number;\n stepId?: string;\n consumed: boolean;\n}\n\ninterface StoryContext {\n /** The story metadata being built */\n meta: StoryMeta;\n /** The current step (for attaching docs) */\n currentStep: StoryStep | null;\n /** Deterministic step counter (resets per test case) */\n stepCounter: number;\n /** Collected attachments with step scope */\n attachments: ScopedAttachment[];\n /** Active timers keyed by token */\n activeTimers: Map<number, TimerEntry>;\n /** Monotonic timer token counter */\n timerCounter: number;\n /** Test file path for registry lookups */\n testPath: string;\n /** Index into the storyRegistry array for this scenario */\n scenarioIndex: number;\n /**\n * Marker step awaiting its assertion count.\n *\n * A marker states the claim and the assertion follows it, so the count is\n * only known once the next step starts or the test ends.\n */\n pendingStep: StoryStep | null;\n /** Assertion counter reading taken when {@link pendingStep} was created. */\n pendingFrom?: number;\n}\n\n/**\n * Jest's live per-test assertion counter, or undefined when it cannot be read.\n *\n * Undefined is not zero: it means we could not observe, and the report must not\n * report an unobserved step as one that asserted nothing.\n */\nfunction readAssertionCount(): number | undefined {\n try {\n const state = (expect as unknown as {\n getState?: () => { assertionCalls?: number };\n }).getState?.();\n return typeof state?.assertionCalls === \"number\" ? state.assertionCalls : undefined;\n } catch {\n return undefined;\n }\n}\n\n/** Record how many assertions ran between `before` and now. */\nfunction recordAssertions(step: StoryStep, before: number | undefined): void {\n if (before === undefined) return;\n const after = readAssertionCount();\n if (after === undefined || after < before) return;\n step.assertions = after - before;\n}\n\n/**\n * Flush at the end of every test, registered from this module.\n *\n * tsup bundles a separate copy of this file into `dist/setup.js`, so a hook\n * registered from the setup entry would flush a different module instance with\n * its own empty context. Registering here keeps the hook and the state\n * together. Runs at import time, which in a Jest test file is inside the root\n * describe scope.\n */\ntry {\n // The global, not a static @jest/globals import: this module is also bundled\n // into the reporter entry, which runs outside any test context.\n const registerAfterEach = (globalThis as {\n afterEach?: (fn: () => void) => void;\n }).afterEach;\n registerAfterEach?.(() => {\n flushPendingAssertions();\n });\n} catch {\n // Imported outside a Jest test context (the reporter, tooling): nothing to\n // register against, and nothing to flush.\n}\n\n/**\n * Close off the marker step waiting on a count.\n *\n * Called from the `afterEach` registered above, so the last marker in a test\n * still collects the assertions written after it.\n */\nfunction flushPendingAssertions(): void {\n const ctx = activeContext;\n if (!ctx) return;\n const step = ctx.pendingStep;\n const before = ctx.pendingFrom;\n ctx.pendingStep = null;\n ctx.pendingFrom = undefined;\n if (!step) return;\n recordAssertions(step, before);\n}\n\n// ============================================================================\n// File-based story collection (works across Jest worker processes)\n// ============================================================================\n\n// Use globalThis to ensure the registry is shared across module instances\n// This is needed because Jest may load the setup file and test files as separate module instances\ndeclare global {\n // eslint-disable-next-line no-var\n var __jestExecutableStoriesRegistry: Map<string, StoryMeta[]> | undefined;\n // eslint-disable-next-line no-var\n var __jestExecutableStoriesExitHandler: boolean | undefined;\n}\n\n/** Feature declarations, keyed by test file path */\nconst featureRegistry = new Map<string, FeatureInput>();\n\n/** Stories collected during test execution, keyed by test file path */\nconst storyRegistry: Map<string, StoryMeta[]> = globalThis.__jestExecutableStoriesRegistry ??= new Map();\n\n/** Attachments collected per story, keyed by test file path → scenario index → attachments */\nconst attachmentRegistry = new Map<string, Map<number, ScopedAttachment[]>>();\n\n/** OTel spans collected per story, keyed by test file path → scenario index → spans */\nconst otelSpansRegistry = new Map<string, Map<number, ReadonlyArray<Record<string, unknown>>>>();\n\n/** Track if we've registered the process exit handler */\nlet exitHandlerRegistered = globalThis.__jestExecutableStoriesExitHandler ?? false;\n\n/** Get the output directory for story JSON files */\nfunction getOutputDir(): string {\n const baseDir = process.env.JEST_STORY_DOCS_DIR ?? \".jest-executable-stories\";\n return path.resolve(process.cwd(), baseDir);\n}\n\n/** Flush all collected stories to JSON files */\nfunction flushStories(): void {\n if (storyRegistry.size === 0) return;\n\n const workerId = process.env.JEST_WORKER_ID ?? \"0\";\n const outputDir = path.join(getOutputDir(), `worker-${workerId}`);\n fs.mkdirSync(outputDir, { recursive: true });\n\n for (const [testFilePath, scenarios] of storyRegistry) {\n if (!scenarios.length) continue;\n const hash = createHash(\"sha1\").update(testFilePath).digest(\"hex\").slice(0, 12);\n const baseName = testFilePath === \"unknown\" ? \"unknown\" : path.basename(testFilePath);\n const outFile = path.join(outputDir, `${baseName}.${hash}.json`);\n\n // Include attachments and otelSpans per scenario (keyed by index, not name)\n const fileAttachments = attachmentRegistry.get(testFilePath);\n const fileOtelSpans = otelSpansRegistry.get(testFilePath);\n const scenariosWithAttachments = scenarios.map((s, i) => ({\n ...s,\n _attachments: fileAttachments?.get(i) ?? [],\n ...(fileOtelSpans?.get(i) ? { _otelSpans: fileOtelSpans.get(i) } : {}),\n }));\n\n const declared = featureRegistry.get(testFilePath);\n const payload = {\n testFilePath,\n ...(declared ? { feature: declared } : {}),\n scenarios: scenariosWithAttachments,\n };\n fs.writeFileSync(outFile, JSON.stringify(payload, null, 2) + \"\\n\", \"utf8\");\n }\n storyRegistry.clear();\n attachmentRegistry.clear();\n otelSpansRegistry.clear();\n featureRegistry.clear();\n}\n\n/** Register process exit handler to flush stories (once per worker) */\nfunction registerExitHandler(): void {\n if (exitHandlerRegistered) return;\n exitHandlerRegistered = true;\n globalThis.__jestExecutableStoriesExitHandler = true;\n // Use 'exit' event - always fired when Node.js is about to exit\n // Note: Only sync operations work here, which is fine for fs.writeFileSync\n process.on(\"exit\", () => {\n flushStories();\n });\n}\n\n// ============================================================================\n// Jest-specific context\n// ============================================================================\n\n/** Active story context - set by story.init() */\nlet activeContext: StoryContext | null = null;\n\n/** Counter to track source order of stories (increments on each story.init call) */\nlet sourceOrderCounter = 0;\n\n/**\n * Get the current story context. Throws if story.init() wasn't called.\n */\nfunction getContext(): StoryContext {\n if (!activeContext) {\n throw new Error(\n \"story.init() must be called first. Use: it('name', () => { story.init(); ... });\"\n );\n }\n return activeContext;\n}\n\n// ============================================================================\n// Helper Functions\n// ============================================================================\n\n/**\n * Normalize ticket option to array of NormalizedTicket objects.\n */\nfunction normalizeTickets(\n ticket: TicketInput | TicketInput[] | undefined,\n): NormalizedTicket[] | undefined {\n if (!ticket) return undefined;\n const arr = Array.isArray(ticket) ? ticket : [ticket];\n return arr.map((t) => (typeof t === 'string' ? { id: t } : t));\n}\n\n/**\n * Extract the suite path from Jest's currentTestName.\n * Jest's currentTestName is formatted as: \"describe1 > describe2 > test name\"\n */\nfunction extractSuitePath(currentTestName: string): { suitePath?: string[]; testName: string } {\n const parts = currentTestName.split(\" > \");\n if (parts.length <= 1) {\n return { testName: currentTestName };\n }\n const testName = parts[parts.length - 1];\n const suitePath = parts.slice(0, -1);\n return { suitePath, testName };\n}\n\n/**\n * Convert StoryDocs inline options to DocEntry array.\n */\nfunction convertStoryDocsToEntries(docs: StoryDocs): DocEntry[] {\n const entries: DocEntry[] = [];\n\n if (docs.note) {\n entries.push({ kind: \"note\", text: docs.note, phase: \"runtime\" });\n }\n if (docs.tag) {\n const names = Array.isArray(docs.tag) ? docs.tag : [docs.tag];\n entries.push({ kind: \"tag\", names, phase: \"runtime\" });\n }\n if (docs.kv) {\n for (const [label, value] of Object.entries(docs.kv)) {\n entries.push({ kind: \"kv\", label, value, phase: \"runtime\" });\n }\n }\n if (docs.code) {\n entries.push({ kind: \"code\", label: docs.code.label, content: docs.code.content, lang: docs.code.lang, phase: \"runtime\" });\n }\n if (docs.json) {\n entries.push({ kind: \"code\", label: docs.json.label, content: JSON.stringify(docs.json.value, null, 2), lang: \"json\", phase: \"runtime\" });\n }\n if (docs.state) {\n warnIfLargeState(docs.state.label, docs.state.value);\n entries.push({ kind: \"state\", label: docs.state.label, value: docs.state.value, phase: \"runtime\" });\n }\n if (docs.table) {\n entries.push({ kind: \"table\", label: docs.table.label, columns: docs.table.columns, rows: docs.table.rows, phase: \"runtime\" });\n }\n if (docs.link) {\n entries.push({ kind: \"link\", label: docs.link.label, url: docs.link.url, phase: \"runtime\" });\n }\n if (docs.section) {\n entries.push({ kind: \"section\", title: docs.section.title, markdown: docs.section.markdown, phase: \"runtime\" });\n }\n if (docs.mermaid) {\n entries.push({ kind: \"mermaid\", code: docs.mermaid.code, title: docs.mermaid.title, phase: \"runtime\" });\n }\n if (docs.screenshot) {\n entries.push({ kind: \"screenshot\", path: docs.screenshot.path, alt: docs.screenshot.alt, phase: \"runtime\" });\n }\n if (docs.video) {\n entries.push({ kind: \"video\", path: docs.video.path, caption: docs.video.caption, poster: docs.video.poster, phase: \"runtime\" });\n }\n if (docs.html) {\n entries.push(buildHtmlDocEntry(docs.html));\n }\n if (docs.custom) {\n entries.push({ kind: \"custom\", type: docs.custom.type, data: docs.custom.data, phase: \"runtime\" });\n }\n\n return entries;\n}\n\n/** Warn (non-fatal) when a state snapshot is suspiciously large. */\nfunction warnIfLargeState(label: string | undefined, value: unknown): void {\n try {\n const length = JSON.stringify(value)?.length ?? 0;\n if (length > 100_000) {\n console.warn(\n `[executable-stories] state \"${label ?? 'state'}\" is ${Math.round(length / 1024)}KB — consider capturing a projection of the entity instead of the whole thing`,\n );\n }\n } catch {\n /* non-serializable value; still recorded */\n }\n}\n\n// ============================================================================\n// Helper to attach doc entry to current step or story-level\n// ============================================================================\n\nfunction attachDoc(entry: DocEntry, children?: DocEntry[]): DocEntry {\n const ctx = getContext();\n if (children && children.length > 0) {\n entry.children = children;\n const childSet = new Set<DocEntry>(children);\n const filterDocs = (docs: DocEntry[]) => docs.filter((d) => !childSet.has(d));\n // Remove children from ALL containers (story-level + every step)\n ctx.meta.docs = filterDocs(ctx.meta.docs ?? []);\n for (const step of ctx.meta.steps) {\n if (step.docs) step.docs = filterDocs(step.docs);\n }\n }\n if (ctx.currentStep) {\n ctx.currentStep.docs ??= [];\n ctx.currentStep.docs.push(entry);\n } else {\n ctx.meta.docs ??= [];\n ctx.meta.docs.push(entry);\n }\n return entry;\n}\n\n// ============================================================================\n// Step Markers\n// ============================================================================\n\nfunction createStepMarker(keyword: StepKeyword) {\n function stepMarker(text: string, docs?: StoryDocs): void;\n function stepMarker(text: string, children: DocEntry[]): void;\n function stepMarker<T>(text: string, body: () => T): T;\n function stepMarker<T>(text: string, docsOrBody?: StoryDocs | DocEntry[] | (() => T)): T | void {\n const ctx = getContext();\n const isCallback = typeof docsOrBody === 'function';\n const isChildrenArray = Array.isArray(docsOrBody);\n\n const resolvedKeyword: StepKeyword =\n (keyword === 'Given' || keyword === 'When' || keyword === 'Then') &&\n ctx.meta.steps.some((s) => s.keyword === keyword)\n ? 'And'\n : keyword;\n\n let stepDocs: DocEntry[] = [];\n if (!isCallback && !isChildrenArray && docsOrBody) {\n stepDocs = convertStoryDocsToEntries(docsOrBody as StoryDocs);\n }\n\n const step: StoryStep = {\n id: `step-${ctx.stepCounter++}`,\n keyword: resolvedKeyword,\n text,\n docs: stepDocs,\n ...(isCallback ? { wrapped: true } : {}),\n };\n\n // Close the previous marker before this step's own assertions begin.\n flushPendingAssertions();\n\n ctx.meta.steps.push(step);\n ctx.currentStep = step;\n if (!isCallback) {\n ctx.pendingStep = step;\n ctx.pendingFrom = readAssertionCount();\n }\n\n // Handle DocEntry[] children: attach as step docs and deduplicate from story-level\n if (isChildrenArray) {\n const children = docsOrBody as DocEntry[];\n if (children.length > 0) {\n const childSet = new Set<DocEntry>(children);\n // Deduplicate from story-level docs\n ctx.meta.docs = (ctx.meta.docs ?? []).filter((d) => !childSet.has(d));\n // Deduplicate from step docs of earlier steps\n for (const prevStep of ctx.meta.steps) {\n if (prevStep !== step && prevStep.docs) {\n prevStep.docs = prevStep.docs.filter((d) => !childSet.has(d));\n }\n }\n step.docs = [...(step.docs ?? []), ...children];\n }\n return;\n }\n\n if (!isCallback) return;\n\n const body = docsOrBody as () => T;\n const start = performance.now();\n const assertionsBefore = readAssertionCount();\n\n try {\n const result = body();\n if (result instanceof Promise) {\n return result.then(\n (val) => { step.durationMs = performance.now() - start; recordAssertions(step, assertionsBefore); return val; },\n (err) => { step.durationMs = performance.now() - start; recordAssertions(step, assertionsBefore); throw err; },\n ) as T;\n }\n step.durationMs = performance.now() - start;\n recordAssertions(step, assertionsBefore);\n return result;\n } catch (err) {\n step.durationMs = performance.now() - start;\n recordAssertions(step, assertionsBefore);\n throw err;\n }\n }\n return stepMarker;\n}\n\n// ============================================================================\n// story.init() - Jest-specific\n// ============================================================================\n\n/**\n * Initialize a story for the current test.\n * Must be called at the start of each test that wants documentation.\n *\n * @param options - Optional story configuration (tags, ticket, meta)\n *\n * @example\n * ```ts\n * it('adds two numbers', () => {\n * story.init();\n * // ... rest of test\n * });\n * ```\n */\n/**\n * Declare what the file's scenarios are for, before any of them run.\n *\n * Scenarios say what the system does. This says why the feature exists and who\n * it serves, so a reader meets the intent before the examples. Call it once per\n * test file, at module scope or at the top of the outermost `describe`.\n *\n * @example\n * ```ts\n * story.feature({\n * kind: 'ability',\n * title: 'Employees can secure their passwords',\n * narrative: 'Weak passwords are how most accounts get taken over.',\n * });\n * ```\n */\nfunction feature(input: FeatureInput): void {\n // Jest only knows the current file once a test is running, so resolve it the\n // same way `init` does and fall back to the module that called us.\n const testPath = expect.getState().testPath || callerFile() || \"unknown\";\n featureRegistry.set(testPath, input);\n registerExitHandler();\n}\n\n/**\n * Best-effort source file of whoever called into this module.\n *\n * `story.feature(...)` usually runs at module scope, where Jest has not yet set\n * `testPath`. The stack is the only place the file name is available then.\n */\nfunction callerFile(): string | undefined {\n const stack = new Error().stack;\n if (!stack) return undefined;\n\n for (const line of stack.split(\"\\n\").slice(1)) {\n const match = /\\((?:file:\\/\\/)?([^()]+?):\\d+:\\d+\\)/.exec(line)\n ?? /at (?:file:\\/\\/)?([^()\\s]+?):\\d+:\\d+/.exec(line);\n const file = match?.[1];\n if (file && !file.includes(\"executable-stories-jest\") && !file.startsWith(\"node:\")) {\n return file;\n }\n }\n\n return undefined;\n}\n\nfunction init(options?: StoryOptions): void {\n // Get current test info from Jest globals\n const state = expect.getState();\n const currentTestName = state.currentTestName || \"Unknown test\";\n const testPath = state.testPath || \"unknown\";\n\n const { suitePath, testName } = extractSuitePath(currentTestName);\n\n const meta: StoryMeta = {\n scenario: testName,\n steps: [],\n suitePath,\n tags: options?.tags,\n tickets: normalizeTickets(options?.ticket),\n covers: options?.covers,\n meta: options?.meta,\n sourceOrder: sourceOrderCounter++,\n };\n\n // OTel bridge: detect active span, flow data bidirectionally\n const otelCtx = tryGetActiveOtelContext();\n if (otelCtx) {\n // OTel -> Story: capture traceId in structured meta\n meta.meta = { ...meta.meta, otel: { traceId: otelCtx.traceId, spanId: otelCtx.spanId } };\n\n // OTel -> Story: inject human-readable doc entries\n meta.docs = meta.docs ?? [];\n meta.docs.push({ kind: 'kv', label: 'Trace ID', value: otelCtx.traceId, phase: 'runtime' });\n\n const template = options?.traceUrlTemplate ?? process.env.OTEL_TRACE_URL_TEMPLATE;\n const url = resolveTraceUrl(template, otelCtx.traceId);\n if (url) {\n meta.docs.push({ kind: 'link', label: 'View Trace', url, phase: 'runtime' });\n }\n\n // Story -> OTel: enrich active span with story attributes\n try {\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n const reqUrl = import.meta.url\n ?? (typeof __filename !== 'undefined' ? `file://${__filename}` : undefined);\n const req = createRequire(reqUrl!);\n const api = req('@opentelemetry/api');\n const span = api.trace?.getActiveSpan?.();\n if (span) {\n span.setAttribute('story.scenario', testName);\n if (options?.tags?.length) span.setAttribute('story.tags', options.tags);\n if (options?.ticket) {\n const tickets = Array.isArray(options.ticket) ? options.ticket : [options.ticket];\n span.setAttribute('story.tickets', tickets.map((t) => typeof t === 'string' ? t : t.id));\n }\n }\n } catch { /* OTel not available */ }\n }\n\n // Store in registry for this file\n const existing = storyRegistry.get(testPath);\n let scenarioIndex: number;\n if (existing) {\n scenarioIndex = existing.length;\n existing.push(meta);\n } else {\n scenarioIndex = 0;\n storyRegistry.set(testPath, [meta]);\n }\n\n // Register exit handler to flush stories when worker exits\n registerExitHandler();\n\n // Set active context\n activeContext = {\n meta,\n currentStep: null,\n stepCounter: 0,\n attachments: [],\n activeTimers: new Map(),\n timerCounter: 0,\n testPath,\n scenarioIndex,\n pendingStep: null,\n };\n\n // Link attachments to the registry for this test file + scenario index\n if (!attachmentRegistry.has(testPath)) {\n attachmentRegistry.set(testPath, new Map());\n }\n attachmentRegistry.get(testPath)!.set(scenarioIndex, activeContext.attachments);\n}\n\n// ============================================================================\n// Step Wrappers: story.fn() and story.expect()\n// ============================================================================\n\n/**\n * Wrap a function body as a step. Records the step with timing and `wrapped: true`.\n * Supports both sync and async functions. Returns whatever the function returns.\n *\n * @param keyword - The BDD keyword (Given, When, Then, And, But)\n * @param text - Step description\n * @param body - The function to execute\n * @returns The return value of body (or a Promise of it if body is async)\n *\n * @example\n * ```ts\n * const data = story.fn('Given', 'setup data', () => ({ a: 5, b: 3 }));\n * const result = await story.fn('When', 'call API', async () => fetch('/api'));\n * ```\n */\nfunction fn<T>(keyword: StepKeyword, text: string, body: () => T): T {\n const ctx = getContext();\n const resolvedKeyword: StepKeyword =\n (keyword === 'Given' || keyword === 'When' || keyword === 'Then') &&\n ctx.meta.steps.some((s) => s.keyword === keyword)\n ? 'And'\n : keyword;\n\n const step: StoryStep = {\n id: `step-${ctx.stepCounter++}`,\n keyword: resolvedKeyword,\n text,\n docs: [],\n wrapped: true,\n };\n\n ctx.meta.steps.push(step);\n ctx.currentStep = step;\n\n const start = performance.now();\n\n try {\n const result = body();\n\n // Handle async functions\n if (result instanceof Promise) {\n return result.then(\n (val) => {\n step.durationMs = performance.now() - start;\n return val;\n },\n (err) => {\n step.durationMs = performance.now() - start;\n throw err;\n },\n ) as T;\n }\n\n step.durationMs = performance.now() - start;\n return result;\n } catch (err) {\n step.durationMs = performance.now() - start;\n throw err;\n }\n}\n\n/**\n * Wrap an assertion as a Then step. Shorthand for `story.fn('Then', text, body)`.\n *\n * @param text - Step description\n * @param body - The assertion function to execute\n *\n * @example\n * ```ts\n * story.expect('the result is 8', () => { expect(result).toBe(8); });\n * ```\n */\nfunction storyExpect<T>(text: string, body: () => T): T {\n return fn('Then', text, body);\n}\n\n// ============================================================================\n// Export story object\n// ============================================================================\n\n/**\n * The main story API object for Jest.\n *\n * @example\n * ```ts\n * import { story } from 'executable-stories-jest';\n *\n * describe('Calculator', () => {\n * it('adds two numbers', () => {\n * story.init();\n *\n * story.given('two numbers 5 and 3');\n * const a = 5, b = 3;\n *\n * story.when('I add them together');\n * const result = a + b;\n *\n * story.then('the result is 8');\n * expect(result).toBe(8);\n * });\n * });\n * ```\n */\nexport const story = {\n // Jest-specific init\n init,\n feature,\n\n // BDD step markers\n given: createStepMarker(\"Given\"),\n when: createStepMarker(\"When\"),\n then: createStepMarker(\"Then\"),\n and: createStepMarker(\"And\"),\n but: createStepMarker(\"But\"),\n\n // AAA pattern aliases\n arrange: createStepMarker(\"Given\"),\n act: createStepMarker(\"When\"),\n assert: createStepMarker(\"Then\"),\n\n // Additional aliases\n setup: createStepMarker(\"Given\"),\n context: createStepMarker(\"Given\"),\n execute: createStepMarker(\"When\"),\n action: createStepMarker(\"When\"),\n verify: createStepMarker(\"Then\"),\n\n // Standalone doc methods\n note(text: string, children?: DocEntry[]): DocEntry {\n return attachDoc({ kind: \"note\", text, phase: \"runtime\" }, children);\n },\n\n tag(name: string | string[], children?: DocEntry[]): DocEntry {\n const names = Array.isArray(name) ? name : [name];\n return attachDoc({ kind: \"tag\", names, phase: \"runtime\" }, children);\n },\n\n kv(options: KvOptions, children?: DocEntry[]): DocEntry {\n return attachDoc({ kind: \"kv\", label: options.label, value: options.value, phase: \"runtime\" }, children);\n },\n\n json(options: JsonOptions, children?: DocEntry[]): DocEntry {\n const content = JSON.stringify(options.value, null, 2);\n return attachDoc({ kind: \"code\", label: options.label, content, lang: \"json\", phase: \"runtime\" }, children);\n },\n\n /**\n * Capture what the world looks like at this step — a data snapshot that\n * becomes a storyboard frame in generated docs. Consecutive snapshots with\n * the same label get diffed downstream. The value must be JSON-serializable.\n * @example\n * ```ts\n * story.given('an empty basket');\n * story.state({ label: 'Basket', value: { items: [], total: 0 } });\n * story.when('a widget is added');\n * story.state({ label: 'Basket', value: { items: ['widget'], total: 49.99 } });\n * ```\n */\n state(options: StateOptions, children?: DocEntry[]): DocEntry {\n warnIfLargeState(options.label, options.value);\n return attachDoc({ kind: \"state\", label: options.label, value: options.value, phase: \"runtime\" }, children);\n },\n\n code(options: CodeOptions, children?: DocEntry[]): DocEntry {\n return attachDoc({ kind: \"code\", label: options.label, content: options.content, lang: options.lang, phase: \"runtime\" }, children);\n },\n\n table(options: TableOptions, children?: DocEntry[]): DocEntry {\n return attachDoc({ kind: \"table\", label: options.label, columns: options.columns, rows: options.rows, phase: \"runtime\" }, children);\n },\n\n link(options: LinkOptions, children?: DocEntry[]): DocEntry {\n return attachDoc({ kind: \"link\", label: options.label, url: options.url, phase: \"runtime\" }, children);\n },\n\n section(options: SectionOptions, children?: DocEntry[]): DocEntry {\n return attachDoc({ kind: \"section\", title: options.title, markdown: options.markdown, phase: \"runtime\" }, children);\n },\n\n mermaid(options: MermaidOptions, children?: DocEntry[]): DocEntry {\n return attachDoc({ kind: \"mermaid\", code: options.code, title: options.title, phase: \"runtime\" }, children);\n },\n\n screenshot(options: ScreenshotOptions, children?: DocEntry[]): DocEntry {\n return attachDoc({ kind: \"screenshot\", path: options.path, alt: options.alt, phase: \"runtime\" }, children);\n },\n\n video(options: VideoOptions, children?: DocEntry[]): DocEntry {\n return attachDoc({ kind: \"video\", path: options.path, caption: options.caption, poster: options.poster, phase: \"runtime\" }, children);\n },\n\n html(options: HtmlOptions, children?: DocEntry[]): DocEntry {\n return attachDoc(buildHtmlDocEntry(options), children);\n },\n\n custom(options: CustomOptions, children?: DocEntry[]): DocEntry {\n return attachDoc({ kind: \"custom\", type: options.type, data: options.data, phase: \"runtime\" }, children);\n },\n\n // Attachments\n attach(options: AttachmentOptions): void {\n const ctx = getContext();\n const stepIndex = ctx.currentStep\n ? ctx.meta.steps.indexOf(ctx.currentStep)\n : undefined;\n ctx.attachments.push({\n ...options,\n stepIndex: stepIndex !== undefined && stepIndex >= 0 ? stepIndex : undefined,\n stepId: ctx.currentStep?.id,\n });\n },\n\n // OTel span attachment\n attachSpans(spans: ReadonlyArray<Record<string, unknown>>): void {\n const ctx = getContext();\n if (!otelSpansRegistry.has(ctx.testPath)) {\n otelSpansRegistry.set(ctx.testPath, new Map());\n }\n otelSpansRegistry.get(ctx.testPath)!.set(ctx.scenarioIndex, spans);\n },\n\n // Step wrappers\n fn,\n expect: storyExpect,\n\n // Step timing\n startTimer(): number {\n const ctx = getContext();\n const token = ctx.timerCounter++;\n const stepIndex = ctx.currentStep\n ? ctx.meta.steps.indexOf(ctx.currentStep)\n : undefined;\n ctx.activeTimers.set(token, {\n start: performance.now(),\n stepIndex: stepIndex !== undefined && stepIndex >= 0 ? stepIndex : undefined,\n stepId: ctx.currentStep?.id,\n consumed: false,\n });\n return token;\n },\n\n endTimer(token: number): void {\n const ctx = getContext();\n const entry = ctx.activeTimers.get(token);\n if (!entry || entry.consumed) return;\n\n entry.consumed = true;\n const durationMs = performance.now() - entry.start;\n\n let step: StoryStep | undefined;\n if (entry.stepId) {\n step = ctx.meta.steps.find((s) => s.id === entry.stepId);\n }\n if (!step && entry.stepIndex !== undefined) {\n step = ctx.meta.steps[entry.stepIndex];\n }\n\n if (step) {\n step.durationMs = durationMs;\n }\n },\n};\n\nexport type Story = typeof story;\n\n// ============================================================================\n// Internal exports for setup file\n// ============================================================================\n\n/**\n * Internal API for the setup file and tests. Not for public use.\n * @internal\n */\nexport const _internal = {\n flushStories,\n flushPendingAssertions,\n /** Clear active context (for tests that assert getContext() throws). */\n clearContext(): void {\n activeContext = null;\n },\n};\n","/**\n * Type definitions for executable-stories-jest.\n *\n * Shared story types are imported from executable-stories-core.\n * This module re-exports them and adds Jest-specific types.\n */\n\n// Re-export shared story types from core\nexport type {\n StepKeyword,\n StepMode,\n DocPhase,\n DocEntry,\n StoryStep,\n StoryMeta,\n NormalizedTicket,\n} from 'executable-stories-core/types/story';\n\nexport { STORY_META_KEY } from 'executable-stories-core/types/story';\n\n/** A ticket reference: either a plain string ID or an object with id and optional url */\nexport type TicketInput = string | { id: string; url?: string };\n\n// ============================================================================\n// Doc Options (for inline docs and standalone methods)\n// ============================================================================\n\n/** Options for kv() - key-value pair */\nexport interface KvOptions {\n label: string;\n value: unknown;\n}\n\n/** Options for json() - JSON code block */\nexport interface JsonOptions {\n label: string;\n value: unknown;\n}\n\n/** Options for state() - data snapshot of the world at this step */\nexport interface StateOptions {\n label?: string;\n value: unknown;\n}\n\n/** Options for code() - code block with optional language */\nexport interface CodeOptions {\n label: string;\n content: string;\n lang?: string;\n}\n\n/** Options for table() - markdown table */\nexport interface TableOptions {\n label: string;\n columns: string[];\n rows: string[][];\n}\n\n/** Options for link() - hyperlink */\nexport interface LinkOptions {\n label: string;\n url: string;\n}\n\n/** Options for section() - titled markdown section */\nexport interface SectionOptions {\n title: string;\n markdown: string;\n}\n\n/** Options for mermaid() - Mermaid diagram */\nexport interface MermaidOptions {\n code: string;\n title?: string;\n}\n\n/** Options for screenshot() - screenshot reference */\nexport interface ScreenshotOptions {\n path: string;\n alt?: string;\n}\n\n/** Options for video() - video reference */\nexport interface VideoOptions {\n path: string;\n caption?: string;\n poster?: string;\n}\n\n/** Options for html() - HTML embedded in a sandboxed iframe. Exactly one of path/url/content. */\nexport interface HtmlOptions {\n /** Local HTML file path (inlined into the report by default) */\n path?: string;\n /** Remote URL rendered via iframe src */\n url?: string;\n /** Inline HTML content rendered via iframe srcdoc */\n content?: string;\n title?: string;\n /** Iframe height: number → px, string passed through (e.g. '60vh'). Default 400px. */\n height?: number | string;\n}\n\n/** Options for custom() - custom doc entry */\nexport interface CustomOptions {\n type: string;\n data: unknown;\n}\n\n// ============================================================================\n// Inline Docs for Steps\n// ============================================================================\n\n/**\n * Inline documentation options for step markers.\n * Pass to story.given(), story.when(), story.then() as second argument.\n */\nexport interface StoryDocs {\n note?: string;\n tag?: string | string[];\n kv?: Record<string, unknown>;\n code?: CodeOptions;\n json?: JsonOptions;\n state?: StateOptions;\n table?: TableOptions;\n link?: LinkOptions;\n section?: SectionOptions;\n mermaid?: MermaidOptions;\n screenshot?: ScreenshotOptions;\n video?: VideoOptions;\n html?: HtmlOptions;\n custom?: CustomOptions;\n}\n\n// ============================================================================\n// Attachment Types\n// ============================================================================\n\n/** Options for attaching files or inline content to a test */\nexport interface AttachmentOptions {\n name: string;\n mediaType: string;\n path?: string;\n body?: string;\n encoding?: \"BASE64\" | \"IDENTITY\";\n charset?: string;\n fileName?: string;\n}\n\n/** Internal: attachment with step scope info */\nexport interface ScopedAttachment extends AttachmentOptions {\n stepIndex?: number;\n stepId?: string;\n}\n\n// ============================================================================\n// Story Options\n// ============================================================================\n\n/**\n * Options for configuring a story via story.init().\n */\n/** One entry in a feature's glossary. */\nexport interface GlossaryTerm {\n /** The term as it appears in scenario and step text. */\n term: string;\n /** What it means, in one or two sentences. */\n definition: string;\n}\n\n/** What `story.feature(...)` accepts. */\nexport interface FeatureInput {\n /** Heading for the feature. */\n title: string;\n /**\n * How to introduce it. `ability` frames the feature as something a person can\n * now do; `business-need` covers cross-cutting concerns like security or\n * performance that no single user asks for. Defaults to `feature`.\n */\n kind?: \"feature\" | \"ability\" | \"business-need\";\n /** Markdown explaining why the feature exists and who it serves. */\n narrative?: string;\n /** Tags applied to every scenario in the file. */\n tags?: string[];\n /** Terms this feature defines, linked wherever they appear in the report. */\n glossary?: GlossaryTerm[];\n}\n\nexport interface StoryOptions {\n tags?: string[];\n ticket?: TicketInput | TicketInput[];\n /** Product-code paths/globs this scenario exercises (project-root-relative). */\n covers?: string[];\n meta?: Record<string, unknown>;\n /** URL template for OTel trace links. Uses {traceId} placeholder. Also settable via OTEL_TRACE_URL_TEMPLATE env var. */\n traceUrlTemplate?: string;\n}\n","/**\n * Jest Executable Stories\n *\n * BDD-style executable documentation for Jest.\n *\n * @example\n * ```ts\n * import { story } from 'executable-stories-jest';\n *\n * describe('Calculator', () => {\n * it('adds two numbers', () => {\n * story.init();\n *\n * story.given('two numbers 5 and 3');\n * const a = 5;\n * const b = 3;\n *\n * story.when('I add them together');\n * const result = a + b;\n *\n * story.then('the result is 8');\n * expect(result).toBe(8);\n * });\n * });\n * ```\n */\n\n// Story API\nimport { story } from \"./story-api\";\nexport { story };\nexport type { Story } from \"./story-api\";\n\n// Top-level step helpers (framework contract)\nexport const given = story.given;\nexport const when = story.when;\nexport const then = story.then;\nexport const and = story.and;\nexport const but = story.but;\n\n// Re-export types\nexport type {\n StoryMeta,\n StoryStep,\n DocEntry,\n StepKeyword,\n StepMode,\n DocPhase,\n NormalizedTicket,\n TicketInput,\n StoryDocs,\n StoryOptions,\n AttachmentOptions,\n KvOptions,\n JsonOptions,\n StateOptions,\n CodeOptions,\n TableOptions,\n LinkOptions,\n SectionOptions,\n MermaidOptions,\n ScreenshotOptions,\n VideoOptions,\n HtmlOptions,\n CustomOptions,\n} from \"./types\";\n\nexport { STORY_META_KEY } from \"./types\";\n"],"mappings":"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","names":[]}
package/dist/reporter.cjs CHANGED
@@ -45,8 +45,19 @@ var StoryReporter = class {
45
45
  startTime = 0;
46
46
  packageVersion;
47
47
  gitSha;
48
- constructor(_globalConfig, reporterOptions = {}) {
48
+ /**
49
+ * Left unknown when Jest hands us no global config. Claiming full coverage
50
+ * without having looked would let a later merge retire scenarios on a guess.
51
+ */
52
+ runScope;
53
+ constructor(globalConfig, reporterOptions = {}) {
49
54
  this.options = reporterOptions;
55
+ const config = globalConfig;
56
+ if (config) {
57
+ const pattern = config.testNamePattern;
58
+ const filtered = typeof pattern === "string" ? pattern.length > 0 : pattern != null;
59
+ this.runScope = filtered ? "filtered" : "full";
60
+ }
50
61
  }
51
62
  /** Get the output directory for story JSON files */
52
63
  getOutputDir() {
@@ -99,10 +110,12 @@ var StoryReporter = class {
99
110
  }
100
111
  const reports = this.readStoryReports();
101
112
  const rawTestCases = [];
113
+ const declaredByFile = /* @__PURE__ */ new Map();
102
114
  for (const report of reports) {
103
115
  const fileResult = fileResults.get(report.testFilePath);
104
116
  const sourceFile = toRelativePosix(report.testFilePath, root);
105
117
  const matchedFullNames = /* @__PURE__ */ new Set();
118
+ declaredByFile.set(sourceFile, matchedFullNames);
106
119
  for (const meta of report.scenarios) {
107
120
  if (!meta?.scenario) continue;
108
121
  const matchingTest = fileResult?.testResults.find((test) => {
@@ -151,6 +164,7 @@ var StoryReporter = class {
151
164
  for (const test of fileResult?.testResults ?? []) {
152
165
  if (test.status !== "todo" || matchedFullNames.has(test.fullName)) continue;
153
166
  const title = test.title ?? test.fullName;
167
+ matchedFullNames.add(test.fullName);
154
168
  rawTestCases.push({
155
169
  title,
156
170
  titlePath: [title],
@@ -164,7 +178,24 @@ var StoryReporter = class {
164
178
  });
165
179
  }
166
180
  }
167
- if (rawTestCases.length === 0) return;
181
+ const coveredSourceFiles = [
182
+ ...new Set(
183
+ (results.testResults ?? []).map((r) => r.testFilePath).filter((f) => typeof f === "string" && f.length > 0).map((f) => toRelativePosix(f, root))
184
+ )
185
+ ].sort();
186
+ const incompleteSourceFiles = [
187
+ ...new Set(
188
+ (results.testResults ?? []).filter((r) => {
189
+ const declared = declaredByFile.get(toRelativePosix(r.testFilePath, root)) ?? /* @__PURE__ */ new Set();
190
+ const undeclaredFailure = (r.testResults ?? []).some(
191
+ (t) => t.status === "failed" && !declared.has(t.fullName)
192
+ );
193
+ const suiteError = Boolean(r.testExecError) || (r.testResults ?? []).length === 0 && Boolean(r.failureMessage);
194
+ return undeclaredFailure || suiteError;
195
+ }).map((r) => toRelativePosix(r.testFilePath, root))
196
+ )
197
+ ].sort();
198
+ if (rawTestCases.length === 0 && coveredSourceFiles.length === 0) return;
168
199
  const rawRun = {
169
200
  testCases: rawTestCases,
170
201
  startedAtMs: this.startTime,
@@ -172,7 +203,10 @@ var StoryReporter = class {
172
203
  projectRoot: root,
173
204
  packageVersion: this.packageVersion,
174
205
  gitSha: this.gitSha,
175
- ci: (0, import_executable_stories_formatters.detectCI)()
206
+ ci: (0, import_executable_stories_formatters.detectCI)(),
207
+ ...this.runScope ? { runScope: this.runScope } : {},
208
+ ...coveredSourceFiles.length > 0 ? { coveredSourceFiles } : {},
209
+ ...incompleteSourceFiles.length > 0 ? { incompleteSourceFiles } : {}
176
210
  };
177
211
  const rawRunPath = this.options.rawRunPath;
178
212
  if (rawRunPath) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/reporter.ts"],"sourcesContent":["/**\n * Jest reporter for executable-stories.\n * Generates reports using the executable-stories-formatters package.\n *\n * Uses file-based communication: story.init() writes JSON files to\n * .jest-executable-stories/ which this reporter reads.\n */\n\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport fg from \"fast-glob\";\nimport type { StoryMeta } from \"executable-stories-formatters\";\n\n// Import from formatters package\nimport {\n ReportGenerator,\n canonicalizeRun,\n readGitSha,\n readPackageVersion,\n detectCI,\n type RawRun,\n type RawTestCase,\n type RawAttachment,\n type RawStepEvent,\n type FormatterOptions,\n} from \"executable-stories-formatters\";\n\n// Re-export types from formatters for convenience\nexport type {\n OutputFormat,\n OutputMode,\n ColocatedStyle,\n OutputRule,\n FormatterOptions,\n} from \"executable-stories-formatters\";\n\n// ============================================================================\n// Reporter Options (delegates to FormatterOptions)\n// ============================================================================\n\nexport interface StoryReporterOptions extends FormatterOptions {\n /** If set, write raw run JSON (schemaVersion 1) to this path for use with the executable-stories CLI/binary */\n rawRunPath?: string;\n}\n\n// ============================================================================\n// Internal Types\n// ============================================================================\n\n/** Shape of JSON files written by story.init() */\ninterface StoryFileReport {\n testFilePath: string;\n scenarios: (StoryMeta & {\n _attachments?: Array<{\n name: string;\n mediaType: string;\n path?: string;\n body?: string;\n encoding?: \"BASE64\" | \"IDENTITY\";\n charset?: string;\n fileName?: string;\n stepIndex?: number;\n stepId?: string;\n }>;\n })[];\n}\n\ninterface JestTestResult {\n testFilePath: string;\n testResults: Array<{\n fullName: string;\n title?: string;\n status: \"passed\" | \"failed\" | \"pending\" | \"todo\";\n duration?: number;\n failureMessages?: string[];\n }>;\n}\n\ninterface JestAggregatedResult {\n testResults: JestTestResult[];\n}\n\n// ============================================================================\n// Utility Functions\n// ============================================================================\n\n/**\n * Convert path to relative posix format.\n */\nfunction toRelativePosix(absolutePath: string, projectRoot: string): string {\n return path.relative(projectRoot, absolutePath).split(path.sep).join(\"/\");\n}\n\n// ============================================================================\n// Reporter Implementation\n// ============================================================================\n\nexport default class StoryReporter {\n private options: StoryReporterOptions;\n private startTime = 0;\n private packageVersion: string | undefined;\n private gitSha: string | undefined;\n\n constructor(_globalConfig: unknown, reporterOptions: StoryReporterOptions = {}) {\n this.options = reporterOptions;\n }\n\n /** Get the output directory for story JSON files */\n private getOutputDir(): string {\n const baseDir = process.env.JEST_STORY_DOCS_DIR ?? \".jest-executable-stories\";\n return path.resolve(process.cwd(), baseDir);\n }\n\n /** Reset run artifacts (clear previous story files) */\n private resetRunArtifacts(): void {\n try {\n fs.rmSync(this.getOutputDir(), { recursive: true, force: true });\n } catch {\n // Ignore cleanup errors\n }\n }\n\n /** Read story reports written by story.init() during test execution */\n private readStoryReports(): StoryFileReport[] {\n const outputDir = this.getOutputDir();\n if (!fs.existsSync(outputDir)) return [];\n const files = fg.sync(\"**/*.json\", {\n cwd: outputDir,\n onlyFiles: true,\n absolute: true,\n });\n const reports: StoryFileReport[] = [];\n for (const file of files) {\n try {\n const raw = fs.readFileSync(file, \"utf8\");\n const parsed = JSON.parse(raw) as StoryFileReport;\n if (!parsed?.testFilePath || !Array.isArray(parsed.scenarios)) continue;\n reports.push(parsed);\n } catch {\n // Ignore unreadable or malformed files\n }\n }\n return reports;\n }\n\n onRunStart(): void {\n this.resetRunArtifacts();\n this.startTime = Date.now();\n const root = process.cwd();\n const includeMetadata = this.options.markdown?.includeMetadata ?? true;\n if (includeMetadata) {\n this.packageVersion = readPackageVersion(root);\n this.gitSha = readGitSha(root);\n }\n }\n\n async onRunComplete(\n _testContexts: Set<unknown>,\n results: JestAggregatedResult\n ): Promise<void> {\n const root = process.cwd();\n\n // Build map of test results by file for status lookup\n const fileResults = new Map<string, JestTestResult>();\n for (const testFileResult of results.testResults) {\n fileResults.set(testFileResult.testFilePath, testFileResult);\n }\n\n // Read story data from JSON files written by story.init()\n const reports = this.readStoryReports();\n\n // Collect test cases\n const rawTestCases: RawTestCase[] = [];\n\n for (const report of reports) {\n const fileResult = fileResults.get(report.testFilePath);\n const sourceFile = toRelativePosix(report.testFilePath, root);\n const matchedFullNames = new Set<string>();\n\n for (const meta of report.scenarios) {\n if (!meta?.scenario) continue;\n\n // Find matching test result\n const matchingTest = fileResult?.testResults.find((test) => {\n const expectedFullName = meta.suitePath\n ? [...meta.suitePath, meta.scenario].join(\" > \")\n : meta.scenario;\n return test.fullName === expectedFullName;\n });\n if (matchingTest) matchedFullNames.add(matchingTest.fullName);\n\n // Map Jest status to raw status\n const statusMap: Record<string, RawTestCase[\"status\"]> = {\n passed: \"pass\",\n failed: \"fail\",\n pending: \"pending\",\n todo: \"pending\",\n };\n\n const status = matchingTest\n ? statusMap[matchingTest.status] ?? \"unknown\"\n : \"pass\";\n\n // Map attachments\n const rawAttachments: RawAttachment[] = (meta._attachments ?? []).map((a: NonNullable<typeof meta._attachments>[number]) => ({\n name: a.name,\n mediaType: a.mediaType,\n path: a.path,\n body: a.body,\n encoding: a.encoding,\n charset: a.charset,\n fileName: a.fileName,\n stepIndex: a.stepIndex,\n stepId: a.stepId,\n }));\n\n // Extract step events (timing)\n const stepEvents: RawStepEvent[] = meta.steps\n .filter((s: { durationMs?: number }) => s.durationMs !== undefined)\n .map((s: { durationMs?: number; text: string }, i: number) => ({\n index: i,\n title: s.text,\n durationMs: s.durationMs,\n }));\n\n rawTestCases.push({\n title: meta.scenario,\n titlePath: meta.suitePath\n ? [...meta.suitePath, meta.scenario]\n : [meta.scenario],\n story: meta,\n sourceFile,\n sourceLine: Math.max(1, meta.sourceOrder ?? 1),\n status,\n durationMs: matchingTest?.duration ?? 0,\n error: matchingTest?.failureMessages?.length\n ? { message: matchingTest.failureMessages.join(\"\\n\") }\n : undefined,\n attachments: rawAttachments.length > 0 ? rawAttachments : undefined,\n stepEvents: stepEvents.length > 0 ? stepEvents : undefined,\n retry: 0,\n retries: 0,\n });\n }\n\n // Bodyless `it.todo(...)` never runs `story.init()`, so it has no story\n // meta — but Jest still reports it with status \"todo\". Emit those as\n // planned scenarios. Only files with story reports reach this loop, so\n // plain (non-story) suites full of todos never leak into the docs.\n for (const test of fileResult?.testResults ?? []) {\n if (test.status !== \"todo\" || matchedFullNames.has(test.fullName)) continue;\n const title = test.title ?? test.fullName;\n rawTestCases.push({\n title,\n titlePath: [title],\n story: { scenario: title, steps: [] },\n sourceFile,\n sourceLine: 1,\n status: \"todo\",\n durationMs: 0,\n retry: 0,\n retries: 0,\n });\n }\n }\n\n if (rawTestCases.length === 0) return;\n\n // Build RawRun\n const rawRun: RawRun = {\n testCases: rawTestCases,\n startedAtMs: this.startTime,\n finishedAtMs: Date.now(),\n projectRoot: root,\n packageVersion: this.packageVersion,\n gitSha: this.gitSha,\n ci: detectCI(),\n };\n\n // Optionally write raw run JSON for CLI/binary consumption\n const rawRunPath = this.options.rawRunPath;\n if (rawRunPath) {\n const absolutePath = path.isAbsolute(rawRunPath)\n ? rawRunPath\n : path.join(root, rawRunPath);\n const dir = path.dirname(absolutePath);\n if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });\n const payload = { schemaVersion: 1, ...rawRun };\n fs.writeFileSync(absolutePath, JSON.stringify(payload, null, 2), \"utf8\");\n }\n\n // Canonicalize\n const canonicalRun = canonicalizeRun(rawRun);\n\n // Generate reports\n const generator = new ReportGenerator(this.options);\n try {\n await generator.generate(canonicalRun);\n } catch (err) {\n console.error(\"Failed to generate reports:\", err);\n }\n }\n}\n"],"mappings":"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","names":["fg"]}
1
+ {"version":3,"sources":["../src/reporter.ts"],"sourcesContent":["/**\n * Jest reporter for executable-stories.\n * Generates reports using the executable-stories-formatters package.\n *\n * Uses file-based communication: story.init() writes JSON files to\n * .jest-executable-stories/ which this reporter reads.\n */\n\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport fg from \"fast-glob\";\nimport type { StoryMeta } from \"executable-stories-formatters\";\n\n// Import from formatters package\nimport {\n ReportGenerator,\n canonicalizeRun,\n readGitSha,\n readPackageVersion,\n detectCI,\n type RawRun,\n type RawTestCase,\n type RawAttachment,\n type RawStepEvent,\n type FormatterOptions,\n} from \"executable-stories-formatters\";\n\n// Re-export types from formatters for convenience\nexport type {\n OutputFormat,\n OutputMode,\n ColocatedStyle,\n OutputRule,\n FormatterOptions,\n} from \"executable-stories-formatters\";\n\n// ============================================================================\n// Reporter Options (delegates to FormatterOptions)\n// ============================================================================\n\nexport interface StoryReporterOptions extends FormatterOptions {\n /** If set, write raw run JSON (schemaVersion 1) to this path for use with the executable-stories CLI/binary */\n rawRunPath?: string;\n}\n\n// ============================================================================\n// Internal Types\n// ============================================================================\n\n/** Shape of JSON files written by story.init() */\ninterface StoryFileReport {\n testFilePath: string;\n scenarios: (StoryMeta & {\n _attachments?: Array<{\n name: string;\n mediaType: string;\n path?: string;\n body?: string;\n encoding?: \"BASE64\" | \"IDENTITY\";\n charset?: string;\n fileName?: string;\n stepIndex?: number;\n stepId?: string;\n }>;\n })[];\n}\n\ninterface JestTestResult {\n testFilePath: string;\n testResults: Array<{\n fullName: string;\n title?: string;\n status: \"passed\" | \"failed\" | \"pending\" | \"todo\";\n duration?: number;\n failureMessages?: string[];\n }>;\n}\n\ninterface JestAggregatedResult {\n testResults: JestTestResult[];\n}\n\n// ============================================================================\n// Utility Functions\n// ============================================================================\n\n/**\n * Convert path to relative posix format.\n */\nfunction toRelativePosix(absolutePath: string, projectRoot: string): string {\n return path.relative(projectRoot, absolutePath).split(path.sep).join(\"/\");\n}\n\n// ============================================================================\n// Reporter Implementation\n// ============================================================================\n\nexport default class StoryReporter {\n private options: StoryReporterOptions;\n private startTime = 0;\n private packageVersion: string | undefined;\n private gitSha: string | undefined;\n /**\n * Left unknown when Jest hands us no global config. Claiming full coverage\n * without having looked would let a later merge retire scenarios on a guess.\n */\n private runScope: \"full\" | \"filtered\" | undefined;\n\n constructor(globalConfig: unknown, reporterOptions: StoryReporterOptions = {}) {\n this.options = reporterOptions;\n // `jest -t` runs only the matching tests, so a run carrying a pattern\n // reports a subset of each file it touches and must not be read as that\n // file's full contents.\n const config = globalConfig as { testNamePattern?: unknown } | undefined;\n if (config) {\n const pattern = config.testNamePattern;\n const filtered = typeof pattern === \"string\" ? pattern.length > 0 : pattern != null;\n this.runScope = filtered ? \"filtered\" : \"full\";\n }\n }\n\n /** Get the output directory for story JSON files */\n private getOutputDir(): string {\n const baseDir = process.env.JEST_STORY_DOCS_DIR ?? \".jest-executable-stories\";\n return path.resolve(process.cwd(), baseDir);\n }\n\n /** Reset run artifacts (clear previous story files) */\n private resetRunArtifacts(): void {\n try {\n fs.rmSync(this.getOutputDir(), { recursive: true, force: true });\n } catch {\n // Ignore cleanup errors\n }\n }\n\n /** Read story reports written by story.init() during test execution */\n private readStoryReports(): StoryFileReport[] {\n const outputDir = this.getOutputDir();\n if (!fs.existsSync(outputDir)) return [];\n const files = fg.sync(\"**/*.json\", {\n cwd: outputDir,\n onlyFiles: true,\n absolute: true,\n });\n const reports: StoryFileReport[] = [];\n for (const file of files) {\n try {\n const raw = fs.readFileSync(file, \"utf8\");\n const parsed = JSON.parse(raw) as StoryFileReport;\n if (!parsed?.testFilePath || !Array.isArray(parsed.scenarios)) continue;\n reports.push(parsed);\n } catch {\n // Ignore unreadable or malformed files\n }\n }\n return reports;\n }\n\n onRunStart(): void {\n this.resetRunArtifacts();\n this.startTime = Date.now();\n const root = process.cwd();\n const includeMetadata = this.options.markdown?.includeMetadata ?? true;\n if (includeMetadata) {\n this.packageVersion = readPackageVersion(root);\n this.gitSha = readGitSha(root);\n }\n }\n\n async onRunComplete(\n _testContexts: Set<unknown>,\n results: JestAggregatedResult\n ): Promise<void> {\n const root = process.cwd();\n\n // Build map of test results by file for status lookup\n const fileResults = new Map<string, JestTestResult>();\n for (const testFileResult of results.testResults) {\n fileResults.set(testFileResult.testFilePath, testFileResult);\n }\n\n // Read story data from JSON files written by story.init()\n const reports = this.readStoryReports();\n\n // Collect test cases\n const rawTestCases: RawTestCase[] = [];\n\n // Which tests actually declared a story, per file. A failed test missing\n // from here never got the chance to declare one.\n const declaredByFile = new Map<string, Set<string>>();\n\n for (const report of reports) {\n const fileResult = fileResults.get(report.testFilePath);\n const sourceFile = toRelativePosix(report.testFilePath, root);\n const matchedFullNames = new Set<string>();\n declaredByFile.set(sourceFile, matchedFullNames);\n\n for (const meta of report.scenarios) {\n if (!meta?.scenario) continue;\n\n // Find matching test result\n const matchingTest = fileResult?.testResults.find((test) => {\n const expectedFullName = meta.suitePath\n ? [...meta.suitePath, meta.scenario].join(\" > \")\n : meta.scenario;\n return test.fullName === expectedFullName;\n });\n if (matchingTest) matchedFullNames.add(matchingTest.fullName);\n\n // Map Jest status to raw status\n const statusMap: Record<string, RawTestCase[\"status\"]> = {\n passed: \"pass\",\n failed: \"fail\",\n pending: \"pending\",\n todo: \"pending\",\n };\n\n const status = matchingTest\n ? statusMap[matchingTest.status] ?? \"unknown\"\n : \"pass\";\n\n // Map attachments\n const rawAttachments: RawAttachment[] = (meta._attachments ?? []).map((a: NonNullable<typeof meta._attachments>[number]) => ({\n name: a.name,\n mediaType: a.mediaType,\n path: a.path,\n body: a.body,\n encoding: a.encoding,\n charset: a.charset,\n fileName: a.fileName,\n stepIndex: a.stepIndex,\n stepId: a.stepId,\n }));\n\n // Extract step events (timing)\n const stepEvents: RawStepEvent[] = meta.steps\n .filter((s: { durationMs?: number }) => s.durationMs !== undefined)\n .map((s: { durationMs?: number; text: string }, i: number) => ({\n index: i,\n title: s.text,\n durationMs: s.durationMs,\n }));\n\n rawTestCases.push({\n title: meta.scenario,\n titlePath: meta.suitePath\n ? [...meta.suitePath, meta.scenario]\n : [meta.scenario],\n story: meta,\n sourceFile,\n sourceLine: Math.max(1, meta.sourceOrder ?? 1),\n status,\n durationMs: matchingTest?.duration ?? 0,\n error: matchingTest?.failureMessages?.length\n ? { message: matchingTest.failureMessages.join(\"\\n\") }\n : undefined,\n attachments: rawAttachments.length > 0 ? rawAttachments : undefined,\n stepEvents: stepEvents.length > 0 ? stepEvents : undefined,\n retry: 0,\n retries: 0,\n });\n }\n\n // Bodyless `it.todo(...)` never runs `story.init()`, so it has no story\n // meta — but Jest still reports it with status \"todo\". Emit those as\n // planned scenarios. Only files with story reports reach this loop, so\n // plain (non-story) suites full of todos never leak into the docs.\n for (const test of fileResult?.testResults ?? []) {\n if (test.status !== \"todo\" || matchedFullNames.has(test.fullName)) continue;\n const title = test.title ?? test.fullName;\n matchedFullNames.add(test.fullName);\n rawTestCases.push({\n title,\n titlePath: [title],\n story: { scenario: title, steps: [] },\n sourceFile,\n sourceLine: 1,\n status: \"todo\",\n durationMs: 0,\n retry: 0,\n retries: 0,\n });\n }\n }\n\n // Every file Jest executed, whether or not it produced a story. Without\n // this a file whose last story was deleted looks like a file that did not\n // run, and keeps its old scenarios for good.\n const coveredSourceFiles = [\n ...new Set(\n (results.testResults ?? [])\n .map((r) => r.testFilePath)\n .filter((f): f is string => typeof f === \"string\" && f.length > 0)\n .map((f) => toRelativePosix(f, root))\n ),\n ].sort();\n\n // A story that never declared itself — a throwing beforeAll, an import\n // error, a timeout before `story.init()` — is missing because the run\n // broke, not because someone deleted it. Checked per test, not per file:\n // one healthy story in a file does not vouch for its broken siblings.\n const incompleteSourceFiles = [\n ...new Set(\n (results.testResults ?? [])\n .filter((r) => {\n const declared =\n declaredByFile.get(toRelativePosix(r.testFilePath, root)) ??\n new Set<string>();\n const undeclaredFailure = (r.testResults ?? []).some(\n (t) => t.status === \"failed\" && !declared.has(t.fullName)\n );\n // No tests at all plus a failure means the module never loaded.\n const suiteError =\n Boolean((r as { testExecError?: unknown }).testExecError) ||\n ((r.testResults ?? []).length === 0 &&\n Boolean((r as { failureMessage?: string | null }).failureMessage));\n return undeclaredFailure || suiteError;\n })\n .map((r) => toRelativePosix(r.testFilePath, root))\n ),\n ].sort();\n\n // Nothing ran and nothing was covered: there is genuinely nothing to say.\n if (rawTestCases.length === 0 && coveredSourceFiles.length === 0) return;\n\n // Build RawRun\n const rawRun: RawRun = {\n testCases: rawTestCases,\n startedAtMs: this.startTime,\n finishedAtMs: Date.now(),\n projectRoot: root,\n packageVersion: this.packageVersion,\n gitSha: this.gitSha,\n ci: detectCI(),\n ...(this.runScope ? { runScope: this.runScope } : {}),\n ...(coveredSourceFiles.length > 0 ? { coveredSourceFiles } : {}),\n ...(incompleteSourceFiles.length > 0 ? { incompleteSourceFiles } : {}),\n };\n\n // Optionally write raw run JSON for CLI/binary consumption\n const rawRunPath = this.options.rawRunPath;\n if (rawRunPath) {\n const absolutePath = path.isAbsolute(rawRunPath)\n ? rawRunPath\n : path.join(root, rawRunPath);\n const dir = path.dirname(absolutePath);\n if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });\n const payload = { schemaVersion: 1, ...rawRun };\n fs.writeFileSync(absolutePath, JSON.stringify(payload, null, 2), \"utf8\");\n }\n\n // Canonicalize\n const canonicalRun = canonicalizeRun(rawRun);\n\n // Generate reports\n const generator = new ReportGenerator(this.options);\n try {\n await generator.generate(canonicalRun);\n } catch (err) {\n console.error(\"Failed to generate reports:\", err);\n }\n }\n}\n"],"mappings":"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","names":["fg"]}
@@ -31,7 +31,12 @@ declare class StoryReporter {
31
31
  private startTime;
32
32
  private packageVersion;
33
33
  private gitSha;
34
- constructor(_globalConfig: unknown, reporterOptions?: StoryReporterOptions);
34
+ /**
35
+ * Left unknown when Jest hands us no global config. Claiming full coverage
36
+ * without having looked would let a later merge retire scenarios on a guess.
37
+ */
38
+ private runScope;
39
+ constructor(globalConfig: unknown, reporterOptions?: StoryReporterOptions);
35
40
  /** Get the output directory for story JSON files */
36
41
  private getOutputDir;
37
42
  /** Reset run artifacts (clear previous story files) */
@@ -31,7 +31,12 @@ declare class StoryReporter {
31
31
  private startTime;
32
32
  private packageVersion;
33
33
  private gitSha;
34
- constructor(_globalConfig: unknown, reporterOptions?: StoryReporterOptions);
34
+ /**
35
+ * Left unknown when Jest hands us no global config. Claiming full coverage
36
+ * without having looked would let a later merge retire scenarios on a guess.
37
+ */
38
+ private runScope;
39
+ constructor(globalConfig: unknown, reporterOptions?: StoryReporterOptions);
35
40
  /** Get the output directory for story JSON files */
36
41
  private getOutputDir;
37
42
  /** Reset run artifacts (clear previous story files) */
package/dist/reporter.js CHANGED
@@ -17,8 +17,19 @@ var StoryReporter = class {
17
17
  startTime = 0;
18
18
  packageVersion;
19
19
  gitSha;
20
- constructor(_globalConfig, reporterOptions = {}) {
20
+ /**
21
+ * Left unknown when Jest hands us no global config. Claiming full coverage
22
+ * without having looked would let a later merge retire scenarios on a guess.
23
+ */
24
+ runScope;
25
+ constructor(globalConfig, reporterOptions = {}) {
21
26
  this.options = reporterOptions;
27
+ const config = globalConfig;
28
+ if (config) {
29
+ const pattern = config.testNamePattern;
30
+ const filtered = typeof pattern === "string" ? pattern.length > 0 : pattern != null;
31
+ this.runScope = filtered ? "filtered" : "full";
32
+ }
22
33
  }
23
34
  /** Get the output directory for story JSON files */
24
35
  getOutputDir() {
@@ -71,10 +82,12 @@ var StoryReporter = class {
71
82
  }
72
83
  const reports = this.readStoryReports();
73
84
  const rawTestCases = [];
85
+ const declaredByFile = /* @__PURE__ */ new Map();
74
86
  for (const report of reports) {
75
87
  const fileResult = fileResults.get(report.testFilePath);
76
88
  const sourceFile = toRelativePosix(report.testFilePath, root);
77
89
  const matchedFullNames = /* @__PURE__ */ new Set();
90
+ declaredByFile.set(sourceFile, matchedFullNames);
78
91
  for (const meta of report.scenarios) {
79
92
  if (!meta?.scenario) continue;
80
93
  const matchingTest = fileResult?.testResults.find((test) => {
@@ -123,6 +136,7 @@ var StoryReporter = class {
123
136
  for (const test of fileResult?.testResults ?? []) {
124
137
  if (test.status !== "todo" || matchedFullNames.has(test.fullName)) continue;
125
138
  const title = test.title ?? test.fullName;
139
+ matchedFullNames.add(test.fullName);
126
140
  rawTestCases.push({
127
141
  title,
128
142
  titlePath: [title],
@@ -136,7 +150,24 @@ var StoryReporter = class {
136
150
  });
137
151
  }
138
152
  }
139
- if (rawTestCases.length === 0) return;
153
+ const coveredSourceFiles = [
154
+ ...new Set(
155
+ (results.testResults ?? []).map((r) => r.testFilePath).filter((f) => typeof f === "string" && f.length > 0).map((f) => toRelativePosix(f, root))
156
+ )
157
+ ].sort();
158
+ const incompleteSourceFiles = [
159
+ ...new Set(
160
+ (results.testResults ?? []).filter((r) => {
161
+ const declared = declaredByFile.get(toRelativePosix(r.testFilePath, root)) ?? /* @__PURE__ */ new Set();
162
+ const undeclaredFailure = (r.testResults ?? []).some(
163
+ (t) => t.status === "failed" && !declared.has(t.fullName)
164
+ );
165
+ const suiteError = Boolean(r.testExecError) || (r.testResults ?? []).length === 0 && Boolean(r.failureMessage);
166
+ return undeclaredFailure || suiteError;
167
+ }).map((r) => toRelativePosix(r.testFilePath, root))
168
+ )
169
+ ].sort();
170
+ if (rawTestCases.length === 0 && coveredSourceFiles.length === 0) return;
140
171
  const rawRun = {
141
172
  testCases: rawTestCases,
142
173
  startedAtMs: this.startTime,
@@ -144,7 +175,10 @@ var StoryReporter = class {
144
175
  projectRoot: root,
145
176
  packageVersion: this.packageVersion,
146
177
  gitSha: this.gitSha,
147
- ci: detectCI()
178
+ ci: detectCI(),
179
+ ...this.runScope ? { runScope: this.runScope } : {},
180
+ ...coveredSourceFiles.length > 0 ? { coveredSourceFiles } : {},
181
+ ...incompleteSourceFiles.length > 0 ? { incompleteSourceFiles } : {}
148
182
  };
149
183
  const rawRunPath = this.options.rawRunPath;
150
184
  if (rawRunPath) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/reporter.ts"],"sourcesContent":["/**\n * Jest reporter for executable-stories.\n * Generates reports using the executable-stories-formatters package.\n *\n * Uses file-based communication: story.init() writes JSON files to\n * .jest-executable-stories/ which this reporter reads.\n */\n\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport fg from \"fast-glob\";\nimport type { StoryMeta } from \"executable-stories-formatters\";\n\n// Import from formatters package\nimport {\n ReportGenerator,\n canonicalizeRun,\n readGitSha,\n readPackageVersion,\n detectCI,\n type RawRun,\n type RawTestCase,\n type RawAttachment,\n type RawStepEvent,\n type FormatterOptions,\n} from \"executable-stories-formatters\";\n\n// Re-export types from formatters for convenience\nexport type {\n OutputFormat,\n OutputMode,\n ColocatedStyle,\n OutputRule,\n FormatterOptions,\n} from \"executable-stories-formatters\";\n\n// ============================================================================\n// Reporter Options (delegates to FormatterOptions)\n// ============================================================================\n\nexport interface StoryReporterOptions extends FormatterOptions {\n /** If set, write raw run JSON (schemaVersion 1) to this path for use with the executable-stories CLI/binary */\n rawRunPath?: string;\n}\n\n// ============================================================================\n// Internal Types\n// ============================================================================\n\n/** Shape of JSON files written by story.init() */\ninterface StoryFileReport {\n testFilePath: string;\n scenarios: (StoryMeta & {\n _attachments?: Array<{\n name: string;\n mediaType: string;\n path?: string;\n body?: string;\n encoding?: \"BASE64\" | \"IDENTITY\";\n charset?: string;\n fileName?: string;\n stepIndex?: number;\n stepId?: string;\n }>;\n })[];\n}\n\ninterface JestTestResult {\n testFilePath: string;\n testResults: Array<{\n fullName: string;\n title?: string;\n status: \"passed\" | \"failed\" | \"pending\" | \"todo\";\n duration?: number;\n failureMessages?: string[];\n }>;\n}\n\ninterface JestAggregatedResult {\n testResults: JestTestResult[];\n}\n\n// ============================================================================\n// Utility Functions\n// ============================================================================\n\n/**\n * Convert path to relative posix format.\n */\nfunction toRelativePosix(absolutePath: string, projectRoot: string): string {\n return path.relative(projectRoot, absolutePath).split(path.sep).join(\"/\");\n}\n\n// ============================================================================\n// Reporter Implementation\n// ============================================================================\n\nexport default class StoryReporter {\n private options: StoryReporterOptions;\n private startTime = 0;\n private packageVersion: string | undefined;\n private gitSha: string | undefined;\n\n constructor(_globalConfig: unknown, reporterOptions: StoryReporterOptions = {}) {\n this.options = reporterOptions;\n }\n\n /** Get the output directory for story JSON files */\n private getOutputDir(): string {\n const baseDir = process.env.JEST_STORY_DOCS_DIR ?? \".jest-executable-stories\";\n return path.resolve(process.cwd(), baseDir);\n }\n\n /** Reset run artifacts (clear previous story files) */\n private resetRunArtifacts(): void {\n try {\n fs.rmSync(this.getOutputDir(), { recursive: true, force: true });\n } catch {\n // Ignore cleanup errors\n }\n }\n\n /** Read story reports written by story.init() during test execution */\n private readStoryReports(): StoryFileReport[] {\n const outputDir = this.getOutputDir();\n if (!fs.existsSync(outputDir)) return [];\n const files = fg.sync(\"**/*.json\", {\n cwd: outputDir,\n onlyFiles: true,\n absolute: true,\n });\n const reports: StoryFileReport[] = [];\n for (const file of files) {\n try {\n const raw = fs.readFileSync(file, \"utf8\");\n const parsed = JSON.parse(raw) as StoryFileReport;\n if (!parsed?.testFilePath || !Array.isArray(parsed.scenarios)) continue;\n reports.push(parsed);\n } catch {\n // Ignore unreadable or malformed files\n }\n }\n return reports;\n }\n\n onRunStart(): void {\n this.resetRunArtifacts();\n this.startTime = Date.now();\n const root = process.cwd();\n const includeMetadata = this.options.markdown?.includeMetadata ?? true;\n if (includeMetadata) {\n this.packageVersion = readPackageVersion(root);\n this.gitSha = readGitSha(root);\n }\n }\n\n async onRunComplete(\n _testContexts: Set<unknown>,\n results: JestAggregatedResult\n ): Promise<void> {\n const root = process.cwd();\n\n // Build map of test results by file for status lookup\n const fileResults = new Map<string, JestTestResult>();\n for (const testFileResult of results.testResults) {\n fileResults.set(testFileResult.testFilePath, testFileResult);\n }\n\n // Read story data from JSON files written by story.init()\n const reports = this.readStoryReports();\n\n // Collect test cases\n const rawTestCases: RawTestCase[] = [];\n\n for (const report of reports) {\n const fileResult = fileResults.get(report.testFilePath);\n const sourceFile = toRelativePosix(report.testFilePath, root);\n const matchedFullNames = new Set<string>();\n\n for (const meta of report.scenarios) {\n if (!meta?.scenario) continue;\n\n // Find matching test result\n const matchingTest = fileResult?.testResults.find((test) => {\n const expectedFullName = meta.suitePath\n ? [...meta.suitePath, meta.scenario].join(\" > \")\n : meta.scenario;\n return test.fullName === expectedFullName;\n });\n if (matchingTest) matchedFullNames.add(matchingTest.fullName);\n\n // Map Jest status to raw status\n const statusMap: Record<string, RawTestCase[\"status\"]> = {\n passed: \"pass\",\n failed: \"fail\",\n pending: \"pending\",\n todo: \"pending\",\n };\n\n const status = matchingTest\n ? statusMap[matchingTest.status] ?? \"unknown\"\n : \"pass\";\n\n // Map attachments\n const rawAttachments: RawAttachment[] = (meta._attachments ?? []).map((a: NonNullable<typeof meta._attachments>[number]) => ({\n name: a.name,\n mediaType: a.mediaType,\n path: a.path,\n body: a.body,\n encoding: a.encoding,\n charset: a.charset,\n fileName: a.fileName,\n stepIndex: a.stepIndex,\n stepId: a.stepId,\n }));\n\n // Extract step events (timing)\n const stepEvents: RawStepEvent[] = meta.steps\n .filter((s: { durationMs?: number }) => s.durationMs !== undefined)\n .map((s: { durationMs?: number; text: string }, i: number) => ({\n index: i,\n title: s.text,\n durationMs: s.durationMs,\n }));\n\n rawTestCases.push({\n title: meta.scenario,\n titlePath: meta.suitePath\n ? [...meta.suitePath, meta.scenario]\n : [meta.scenario],\n story: meta,\n sourceFile,\n sourceLine: Math.max(1, meta.sourceOrder ?? 1),\n status,\n durationMs: matchingTest?.duration ?? 0,\n error: matchingTest?.failureMessages?.length\n ? { message: matchingTest.failureMessages.join(\"\\n\") }\n : undefined,\n attachments: rawAttachments.length > 0 ? rawAttachments : undefined,\n stepEvents: stepEvents.length > 0 ? stepEvents : undefined,\n retry: 0,\n retries: 0,\n });\n }\n\n // Bodyless `it.todo(...)` never runs `story.init()`, so it has no story\n // meta — but Jest still reports it with status \"todo\". Emit those as\n // planned scenarios. Only files with story reports reach this loop, so\n // plain (non-story) suites full of todos never leak into the docs.\n for (const test of fileResult?.testResults ?? []) {\n if (test.status !== \"todo\" || matchedFullNames.has(test.fullName)) continue;\n const title = test.title ?? test.fullName;\n rawTestCases.push({\n title,\n titlePath: [title],\n story: { scenario: title, steps: [] },\n sourceFile,\n sourceLine: 1,\n status: \"todo\",\n durationMs: 0,\n retry: 0,\n retries: 0,\n });\n }\n }\n\n if (rawTestCases.length === 0) return;\n\n // Build RawRun\n const rawRun: RawRun = {\n testCases: rawTestCases,\n startedAtMs: this.startTime,\n finishedAtMs: Date.now(),\n projectRoot: root,\n packageVersion: this.packageVersion,\n gitSha: this.gitSha,\n ci: detectCI(),\n };\n\n // Optionally write raw run JSON for CLI/binary consumption\n const rawRunPath = this.options.rawRunPath;\n if (rawRunPath) {\n const absolutePath = path.isAbsolute(rawRunPath)\n ? rawRunPath\n : path.join(root, rawRunPath);\n const dir = path.dirname(absolutePath);\n if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });\n const payload = { schemaVersion: 1, ...rawRun };\n fs.writeFileSync(absolutePath, JSON.stringify(payload, null, 2), \"utf8\");\n }\n\n // Canonicalize\n const canonicalRun = canonicalizeRun(rawRun);\n\n // Generate reports\n const generator = new ReportGenerator(this.options);\n try {\n await generator.generate(canonicalRun);\n } catch (err) {\n console.error(\"Failed to generate reports:\", err);\n }\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/reporter.ts"],"sourcesContent":["/**\n * Jest reporter for executable-stories.\n * Generates reports using the executable-stories-formatters package.\n *\n * Uses file-based communication: story.init() writes JSON files to\n * .jest-executable-stories/ which this reporter reads.\n */\n\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport fg from \"fast-glob\";\nimport type { StoryMeta } from \"executable-stories-formatters\";\n\n// Import from formatters package\nimport {\n ReportGenerator,\n canonicalizeRun,\n readGitSha,\n readPackageVersion,\n detectCI,\n type RawRun,\n type RawTestCase,\n type RawAttachment,\n type RawStepEvent,\n type FormatterOptions,\n} from \"executable-stories-formatters\";\n\n// Re-export types from formatters for convenience\nexport type {\n OutputFormat,\n OutputMode,\n ColocatedStyle,\n OutputRule,\n FormatterOptions,\n} from \"executable-stories-formatters\";\n\n// ============================================================================\n// Reporter Options (delegates to FormatterOptions)\n// ============================================================================\n\nexport interface StoryReporterOptions extends FormatterOptions {\n /** If set, write raw run JSON (schemaVersion 1) to this path for use with the executable-stories CLI/binary */\n rawRunPath?: string;\n}\n\n// ============================================================================\n// Internal Types\n// ============================================================================\n\n/** Shape of JSON files written by story.init() */\ninterface StoryFileReport {\n testFilePath: string;\n scenarios: (StoryMeta & {\n _attachments?: Array<{\n name: string;\n mediaType: string;\n path?: string;\n body?: string;\n encoding?: \"BASE64\" | \"IDENTITY\";\n charset?: string;\n fileName?: string;\n stepIndex?: number;\n stepId?: string;\n }>;\n })[];\n}\n\ninterface JestTestResult {\n testFilePath: string;\n testResults: Array<{\n fullName: string;\n title?: string;\n status: \"passed\" | \"failed\" | \"pending\" | \"todo\";\n duration?: number;\n failureMessages?: string[];\n }>;\n}\n\ninterface JestAggregatedResult {\n testResults: JestTestResult[];\n}\n\n// ============================================================================\n// Utility Functions\n// ============================================================================\n\n/**\n * Convert path to relative posix format.\n */\nfunction toRelativePosix(absolutePath: string, projectRoot: string): string {\n return path.relative(projectRoot, absolutePath).split(path.sep).join(\"/\");\n}\n\n// ============================================================================\n// Reporter Implementation\n// ============================================================================\n\nexport default class StoryReporter {\n private options: StoryReporterOptions;\n private startTime = 0;\n private packageVersion: string | undefined;\n private gitSha: string | undefined;\n /**\n * Left unknown when Jest hands us no global config. Claiming full coverage\n * without having looked would let a later merge retire scenarios on a guess.\n */\n private runScope: \"full\" | \"filtered\" | undefined;\n\n constructor(globalConfig: unknown, reporterOptions: StoryReporterOptions = {}) {\n this.options = reporterOptions;\n // `jest -t` runs only the matching tests, so a run carrying a pattern\n // reports a subset of each file it touches and must not be read as that\n // file's full contents.\n const config = globalConfig as { testNamePattern?: unknown } | undefined;\n if (config) {\n const pattern = config.testNamePattern;\n const filtered = typeof pattern === \"string\" ? pattern.length > 0 : pattern != null;\n this.runScope = filtered ? \"filtered\" : \"full\";\n }\n }\n\n /** Get the output directory for story JSON files */\n private getOutputDir(): string {\n const baseDir = process.env.JEST_STORY_DOCS_DIR ?? \".jest-executable-stories\";\n return path.resolve(process.cwd(), baseDir);\n }\n\n /** Reset run artifacts (clear previous story files) */\n private resetRunArtifacts(): void {\n try {\n fs.rmSync(this.getOutputDir(), { recursive: true, force: true });\n } catch {\n // Ignore cleanup errors\n }\n }\n\n /** Read story reports written by story.init() during test execution */\n private readStoryReports(): StoryFileReport[] {\n const outputDir = this.getOutputDir();\n if (!fs.existsSync(outputDir)) return [];\n const files = fg.sync(\"**/*.json\", {\n cwd: outputDir,\n onlyFiles: true,\n absolute: true,\n });\n const reports: StoryFileReport[] = [];\n for (const file of files) {\n try {\n const raw = fs.readFileSync(file, \"utf8\");\n const parsed = JSON.parse(raw) as StoryFileReport;\n if (!parsed?.testFilePath || !Array.isArray(parsed.scenarios)) continue;\n reports.push(parsed);\n } catch {\n // Ignore unreadable or malformed files\n }\n }\n return reports;\n }\n\n onRunStart(): void {\n this.resetRunArtifacts();\n this.startTime = Date.now();\n const root = process.cwd();\n const includeMetadata = this.options.markdown?.includeMetadata ?? true;\n if (includeMetadata) {\n this.packageVersion = readPackageVersion(root);\n this.gitSha = readGitSha(root);\n }\n }\n\n async onRunComplete(\n _testContexts: Set<unknown>,\n results: JestAggregatedResult\n ): Promise<void> {\n const root = process.cwd();\n\n // Build map of test results by file for status lookup\n const fileResults = new Map<string, JestTestResult>();\n for (const testFileResult of results.testResults) {\n fileResults.set(testFileResult.testFilePath, testFileResult);\n }\n\n // Read story data from JSON files written by story.init()\n const reports = this.readStoryReports();\n\n // Collect test cases\n const rawTestCases: RawTestCase[] = [];\n\n // Which tests actually declared a story, per file. A failed test missing\n // from here never got the chance to declare one.\n const declaredByFile = new Map<string, Set<string>>();\n\n for (const report of reports) {\n const fileResult = fileResults.get(report.testFilePath);\n const sourceFile = toRelativePosix(report.testFilePath, root);\n const matchedFullNames = new Set<string>();\n declaredByFile.set(sourceFile, matchedFullNames);\n\n for (const meta of report.scenarios) {\n if (!meta?.scenario) continue;\n\n // Find matching test result\n const matchingTest = fileResult?.testResults.find((test) => {\n const expectedFullName = meta.suitePath\n ? [...meta.suitePath, meta.scenario].join(\" > \")\n : meta.scenario;\n return test.fullName === expectedFullName;\n });\n if (matchingTest) matchedFullNames.add(matchingTest.fullName);\n\n // Map Jest status to raw status\n const statusMap: Record<string, RawTestCase[\"status\"]> = {\n passed: \"pass\",\n failed: \"fail\",\n pending: \"pending\",\n todo: \"pending\",\n };\n\n const status = matchingTest\n ? statusMap[matchingTest.status] ?? \"unknown\"\n : \"pass\";\n\n // Map attachments\n const rawAttachments: RawAttachment[] = (meta._attachments ?? []).map((a: NonNullable<typeof meta._attachments>[number]) => ({\n name: a.name,\n mediaType: a.mediaType,\n path: a.path,\n body: a.body,\n encoding: a.encoding,\n charset: a.charset,\n fileName: a.fileName,\n stepIndex: a.stepIndex,\n stepId: a.stepId,\n }));\n\n // Extract step events (timing)\n const stepEvents: RawStepEvent[] = meta.steps\n .filter((s: { durationMs?: number }) => s.durationMs !== undefined)\n .map((s: { durationMs?: number; text: string }, i: number) => ({\n index: i,\n title: s.text,\n durationMs: s.durationMs,\n }));\n\n rawTestCases.push({\n title: meta.scenario,\n titlePath: meta.suitePath\n ? [...meta.suitePath, meta.scenario]\n : [meta.scenario],\n story: meta,\n sourceFile,\n sourceLine: Math.max(1, meta.sourceOrder ?? 1),\n status,\n durationMs: matchingTest?.duration ?? 0,\n error: matchingTest?.failureMessages?.length\n ? { message: matchingTest.failureMessages.join(\"\\n\") }\n : undefined,\n attachments: rawAttachments.length > 0 ? rawAttachments : undefined,\n stepEvents: stepEvents.length > 0 ? stepEvents : undefined,\n retry: 0,\n retries: 0,\n });\n }\n\n // Bodyless `it.todo(...)` never runs `story.init()`, so it has no story\n // meta — but Jest still reports it with status \"todo\". Emit those as\n // planned scenarios. Only files with story reports reach this loop, so\n // plain (non-story) suites full of todos never leak into the docs.\n for (const test of fileResult?.testResults ?? []) {\n if (test.status !== \"todo\" || matchedFullNames.has(test.fullName)) continue;\n const title = test.title ?? test.fullName;\n matchedFullNames.add(test.fullName);\n rawTestCases.push({\n title,\n titlePath: [title],\n story: { scenario: title, steps: [] },\n sourceFile,\n sourceLine: 1,\n status: \"todo\",\n durationMs: 0,\n retry: 0,\n retries: 0,\n });\n }\n }\n\n // Every file Jest executed, whether or not it produced a story. Without\n // this a file whose last story was deleted looks like a file that did not\n // run, and keeps its old scenarios for good.\n const coveredSourceFiles = [\n ...new Set(\n (results.testResults ?? [])\n .map((r) => r.testFilePath)\n .filter((f): f is string => typeof f === \"string\" && f.length > 0)\n .map((f) => toRelativePosix(f, root))\n ),\n ].sort();\n\n // A story that never declared itself — a throwing beforeAll, an import\n // error, a timeout before `story.init()` — is missing because the run\n // broke, not because someone deleted it. Checked per test, not per file:\n // one healthy story in a file does not vouch for its broken siblings.\n const incompleteSourceFiles = [\n ...new Set(\n (results.testResults ?? [])\n .filter((r) => {\n const declared =\n declaredByFile.get(toRelativePosix(r.testFilePath, root)) ??\n new Set<string>();\n const undeclaredFailure = (r.testResults ?? []).some(\n (t) => t.status === \"failed\" && !declared.has(t.fullName)\n );\n // No tests at all plus a failure means the module never loaded.\n const suiteError =\n Boolean((r as { testExecError?: unknown }).testExecError) ||\n ((r.testResults ?? []).length === 0 &&\n Boolean((r as { failureMessage?: string | null }).failureMessage));\n return undeclaredFailure || suiteError;\n })\n .map((r) => toRelativePosix(r.testFilePath, root))\n ),\n ].sort();\n\n // Nothing ran and nothing was covered: there is genuinely nothing to say.\n if (rawTestCases.length === 0 && coveredSourceFiles.length === 0) return;\n\n // Build RawRun\n const rawRun: RawRun = {\n testCases: rawTestCases,\n startedAtMs: this.startTime,\n finishedAtMs: Date.now(),\n projectRoot: root,\n packageVersion: this.packageVersion,\n gitSha: this.gitSha,\n ci: detectCI(),\n ...(this.runScope ? { runScope: this.runScope } : {}),\n ...(coveredSourceFiles.length > 0 ? { coveredSourceFiles } : {}),\n ...(incompleteSourceFiles.length > 0 ? { incompleteSourceFiles } : {}),\n };\n\n // Optionally write raw run JSON for CLI/binary consumption\n const rawRunPath = this.options.rawRunPath;\n if (rawRunPath) {\n const absolutePath = path.isAbsolute(rawRunPath)\n ? rawRunPath\n : path.join(root, rawRunPath);\n const dir = path.dirname(absolutePath);\n if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });\n const payload = { schemaVersion: 1, ...rawRun };\n fs.writeFileSync(absolutePath, JSON.stringify(payload, null, 2), \"utf8\");\n }\n\n // Canonicalize\n const canonicalRun = canonicalizeRun(rawRun);\n\n // Generate reports\n const generator = new ReportGenerator(this.options);\n try {\n await generator.generate(canonicalRun);\n } catch (err) {\n console.error(\"Failed to generate reports:\", err);\n }\n }\n}\n"],"mappings":"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","names":[]}