Reporters, Coverage, and Extending Vitest: The Output and Plugin Layer
Prerequisites
- ›Articles 1-4: Full understanding of Vitest architecture, boot, execution, and runner
- ›Basic familiarity with code coverage concepts
- ›Understanding of WebSocket communication (for UI section)
Reporters, Coverage, and Extending Vitest: The Output and Plugin Layer
All the work we've traced through the previous four articles — config resolution, pool management, worker boot, test collection, and execution — ultimately exists to produce output. Whether that's colored terminal output, a JSON file, a JUnit XML report, a coverage summary, or a real-time browser dashboard, the reporter and output systems are where Vitest's internal state becomes visible to developers.
This final article covers the Reporter interface and its lifecycle hooks, the hierarchy of reported task objects, the built-in reporter catalog, the coverage provider system, test sequencing strategies, the WebSocket API powering the Vitest UI, the programmatic Node API, and the sub-package architecture that makes expect, spy, and snapshot independently composable.
The Reporter Interface and Lifecycle Hooks
The Reporter interface in packages/vitest/src/node/types/reporter.ts defines every event a reporter can listen to:
sequenceDiagram
participant V as Vitest
participant TR as TestRun
participant R as Reporter
V->>R: onInit(vitest)
Note over TR: Test run begins
TR->>R: onTestRunStart(specifications)
loop For each test module
TR->>R: onTestModuleQueued(testModule)
TR->>R: onTestModuleCollected(testModule)
TR->>R: onTestModuleStart(testModule)
loop For each test
TR->>R: onTestCaseReady(testCase)
TR->>R: onTestCaseResult(testCase)
end
loop For each suite
TR->>R: onTestSuiteReady(testSuite)
TR->>R: onTestSuiteResult(testSuite)
end
TR->>R: onTestModuleEnd(testModule)
end
TR->>R: onTestRunEnd(testModules, errors, reason)
Note over R: Optional events
R-->>R: onUserConsoleLog(log)
R-->>R: onHookStart(hook) / onHookEnd(hook)
R-->>R: onTestCaseAnnotate(testCase, annotation)
R-->>R: onCoverage(coverage)
The lifecycle is designed to be granular. A reporter doesn't need to implement every hook — all are optional. The onTestRunStart receives the full list of specifications before any tests execute. onTestModuleQueued fires when a file is sent to a worker but before it's loaded. onTestModuleCollected fires after the file's tests have been discovered. Individual test results arrive via onTestCaseResult.
The TestRunEndReason can be 'passed', 'failed', or 'interrupted' — the last indicating user cancellation or bail-out.
The ReportedTask Hierarchy
Reporters don't work with raw runner tasks — they receive rich wrapper objects. The hierarchy in packages/vitest/src/node/reporters/reported-tasks.ts provides a clean API:
classDiagram
class ReportedTaskImplementation {
+task: RunnerTask
+project: TestProject
+id: string
+location: LocationInfo
+ok(): boolean
+meta(): TaskMeta
}
class TestModule {
+type: "module"
+moduleId: string
+children: TestCollection
+state(): TestModuleState
+diagnostic(): ModuleDiagnostic
}
class TestSuite {
+type: "suite"
+name: string
+parent: TestSuite | TestModule
+children: TestCollection
+state(): TestSuiteState
}
class TestCase {
+type: "test"
+name: string
+fullName(): string
+parent: TestSuite | TestModule
+result(): TestResult
+diagnostic(): TestDiagnostic
+annotations(): TestAnnotation[]
}
ReportedTaskImplementation <|-- TestModule
ReportedTaskImplementation <|-- TestSuite
ReportedTaskImplementation <|-- TestCase
TestModule represents a test file (what @vitest/runner calls a File). TestSuite wraps describe blocks. TestCase wraps individual tests. Each provides typed accessors for results, diagnostics (duration, retries, heap usage), and navigation (parent, children).
The TestCollection on modules and suites is iterable, supporting for...of loops over children. The state() method returns the computed state: 'pending', 'queued', 'running', 'passed', 'failed', or 'skipped'.
Built-in Reporters
Vitest ships with 12 reporters, registered in packages/vitest/src/node/reporters/index.ts#L49-L62:
export const ReportersMap = {
'default': DefaultReporter,
'agent': AgentReporter,
'blob': BlobReporter,
'verbose': VerboseReporter,
'dot': DotReporter,
'json': JsonReporter,
'tap': TapReporter,
'tap-flat': TapFlatReporter,
'junit': JUnitReporter,
'tree': TreeReporter,
'hanging-process': HangingProcessReporter,
'github-actions': GithubActionsReporter,
}
| Reporter | Output | Use Case |
|---|---|---|
default |
Colored terminal with progress | Interactive development |
verbose |
Every test listed | CI with detailed output |
dot |
One dot per test | Minimal CI output |
tree |
Tree-structured output | Visual hierarchy |
json |
JSON file | Machine consumption |
junit |
JUnit XML | CI system integration |
tap / tap-flat |
TAP protocol | TAP consumers |
github-actions |
GitHub annotations | PR integration |
blob |
Binary blob | Sharding/merge workflows |
agent |
Structured for AI agents | Automated tooling |
hanging-process |
Process diagnostics | Debugging stuck tests |
Most reporters extend BaseReporter from packages/vitest/src/node/reporters/base.ts#L43-L65, which provides common functionality: TTY detection, silent mode, error formatting, summary rendering, and the banner display.
Tip: You can use multiple reporters simultaneously:
--reporter=default --reporter=json --outputFile=results.json. Thedefaultreporter writes to stdout whilejsonwrites to a file. Custom reporters can be specified as module paths:--reporter=./my-reporter.ts.
Coverage Provider System
Coverage in Vitest is pluggable. The provider system in packages/vitest/src/node/coverage.ts loads provider modules on demand:
flowchart TD
Config["coverage.provider: 'v8' | 'istanbul' | 'custom'"] --> Resolve["resolveCoverageProviderModule()"]
Resolve --> V8["@vitest/coverage-v8"]
Resolve --> Istanbul["@vitest/coverage-istanbul"]
Resolve --> Custom["Custom module"]
V8 & Istanbul & Custom --> Provider["CoverageProvider interface"]
Provider --> Init["initialize(ctx)"]
subgraph "Test Execution"
Init --> StartWorker["startCoverageInsideWorker()"]
StartWorker --> Tests["Tests run"]
Tests --> StopWorker["stopCoverageInsideWorker()"]
end
StopWorker --> Report["generateCoverage()"]
Report --> Transform["CoverageTransform plugin<br>(source map remapping)"]
Transform --> Output["Coverage reports<br>(text, html, json, clover)"]
The BaseCoverageProvider class provides shared functionality: threshold checking, file globbing, report generation. The V8 provider uses V8's built-in coverage, which is fast but sometimes less accurate for source-mapped code. Istanbul instruments code at the source level, providing more reliable coverage at the cost of transform overhead.
The CoverageTransform Vite plugin handles source map remapping — ensuring coverage maps point to original source positions, not transformed output. Coverage data is collected per-worker (via the startCoverageInsideWorker/stopCoverageInsideWorker functions called in runBaseTests) and merged on the Node side.
Test Sequencers
Test ordering is controlled by sequencers. The BaseSequencer in packages/vitest/src/node/sequencers/BaseSequencer.ts implements a smart default ordering:
flowchart TD
Sort["BaseSequencer.sort(files)"] --> GroupOrder["1. sequence.groupOrder"]
GroupOrder --> ProjectName["2. Project name (alphabetical)"]
ProjectName --> Isolation["3. Isolated files first"]
Isolation --> Cache{Has cached results?}
Cache -- "no" --> Size["Sort by file size (larger first)"]
Cache -- "yes" --> Failed{Previously failed?}
Failed -- "yes" --> First["Run first"]
Failed -- "no" --> Duration["Sort by duration (longer first)"]
The ordering prioritizes failed tests first (fast feedback), then longer tests (optimizes total wall time by starting expensive tests early). The shard() method uses SHA-1 hashing for deterministic distribution across shards.
Custom sequencers can extend BaseSequencer and override sort() — for example, to prioritize tests related to changed files or to implement custom grouping strategies.
WebSocket API and the Vitest UI
The Vitest UI communicates with the test runner through a WebSocket server established in packages/vitest/src/api/setup.ts:
sequenceDiagram
participant UI as Vitest UI (Browser)
participant WS as WebSocket Server
participant V as Vitest Process
UI->>WS: Connect to /__vitest_api__
WS->>V: createBirpc(handlers, events)
Note over UI,V: Bidirectional RPC established
UI->>V: getFiles()
V-->>UI: File list with test results
UI->>V: rerun(files)
V->>V: Schedule test run
V->>UI: onTaskUpdate(packs, events)
V->>UI: onFinished(files, errors)
V->>UI: onUserConsoleLog(log)
UI->>V: getModuleGraph(id)
V-->>UI: Module dependency graph
The server upgrades HTTP connections on the /__vitest_api__ path to WebSocket. It validates requests against the API config, then establishes a birpc channel. The WebSocketHandlers interface exposes methods like getFiles(), getTransformResult(), getModuleGraph(), and rerun(). Events flow back through the WebSocketEvents interface, which mirrors the Reporter lifecycle.
The @vitest/ws-client package provides the browser-side client, and @vitest/ui is a Vue application that consumes this API to render the dashboard with real-time test results, module graphs, and error displays.
The Programmatic Node API
The public Node API exported from packages/vitest/src/public/node.ts is designed for IDE extensions, custom build tools, and programmatic test execution:
// Core functions
export { startVitest } from '../node/cli/cli-api'
export { createVitest } from '../node/create'
export { VitestPlugin } from '../node/plugins'
// Reporter infrastructure
export { ReportersMap, DefaultReporter, ... } from '../node/reporters'
// Pool workers (for custom pools)
export { ThreadsPoolWorker, ForksPoolWorker, ... } from '../node/pools/workers/...'
// Sequencer
export { BaseSequencer } from '../node/sequencers/BaseSequencer'
// Types
export type { Vitest, Reporter, TestProject, TestSpecification, ... }
The distinction between createVitest and startVitest is important: createVitest creates and initializes the Vitest instance but doesn't run tests. startVitest creates the instance and runs tests. For IDE integrations, you typically want createVitest so you can control when and which tests execute.
The API also re-exports Vite utilities: createViteServer, parseAst, and version information — making it unnecessary to depend on Vite separately when building Vitest tooling.
Tip: For VS Code extension development, use
createVitestwith thereportersoption to inject a custom reporter that maps test results to VS Code's Test API. TheTestSpecificationclass lets you run individual files or even specific tests by line number.
Sub-Package Architecture: expect, spy, snapshot
The assertion, mocking, and snapshot systems are implemented as independent packages that compose into Vitest's test API.
@vitest/expect (packages/expect/src/index.ts) builds on Chai, adding Jest-compatible matchers:
graph TD
subgraph "@vitest/expect"
Chai["chai (base)"]
JCE["JestChaiExpect<br>(toBe, toEqual, toThrow...)"]
JAM["JestAsymmetricMatchers<br>(any, anything, objectContaining...)"]
JE["JestExtend<br>(expect.extend())"]
CM["customMatchers registry"]
Chai --> JCE
Chai --> JAM
JCE --> JE
JE --> CM
end
subgraph "@vitest/spy"
Spy["tinyspy (base)"]
MockFn["createMockInstance()"]
MockRestore["Mock lifecycle<br>(clear, reset, restore)"]
Spy --> MockFn
MockFn --> MockRestore
end
subgraph "@vitest/snapshot"
Client["SnapshotClient"]
State["SnapshotState"]
Plugins["Serializer plugins"]
Client --> State
State --> Plugins
end
The JestChaiExpect plugin adds all the familiar matchers (toBe, toEqual, toHaveBeenCalled, toMatchSnapshot). JestExtend enables expect.extend() for custom matchers. The GLOBAL_EXPECT constant is used to store the current expect instance in a global scope, enabling the scoped expect that TestRunner configures per-test.
@vitest/spy (packages/spy/src/index.ts) provides mock functions built on tinyspy. The createMockInstance() function returns a Mock object with a full API: mockImplementation(), mockReturnValue(), mockResolvedValue(), etc. A global MOCK_RESTORE set tracks all mocks for bulk restoration with vi.restoreAllMocks().
@vitest/snapshot (packages/snapshot/src/index.ts) handles both file-based and inline snapshots. SnapshotClient manages the assertion flow, SnapshotState tracks snapshot data per file, and custom serializers can be added via addSerializer(). Snapshot environments are pluggable — the default writes .snap files to disk, but custom environments can store snapshots differently (in a database, for example).
These packages are consumed by the vitest integration layer — src/integrations/chai/ wires up expect, src/integrations/spy.ts configures the spy module, and src/integrations/snapshot/ connects snapshot assertions to the runner lifecycle.
Wrapping Up the Series
Over five articles, we've traced Vitest's complete architecture:
- Architecture — The 17-package monorepo with its clean separation between Node orchestration and worker runtime
- Boot sequence — From binary entry through CLI parsing, config discovery, Vite server creation, and plugin hooks
- Pool system — The three-layer
Pool/PoolRunner/PoolWorkerdesign with birpc communication - Runner — The framework-agnostic test DSL, collection, execution engine, hook system, and fixtures
- Output layer — Reporters, coverage, sequencers, the WebSocket UI API, and composable sub-packages
The design philosophy that emerges is one of layered composition. @vitest/runner knows nothing about Vite. @vitest/expect knows nothing about test execution. The vitest package composes all these pieces, using Vite's dev server as the transformation backbone and birpc as the communication bridge. This architecture makes the system testable, extensible, and — once you understand the layers — surprisingly navigable for a framework of this scope.