executable-stories-vitest 8.1.15 → 8.1.17

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.
@@ -1,4 +1,4 @@
1
- import { Reporter, Vitest, TestModule, SerializedError, TestRunEndReason } from 'vitest/node';
1
+ import { TestModule, SerializedError, TestRunEndReason } from 'vitest/node';
2
2
  import { FormatterOptions } from 'executable-stories-formatters';
3
3
  export { ColocatedStyle, FormatterOptions, OutputFormat, OutputMode, OutputRule } from 'executable-stories-formatters';
4
4
 
@@ -8,8 +8,28 @@ export { ColocatedStyle, FormatterOptions, OutputFormat, OutputMode, OutputRule
8
8
  *
9
9
  * Do not add value imports from "vitest" or "./story-api.js" here; this entry is loaded in vitest.config
10
10
  * before Vitest is ready. Use only `import type` from those modules.
11
+ *
12
+ * Type strategy: this reporter is duck-typed against vitest's runner contract,
13
+ * not nominally typed via `implements Reporter`. That keeps it compatible with
14
+ * any vitest-compatible runner (including forks like vite-plus) where the
15
+ * `Vitest` class type carries different private fields and would otherwise
16
+ * fail nominal assignability checks at the consumer's call site.
17
+ *
18
+ * Vitest types are imported only for parameter typing of internal helpers,
19
+ * never as `implements`. Anything we touch on `ctx` is captured by the local
20
+ * `VitestContext` structural type below.
11
21
  */
12
22
 
23
+ type VitestContext = {
24
+ config?: {
25
+ root?: string;
26
+ };
27
+ };
28
+ interface StoryReporterProtocol {
29
+ onInit(ctx: VitestContext): void;
30
+ onCoverage(coverage: unknown): void;
31
+ onTestRunEnd(testModules: readonly unknown[], unhandledErrors: readonly unknown[], reason: unknown): Promise<void>;
32
+ }
13
33
  interface StoryReporterOptions extends FormatterOptions {
14
34
  /** When GITHUB_ACTIONS, append report to job summary via @actions/core. Default: true */
15
35
  enableGithubActionsSummary?: boolean;
@@ -21,16 +41,18 @@ interface StoryReporterOptions extends FormatterOptions {
21
41
  *
22
42
  * Reads `task.meta.story` from each test and generates reports in configured formats.
23
43
  * Supports output routing (aggregated/colocated) and multiple output formats.
44
+ *
45
+ * Implements StoryReporterProtocol for type-safe duck-typing across vitest forks.
24
46
  */
25
- declare class StoryReporter implements Reporter {
47
+ declare class StoryReporter implements StoryReporterProtocol {
26
48
  private options;
27
49
  private ctx;
28
50
  private startTime;
29
51
  private packageVersion;
30
52
  private gitSha;
31
- private coverageData;
53
+ private coverageByFile;
32
54
  constructor(options?: StoryReporterOptions);
33
- onInit(ctx: Vitest): void;
55
+ onInit(ctx: VitestContext): void;
34
56
  onCoverage(coverage: unknown): void;
35
57
  onTestRunEnd(testModules: ReadonlyArray<TestModule>, _unhandledErrors: ReadonlyArray<SerializedError>, reason: TestRunEndReason): Promise<void>;
36
58
  /**
@@ -40,5 +62,6 @@ declare class StoryReporter implements Reporter {
40
62
  private getStoryMeta;
41
63
  private appendGithubSummary;
42
64
  }
65
+ declare function createStoryReporter(options?: StoryReporterOptions): StoryReporterProtocol;
43
66
 
44
- export { StoryReporter, type StoryReporterOptions, StoryReporter as default };
67
+ export { StoryReporter, type StoryReporterOptions, type StoryReporterProtocol, type VitestContext, createStoryReporter, StoryReporter as default };
package/dist/reporter.js CHANGED
@@ -95,7 +95,7 @@ var StoryReporter = class {
95
95
  startTime = 0;
96
96
  packageVersion;
97
97
  gitSha;
98
- coverageData;
98
+ coverageByFile = {};
99
99
  constructor(options = {}) {
100
100
  this.options = options;
101
101
  }
@@ -112,7 +112,7 @@ var StoryReporter = class {
112
112
  onCoverage(coverage) {
113
113
  const data = normalizeCoveragePayload(coverage);
114
114
  if (data) {
115
- this.coverageData = summarizeCoverage(data);
115
+ this.coverageByFile = { ...this.coverageByFile, ...data };
116
116
  }
117
117
  }
118
118
  async onTestRunEnd(testModules, _unhandledErrors, reason) {
@@ -130,15 +130,20 @@ var StoryReporter = class {
130
130
  };
131
131
  const rawRunPath = this.options.rawRunPath;
132
132
  if (rawRunPath) {
133
- const absolutePath = path.isAbsolute(rawRunPath) ? rawRunPath : path.join(root, rawRunPath);
134
- const dir = path.dirname(absolutePath);
135
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
136
- const payload = { schemaVersion: 1, ...rawRun };
137
- fs.writeFileSync(absolutePath, JSON.stringify(payload, null, 2), "utf8");
133
+ try {
134
+ const absolutePath = path.isAbsolute(rawRunPath) ? rawRunPath : path.join(root, rawRunPath);
135
+ const dir = path.dirname(absolutePath);
136
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
137
+ const payload = { schemaVersion: 1, ...rawRun };
138
+ fs.writeFileSync(absolutePath, JSON.stringify(payload, null, 2), "utf8");
139
+ } catch (err) {
140
+ console.error("Failed to write raw run JSON:", err);
141
+ }
138
142
  }
139
143
  const canonicalRun = canonicalizeRun(rawRun);
140
- if (this.coverageData) {
141
- canonicalRun.coverage = toCoverageSummary(this.coverageData);
144
+ const coverageData = summarizeCoverage(this.coverageByFile);
145
+ if (coverageData) {
146
+ canonicalRun.coverage = toCoverageSummary(coverageData);
142
147
  }
143
148
  const generator = new ReportGenerator(this.options);
144
149
  try {
@@ -257,6 +262,10 @@ var StoryReporter = class {
257
262
  }
258
263
  }
259
264
  const retryCount = result?.retryCount ?? 0;
265
+ const configuredRetries = Math.max(
266
+ retryCount,
267
+ test.retries ?? test.options?.retry ?? 0
268
+ );
260
269
  testCases.push({
261
270
  title: meta.scenario,
262
271
  titlePath: meta.suitePath ? [...meta.suitePath, meta.scenario] : [meta.scenario],
@@ -269,7 +278,7 @@ var StoryReporter = class {
269
278
  attachments: attachments.length > 0 ? attachments : void 0,
270
279
  stepEvents: stepEvents.length > 0 ? stepEvents : void 0,
271
280
  retry: retryCount,
272
- retries: 0
281
+ retries: configuredRetries
273
282
  });
274
283
  }
275
284
  }
@@ -288,8 +297,12 @@ var StoryReporter = class {
288
297
  }
289
298
  }
290
299
  };
300
+ function createStoryReporter(options) {
301
+ return new StoryReporter(options);
302
+ }
291
303
  export {
292
304
  StoryReporter,
305
+ createStoryReporter,
293
306
  StoryReporter as default
294
307
  };
295
308
  //# sourceMappingURL=reporter.js.map
@@ -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 */\nimport type {\n Reporter,\n SerializedError,\n TestModule,\n TestCase,\n TestRunEndReason,\n Vitest,\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 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// 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 */\nexport default class StoryReporter implements Reporter {\n private options: StoryReporterOptions;\n private ctx: Vitest | undefined;\n private startTime: number = 0;\n private packageVersion: string | undefined;\n private gitSha: string | undefined;\n private coverageData: CoverageData | undefined;\n\n constructor(options: StoryReporterOptions = {}) {\n this.options = options;\n }\n\n onInit(ctx: Vitest): 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.coverageData = summarizeCoverage(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\n // Build RawRun\n const rawRun: RawRun = {\n testCases: rawTestCases,\n startedAtMs: this.startTime,\n finishedAtMs: Date.now(),\n projectRoot: root,\n packageVersion: this.packageVersion,\n gitSha: this.gitSha,\n ci: detectCI(),\n };\n\n // Optionally write raw run JSON for CLI/binary consumption\n const rawRunPath = this.options.rawRunPath;\n if (rawRunPath) {\n const absolutePath = path.isAbsolute(rawRunPath)\n ? rawRunPath\n : path.join(root, rawRunPath);\n const dir = path.dirname(absolutePath);\n if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });\n const payload = { schemaVersion: 1, ...rawRun };\n fs.writeFileSync(absolutePath, JSON.stringify(payload, null, 2), \"utf8\");\n }\n\n // Canonicalize\n const canonicalRun = canonicalizeRun(rawRun);\n\n // Add coverage if available\n if (this.coverageData) {\n canonicalRun.coverage = toCoverageSummary(this.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 for (const test of collection.allTests()) {\n const meta = this.getStoryMeta(test);\n if (!meta?.scenario || !Array.isArray(meta.steps)) continue;\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\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: 0,\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\nexport { StoryReporter };\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 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\n // Build RawRun\n const rawRun: RawRun = {\n testCases: rawTestCases,\n startedAtMs: this.startTime,\n finishedAtMs: Date.now(),\n projectRoot: root,\n packageVersion: this.packageVersion,\n gitSha: this.gitSha,\n ci: detectCI(),\n };\n\n // Optionally write raw run JSON for CLI/binary consumption\n const rawRunPath = this.options.rawRunPath;\n if (rawRunPath) {\n 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 for (const test of collection.allTests()) {\n const meta = this.getStoryMeta(test);\n if (!meta?.scenario || !Array.isArray(meta.steps)) continue;\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\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"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "executable-stories-vitest",
3
- "version": "8.1.15",
3
+ "version": "8.1.17",
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",