executable-stories-vitest 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/README.md +12 -0
- package/dist/index.cjs +111 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +111 -2
- package/dist/index.js.map +1 -1
- package/dist/reporter.cjs +66 -1
- package/dist/reporter.cjs.map +1 -1
- package/dist/reporter.d.cts +28 -0
- package/dist/reporter.d.ts +28 -0
- package/dist/reporter.js +66 -1
- package/dist/reporter.js.map +1 -1
- package/package.json +3 -3
package/dist/reporter.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/reporter.ts"],"sourcesContent":["/**\n * Vitest reporter that reads task.meta.story from tests and writes reports\n * using the executable-stories-formatters package.\n *\n * Do not add value imports from \"vitest\" or \"./story-api.js\" here; this entry is loaded in vitest.config\n * before Vitest is ready. Use only `import type` from those modules.\n *\n * Type strategy: this reporter is duck-typed against vitest's runner contract,\n * not nominally typed via `implements Reporter`. That keeps it compatible with\n * any vitest-compatible runner (including forks like vite-plus) where the\n * `Vitest` class type carries different private fields and would otherwise\n * fail nominal assignability checks at the consumer's call site.\n *\n * Vitest types are imported only for parameter typing of internal helpers,\n * never as `implements`. Anything we touch on `ctx` is captured by the local\n * `VitestContext` structural type below.\n */\nimport type {\n SerializedError,\n TestModule,\n TestCase,\n TestRunEndReason,\n} from \"vitest/node\";\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport type { StoryMeta } from \"./types\";\n\n// Import from formatters package\nimport {\n ReportGenerator,\n canonicalizeRun,\n readGitSha,\n readPackageVersion,\n detectCI,\n sendNotifications,\n toCIInfo,\n loadHistory,\n updateHistory,\n saveHistory,\n type RawFeature,\n type RawRun,\n type RawTestCase,\n type RawAttachment,\n type RawStepEvent,\n type FormatterOptions,\n type CoverageSummary,\n type OtelSpan,\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// Structural runner context\n// ----------------------------------------------------------------------------\n// We only read `ctx.config.root`. Avoid importing the full `Vitest` class as\n// a parameter type — its private fields (`_clearScreenPending`, etc.) make\n// nominal assignability fail across vitest forks. A structural type lets the\n// reporter accept any vitest-compatible runner.\n// ============================================================================\n\nexport type VitestContext = {\n config?: {\n root?: string;\n };\n};\n\n// ============================================================================\n// Reporter Protocol (duck-typed interface for reporters)\n// ============================================================================\n// Exported protocol that captures the reporter contract. This allows\n// StoryReporter to be properly typed without nominal `implements Reporter`,\n// enabling duck-typing compatibility across vitest forks while maintaining\n// type safety at the consumer call site.\n// ============================================================================\n\n// Public-facing protocol uses loose params on runner-supplied callbacks so the\n// returned reporter is structurally assignable to any vitest-compatible runner's\n// `Reporter` type (vitest, vite-plus, future forks). The concrete StoryReporter\n// class still uses strict vitest types internally — method bivariance lets the\n// narrower class satisfy this looser protocol.\nexport interface StoryReporterProtocol {\n onInit(ctx: VitestContext): void;\n onCoverage(coverage: unknown): void;\n onTestRunEnd(\n testModules: readonly unknown[],\n unhandledErrors: readonly unknown[],\n reason: unknown,\n ): Promise<void>;\n}\n\n// ============================================================================\n// Reporter Options (delegates to FormatterOptions)\n// ============================================================================\n\nexport interface StoryReporterOptions extends FormatterOptions {\n /** When GITHUB_ACTIONS, append report to job summary via @actions/core. Default: true */\n enableGithubActionsSummary?: boolean;\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// Coverage Types\n// ============================================================================\n\ntype CoverageMetric = { total: number; covered: number; pct: number };\ntype CoverageData = {\n statements: CoverageMetric;\n branches: CoverageMetric;\n functions: CoverageMetric;\n lines?: CoverageMetric;\n};\ntype CoverageFile = {\n s: Record<string, number>;\n f: Record<string, number>;\n b: Record<string, number[]>;\n l?: Record<string, number>;\n};\n\n// ============================================================================\n// Utility Functions\n// ============================================================================\n\n/**\n * Normalize Vitest onCoverage payload to coverage data.\n */\nfunction normalizeCoveragePayload(\n coverage: unknown\n): Record<string, CoverageFile> | undefined {\n if (!coverage || typeof coverage !== \"object\" || Array.isArray(coverage))\n return undefined;\n const raw = coverage as Record<string, unknown>;\n const data: Record<string, CoverageFile> = {};\n for (const [filePath, file] of Object.entries(raw)) {\n if (\n file &&\n typeof file === \"object\" &&\n \"s\" in file &&\n \"f\" in file &&\n \"b\" in file\n ) {\n data[filePath] = file as CoverageFile;\n }\n }\n if (Object.keys(data).length === 0) return undefined;\n return data;\n}\n\n/**\n * Summarize coverage data.\n */\nfunction summarizeCoverage(\n data: Record<string, CoverageFile>\n): CoverageData | undefined {\n let statementsTotal = 0;\n let statementsCovered = 0;\n let functionsTotal = 0;\n let functionsCovered = 0;\n let branchesTotal = 0;\n let branchesCovered = 0;\n let linesTotal = 0;\n let linesCovered = 0;\n let hasLines = false;\n\n for (const file of Object.values(data)) {\n for (const count of Object.values(file.s)) {\n statementsTotal += 1;\n if (count > 0) statementsCovered += 1;\n }\n for (const count of Object.values(file.f)) {\n functionsTotal += 1;\n if (count > 0) functionsCovered += 1;\n }\n for (const counts of Object.values(file.b)) {\n for (const count of counts) {\n branchesTotal += 1;\n if (count > 0) branchesCovered += 1;\n }\n }\n if (file.l) {\n hasLines = true;\n for (const count of Object.values(file.l)) {\n linesTotal += 1;\n if (count > 0) linesCovered += 1;\n }\n }\n }\n\n if (\n statementsTotal === 0 &&\n functionsTotal === 0 &&\n branchesTotal === 0 &&\n !hasLines\n ) {\n return undefined;\n }\n\n const metric = (covered: number, total: number): CoverageMetric => ({\n total,\n covered,\n pct: total === 0 ? 100 : Math.round((covered / total) * 100),\n });\n\n const summary: CoverageData = {\n statements: metric(statementsCovered, statementsTotal),\n branches: metric(branchesCovered, branchesTotal),\n functions: metric(functionsCovered, functionsTotal),\n };\n if (hasLines) {\n summary.lines = metric(linesCovered, linesTotal);\n }\n return summary;\n}\n\n/**\n * Convert internal coverage data to formatters CoverageSummary.\n */\nfunction toCoverageSummary(\n data: CoverageData | undefined\n): CoverageSummary | undefined {\n if (!data) return undefined;\n return {\n statementsPct: data.statements.pct,\n branchesPct: data.branches.pct,\n functionsPct: data.functions.pct,\n linesPct: data.lines?.pct,\n };\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\n/**\n * Vitest reporter that generates reports using executable-stories-formatters.\n *\n * Reads `task.meta.story` from each test and generates reports in configured formats.\n * Supports output routing (aggregated/colocated) and multiple output formats.\n *\n * Implements StoryReporterProtocol for type-safe duck-typing across vitest forks.\n */\nexport default class StoryReporter implements StoryReporterProtocol {\n private options: StoryReporterOptions;\n private ctx: VitestContext | undefined;\n private startTime: number = 0;\n private packageVersion: string | undefined;\n private gitSha: string | undefined;\n private coverageByFile: Record<string, CoverageFile> = {};\n\n constructor(options: StoryReporterOptions = {}) {\n this.options = options;\n }\n\n onInit(ctx: VitestContext): void {\n this.ctx = ctx;\n this.startTime = Date.now();\n const root = ctx.config?.root ?? process.cwd();\n\n // Read metadata if needed\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 onCoverage(coverage: unknown): void {\n const data = normalizeCoveragePayload(coverage);\n if (data) {\n this.coverageByFile = { ...this.coverageByFile, ...data };\n }\n }\n\n async onTestRunEnd(\n testModules: ReadonlyArray<TestModule>,\n _unhandledErrors: ReadonlyArray<SerializedError>,\n reason: TestRunEndReason\n ): Promise<void> {\n if (reason === \"interrupted\") return;\n\n const root = this.ctx?.config?.root ?? process.cwd();\n\n // Collect test cases\n const rawTestCases = this.collectTestCases(testModules, root);\n const features = collectFeatures(testModules, root);\n\n // Build RawRun\n const rawRun: RawRun = {\n testCases: rawTestCases,\n ...(features.length > 0 ? { features } : {}),\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 try {\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 } catch (err) {\n console.error(\"Failed to write raw run JSON:\", err);\n }\n }\n\n // Canonicalize\n const canonicalRun = canonicalizeRun(rawRun);\n\n // Add coverage if available\n const coverageData = summarizeCoverage(this.coverageByFile);\n if (coverageData) {\n canonicalRun.coverage = toCoverageSummary(coverageData);\n }\n\n // 1. Generate reports\n const generator = new ReportGenerator(this.options);\n try {\n const results = await generator.generate(canonicalRun);\n\n // Append to GitHub Actions summary if enabled\n const enableGithubSummary = this.options.enableGithubActionsSummary ?? true;\n if (process.env.GITHUB_ACTIONS === \"true\" && enableGithubSummary) {\n const markdownPaths = results.get(\"markdown\") ?? [];\n if (markdownPaths.length > 0) {\n const firstPath = markdownPaths[0];\n const content = fs.readFileSync(firstPath, \"utf8\");\n await this.appendGithubSummary(content).catch(() => {});\n }\n }\n } catch (err) {\n console.error(\"Failed to generate reports:\", err);\n }\n\n // 2. Update history (independent of report generation)\n try {\n const histOpts = this.options.history;\n if (histOpts?.filePath) {\n const historyPath = path.isAbsolute(histOpts.filePath)\n ? histOpts.filePath\n : path.join(root, histOpts.filePath);\n const store = loadHistory(\n { filePath: historyPath },\n {\n readFile: (p: string) => { try { return fs.readFileSync(p, \"utf8\"); } catch { return undefined; } },\n logger: console,\n },\n );\n const updated = updateHistory({ store, run: canonicalRun, maxRuns: histOpts.maxRuns ?? 10 });\n const dir = path.dirname(historyPath);\n if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });\n saveHistory(\n { filePath: historyPath, store: updated },\n { writeFile: (p: string, c: string) => fs.writeFileSync(p, c, \"utf8\") },\n );\n }\n } catch (err) {\n console.error(\"Failed to update history:\", err);\n }\n\n // 3. Send notifications (independent of both above)\n try {\n if (this.options.notification) {\n await sendNotifications(\n { run: canonicalRun, notification: this.options.notification },\n { fetch: globalThis.fetch, logger: console, toCIInfo },\n );\n }\n } catch (err) {\n console.error(\"Failed to send notifications:\", err);\n }\n }\n\n /**\n * Collect test cases from Vitest test modules.\n */\n private collectTestCases(\n testModules: ReadonlyArray<TestModule>,\n root: string\n ): RawTestCase[] {\n const testCases: RawTestCase[] = [];\n\n for (const mod of testModules) {\n const collection = mod.children;\n if (!collection) continue;\n\n const moduleId =\n mod.moduleId ??\n (mod as { relativeModuleId?: string }).relativeModuleId ??\n \"\";\n const absoluteModuleId = path.isAbsolute(moduleId)\n ? moduleId\n : path.resolve(root, moduleId);\n const sourceFile = toRelativePosix(absoluteModuleId, root);\n\n // Bodyless `it.todo(...)` never runs `story.init()`, so those tests have\n // no story meta. Collect them separately and emit them as planned\n // scenarios — but only for modules that contain story tests, so plain\n // (non-story) suites full of todos don't leak into the generated docs.\n const plannedTests: TestCase[] = [];\n let moduleHasStoryTests = false;\n\n for (const test of collection.allTests()) {\n const meta = this.getStoryMeta(test);\n if (!meta?.scenario || !Array.isArray(meta.steps)) {\n if (test.options?.mode === \"todo\") plannedTests.push(test);\n continue;\n }\n moduleHasStoryTests = true;\n\n const result = test.result?.();\n const state = result?.state ?? \"pending\";\n const durationMs =\n typeof (result as { duration?: number } | undefined)?.duration ===\n \"number\"\n ? (result as unknown as { duration: number }).duration\n : 0;\n\n // Get error details\n let errorMessage: string | undefined;\n let errorStack: string | undefined;\n if (state === \"failed\" && result) {\n const errors = (result as { errors?: SerializedError[] }).errors;\n if (errors?.length) {\n const err = errors[0];\n errorMessage = err.message;\n errorStack = err.stack;\n }\n }\n\n // Map Vitest state to raw status\n const statusMap: Record<string, string> = {\n passed: \"pass\",\n failed: \"fail\",\n skipped: \"skip\",\n pending: \"pending\",\n todo: \"pending\",\n };\n\n // Extract attachments from task.meta.storyAttachments\n const taskMeta = test.meta() as Record<string, unknown>;\n const scopedAttachments = (taskMeta?.storyAttachments ?? []) as 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 const attachments: RawAttachment[] = scopedAttachments.map((a) => ({\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) from story steps\n const stepEvents: RawStepEvent[] = meta.steps\n .filter((s: { durationMs?: number }) => s.durationMs !== undefined)\n .map((s: { durationMs?: number }, i: number) => ({\n index: i,\n title: (s as { text: string }).text,\n durationMs: s.durationMs,\n }));\n\n // Read autotel OTel spans from task.meta.otelSpans\n const otelSpans = taskMeta?.otelSpans;\n if (Array.isArray(otelSpans) && otelSpans.length > 0) {\n const valid = otelSpans.filter(\n (s: unknown): s is OtelSpan =>\n s != null &&\n typeof s === \"object\" &&\n typeof (s as Record<string, unknown>).spanId === \"string\" &&\n typeof (s as Record<string, unknown>).name === \"string\",\n );\n if (valid.length > 0) {\n meta.otelSpans = valid;\n }\n }\n\n // Retry info from Vitest result\n const retryCount = (result as { retryCount?: number } | undefined)?.retryCount ?? 0;\n const configuredRetries = Math.max(\n retryCount,\n (test as { retries?: number }).retries ??\n (test as { options?: { retry?: number } }).options?.retry ??\n 0,\n );\n\n testCases.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: (statusMap[state] ?? \"unknown\") as RawTestCase[\"status\"],\n durationMs,\n error: errorMessage\n ? { message: errorMessage, stack: errorStack }\n : undefined,\n attachments: attachments.length > 0 ? attachments : undefined,\n stepEvents: stepEvents.length > 0 ? stepEvents : undefined,\n retry: retryCount,\n retries: configuredRetries,\n });\n }\n\n if (moduleHasStoryTests) {\n for (const test of plannedTests) {\n const titlePath = test.fullName.split(\" > \");\n testCases.push({\n title: test.name,\n titlePath,\n story: {\n scenario: test.name,\n steps: [],\n suitePath: titlePath.slice(0, -1),\n },\n sourceFile,\n sourceLine: 1,\n status: \"todo\",\n durationMs: 0,\n });\n }\n }\n }\n\n return testCases;\n }\n\n private getStoryMeta(test: TestCase): StoryMeta | undefined {\n const meta = test.meta() as Record<string, unknown>;\n return meta?.[\"story\"] as StoryMeta | undefined;\n }\n\n private async appendGithubSummary(reportText: string): Promise<void> {\n try {\n const { summary } = await import(\"@actions/core\");\n summary.addRaw(reportText);\n await summary.write();\n } catch {\n // @actions/core not available or not in Actions\n }\n }\n}\n\n// ============================================================================\n// Factory Function (recommended API)\n// ============================================================================\n// Type-safe factory for creating story reporters. Returns a properly-typed\n// instance that implements StoryReporterProtocol, enabling clean usage in\n// vite.config.ts without type casts.\n//\n// Usage:\n// import { createStoryReporter } from 'executable-stories-vitest/reporter';\n// export default defineConfig({\n// test: {\n// reporters: [\n// 'default',\n// createStoryReporter({\n// formats: ['html', 'markdown'],\n// outputDir: 'reports',\n// }),\n// ],\n// },\n// });\n// ============================================================================\n\nexport function createStoryReporter(\n options?: StoryReporterOptions\n): StoryReporterProtocol {\n return new StoryReporter(options);\n}\n\nexport { StoryReporter };\n\n/**\n * Read the feature declarations tests attached to their metadata.\n *\n * `story.feature(...)` runs in the worker, so the declaration travels on\n * `task.meta` like everything else. Every test in a file carries the same one;\n * keying by the module's path collapses them back to one per file.\n */\nfunction collectFeatures(\n testModules: ReadonlyArray<TestModule>,\n root: string\n): RawFeature[] {\n const byFile = new Map<string, RawFeature>();\n\n for (const mod of testModules) {\n const collection = mod.children;\n if (!collection) continue;\n\n const moduleId =\n mod.moduleId ?? (mod as { relativeModuleId?: string }).relativeModuleId ?? \"\";\n const absoluteModuleId = path.isAbsolute(moduleId)\n ? moduleId\n : path.resolve(root, moduleId);\n const sourceFile = toRelativePosix(absoluteModuleId, root);\n\n for (const test of collection.allTests()) {\n const declared = (test.meta() as { storyFeature?: RawFeature } | undefined)?.storyFeature;\n if (declared?.title) {\n byFile.set(sourceFile, { ...declared, sourceFile });\n break;\n }\n }\n }\n\n return [...byFile.values()];\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/reporter.ts"],"sourcesContent":["/**\n * Vitest reporter that reads task.meta.story from tests and writes reports\n * using the executable-stories-formatters package.\n *\n * Do not add value imports from \"vitest\" or \"./story-api.js\" here; this entry is loaded in vitest.config\n * before Vitest is ready. Use only `import type` from those modules.\n *\n * Type strategy: this reporter is duck-typed against vitest's runner contract,\n * not nominally typed via `implements Reporter`. That keeps it compatible with\n * any vitest-compatible runner (including forks like vite-plus) where the\n * `Vitest` class type carries different private fields and would otherwise\n * fail nominal assignability checks at the consumer's call site.\n *\n * Vitest types are imported only for parameter typing of internal helpers,\n * never as `implements`. Anything we touch on `ctx` is captured by the local\n * `VitestContext` structural type below.\n */\nimport type {\n SerializedError,\n TestModule,\n TestCase,\n TestRunEndReason,\n} from \"vitest/node\";\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport type { StoryMeta } from \"./types\";\n\n// Import from formatters package\nimport {\n ReportGenerator,\n canonicalizeRun,\n readGitSha,\n readPackageVersion,\n detectCI,\n sendNotifications,\n toCIInfo,\n loadHistory,\n updateHistory,\n saveHistory,\n type RawFeature,\n type RawRun,\n type RawTestCase,\n type RawAttachment,\n type RawStepEvent,\n type FormatterOptions,\n type CoverageSummary,\n type OtelSpan,\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// Structural runner context\n// ----------------------------------------------------------------------------\n// We only read `ctx.config.root`. Avoid importing the full `Vitest` class as\n// a parameter type — its private fields (`_clearScreenPending`, etc.) make\n// nominal assignability fail across vitest forks. A structural type lets the\n// reporter accept any vitest-compatible runner.\n// ============================================================================\n\nexport type VitestContext = {\n config?: {\n root?: string;\n /**\n * Set by `vitest -t` / `--testNamePattern`. Vitest reports only the tests\n * that match, so a run carrying one cannot speak for the rest of a file.\n */\n testNamePattern?: string | RegExp;\n };\n};\n\n// ============================================================================\n// Reporter Protocol (duck-typed interface for reporters)\n// ============================================================================\n// Exported protocol that captures the reporter contract. This allows\n// StoryReporter to be properly typed without nominal `implements Reporter`,\n// enabling duck-typing compatibility across vitest forks while maintaining\n// type safety at the consumer call site.\n// ============================================================================\n\n// Public-facing protocol uses loose params on runner-supplied callbacks so the\n// returned reporter is structurally assignable to any vitest-compatible runner's\n// `Reporter` type (vitest, vite-plus, future forks). The concrete StoryReporter\n// class still uses strict vitest types internally — method bivariance lets the\n// narrower class satisfy this looser protocol.\nexport interface StoryReporterProtocol {\n onInit(ctx: VitestContext): void;\n onCoverage(coverage: unknown): void;\n onTestRunEnd(\n testModules: readonly unknown[],\n unhandledErrors: readonly unknown[],\n reason: unknown,\n ): Promise<void>;\n}\n\n// ============================================================================\n// Reporter Options (delegates to FormatterOptions)\n// ============================================================================\n\nexport interface StoryReporterOptions extends FormatterOptions {\n /** When GITHUB_ACTIONS, append report to job summary via @actions/core. Default: true */\n enableGithubActionsSummary?: boolean;\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// Coverage Types\n// ============================================================================\n\ntype CoverageMetric = { total: number; covered: number; pct: number };\ntype CoverageData = {\n statements: CoverageMetric;\n branches: CoverageMetric;\n functions: CoverageMetric;\n lines?: CoverageMetric;\n};\ntype CoverageFile = {\n s: Record<string, number>;\n f: Record<string, number>;\n b: Record<string, number[]>;\n l?: Record<string, number>;\n};\n\n// ============================================================================\n// Utility Functions\n// ============================================================================\n\n/**\n * Normalize Vitest onCoverage payload to coverage data.\n */\nfunction normalizeCoveragePayload(\n coverage: unknown\n): Record<string, CoverageFile> | undefined {\n if (!coverage || typeof coverage !== \"object\" || Array.isArray(coverage))\n return undefined;\n const raw = coverage as Record<string, unknown>;\n const data: Record<string, CoverageFile> = {};\n for (const [filePath, file] of Object.entries(raw)) {\n if (\n file &&\n typeof file === \"object\" &&\n \"s\" in file &&\n \"f\" in file &&\n \"b\" in file\n ) {\n data[filePath] = file as CoverageFile;\n }\n }\n if (Object.keys(data).length === 0) return undefined;\n return data;\n}\n\n/**\n * Summarize coverage data.\n */\nfunction summarizeCoverage(\n data: Record<string, CoverageFile>\n): CoverageData | undefined {\n let statementsTotal = 0;\n let statementsCovered = 0;\n let functionsTotal = 0;\n let functionsCovered = 0;\n let branchesTotal = 0;\n let branchesCovered = 0;\n let linesTotal = 0;\n let linesCovered = 0;\n let hasLines = false;\n\n for (const file of Object.values(data)) {\n for (const count of Object.values(file.s)) {\n statementsTotal += 1;\n if (count > 0) statementsCovered += 1;\n }\n for (const count of Object.values(file.f)) {\n functionsTotal += 1;\n if (count > 0) functionsCovered += 1;\n }\n for (const counts of Object.values(file.b)) {\n for (const count of counts) {\n branchesTotal += 1;\n if (count > 0) branchesCovered += 1;\n }\n }\n if (file.l) {\n hasLines = true;\n for (const count of Object.values(file.l)) {\n linesTotal += 1;\n if (count > 0) linesCovered += 1;\n }\n }\n }\n\n if (\n statementsTotal === 0 &&\n functionsTotal === 0 &&\n branchesTotal === 0 &&\n !hasLines\n ) {\n return undefined;\n }\n\n const metric = (covered: number, total: number): CoverageMetric => ({\n total,\n covered,\n pct: total === 0 ? 100 : Math.round((covered / total) * 100),\n });\n\n const summary: CoverageData = {\n statements: metric(statementsCovered, statementsTotal),\n branches: metric(branchesCovered, branchesTotal),\n functions: metric(functionsCovered, functionsTotal),\n };\n if (hasLines) {\n summary.lines = metric(linesCovered, linesTotal);\n }\n return summary;\n}\n\n/**\n * Convert internal coverage data to formatters CoverageSummary.\n */\nfunction toCoverageSummary(\n data: CoverageData | undefined\n): CoverageSummary | undefined {\n if (!data) return undefined;\n return {\n statementsPct: data.statements.pct,\n branchesPct: data.branches.pct,\n functionsPct: data.functions.pct,\n linesPct: data.lines?.pct,\n };\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\n/**\n * Vitest reporter that generates reports using executable-stories-formatters.\n *\n * Reads `task.meta.story` from each test and generates reports in configured formats.\n * Supports output routing (aggregated/colocated) and multiple output formats.\n *\n * Implements StoryReporterProtocol for type-safe duck-typing across vitest forks.\n */\nexport default class StoryReporter implements StoryReporterProtocol {\n private options: StoryReporterOptions;\n private ctx: VitestContext | undefined;\n private startTime: number = 0;\n private packageVersion: string | undefined;\n private gitSha: string | undefined;\n /**\n * Left unknown until onInit sees a config. Claiming full coverage without\n * having looked would let a later merge retire scenarios on a guess.\n */\n private runScope: \"full\" | \"filtered\" | undefined;\n private coverageByFile: Record<string, CoverageFile> = {};\n\n constructor(options: StoryReporterOptions = {}) {\n this.options = options;\n }\n\n onInit(ctx: VitestContext): void {\n this.ctx = ctx;\n this.startTime = Date.now();\n const root = ctx.config?.root ?? process.cwd();\n // `vitest -t` reports the matching tests and nothing else, so consumers\n // must not read this run as the full contents of the files it touches.\n // Vitest hands us the pattern either way, so the answer is never a guess.\n if (ctx.config) {\n this.runScope = ctx.config.testNamePattern != null ? \"filtered\" : \"full\";\n }\n\n // Read metadata if needed\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 onCoverage(coverage: unknown): void {\n const data = normalizeCoveragePayload(coverage);\n if (data) {\n this.coverageByFile = { ...this.coverageByFile, ...data };\n }\n }\n\n async onTestRunEnd(\n testModules: ReadonlyArray<TestModule>,\n _unhandledErrors: ReadonlyArray<SerializedError>,\n reason: TestRunEndReason\n ): Promise<void> {\n if (reason === \"interrupted\") return;\n\n const root = this.ctx?.config?.root ?? process.cwd();\n\n // Collect test cases\n const rawTestCases = this.collectTestCases(testModules, root);\n const coveredSourceFiles = this.collectCoveredSourceFiles(testModules, root);\n const incompleteSourceFiles = this.collectIncompleteSourceFiles(testModules, root);\n const features = collectFeatures(testModules, root);\n\n // Build RawRun\n const rawRun: RawRun = {\n testCases: rawTestCases,\n ...(features.length > 0 ? { features } : {}),\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 try {\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 } catch (err) {\n console.error(\"Failed to write raw run JSON:\", err);\n }\n }\n\n // Canonicalize\n const canonicalRun = canonicalizeRun(rawRun);\n\n // Add coverage if available\n const coverageData = summarizeCoverage(this.coverageByFile);\n if (coverageData) {\n canonicalRun.coverage = toCoverageSummary(coverageData);\n }\n\n // 1. Generate reports\n const generator = new ReportGenerator(this.options);\n try {\n const results = await generator.generate(canonicalRun);\n\n // Append to GitHub Actions summary if enabled\n const enableGithubSummary = this.options.enableGithubActionsSummary ?? true;\n if (process.env.GITHUB_ACTIONS === \"true\" && enableGithubSummary) {\n const markdownPaths = results.get(\"markdown\") ?? [];\n if (markdownPaths.length > 0) {\n const firstPath = markdownPaths[0];\n const content = fs.readFileSync(firstPath, \"utf8\");\n await this.appendGithubSummary(content).catch(() => {});\n }\n }\n } catch (err) {\n console.error(\"Failed to generate reports:\", err);\n }\n\n // 2. Update history (independent of report generation)\n try {\n const histOpts = this.options.history;\n if (histOpts?.filePath) {\n const historyPath = path.isAbsolute(histOpts.filePath)\n ? histOpts.filePath\n : path.join(root, histOpts.filePath);\n const store = loadHistory(\n { filePath: historyPath },\n {\n readFile: (p: string) => { try { return fs.readFileSync(p, \"utf8\"); } catch { return undefined; } },\n logger: console,\n },\n );\n const updated = updateHistory({ store, run: canonicalRun, maxRuns: histOpts.maxRuns ?? 10 });\n const dir = path.dirname(historyPath);\n if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });\n saveHistory(\n { filePath: historyPath, store: updated },\n { writeFile: (p: string, c: string) => fs.writeFileSync(p, c, \"utf8\") },\n );\n }\n } catch (err) {\n console.error(\"Failed to update history:\", err);\n }\n\n // 3. Send notifications (independent of both above)\n try {\n if (this.options.notification) {\n await sendNotifications(\n { run: canonicalRun, notification: this.options.notification },\n { fetch: globalThis.fetch, logger: console, toCIInfo },\n );\n }\n } catch (err) {\n console.error(\"Failed to send notifications:\", err);\n }\n }\n\n /**\n * Collect test cases from Vitest test modules.\n */\n /**\n * Every module vitest ran, whether or not it produced a story.\n *\n * The collected test cases only name modules that produced something, so a\n * file whose last story was deleted would otherwise look like a file that did\n * not run and keep its old scenarios for good.\n */\n private collectCoveredSourceFiles(\n testModules: ReadonlyArray<TestModule>,\n root: string\n ): string[] {\n const files = new Set<string>();\n for (const mod of testModules) {\n const moduleId =\n mod.moduleId ?? (mod as { relativeModuleId?: string }).relativeModuleId ?? \"\";\n if (!moduleId) continue;\n const absolute = path.isAbsolute(moduleId) ? moduleId : path.resolve(root, moduleId);\n files.add(toRelativePosix(absolute, root));\n }\n return [...files].sort();\n }\n\n /**\n * Files whose scenarios could not be collected in full.\n *\n * A module that errored, or a test that failed without ever declaring a\n * story, is missing scenarios because the run broke. Treating that as\n * deletion would throw away documentation at the exact moment the suite is\n * unhealthy. Checked per test, not per file: one healthy story in a module\n * does not vouch for its broken siblings.\n */\n private collectIncompleteSourceFiles(\n testModules: ReadonlyArray<TestModule>,\n root: string\n ): string[] {\n const incomplete = new Set<string>();\n for (const mod of testModules) {\n const moduleId =\n mod.moduleId ?? (mod as { relativeModuleId?: string }).relativeModuleId ?? \"\";\n if (!moduleId) continue;\n const absolute = path.isAbsolute(moduleId) ? moduleId : path.resolve(root, moduleId);\n const sourceFile = toRelativePosix(absolute, root);\n\n const errors = (mod as { errors?: () => unknown[] }).errors?.() ?? [];\n const collection = mod.children;\n if (errors.length > 0 || !collection) {\n const state = (mod as { state?: () => string }).state?.();\n if (errors.length > 0 || state === \"failed\") incomplete.add(sourceFile);\n continue;\n }\n\n for (const test of collection.allTests()) {\n const meta = this.getStoryMeta(test);\n if (meta?.scenario && Array.isArray(meta.steps)) continue;\n if (test.options?.mode === \"todo\") continue;\n if (test.result?.()?.state === \"failed\") {\n incomplete.add(sourceFile);\n break;\n }\n }\n }\n return [...incomplete].sort();\n }\n\n private collectTestCases(\n testModules: ReadonlyArray<TestModule>,\n root: string\n ): RawTestCase[] {\n const testCases: RawTestCase[] = [];\n\n for (const mod of testModules) {\n const collection = mod.children;\n if (!collection) continue;\n\n const moduleId =\n mod.moduleId ??\n (mod as { relativeModuleId?: string }).relativeModuleId ??\n \"\";\n const absoluteModuleId = path.isAbsolute(moduleId)\n ? moduleId\n : path.resolve(root, moduleId);\n const sourceFile = toRelativePosix(absoluteModuleId, root);\n\n // Bodyless `it.todo(...)` never runs `story.init()`, so those tests have\n // no story meta. Collect them separately and emit them as planned\n // scenarios — but only for modules that contain story tests, so plain\n // (non-story) suites full of todos don't leak into the generated docs.\n const plannedTests: TestCase[] = [];\n let moduleHasStoryTests = false;\n\n for (const test of collection.allTests()) {\n const meta = this.getStoryMeta(test);\n if (!meta?.scenario || !Array.isArray(meta.steps)) {\n if (test.options?.mode === \"todo\") plannedTests.push(test);\n continue;\n }\n moduleHasStoryTests = true;\n\n const result = test.result?.();\n const state = result?.state ?? \"pending\";\n const durationMs =\n typeof (result as { duration?: number } | undefined)?.duration ===\n \"number\"\n ? (result as unknown as { duration: number }).duration\n : 0;\n\n // Get error details\n let errorMessage: string | undefined;\n let errorStack: string | undefined;\n if (state === \"failed\" && result) {\n const errors = (result as { errors?: SerializedError[] }).errors;\n if (errors?.length) {\n const err = errors[0];\n errorMessage = err.message;\n errorStack = err.stack;\n }\n }\n\n // Map Vitest state to raw status\n const statusMap: Record<string, string> = {\n passed: \"pass\",\n failed: \"fail\",\n skipped: \"skip\",\n pending: \"pending\",\n todo: \"pending\",\n };\n\n // Extract attachments from task.meta.storyAttachments\n const taskMeta = test.meta() as Record<string, unknown>;\n const scopedAttachments = (taskMeta?.storyAttachments ?? []) as 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 const attachments: RawAttachment[] = scopedAttachments.map((a) => ({\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) from story steps\n const stepEvents: RawStepEvent[] = meta.steps\n .filter((s: { durationMs?: number }) => s.durationMs !== undefined)\n .map((s: { durationMs?: number }, i: number) => ({\n index: i,\n title: (s as { text: string }).text,\n durationMs: s.durationMs,\n }));\n\n // Read autotel OTel spans from task.meta.otelSpans\n const otelSpans = taskMeta?.otelSpans;\n if (Array.isArray(otelSpans) && otelSpans.length > 0) {\n const valid = otelSpans.filter(\n (s: unknown): s is OtelSpan =>\n s != null &&\n typeof s === \"object\" &&\n typeof (s as Record<string, unknown>).spanId === \"string\" &&\n typeof (s as Record<string, unknown>).name === \"string\",\n );\n if (valid.length > 0) {\n meta.otelSpans = valid;\n }\n }\n\n // Retry info from Vitest result\n const retryCount = (result as { retryCount?: number } | undefined)?.retryCount ?? 0;\n const configuredRetries = Math.max(\n retryCount,\n (test as { retries?: number }).retries ??\n (test as { options?: { retry?: number } }).options?.retry ??\n 0,\n );\n\n testCases.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: (statusMap[state] ?? \"unknown\") as RawTestCase[\"status\"],\n durationMs,\n error: errorMessage\n ? { message: errorMessage, stack: errorStack }\n : undefined,\n attachments: attachments.length > 0 ? attachments : undefined,\n stepEvents: stepEvents.length > 0 ? stepEvents : undefined,\n retry: retryCount,\n retries: configuredRetries,\n });\n }\n\n if (moduleHasStoryTests) {\n for (const test of plannedTests) {\n const titlePath = test.fullName.split(\" > \");\n testCases.push({\n title: test.name,\n titlePath,\n story: {\n scenario: test.name,\n steps: [],\n suitePath: titlePath.slice(0, -1),\n },\n sourceFile,\n sourceLine: 1,\n status: \"todo\",\n durationMs: 0,\n });\n }\n }\n }\n\n return testCases;\n }\n\n private getStoryMeta(test: TestCase): StoryMeta | undefined {\n const meta = test.meta() as Record<string, unknown>;\n return meta?.[\"story\"] as StoryMeta | undefined;\n }\n\n private async appendGithubSummary(reportText: string): Promise<void> {\n try {\n const { summary } = await import(\"@actions/core\");\n summary.addRaw(reportText);\n await summary.write();\n } catch {\n // @actions/core not available or not in Actions\n }\n }\n}\n\n// ============================================================================\n// Factory Function (recommended API)\n// ============================================================================\n// Type-safe factory for creating story reporters. Returns a properly-typed\n// instance that implements StoryReporterProtocol, enabling clean usage in\n// vite.config.ts without type casts.\n//\n// Usage:\n// import { createStoryReporter } from 'executable-stories-vitest/reporter';\n// export default defineConfig({\n// test: {\n// reporters: [\n// 'default',\n// createStoryReporter({\n// formats: ['html', 'markdown'],\n// outputDir: 'reports',\n// }),\n// ],\n// },\n// });\n// ============================================================================\n\nexport function createStoryReporter(\n options?: StoryReporterOptions\n): StoryReporterProtocol {\n return new StoryReporter(options);\n}\n\nexport { StoryReporter };\n\n/**\n * Read the feature declarations tests attached to their metadata.\n *\n * `story.feature(...)` runs in the worker, so the declaration travels on\n * `task.meta` like everything else. Every test in a file carries the same one;\n * keying by the module's path collapses them back to one per file.\n */\nfunction collectFeatures(\n testModules: ReadonlyArray<TestModule>,\n root: string\n): RawFeature[] {\n const byFile = new Map<string, RawFeature>();\n\n for (const mod of testModules) {\n const collection = mod.children;\n if (!collection) continue;\n\n const moduleId =\n mod.moduleId ?? (mod as { relativeModuleId?: string }).relativeModuleId ?? \"\";\n const absoluteModuleId = path.isAbsolute(moduleId)\n ? moduleId\n : path.resolve(root, moduleId);\n const sourceFile = toRelativePosix(absoluteModuleId, root);\n\n for (const test of collection.allTests()) {\n const declared = (test.meta() as { storyFeature?: RawFeature } | undefined)?.storyFeature;\n if (declared?.title) {\n byFile.set(sourceFile, { ...declared, sourceFile });\n break;\n }\n }\n }\n\n return [...byFile.values()];\n}\n"],"mappings":"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","names":[]}
|
package/dist/reporter.d.cts
CHANGED
|
@@ -23,6 +23,11 @@ export { ColocatedStyle, FormatterOptions, OutputFormat, OutputMode, OutputRule
|
|
|
23
23
|
type VitestContext = {
|
|
24
24
|
config?: {
|
|
25
25
|
root?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Set by `vitest -t` / `--testNamePattern`. Vitest reports only the tests
|
|
28
|
+
* that match, so a run carrying one cannot speak for the rest of a file.
|
|
29
|
+
*/
|
|
30
|
+
testNamePattern?: string | RegExp;
|
|
26
31
|
};
|
|
27
32
|
};
|
|
28
33
|
interface StoryReporterProtocol {
|
|
@@ -50,6 +55,11 @@ declare class StoryReporter implements StoryReporterProtocol {
|
|
|
50
55
|
private startTime;
|
|
51
56
|
private packageVersion;
|
|
52
57
|
private gitSha;
|
|
58
|
+
/**
|
|
59
|
+
* Left unknown until onInit sees a config. Claiming full coverage without
|
|
60
|
+
* having looked would let a later merge retire scenarios on a guess.
|
|
61
|
+
*/
|
|
62
|
+
private runScope;
|
|
53
63
|
private coverageByFile;
|
|
54
64
|
constructor(options?: StoryReporterOptions);
|
|
55
65
|
onInit(ctx: VitestContext): void;
|
|
@@ -58,6 +68,24 @@ declare class StoryReporter implements StoryReporterProtocol {
|
|
|
58
68
|
/**
|
|
59
69
|
* Collect test cases from Vitest test modules.
|
|
60
70
|
*/
|
|
71
|
+
/**
|
|
72
|
+
* Every module vitest ran, whether or not it produced a story.
|
|
73
|
+
*
|
|
74
|
+
* The collected test cases only name modules that produced something, so a
|
|
75
|
+
* file whose last story was deleted would otherwise look like a file that did
|
|
76
|
+
* not run and keep its old scenarios for good.
|
|
77
|
+
*/
|
|
78
|
+
private collectCoveredSourceFiles;
|
|
79
|
+
/**
|
|
80
|
+
* Files whose scenarios could not be collected in full.
|
|
81
|
+
*
|
|
82
|
+
* A module that errored, or a test that failed without ever declaring a
|
|
83
|
+
* story, is missing scenarios because the run broke. Treating that as
|
|
84
|
+
* deletion would throw away documentation at the exact moment the suite is
|
|
85
|
+
* unhealthy. Checked per test, not per file: one healthy story in a module
|
|
86
|
+
* does not vouch for its broken siblings.
|
|
87
|
+
*/
|
|
88
|
+
private collectIncompleteSourceFiles;
|
|
61
89
|
private collectTestCases;
|
|
62
90
|
private getStoryMeta;
|
|
63
91
|
private appendGithubSummary;
|
package/dist/reporter.d.ts
CHANGED
|
@@ -23,6 +23,11 @@ export { ColocatedStyle, FormatterOptions, OutputFormat, OutputMode, OutputRule
|
|
|
23
23
|
type VitestContext = {
|
|
24
24
|
config?: {
|
|
25
25
|
root?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Set by `vitest -t` / `--testNamePattern`. Vitest reports only the tests
|
|
28
|
+
* that match, so a run carrying one cannot speak for the rest of a file.
|
|
29
|
+
*/
|
|
30
|
+
testNamePattern?: string | RegExp;
|
|
26
31
|
};
|
|
27
32
|
};
|
|
28
33
|
interface StoryReporterProtocol {
|
|
@@ -50,6 +55,11 @@ declare class StoryReporter implements StoryReporterProtocol {
|
|
|
50
55
|
private startTime;
|
|
51
56
|
private packageVersion;
|
|
52
57
|
private gitSha;
|
|
58
|
+
/**
|
|
59
|
+
* Left unknown until onInit sees a config. Claiming full coverage without
|
|
60
|
+
* having looked would let a later merge retire scenarios on a guess.
|
|
61
|
+
*/
|
|
62
|
+
private runScope;
|
|
53
63
|
private coverageByFile;
|
|
54
64
|
constructor(options?: StoryReporterOptions);
|
|
55
65
|
onInit(ctx: VitestContext): void;
|
|
@@ -58,6 +68,24 @@ declare class StoryReporter implements StoryReporterProtocol {
|
|
|
58
68
|
/**
|
|
59
69
|
* Collect test cases from Vitest test modules.
|
|
60
70
|
*/
|
|
71
|
+
/**
|
|
72
|
+
* Every module vitest ran, whether or not it produced a story.
|
|
73
|
+
*
|
|
74
|
+
* The collected test cases only name modules that produced something, so a
|
|
75
|
+
* file whose last story was deleted would otherwise look like a file that did
|
|
76
|
+
* not run and keep its old scenarios for good.
|
|
77
|
+
*/
|
|
78
|
+
private collectCoveredSourceFiles;
|
|
79
|
+
/**
|
|
80
|
+
* Files whose scenarios could not be collected in full.
|
|
81
|
+
*
|
|
82
|
+
* A module that errored, or a test that failed without ever declaring a
|
|
83
|
+
* story, is missing scenarios because the run broke. Treating that as
|
|
84
|
+
* deletion would throw away documentation at the exact moment the suite is
|
|
85
|
+
* unhealthy. Checked per test, not per file: one healthy story in a module
|
|
86
|
+
* does not vouch for its broken siblings.
|
|
87
|
+
*/
|
|
88
|
+
private collectIncompleteSourceFiles;
|
|
61
89
|
private collectTestCases;
|
|
62
90
|
private getStoryMeta;
|
|
63
91
|
private appendGithubSummary;
|
package/dist/reporter.js
CHANGED
|
@@ -95,6 +95,11 @@ var StoryReporter = class {
|
|
|
95
95
|
startTime = 0;
|
|
96
96
|
packageVersion;
|
|
97
97
|
gitSha;
|
|
98
|
+
/**
|
|
99
|
+
* Left unknown until onInit sees a config. Claiming full coverage without
|
|
100
|
+
* having looked would let a later merge retire scenarios on a guess.
|
|
101
|
+
*/
|
|
102
|
+
runScope;
|
|
98
103
|
coverageByFile = {};
|
|
99
104
|
constructor(options = {}) {
|
|
100
105
|
this.options = options;
|
|
@@ -103,6 +108,9 @@ var StoryReporter = class {
|
|
|
103
108
|
this.ctx = ctx;
|
|
104
109
|
this.startTime = Date.now();
|
|
105
110
|
const root = ctx.config?.root ?? process.cwd();
|
|
111
|
+
if (ctx.config) {
|
|
112
|
+
this.runScope = ctx.config.testNamePattern != null ? "filtered" : "full";
|
|
113
|
+
}
|
|
106
114
|
const includeMetadata = this.options.markdown?.includeMetadata ?? true;
|
|
107
115
|
if (includeMetadata) {
|
|
108
116
|
this.packageVersion = readPackageVersion(root);
|
|
@@ -119,6 +127,8 @@ var StoryReporter = class {
|
|
|
119
127
|
if (reason === "interrupted") return;
|
|
120
128
|
const root = this.ctx?.config?.root ?? process.cwd();
|
|
121
129
|
const rawTestCases = this.collectTestCases(testModules, root);
|
|
130
|
+
const coveredSourceFiles = this.collectCoveredSourceFiles(testModules, root);
|
|
131
|
+
const incompleteSourceFiles = this.collectIncompleteSourceFiles(testModules, root);
|
|
122
132
|
const features = collectFeatures(testModules, root);
|
|
123
133
|
const rawRun = {
|
|
124
134
|
testCases: rawTestCases,
|
|
@@ -128,7 +138,10 @@ var StoryReporter = class {
|
|
|
128
138
|
projectRoot: root,
|
|
129
139
|
packageVersion: this.packageVersion,
|
|
130
140
|
gitSha: this.gitSha,
|
|
131
|
-
ci: detectCI()
|
|
141
|
+
ci: detectCI(),
|
|
142
|
+
...this.runScope ? { runScope: this.runScope } : {},
|
|
143
|
+
...coveredSourceFiles.length > 0 ? { coveredSourceFiles } : {},
|
|
144
|
+
...incompleteSourceFiles.length > 0 ? { incompleteSourceFiles } : {}
|
|
132
145
|
};
|
|
133
146
|
const rawRunPath = this.options.rawRunPath;
|
|
134
147
|
if (rawRunPath) {
|
|
@@ -205,6 +218,58 @@ var StoryReporter = class {
|
|
|
205
218
|
/**
|
|
206
219
|
* Collect test cases from Vitest test modules.
|
|
207
220
|
*/
|
|
221
|
+
/**
|
|
222
|
+
* Every module vitest ran, whether or not it produced a story.
|
|
223
|
+
*
|
|
224
|
+
* The collected test cases only name modules that produced something, so a
|
|
225
|
+
* file whose last story was deleted would otherwise look like a file that did
|
|
226
|
+
* not run and keep its old scenarios for good.
|
|
227
|
+
*/
|
|
228
|
+
collectCoveredSourceFiles(testModules, root) {
|
|
229
|
+
const files = /* @__PURE__ */ new Set();
|
|
230
|
+
for (const mod of testModules) {
|
|
231
|
+
const moduleId = mod.moduleId ?? mod.relativeModuleId ?? "";
|
|
232
|
+
if (!moduleId) continue;
|
|
233
|
+
const absolute = path.isAbsolute(moduleId) ? moduleId : path.resolve(root, moduleId);
|
|
234
|
+
files.add(toRelativePosix(absolute, root));
|
|
235
|
+
}
|
|
236
|
+
return [...files].sort();
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Files whose scenarios could not be collected in full.
|
|
240
|
+
*
|
|
241
|
+
* A module that errored, or a test that failed without ever declaring a
|
|
242
|
+
* story, is missing scenarios because the run broke. Treating that as
|
|
243
|
+
* deletion would throw away documentation at the exact moment the suite is
|
|
244
|
+
* unhealthy. Checked per test, not per file: one healthy story in a module
|
|
245
|
+
* does not vouch for its broken siblings.
|
|
246
|
+
*/
|
|
247
|
+
collectIncompleteSourceFiles(testModules, root) {
|
|
248
|
+
const incomplete = /* @__PURE__ */ new Set();
|
|
249
|
+
for (const mod of testModules) {
|
|
250
|
+
const moduleId = mod.moduleId ?? mod.relativeModuleId ?? "";
|
|
251
|
+
if (!moduleId) continue;
|
|
252
|
+
const absolute = path.isAbsolute(moduleId) ? moduleId : path.resolve(root, moduleId);
|
|
253
|
+
const sourceFile = toRelativePosix(absolute, root);
|
|
254
|
+
const errors = mod.errors?.() ?? [];
|
|
255
|
+
const collection = mod.children;
|
|
256
|
+
if (errors.length > 0 || !collection) {
|
|
257
|
+
const state = mod.state?.();
|
|
258
|
+
if (errors.length > 0 || state === "failed") incomplete.add(sourceFile);
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
for (const test of collection.allTests()) {
|
|
262
|
+
const meta = this.getStoryMeta(test);
|
|
263
|
+
if (meta?.scenario && Array.isArray(meta.steps)) continue;
|
|
264
|
+
if (test.options?.mode === "todo") continue;
|
|
265
|
+
if (test.result?.()?.state === "failed") {
|
|
266
|
+
incomplete.add(sourceFile);
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return [...incomplete].sort();
|
|
272
|
+
}
|
|
208
273
|
collectTestCases(testModules, root) {
|
|
209
274
|
const testCases = [];
|
|
210
275
|
for (const mod of testModules) {
|
package/dist/reporter.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/reporter.ts"],"sourcesContent":["/**\n * Vitest reporter that reads task.meta.story from tests and writes reports\n * using the executable-stories-formatters package.\n *\n * Do not add value imports from \"vitest\" or \"./story-api.js\" here; this entry is loaded in vitest.config\n * before Vitest is ready. Use only `import type` from those modules.\n *\n * Type strategy: this reporter is duck-typed against vitest's runner contract,\n * not nominally typed via `implements Reporter`. That keeps it compatible with\n * any vitest-compatible runner (including forks like vite-plus) where the\n * `Vitest` class type carries different private fields and would otherwise\n * fail nominal assignability checks at the consumer's call site.\n *\n * Vitest types are imported only for parameter typing of internal helpers,\n * never as `implements`. Anything we touch on `ctx` is captured by the local\n * `VitestContext` structural type below.\n */\nimport type {\n SerializedError,\n TestModule,\n TestCase,\n TestRunEndReason,\n} from \"vitest/node\";\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport type { StoryMeta } from \"./types\";\n\n// Import from formatters package\nimport {\n ReportGenerator,\n canonicalizeRun,\n readGitSha,\n readPackageVersion,\n detectCI,\n sendNotifications,\n toCIInfo,\n loadHistory,\n updateHistory,\n saveHistory,\n type RawFeature,\n type RawRun,\n type RawTestCase,\n type RawAttachment,\n type RawStepEvent,\n type FormatterOptions,\n type CoverageSummary,\n type OtelSpan,\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// Structural runner context\n// ----------------------------------------------------------------------------\n// We only read `ctx.config.root`. Avoid importing the full `Vitest` class as\n// a parameter type — its private fields (`_clearScreenPending`, etc.) make\n// nominal assignability fail across vitest forks. A structural type lets the\n// reporter accept any vitest-compatible runner.\n// ============================================================================\n\nexport type VitestContext = {\n config?: {\n root?: string;\n };\n};\n\n// ============================================================================\n// Reporter Protocol (duck-typed interface for reporters)\n// ============================================================================\n// Exported protocol that captures the reporter contract. This allows\n// StoryReporter to be properly typed without nominal `implements Reporter`,\n// enabling duck-typing compatibility across vitest forks while maintaining\n// type safety at the consumer call site.\n// ============================================================================\n\n// Public-facing protocol uses loose params on runner-supplied callbacks so the\n// returned reporter is structurally assignable to any vitest-compatible runner's\n// `Reporter` type (vitest, vite-plus, future forks). The concrete StoryReporter\n// class still uses strict vitest types internally — method bivariance lets the\n// narrower class satisfy this looser protocol.\nexport interface StoryReporterProtocol {\n onInit(ctx: VitestContext): void;\n onCoverage(coverage: unknown): void;\n onTestRunEnd(\n testModules: readonly unknown[],\n unhandledErrors: readonly unknown[],\n reason: unknown,\n ): Promise<void>;\n}\n\n// ============================================================================\n// Reporter Options (delegates to FormatterOptions)\n// ============================================================================\n\nexport interface StoryReporterOptions extends FormatterOptions {\n /** When GITHUB_ACTIONS, append report to job summary via @actions/core. Default: true */\n enableGithubActionsSummary?: boolean;\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// Coverage Types\n// ============================================================================\n\ntype CoverageMetric = { total: number; covered: number; pct: number };\ntype CoverageData = {\n statements: CoverageMetric;\n branches: CoverageMetric;\n functions: CoverageMetric;\n lines?: CoverageMetric;\n};\ntype CoverageFile = {\n s: Record<string, number>;\n f: Record<string, number>;\n b: Record<string, number[]>;\n l?: Record<string, number>;\n};\n\n// ============================================================================\n// Utility Functions\n// ============================================================================\n\n/**\n * Normalize Vitest onCoverage payload to coverage data.\n */\nfunction normalizeCoveragePayload(\n coverage: unknown\n): Record<string, CoverageFile> | undefined {\n if (!coverage || typeof coverage !== \"object\" || Array.isArray(coverage))\n return undefined;\n const raw = coverage as Record<string, unknown>;\n const data: Record<string, CoverageFile> = {};\n for (const [filePath, file] of Object.entries(raw)) {\n if (\n file &&\n typeof file === \"object\" &&\n \"s\" in file &&\n \"f\" in file &&\n \"b\" in file\n ) {\n data[filePath] = file as CoverageFile;\n }\n }\n if (Object.keys(data).length === 0) return undefined;\n return data;\n}\n\n/**\n * Summarize coverage data.\n */\nfunction summarizeCoverage(\n data: Record<string, CoverageFile>\n): CoverageData | undefined {\n let statementsTotal = 0;\n let statementsCovered = 0;\n let functionsTotal = 0;\n let functionsCovered = 0;\n let branchesTotal = 0;\n let branchesCovered = 0;\n let linesTotal = 0;\n let linesCovered = 0;\n let hasLines = false;\n\n for (const file of Object.values(data)) {\n for (const count of Object.values(file.s)) {\n statementsTotal += 1;\n if (count > 0) statementsCovered += 1;\n }\n for (const count of Object.values(file.f)) {\n functionsTotal += 1;\n if (count > 0) functionsCovered += 1;\n }\n for (const counts of Object.values(file.b)) {\n for (const count of counts) {\n branchesTotal += 1;\n if (count > 0) branchesCovered += 1;\n }\n }\n if (file.l) {\n hasLines = true;\n for (const count of Object.values(file.l)) {\n linesTotal += 1;\n if (count > 0) linesCovered += 1;\n }\n }\n }\n\n if (\n statementsTotal === 0 &&\n functionsTotal === 0 &&\n branchesTotal === 0 &&\n !hasLines\n ) {\n return undefined;\n }\n\n const metric = (covered: number, total: number): CoverageMetric => ({\n total,\n covered,\n pct: total === 0 ? 100 : Math.round((covered / total) * 100),\n });\n\n const summary: CoverageData = {\n statements: metric(statementsCovered, statementsTotal),\n branches: metric(branchesCovered, branchesTotal),\n functions: metric(functionsCovered, functionsTotal),\n };\n if (hasLines) {\n summary.lines = metric(linesCovered, linesTotal);\n }\n return summary;\n}\n\n/**\n * Convert internal coverage data to formatters CoverageSummary.\n */\nfunction toCoverageSummary(\n data: CoverageData | undefined\n): CoverageSummary | undefined {\n if (!data) return undefined;\n return {\n statementsPct: data.statements.pct,\n branchesPct: data.branches.pct,\n functionsPct: data.functions.pct,\n linesPct: data.lines?.pct,\n };\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\n/**\n * Vitest reporter that generates reports using executable-stories-formatters.\n *\n * Reads `task.meta.story` from each test and generates reports in configured formats.\n * Supports output routing (aggregated/colocated) and multiple output formats.\n *\n * Implements StoryReporterProtocol for type-safe duck-typing across vitest forks.\n */\nexport default class StoryReporter implements StoryReporterProtocol {\n private options: StoryReporterOptions;\n private ctx: VitestContext | undefined;\n private startTime: number = 0;\n private packageVersion: string | undefined;\n private gitSha: string | undefined;\n private coverageByFile: Record<string, CoverageFile> = {};\n\n constructor(options: StoryReporterOptions = {}) {\n this.options = options;\n }\n\n onInit(ctx: VitestContext): void {\n this.ctx = ctx;\n this.startTime = Date.now();\n const root = ctx.config?.root ?? process.cwd();\n\n // Read metadata if needed\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 onCoverage(coverage: unknown): void {\n const data = normalizeCoveragePayload(coverage);\n if (data) {\n this.coverageByFile = { ...this.coverageByFile, ...data };\n }\n }\n\n async onTestRunEnd(\n testModules: ReadonlyArray<TestModule>,\n _unhandledErrors: ReadonlyArray<SerializedError>,\n reason: TestRunEndReason\n ): Promise<void> {\n if (reason === \"interrupted\") return;\n\n const root = this.ctx?.config?.root ?? process.cwd();\n\n // Collect test cases\n const rawTestCases = this.collectTestCases(testModules, root);\n const features = collectFeatures(testModules, root);\n\n // Build RawRun\n const rawRun: RawRun = {\n testCases: rawTestCases,\n ...(features.length > 0 ? { features } : {}),\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 try {\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 } catch (err) {\n console.error(\"Failed to write raw run JSON:\", err);\n }\n }\n\n // Canonicalize\n const canonicalRun = canonicalizeRun(rawRun);\n\n // Add coverage if available\n const coverageData = summarizeCoverage(this.coverageByFile);\n if (coverageData) {\n canonicalRun.coverage = toCoverageSummary(coverageData);\n }\n\n // 1. Generate reports\n const generator = new ReportGenerator(this.options);\n try {\n const results = await generator.generate(canonicalRun);\n\n // Append to GitHub Actions summary if enabled\n const enableGithubSummary = this.options.enableGithubActionsSummary ?? true;\n if (process.env.GITHUB_ACTIONS === \"true\" && enableGithubSummary) {\n const markdownPaths = results.get(\"markdown\") ?? [];\n if (markdownPaths.length > 0) {\n const firstPath = markdownPaths[0];\n const content = fs.readFileSync(firstPath, \"utf8\");\n await this.appendGithubSummary(content).catch(() => {});\n }\n }\n } catch (err) {\n console.error(\"Failed to generate reports:\", err);\n }\n\n // 2. Update history (independent of report generation)\n try {\n const histOpts = this.options.history;\n if (histOpts?.filePath) {\n const historyPath = path.isAbsolute(histOpts.filePath)\n ? histOpts.filePath\n : path.join(root, histOpts.filePath);\n const store = loadHistory(\n { filePath: historyPath },\n {\n readFile: (p: string) => { try { return fs.readFileSync(p, \"utf8\"); } catch { return undefined; } },\n logger: console,\n },\n );\n const updated = updateHistory({ store, run: canonicalRun, maxRuns: histOpts.maxRuns ?? 10 });\n const dir = path.dirname(historyPath);\n if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });\n saveHistory(\n { filePath: historyPath, store: updated },\n { writeFile: (p: string, c: string) => fs.writeFileSync(p, c, \"utf8\") },\n );\n }\n } catch (err) {\n console.error(\"Failed to update history:\", err);\n }\n\n // 3. Send notifications (independent of both above)\n try {\n if (this.options.notification) {\n await sendNotifications(\n { run: canonicalRun, notification: this.options.notification },\n { fetch: globalThis.fetch, logger: console, toCIInfo },\n );\n }\n } catch (err) {\n console.error(\"Failed to send notifications:\", err);\n }\n }\n\n /**\n * Collect test cases from Vitest test modules.\n */\n private collectTestCases(\n testModules: ReadonlyArray<TestModule>,\n root: string\n ): RawTestCase[] {\n const testCases: RawTestCase[] = [];\n\n for (const mod of testModules) {\n const collection = mod.children;\n if (!collection) continue;\n\n const moduleId =\n mod.moduleId ??\n (mod as { relativeModuleId?: string }).relativeModuleId ??\n \"\";\n const absoluteModuleId = path.isAbsolute(moduleId)\n ? moduleId\n : path.resolve(root, moduleId);\n const sourceFile = toRelativePosix(absoluteModuleId, root);\n\n // Bodyless `it.todo(...)` never runs `story.init()`, so those tests have\n // no story meta. Collect them separately and emit them as planned\n // scenarios — but only for modules that contain story tests, so plain\n // (non-story) suites full of todos don't leak into the generated docs.\n const plannedTests: TestCase[] = [];\n let moduleHasStoryTests = false;\n\n for (const test of collection.allTests()) {\n const meta = this.getStoryMeta(test);\n if (!meta?.scenario || !Array.isArray(meta.steps)) {\n if (test.options?.mode === \"todo\") plannedTests.push(test);\n continue;\n }\n moduleHasStoryTests = true;\n\n const result = test.result?.();\n const state = result?.state ?? \"pending\";\n const durationMs =\n typeof (result as { duration?: number } | undefined)?.duration ===\n \"number\"\n ? (result as unknown as { duration: number }).duration\n : 0;\n\n // Get error details\n let errorMessage: string | undefined;\n let errorStack: string | undefined;\n if (state === \"failed\" && result) {\n const errors = (result as { errors?: SerializedError[] }).errors;\n if (errors?.length) {\n const err = errors[0];\n errorMessage = err.message;\n errorStack = err.stack;\n }\n }\n\n // Map Vitest state to raw status\n const statusMap: Record<string, string> = {\n passed: \"pass\",\n failed: \"fail\",\n skipped: \"skip\",\n pending: \"pending\",\n todo: \"pending\",\n };\n\n // Extract attachments from task.meta.storyAttachments\n const taskMeta = test.meta() as Record<string, unknown>;\n const scopedAttachments = (taskMeta?.storyAttachments ?? []) as 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 const attachments: RawAttachment[] = scopedAttachments.map((a) => ({\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) from story steps\n const stepEvents: RawStepEvent[] = meta.steps\n .filter((s: { durationMs?: number }) => s.durationMs !== undefined)\n .map((s: { durationMs?: number }, i: number) => ({\n index: i,\n title: (s as { text: string }).text,\n durationMs: s.durationMs,\n }));\n\n // Read autotel OTel spans from task.meta.otelSpans\n const otelSpans = taskMeta?.otelSpans;\n if (Array.isArray(otelSpans) && otelSpans.length > 0) {\n const valid = otelSpans.filter(\n (s: unknown): s is OtelSpan =>\n s != null &&\n typeof s === \"object\" &&\n typeof (s as Record<string, unknown>).spanId === \"string\" &&\n typeof (s as Record<string, unknown>).name === \"string\",\n );\n if (valid.length > 0) {\n meta.otelSpans = valid;\n }\n }\n\n // Retry info from Vitest result\n const retryCount = (result as { retryCount?: number } | undefined)?.retryCount ?? 0;\n const configuredRetries = Math.max(\n retryCount,\n (test as { retries?: number }).retries ??\n (test as { options?: { retry?: number } }).options?.retry ??\n 0,\n );\n\n testCases.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: (statusMap[state] ?? \"unknown\") as RawTestCase[\"status\"],\n durationMs,\n error: errorMessage\n ? { message: errorMessage, stack: errorStack }\n : undefined,\n attachments: attachments.length > 0 ? attachments : undefined,\n stepEvents: stepEvents.length > 0 ? stepEvents : undefined,\n retry: retryCount,\n retries: configuredRetries,\n });\n }\n\n if (moduleHasStoryTests) {\n for (const test of plannedTests) {\n const titlePath = test.fullName.split(\" > \");\n testCases.push({\n title: test.name,\n titlePath,\n story: {\n scenario: test.name,\n steps: [],\n suitePath: titlePath.slice(0, -1),\n },\n sourceFile,\n sourceLine: 1,\n status: \"todo\",\n durationMs: 0,\n });\n }\n }\n }\n\n return testCases;\n }\n\n private getStoryMeta(test: TestCase): StoryMeta | undefined {\n const meta = test.meta() as Record<string, unknown>;\n return meta?.[\"story\"] as StoryMeta | undefined;\n }\n\n private async appendGithubSummary(reportText: string): Promise<void> {\n try {\n const { summary } = await import(\"@actions/core\");\n summary.addRaw(reportText);\n await summary.write();\n } catch {\n // @actions/core not available or not in Actions\n }\n }\n}\n\n// ============================================================================\n// Factory Function (recommended API)\n// ============================================================================\n// Type-safe factory for creating story reporters. Returns a properly-typed\n// instance that implements StoryReporterProtocol, enabling clean usage in\n// vite.config.ts without type casts.\n//\n// Usage:\n// import { createStoryReporter } from 'executable-stories-vitest/reporter';\n// export default defineConfig({\n// test: {\n// reporters: [\n// 'default',\n// createStoryReporter({\n// formats: ['html', 'markdown'],\n// outputDir: 'reports',\n// }),\n// ],\n// },\n// });\n// ============================================================================\n\nexport function createStoryReporter(\n options?: StoryReporterOptions\n): StoryReporterProtocol {\n return new StoryReporter(options);\n}\n\nexport { StoryReporter };\n\n/**\n * Read the feature declarations tests attached to their metadata.\n *\n * `story.feature(...)` runs in the worker, so the declaration travels on\n * `task.meta` like everything else. Every test in a file carries the same one;\n * keying by the module's path collapses them back to one per file.\n */\nfunction collectFeatures(\n testModules: ReadonlyArray<TestModule>,\n root: string\n): RawFeature[] {\n const byFile = new Map<string, RawFeature>();\n\n for (const mod of testModules) {\n const collection = mod.children;\n if (!collection) continue;\n\n const moduleId =\n mod.moduleId ?? (mod as { relativeModuleId?: string }).relativeModuleId ?? \"\";\n const absoluteModuleId = path.isAbsolute(moduleId)\n ? moduleId\n : path.resolve(root, moduleId);\n const sourceFile = toRelativePosix(absoluteModuleId, root);\n\n for (const test of collection.allTests()) {\n const declared = (test.meta() as { storyFeature?: RawFeature } | undefined)?.storyFeature;\n if (declared?.title) {\n byFile.set(sourceFile, { ...declared, sourceFile });\n break;\n }\n }\n }\n\n return [...byFile.values()];\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/reporter.ts"],"sourcesContent":["/**\n * Vitest reporter that reads task.meta.story from tests and writes reports\n * using the executable-stories-formatters package.\n *\n * Do not add value imports from \"vitest\" or \"./story-api.js\" here; this entry is loaded in vitest.config\n * before Vitest is ready. Use only `import type` from those modules.\n *\n * Type strategy: this reporter is duck-typed against vitest's runner contract,\n * not nominally typed via `implements Reporter`. That keeps it compatible with\n * any vitest-compatible runner (including forks like vite-plus) where the\n * `Vitest` class type carries different private fields and would otherwise\n * fail nominal assignability checks at the consumer's call site.\n *\n * Vitest types are imported only for parameter typing of internal helpers,\n * never as `implements`. Anything we touch on `ctx` is captured by the local\n * `VitestContext` structural type below.\n */\nimport type {\n SerializedError,\n TestModule,\n TestCase,\n TestRunEndReason,\n} from \"vitest/node\";\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport type { StoryMeta } from \"./types\";\n\n// Import from formatters package\nimport {\n ReportGenerator,\n canonicalizeRun,\n readGitSha,\n readPackageVersion,\n detectCI,\n sendNotifications,\n toCIInfo,\n loadHistory,\n updateHistory,\n saveHistory,\n type RawFeature,\n type RawRun,\n type RawTestCase,\n type RawAttachment,\n type RawStepEvent,\n type FormatterOptions,\n type CoverageSummary,\n type OtelSpan,\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// Structural runner context\n// ----------------------------------------------------------------------------\n// We only read `ctx.config.root`. Avoid importing the full `Vitest` class as\n// a parameter type — its private fields (`_clearScreenPending`, etc.) make\n// nominal assignability fail across vitest forks. A structural type lets the\n// reporter accept any vitest-compatible runner.\n// ============================================================================\n\nexport type VitestContext = {\n config?: {\n root?: string;\n /**\n * Set by `vitest -t` / `--testNamePattern`. Vitest reports only the tests\n * that match, so a run carrying one cannot speak for the rest of a file.\n */\n testNamePattern?: string | RegExp;\n };\n};\n\n// ============================================================================\n// Reporter Protocol (duck-typed interface for reporters)\n// ============================================================================\n// Exported protocol that captures the reporter contract. This allows\n// StoryReporter to be properly typed without nominal `implements Reporter`,\n// enabling duck-typing compatibility across vitest forks while maintaining\n// type safety at the consumer call site.\n// ============================================================================\n\n// Public-facing protocol uses loose params on runner-supplied callbacks so the\n// returned reporter is structurally assignable to any vitest-compatible runner's\n// `Reporter` type (vitest, vite-plus, future forks). The concrete StoryReporter\n// class still uses strict vitest types internally — method bivariance lets the\n// narrower class satisfy this looser protocol.\nexport interface StoryReporterProtocol {\n onInit(ctx: VitestContext): void;\n onCoverage(coverage: unknown): void;\n onTestRunEnd(\n testModules: readonly unknown[],\n unhandledErrors: readonly unknown[],\n reason: unknown,\n ): Promise<void>;\n}\n\n// ============================================================================\n// Reporter Options (delegates to FormatterOptions)\n// ============================================================================\n\nexport interface StoryReporterOptions extends FormatterOptions {\n /** When GITHUB_ACTIONS, append report to job summary via @actions/core. Default: true */\n enableGithubActionsSummary?: boolean;\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// Coverage Types\n// ============================================================================\n\ntype CoverageMetric = { total: number; covered: number; pct: number };\ntype CoverageData = {\n statements: CoverageMetric;\n branches: CoverageMetric;\n functions: CoverageMetric;\n lines?: CoverageMetric;\n};\ntype CoverageFile = {\n s: Record<string, number>;\n f: Record<string, number>;\n b: Record<string, number[]>;\n l?: Record<string, number>;\n};\n\n// ============================================================================\n// Utility Functions\n// ============================================================================\n\n/**\n * Normalize Vitest onCoverage payload to coverage data.\n */\nfunction normalizeCoveragePayload(\n coverage: unknown\n): Record<string, CoverageFile> | undefined {\n if (!coverage || typeof coverage !== \"object\" || Array.isArray(coverage))\n return undefined;\n const raw = coverage as Record<string, unknown>;\n const data: Record<string, CoverageFile> = {};\n for (const [filePath, file] of Object.entries(raw)) {\n if (\n file &&\n typeof file === \"object\" &&\n \"s\" in file &&\n \"f\" in file &&\n \"b\" in file\n ) {\n data[filePath] = file as CoverageFile;\n }\n }\n if (Object.keys(data).length === 0) return undefined;\n return data;\n}\n\n/**\n * Summarize coverage data.\n */\nfunction summarizeCoverage(\n data: Record<string, CoverageFile>\n): CoverageData | undefined {\n let statementsTotal = 0;\n let statementsCovered = 0;\n let functionsTotal = 0;\n let functionsCovered = 0;\n let branchesTotal = 0;\n let branchesCovered = 0;\n let linesTotal = 0;\n let linesCovered = 0;\n let hasLines = false;\n\n for (const file of Object.values(data)) {\n for (const count of Object.values(file.s)) {\n statementsTotal += 1;\n if (count > 0) statementsCovered += 1;\n }\n for (const count of Object.values(file.f)) {\n functionsTotal += 1;\n if (count > 0) functionsCovered += 1;\n }\n for (const counts of Object.values(file.b)) {\n for (const count of counts) {\n branchesTotal += 1;\n if (count > 0) branchesCovered += 1;\n }\n }\n if (file.l) {\n hasLines = true;\n for (const count of Object.values(file.l)) {\n linesTotal += 1;\n if (count > 0) linesCovered += 1;\n }\n }\n }\n\n if (\n statementsTotal === 0 &&\n functionsTotal === 0 &&\n branchesTotal === 0 &&\n !hasLines\n ) {\n return undefined;\n }\n\n const metric = (covered: number, total: number): CoverageMetric => ({\n total,\n covered,\n pct: total === 0 ? 100 : Math.round((covered / total) * 100),\n });\n\n const summary: CoverageData = {\n statements: metric(statementsCovered, statementsTotal),\n branches: metric(branchesCovered, branchesTotal),\n functions: metric(functionsCovered, functionsTotal),\n };\n if (hasLines) {\n summary.lines = metric(linesCovered, linesTotal);\n }\n return summary;\n}\n\n/**\n * Convert internal coverage data to formatters CoverageSummary.\n */\nfunction toCoverageSummary(\n data: CoverageData | undefined\n): CoverageSummary | undefined {\n if (!data) return undefined;\n return {\n statementsPct: data.statements.pct,\n branchesPct: data.branches.pct,\n functionsPct: data.functions.pct,\n linesPct: data.lines?.pct,\n };\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\n/**\n * Vitest reporter that generates reports using executable-stories-formatters.\n *\n * Reads `task.meta.story` from each test and generates reports in configured formats.\n * Supports output routing (aggregated/colocated) and multiple output formats.\n *\n * Implements StoryReporterProtocol for type-safe duck-typing across vitest forks.\n */\nexport default class StoryReporter implements StoryReporterProtocol {\n private options: StoryReporterOptions;\n private ctx: VitestContext | undefined;\n private startTime: number = 0;\n private packageVersion: string | undefined;\n private gitSha: string | undefined;\n /**\n * Left unknown until onInit sees a config. Claiming full coverage without\n * having looked would let a later merge retire scenarios on a guess.\n */\n private runScope: \"full\" | \"filtered\" | undefined;\n private coverageByFile: Record<string, CoverageFile> = {};\n\n constructor(options: StoryReporterOptions = {}) {\n this.options = options;\n }\n\n onInit(ctx: VitestContext): void {\n this.ctx = ctx;\n this.startTime = Date.now();\n const root = ctx.config?.root ?? process.cwd();\n // `vitest -t` reports the matching tests and nothing else, so consumers\n // must not read this run as the full contents of the files it touches.\n // Vitest hands us the pattern either way, so the answer is never a guess.\n if (ctx.config) {\n this.runScope = ctx.config.testNamePattern != null ? \"filtered\" : \"full\";\n }\n\n // Read metadata if needed\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 onCoverage(coverage: unknown): void {\n const data = normalizeCoveragePayload(coverage);\n if (data) {\n this.coverageByFile = { ...this.coverageByFile, ...data };\n }\n }\n\n async onTestRunEnd(\n testModules: ReadonlyArray<TestModule>,\n _unhandledErrors: ReadonlyArray<SerializedError>,\n reason: TestRunEndReason\n ): Promise<void> {\n if (reason === \"interrupted\") return;\n\n const root = this.ctx?.config?.root ?? process.cwd();\n\n // Collect test cases\n const rawTestCases = this.collectTestCases(testModules, root);\n const coveredSourceFiles = this.collectCoveredSourceFiles(testModules, root);\n const incompleteSourceFiles = this.collectIncompleteSourceFiles(testModules, root);\n const features = collectFeatures(testModules, root);\n\n // Build RawRun\n const rawRun: RawRun = {\n testCases: rawTestCases,\n ...(features.length > 0 ? { features } : {}),\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 try {\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 } catch (err) {\n console.error(\"Failed to write raw run JSON:\", err);\n }\n }\n\n // Canonicalize\n const canonicalRun = canonicalizeRun(rawRun);\n\n // Add coverage if available\n const coverageData = summarizeCoverage(this.coverageByFile);\n if (coverageData) {\n canonicalRun.coverage = toCoverageSummary(coverageData);\n }\n\n // 1. Generate reports\n const generator = new ReportGenerator(this.options);\n try {\n const results = await generator.generate(canonicalRun);\n\n // Append to GitHub Actions summary if enabled\n const enableGithubSummary = this.options.enableGithubActionsSummary ?? true;\n if (process.env.GITHUB_ACTIONS === \"true\" && enableGithubSummary) {\n const markdownPaths = results.get(\"markdown\") ?? [];\n if (markdownPaths.length > 0) {\n const firstPath = markdownPaths[0];\n const content = fs.readFileSync(firstPath, \"utf8\");\n await this.appendGithubSummary(content).catch(() => {});\n }\n }\n } catch (err) {\n console.error(\"Failed to generate reports:\", err);\n }\n\n // 2. Update history (independent of report generation)\n try {\n const histOpts = this.options.history;\n if (histOpts?.filePath) {\n const historyPath = path.isAbsolute(histOpts.filePath)\n ? histOpts.filePath\n : path.join(root, histOpts.filePath);\n const store = loadHistory(\n { filePath: historyPath },\n {\n readFile: (p: string) => { try { return fs.readFileSync(p, \"utf8\"); } catch { return undefined; } },\n logger: console,\n },\n );\n const updated = updateHistory({ store, run: canonicalRun, maxRuns: histOpts.maxRuns ?? 10 });\n const dir = path.dirname(historyPath);\n if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });\n saveHistory(\n { filePath: historyPath, store: updated },\n { writeFile: (p: string, c: string) => fs.writeFileSync(p, c, \"utf8\") },\n );\n }\n } catch (err) {\n console.error(\"Failed to update history:\", err);\n }\n\n // 3. Send notifications (independent of both above)\n try {\n if (this.options.notification) {\n await sendNotifications(\n { run: canonicalRun, notification: this.options.notification },\n { fetch: globalThis.fetch, logger: console, toCIInfo },\n );\n }\n } catch (err) {\n console.error(\"Failed to send notifications:\", err);\n }\n }\n\n /**\n * Collect test cases from Vitest test modules.\n */\n /**\n * Every module vitest ran, whether or not it produced a story.\n *\n * The collected test cases only name modules that produced something, so a\n * file whose last story was deleted would otherwise look like a file that did\n * not run and keep its old scenarios for good.\n */\n private collectCoveredSourceFiles(\n testModules: ReadonlyArray<TestModule>,\n root: string\n ): string[] {\n const files = new Set<string>();\n for (const mod of testModules) {\n const moduleId =\n mod.moduleId ?? (mod as { relativeModuleId?: string }).relativeModuleId ?? \"\";\n if (!moduleId) continue;\n const absolute = path.isAbsolute(moduleId) ? moduleId : path.resolve(root, moduleId);\n files.add(toRelativePosix(absolute, root));\n }\n return [...files].sort();\n }\n\n /**\n * Files whose scenarios could not be collected in full.\n *\n * A module that errored, or a test that failed without ever declaring a\n * story, is missing scenarios because the run broke. Treating that as\n * deletion would throw away documentation at the exact moment the suite is\n * unhealthy. Checked per test, not per file: one healthy story in a module\n * does not vouch for its broken siblings.\n */\n private collectIncompleteSourceFiles(\n testModules: ReadonlyArray<TestModule>,\n root: string\n ): string[] {\n const incomplete = new Set<string>();\n for (const mod of testModules) {\n const moduleId =\n mod.moduleId ?? (mod as { relativeModuleId?: string }).relativeModuleId ?? \"\";\n if (!moduleId) continue;\n const absolute = path.isAbsolute(moduleId) ? moduleId : path.resolve(root, moduleId);\n const sourceFile = toRelativePosix(absolute, root);\n\n const errors = (mod as { errors?: () => unknown[] }).errors?.() ?? [];\n const collection = mod.children;\n if (errors.length > 0 || !collection) {\n const state = (mod as { state?: () => string }).state?.();\n if (errors.length > 0 || state === \"failed\") incomplete.add(sourceFile);\n continue;\n }\n\n for (const test of collection.allTests()) {\n const meta = this.getStoryMeta(test);\n if (meta?.scenario && Array.isArray(meta.steps)) continue;\n if (test.options?.mode === \"todo\") continue;\n if (test.result?.()?.state === \"failed\") {\n incomplete.add(sourceFile);\n break;\n }\n }\n }\n return [...incomplete].sort();\n }\n\n private collectTestCases(\n testModules: ReadonlyArray<TestModule>,\n root: string\n ): RawTestCase[] {\n const testCases: RawTestCase[] = [];\n\n for (const mod of testModules) {\n const collection = mod.children;\n if (!collection) continue;\n\n const moduleId =\n mod.moduleId ??\n (mod as { relativeModuleId?: string }).relativeModuleId ??\n \"\";\n const absoluteModuleId = path.isAbsolute(moduleId)\n ? moduleId\n : path.resolve(root, moduleId);\n const sourceFile = toRelativePosix(absoluteModuleId, root);\n\n // Bodyless `it.todo(...)` never runs `story.init()`, so those tests have\n // no story meta. Collect them separately and emit them as planned\n // scenarios — but only for modules that contain story tests, so plain\n // (non-story) suites full of todos don't leak into the generated docs.\n const plannedTests: TestCase[] = [];\n let moduleHasStoryTests = false;\n\n for (const test of collection.allTests()) {\n const meta = this.getStoryMeta(test);\n if (!meta?.scenario || !Array.isArray(meta.steps)) {\n if (test.options?.mode === \"todo\") plannedTests.push(test);\n continue;\n }\n moduleHasStoryTests = true;\n\n const result = test.result?.();\n const state = result?.state ?? \"pending\";\n const durationMs =\n typeof (result as { duration?: number } | undefined)?.duration ===\n \"number\"\n ? (result as unknown as { duration: number }).duration\n : 0;\n\n // Get error details\n let errorMessage: string | undefined;\n let errorStack: string | undefined;\n if (state === \"failed\" && result) {\n const errors = (result as { errors?: SerializedError[] }).errors;\n if (errors?.length) {\n const err = errors[0];\n errorMessage = err.message;\n errorStack = err.stack;\n }\n }\n\n // Map Vitest state to raw status\n const statusMap: Record<string, string> = {\n passed: \"pass\",\n failed: \"fail\",\n skipped: \"skip\",\n pending: \"pending\",\n todo: \"pending\",\n };\n\n // Extract attachments from task.meta.storyAttachments\n const taskMeta = test.meta() as Record<string, unknown>;\n const scopedAttachments = (taskMeta?.storyAttachments ?? []) as 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 const attachments: RawAttachment[] = scopedAttachments.map((a) => ({\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) from story steps\n const stepEvents: RawStepEvent[] = meta.steps\n .filter((s: { durationMs?: number }) => s.durationMs !== undefined)\n .map((s: { durationMs?: number }, i: number) => ({\n index: i,\n title: (s as { text: string }).text,\n durationMs: s.durationMs,\n }));\n\n // Read autotel OTel spans from task.meta.otelSpans\n const otelSpans = taskMeta?.otelSpans;\n if (Array.isArray(otelSpans) && otelSpans.length > 0) {\n const valid = otelSpans.filter(\n (s: unknown): s is OtelSpan =>\n s != null &&\n typeof s === \"object\" &&\n typeof (s as Record<string, unknown>).spanId === \"string\" &&\n typeof (s as Record<string, unknown>).name === \"string\",\n );\n if (valid.length > 0) {\n meta.otelSpans = valid;\n }\n }\n\n // Retry info from Vitest result\n const retryCount = (result as { retryCount?: number } | undefined)?.retryCount ?? 0;\n const configuredRetries = Math.max(\n retryCount,\n (test as { retries?: number }).retries ??\n (test as { options?: { retry?: number } }).options?.retry ??\n 0,\n );\n\n testCases.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: (statusMap[state] ?? \"unknown\") as RawTestCase[\"status\"],\n durationMs,\n error: errorMessage\n ? { message: errorMessage, stack: errorStack }\n : undefined,\n attachments: attachments.length > 0 ? attachments : undefined,\n stepEvents: stepEvents.length > 0 ? stepEvents : undefined,\n retry: retryCount,\n retries: configuredRetries,\n });\n }\n\n if (moduleHasStoryTests) {\n for (const test of plannedTests) {\n const titlePath = test.fullName.split(\" > \");\n testCases.push({\n title: test.name,\n titlePath,\n story: {\n scenario: test.name,\n steps: [],\n suitePath: titlePath.slice(0, -1),\n },\n sourceFile,\n sourceLine: 1,\n status: \"todo\",\n durationMs: 0,\n });\n }\n }\n }\n\n return testCases;\n }\n\n private getStoryMeta(test: TestCase): StoryMeta | undefined {\n const meta = test.meta() as Record<string, unknown>;\n return meta?.[\"story\"] as StoryMeta | undefined;\n }\n\n private async appendGithubSummary(reportText: string): Promise<void> {\n try {\n const { summary } = await import(\"@actions/core\");\n summary.addRaw(reportText);\n await summary.write();\n } catch {\n // @actions/core not available or not in Actions\n }\n }\n}\n\n// ============================================================================\n// Factory Function (recommended API)\n// ============================================================================\n// Type-safe factory for creating story reporters. Returns a properly-typed\n// instance that implements StoryReporterProtocol, enabling clean usage in\n// vite.config.ts without type casts.\n//\n// Usage:\n// import { createStoryReporter } from 'executable-stories-vitest/reporter';\n// export default defineConfig({\n// test: {\n// reporters: [\n// 'default',\n// createStoryReporter({\n// formats: ['html', 'markdown'],\n// outputDir: 'reports',\n// }),\n// ],\n// },\n// });\n// ============================================================================\n\nexport function createStoryReporter(\n options?: StoryReporterOptions\n): StoryReporterProtocol {\n return new StoryReporter(options);\n}\n\nexport { StoryReporter };\n\n/**\n * Read the feature declarations tests attached to their metadata.\n *\n * `story.feature(...)` runs in the worker, so the declaration travels on\n * `task.meta` like everything else. Every test in a file carries the same one;\n * keying by the module's path collapses them back to one per file.\n */\nfunction collectFeatures(\n testModules: ReadonlyArray<TestModule>,\n root: string\n): RawFeature[] {\n const byFile = new Map<string, RawFeature>();\n\n for (const mod of testModules) {\n const collection = mod.children;\n if (!collection) continue;\n\n const moduleId =\n mod.moduleId ?? (mod as { relativeModuleId?: string }).relativeModuleId ?? \"\";\n const absoluteModuleId = path.isAbsolute(moduleId)\n ? moduleId\n : path.resolve(root, moduleId);\n const sourceFile = toRelativePosix(absoluteModuleId, root);\n\n for (const test of collection.allTests()) {\n const declared = (test.meta() as { storyFeature?: RawFeature } | undefined)?.storyFeature;\n if (declared?.title) {\n byFile.set(sourceFile, { ...declared, sourceFile });\n break;\n }\n }\n }\n\n return [...byFile.values()];\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "executable-stories-vitest",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.8.0",
|
|
4
4
|
"description": "TS-first story/given/when/then helpers for Vitest with Markdown user-story doc generation.",
|
|
5
5
|
"author": "Jag Reehal <jag@jagreehal.com>",
|
|
6
6
|
"homepage": "https://github.com/jagreehal/executable-stories#readme",
|
|
@@ -41,8 +41,8 @@
|
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"fast-glob": "^3.3.3",
|
|
43
43
|
"picomatch": "^4.0.4",
|
|
44
|
-
"executable-stories-core": "0.
|
|
45
|
-
"executable-stories-formatters": "1.
|
|
44
|
+
"executable-stories-core": "0.21.0",
|
|
45
|
+
"executable-stories-formatters": "1.13.0"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@opentelemetry/api": "^1.9.1",
|