How to Build Custom Reporters
Docs for Vitest SDK v0.3.3
This guide shows you how to create custom reporters that process LiveDoc test results. By the end, you'll be able to build post-reporters for data export and UI reporters for custom console output.
- A working LiveDoc Vitest setup (imports or globals)
- Familiarity with the Reporters reference
Overview
LiveDoc provides two reporter extension points:
| Type | When It Runs | Use Case |
|---|---|---|
| Post Reporter | After all tests complete | JSON export, CI integration, summaries |
| UI Reporter | During test execution | Custom console output, live dashboards |
Post reporters are simpler and cover most needs. Start there unless you need real-time output.
Step 1: Create a Post Reporter
Implement the IPostReporter interface with a single execute method:
// reporters/SummaryReporter.ts
import * as fs from 'fs';
import { IPostReporter, ExecutionResults } from '@swedevtools/livedoc-vitest/reporter';
export class SummaryReporter implements IPostReporter {
execute(results: ExecutionResults, options?: any): void {
const outputFile = options?.['summary-output'] ?? 'test-summary.json';
const summary = {
timestamp: new Date().toISOString(),
totalFeatures: results.features.length,
features: results.features.map(f => ({
title: f.title,
tags: f.tags,
passed: f.statistics.passedCount,
failed: f.statistics.failedCount,
scenarios: f.scenarios.map(s => ({
title: s.title,
status: s.statistics.failedCount === 0 ? 'passed' : 'failed',
})),
})),
};
fs.writeFileSync(outputFile, JSON.stringify(summary, null, 2));
console.log(`📄 Summary written to: ${outputFile}`);
}
}
Step 2: Register the Post Reporter
Add your reporter to the postReporters array in the LiveDocSpecReporter
options:
// vitest.config.ts
import { defineConfig } from 'vitest/config';
import { LiveDocSpecReporter } from '@swedevtools/livedoc-vitest/reporter';
import { SummaryReporter } from './reporters/SummaryReporter';
export default defineConfig({
test: {
reporters: [
new LiveDocSpecReporter({
detailLevel: 'spec+summary+headers',
postReporters: [new SummaryReporter()],
'summary-output': './test-results/summary.json',
}),
],
},
});
Custom options (like 'summary-output') are passed through to your reporter
via the options parameter.
Step 3: Create a UI Reporter
For custom console output during execution, extend LiveDocReporter and
override its lifecycle hooks:
// reporters/EmojiReporter.ts
import { LiveDocReporter } from '@swedevtools/livedoc-vitest/reporter';
import * as model from '@swedevtools/livedoc-vitest/reporter';
export class EmojiReporter extends LiveDocReporter {
protected featureStart(feature: model.Feature): void {
this.write(`${feature.title}: `);
}
protected featureEnd(feature: model.Feature): void {
this.writeLine('');
}
protected scenarioEnd(scenario: model.Scenario): void {
const emoji = scenario.statistics.failedCount === 0 ? '✅' : '❌';
this.write(`${emoji} `);
}
protected scenarioExampleEnd(example: model.ScenarioExample): void {
const emoji = example.statistics.failedCount === 0 ? '✅' : '❌';
this.write(`${emoji} `);
}
}
Output:
Shopping Cart: ✅ ✅ ✅
User Login: ✅ ❌ ✅
Step 4: Understand the ExecutionResults Model
Post reporters receive the complete test results tree:
ExecutionResults
├── features[]
│ ├── title, description, tags, filename
│ ├── statistics { passedCount, failedCount, pendingCount }
│ ├── background?
│ │ └── steps[]
│ ├── scenarios[]
│ │ ├── title, description, tags
│ │ ├── statistics
│ │ └── steps[]
│ │ ├── title, displayTitle, type (given/when/then/and/but)
│ │ ├── values[], table[], docString
│ │ └── status, error?, duration
│ └── scenarioOutlines[]
│ ├── title, tables[]
│ └── examples[]
│ └── (same structure as scenario)
└── statistics (aggregated totals)
Iterating Over Results
execute(results: ExecutionResults): void {
for (const feature of results.features) {
console.log(`Feature: ${feature.title}`);
console.log(` Tags: ${feature.tags.join(', ')}`);
console.log(` Passed: ${feature.statistics.passedCount}`);
console.log(` Failed: ${feature.statistics.failedCount}`);
for (const scenario of feature.scenarios) {
const icon = scenario.statistics.failedCount === 0 ? '✓' : '✗';
console.log(` ${icon} ${scenario.title}`);
for (const step of scenario.steps) {
console.log(` ${step.type}: ${step.displayTitle}`);
if (step.error) {
console.log(` Error: ${step.error.message}`);
}
}
}
}
}
Complete Example
// reporters/MarkdownReporter.ts
import * as fs from 'fs';
import { IPostReporter, ExecutionResults } from '@swedevtools/livedoc-vitest/reporter';
export class MarkdownReporter implements IPostReporter {
execute(results: ExecutionResults, options?: any): void {
const outputFile = options?.['md-output'] ?? 'test-report.md';
const lines: string[] = ['# Test Report', ''];
for (const feature of results.features) {
const featureIcon = feature.statistics.failedCount === 0 ? '✅' : '❌';
lines.push(`## ${featureIcon} ${feature.title}`);
if (feature.description) {
lines.push('', `> ${feature.description}`, '');
}
for (const scenario of feature.scenarios) {
const scenarioIcon = scenario.statistics.failedCount === 0 ? '✓' : '✗';
lines.push(`### ${scenarioIcon} ${scenario.title}`);
for (const step of scenario.steps) {
const stepIcon = step.status === 'passed' ? '✓' : '✗';
lines.push(`- ${stepIcon} **${step.type}** ${step.displayTitle}`);
}
lines.push('');
}
}
fs.writeFileSync(outputFile, lines.join('\n'));
console.log(`📝 Markdown report written to: ${outputFile}`);
}
}
// vitest.config.ts
import { defineConfig } from 'vitest/config';
import { LiveDocSpecReporter } from '@swedevtools/livedoc-vitest/reporter';
import { MarkdownReporter } from './reporters/MarkdownReporter';
export default defineConfig({
test: {
reporters: [
new LiveDocSpecReporter({
detailLevel: 'spec+summary+headers',
postReporters: [new MarkdownReporter()],
'md-output': './test-results/report.md',
}),
],
},
});
Common Variations
Extending a Built-In Reporter
Override specific lifecycle hooks while keeping the default behavior:
import { LiveDocSpec } from '@swedevtools/livedoc-vitest/reporter';
import * as model from '@swedevtools/livedoc-vitest/reporter';
export class BrandedSpecReporter extends LiveDocSpec {
protected featureStart(feature: model.Feature): void {
this.writeLine('═'.repeat(60));
super.featureStart(feature);
}
}
Using Utility Methods
LiveDocReporter provides built-in helpers:
// Format a data table for console output
const table = [['Name', 'Price'], ['Widget', '$10'], ['Gadget', '$25']];
this.writeLine(this.formatTable(table, HeaderType.Top));
// Highlight regex matches in text
const text = this.highlight("the price is $100", /\$\d+/g, this.colorTheme.highlight);
// Bind <placeholders> to model values
const bound = this.bind("the customer pays <amount>", { amount: "100" }, this.colorTheme.highlight);
Exploring the Data Model
Use the built-in JsonReporter to dump the full results structure:
import { LiveDocSpecReporter, JsonReporter } from '@swedevtools/livedoc-vitest/reporter';
new LiveDocSpecReporter({
postReporters: [new JsonReporter()],
'json-output': 'debug-results.json',
});
Troubleshooting
| Problem | Cause | Solution |
|---|---|---|
| Post reporter never called | Not added to postReporters array | Add to LiveDocSpecReporter({ postReporters: [...] }) |
| Options not passed | Options on wrong object | Put custom options on the LiveDocSpecReporter options object, not Vitest |
| UI reporter slows tests | Blocking I/O in lifecycle hooks | Keep UI reporter hooks fast — use post reporters for file I/O |
| Empty results | Reporter registered but no tests match | Check your test patterns and filters |
| Type errors on model | Wrong import path | Import from @swedevtools/livedoc-vitest/reporter |
Related
- Viewer Integration — built-in LiveDoc Viewer reporter
- CI/CD Integration — using reporters in pipelines
- Reporters Reference — full API documentation
- Configuration — reporter configuration options