Vitest reporter options
Supports Vitest 4 and Vitest 5, from the declared floor of 4.1.5 upward. The reporter reads the same runner contract on both, so the options below apply unchanged. On Vitest 5, Node >= 22.12 and Vite >= 6.4 are required by Vitest itself.
Use the /reporter subpath in your config so Vitest is not loaded in the config context:
import { StoryReporter } from 'executable-stories-vitest/reporter';import { defineConfig } from 'vitest/config';
export default defineConfig({ test: { reporters: [ 'default', new StoryReporter({ /* options */ }), ], },});Options reference
Section titled “Options reference”The reporter uses FormatterOptions from executable-stories-formatters. All options are optional. When you pass no options, the formatters package defaults apply (formats: ["html"], outputDir: "reports", outputName: "index"). To get Markdown written to docs/user-stories.md, pass options explicitly as in the examples below.
Output configuration
Section titled “Output configuration”| Option | Type | Default | Description |
|---|---|---|---|
formats |
OutputFormat[] |
["html"] |
Output formats: "markdown", "html", "junit", "cucumber-json", "cucumber-messages", "cucumber-html". |
outputDir |
string |
"reports" |
Base directory for output files. |
outputName |
string |
"index" |
Base filename (without extension). |
outputNameTimestamp |
boolean |
false |
Append a UTC timestamp suffix to the output filename. |
output |
OutputConfig |
{ mode: "aggregated" } |
Output routing configuration. |
Every run also maintains one canonical JSON report per test source under
<outputDir>/by-file/. The output option routes rendered views; it does not change that
storage boundary. Documentation formats render accumulated state, while JUnit, Cucumber,
and release manifests contain only the current execution. Vitest detects name filtering
and incomplete collection automatically before deciding whether missing scenarios may be retired.
OutputConfig
Section titled “OutputConfig”| Field | Type | Default | Description |
|---|---|---|---|
mode |
"aggregated" | "colocated" |
"aggregated" |
Single file vs one file per source. |
colocatedStyle |
"mirrored" | "adjacent" | "flat" |
"mirrored" |
Colocated: mirrored under outputDir, next to source, or directly under outputDir with a clean name. |
rules |
OutputRule[] |
[] |
Pattern-based overrides (first match wins). |
Markdown options
Section titled “Markdown options”Nested under markdown:
| Option | Type | Default | Description |
|---|---|---|---|
title |
string |
"User Stories" |
Report title. |
includeStatusIcons |
boolean |
true |
Show ✅❌⏩ icons. |
includeErrors |
boolean |
true |
Show failure details. |
includeMetadata |
boolean |
true |
Show date/version/git SHA. |
sortScenarios |
"alpha" | "source" |
"source" |
Sort order for scenarios. |
suiteSeparator |
string |
" - " |
Separator for nested describes. |
includeFrontMatter |
boolean |
false |
Include YAML front-matter. |
includeSummaryTable |
boolean |
false |
Add summary statistics table. |
permalinkBaseUrl |
string |
— | Base URL for source links (e.g. GitHub blob). |
ticketUrlTemplate |
string |
— | URL template for ticket links. Use {ticket} as placeholder. |
traceUrlTemplate |
string |
— | URL template for trace links. Use {traceId} as placeholder. |
includeSourceLinks |
boolean |
true |
Include source links when permalinkBaseUrl is set. |
Filtering, history, and notifications
Section titled “Filtering, history, and notifications”Top-level FormatterOptions also support:
include/excludefor filtering bysourceFileincludeTags/excludeTagsfor filtering by story tagshistory.filePathandhistory.maxRunsfor HTML flakiness, stability, and performance trendsnotification.*for Slack, Teams, and generic webhook notifications
Other format options
Section titled “Other format options”| Option | Type | Description |
|---|---|---|
html |
HtmlOptions |
title, darkMode, searchable, startCollapsed, embedScreenshots. |
junit |
JUnitOptions |
suiteName, includeOutput. |
cucumberJson |
{ pretty?: boolean } |
Pretty-print JSON output. |
Vitest-specific
Section titled “Vitest-specific”| Option | Type | Default | Description |
|---|---|---|---|
enableGithubActionsSummary |
boolean |
true |
When GITHUB_ACTIONS, append report to job summary. |
rawRunPath |
string |
— | Write the raw run JSON to disk for later CLI use. |
OpenTelemetry spans
Section titled “OpenTelemetry spans”Spans put a trace waterfall on a scenario and let --format span-graph draw the
architecture the run exercised. With Vitest’s own OpenTelemetry support enabled,
they are collected automatically: storySpanCollector() is a SpanProcessor
exported from executable-stories-vitest/otel that keeps the spans ending
during a test, and the story claims them at test end. Tests are written
unchanged.
// otel.js: the SDK module Vitest loadsimport { NodeSDK } from '@opentelemetry/sdk-node';import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';import { storySpanCollector } from 'executable-stories-vitest/otel';
const sdk = new NodeSDK({ // Alongside your own exporter's processor, not instead of it: NodeSDK builds // a processor from `traceExporter` only when `spanProcessors` is absent. spanProcessors: [storySpanCollector(), new BatchSpanProcessor(exporter)],});sdk.start();export default sdk;export default defineConfig({ test: { experimental: { openTelemetry: { enabled: true, sdkPath: './otel.js' } }, },});Auto-instrumentation supplies peer.service, db.system and
messaging.destination.name, the attributes span-graph names components from.
A trace keeps its first 500 spans, spans ending after the test returns are not
attached, and an explicit story.attachSpans() takes precedence. A trace that
collected nothing leaves no key behind, so a suite with no OpenTelemetry setup
is untouched.
Examples
Section titled “Examples”Aggregated markdown
Section titled “Aggregated markdown”import { StoryReporter } from 'executable-stories-vitest/reporter';import { defineConfig } from 'vitest/config';
export default defineConfig({ test: { reporters: [ 'default', new StoryReporter({ formats: ['markdown'], outputDir: 'docs', outputName: 'user-stories', output: { mode: 'aggregated' }, markdown: { title: 'User Stories', includeStatusIcons: true, includeMetadata: true, }, }), ], },});Multiple formats
Section titled “Multiple formats”new StoryReporter({ formats: ['markdown', 'html', 'cucumber-json'], outputDir: 'reports', outputName: 'test-results', output: { mode: 'aggregated' },});Colocated output
Section titled “Colocated output”new StoryReporter({ formats: ['markdown'], outputDir: 'docs', output: { mode: 'colocated', colocatedStyle: 'mirrored', // Files mirror source structure under outputDir },});Rule-based routing
Section titled “Rule-based routing”new StoryReporter({ formats: ['markdown'], output: { mode: 'aggregated', rules: [ { match: '**/*.story.test.ts', mode: 'colocated', colocatedStyle: 'adjacent', }, { match: 'e2e/**', mode: 'aggregated', outputDir: 'docs/e2e' }, ], },});