Implementation:Webdriverio Webdriverio WDIOReporter Base Class
Overview
Concrete base class for creating custom test reporters provided by the @wdio/reporter package.
Description
WDIOReporter is an EventEmitter subclass that manages event subscription, output streams, and test statistics. Custom reporters extend this class, override event handler methods, and use this.write() for output. The base class tracks suites, tests, hooks, counts, and failure information.
The class handles all the boilerplate of reporter development:
- Subscribes to all test lifecycle events in the constructor
- Creates
SuiteStats,TestStats,HookStats, andRunnerStatsobjects from raw event data - Maintains a
currentSuitesstack for hierarchical suite tracking - Manages output stream creation and cleanup (including deleting empty log files)
- Provides the
isSynchronisedgetter for async reporters that need to delay process shutdown
Source Files
| File | Purpose | Lines |
|---|---|---|
packages/wdio-reporter/src/index.ts |
WDIOReporter base class | L18-295 |
examples/wdio/custom-reporter/my.custom.reporter.js |
Reference example reporter | L1-27 |
Code Reference
Class Signature
// packages/wdio-reporter/src/index.ts:L18-295
import { EventEmitter } from 'node:events'
import type { WriteStream } from 'node:fs'
import type { Reporters, Options } from '@wdio/types'
type CustomWriteStream = { write: (content: unknown) => boolean }
export default class WDIOReporter extends EventEmitter {
outputStream: WriteStream | CustomWriteStream
failures: number = 0
suites: Record<string, SuiteStats> = {}
hooks: Record<string, HookStats> = {}
tests: Record<string, TestStats> = {}
currentSuites: SuiteStats[] = []
counts = {
suites: 0,
tests: 0,
hooks: 0,
passes: 0,
skipping: 0,
failures: 0,
pending: 0
}
retries: number = 0
runnerStat?: RunnerStats
isContentPresent: boolean = false
specs: string[] = []
currentSpec?: string
constructor(public options: Partial<Reporters.Options>)
/**
* Getter to indicate if async work is complete.
* Override in subclass to delay process shutdown.
*/
get isSynchronised(): boolean
/**
* Write content to the reporter output stream.
*/
write(content: unknown): void
// === Overridable Event Handlers ===
onRunnerStart(_runnerStats: RunnerStats): void
onBeforeCommand(_commandArgs: BeforeCommandArgs): void
onAfterCommand(_commandArgs: AfterCommandArgs): void
onBeforeAssertion(_assertionArgs: unknown): void
onAfterAssertion(_assertionArgs: unknown): void
onSuiteStart(_suiteStats: SuiteStats): void
onHookStart(_hookStat: HookStats): void
onHookEnd(_hookStats: HookStats): void
onTestStart(_testStats: TestStats): void
onTestPass(_testStats: TestStats): void
onTestFail(_testStats: TestStats): void
onTestRetry(_testStats: TestStats): void
onTestSkip(_testStats: TestStats): void
onTestPending(_testStats: TestStats): void
onTestEnd(_testStats: TestStats): void
onSuiteRetry(_suiteStats: SuiteStats): void
onSuiteEnd(_suiteStats: SuiteStats): void
onRunnerEnd(_runnerStats: RunnerStats): void
}
Constructor Implementation
// packages/wdio-reporter/src/index.ts:L40-258
constructor(public options: Partial<Reporters.Options>) {
super()
// Ensure the report directory exists
if (this.options.outputDir) {
fs.mkdirSync(this.options.outputDir, { recursive: true })
}
// Configure output stream: use writeStream if stdout mode, else create file
this.outputStream = (this.options.stdout || !this.options.logFile)
&& this.options.writeStream
? this.options.writeStream as CustomWriteStream
: fs.createWriteStream(this.options.logFile!)
// Subscribe to all lifecycle events
this.on('runner:start', (runner) => { /* create RunnerStats, call onRunnerStart */ })
this.on('suite:start', (params) => { /* create SuiteStats, call onSuiteStart */ })
this.on('hook:start', (hook) => { /* create HookStats, call onHookStart */ })
this.on('hook:end', (hook) => { /* update HookStats, call onHookEnd */ })
this.on('test:start', (test) => { /* create TestStats, call onTestStart */ })
this.on('test:pass', (test) => { /* update TestStats, call onTestPass */ })
this.on('test:fail', (test) => { /* update TestStats, call onTestFail */ })
this.on('test:retry', (test) => { /* update TestStats, call onTestRetry */ })
this.on('test:pending', (test) => { /* update TestStats, call onTestSkip */ })
this.on('test:end', (test) => { /* call onTestEnd */ })
this.on('suite:end', (suite) => { /* complete SuiteStats, call onSuiteEnd */ })
this.on('runner:end', (runner) => { /* complete RunnerStats, call onRunnerEnd, clean up empty files */ })
// Command events
this.on('client:beforeCommand', (payload) => { /* append to currentTest.output */ })
this.on('client:afterCommand', (payload) => { /* append to currentTest.output */ })
}
write() Method
// packages/wdio-reporter/src/index.ts:L271-276
write(content: unknown) {
if (content) {
this.isContentPresent = true
}
this.outputStream.write(content)
}
Full Example Custom Reporter
// examples/wdio/custom-reporter/my.custom.reporter.js
import WDIOReporter from '@wdio/reporter'
export default class CustomReporter extends WDIOReporter {
constructor(options) {
super(options)
console.log('initialized custom reporter with the following reporter options:', options)
this.write('Some log line')
}
onRunnerStart() { console.log('onRunnerStart') }
onBeforeCommand() { console.log('onBeforeCommand') }
onAfterCommand() { console.log('onAfterCommand') }
onSuiteStart() { console.log('onSuiteStart') }
onHookStart() { console.log('onHookStart') }
onHookEnd() { console.log('onHookEnd') }
onTestStart() { console.log('onTestStart') }
onTestPass() { console.log('onTestPass') }
onTestFail() { console.log('onTestFail') }
onTestRetry() { console.log('onTestRetry') }
onTestSkip() { console.log('onTestSkip') }
onTestEnd() { console.log('onTestEnd') }
onSuiteEnd() { console.log('onSuiteEnd') }
onRunnerEnd() { console.log('onRunnerEnd') }
}
I/O Contract
Import:
import WDIOReporter from '@wdio/reporter'
Constructor Options:
| Option | Type | Description |
|---|---|---|
outputDir |
string |
Directory for report files (created automatically if missing) |
logFile |
string |
Full path to the log file for this reporter |
stdout |
boolean |
If true, use the provided writeStream instead of file output |
writeStream |
WriteStream |
Custom write stream for output (used when stdout is true)
|
Inputs to Event Handlers:
RunnerStats-- Session ID, capabilities, specs, config, timing, failures, retriesSuiteStats-- Title, file, full title, duration, child tests/hooks, parent suiteTestStats-- Title, full title, duration, state, errors, output (commands), retriesHookStats-- Title, duration, errors, parent suite
Outputs:
- Content written via
this.write()to the configured output stream - File output cleaned up automatically if no content was written
Synchronization:
- Override
get isSynchronised()to returnfalsewhile async operations are pending - The test runner will wait for
isSynchronisedto returntruebefore exiting
Registration
// wdio.conf.ts
// Class reference
reporters: [CustomReporter]
// Class reference with options
reporters: [[CustomReporter, { outputDir: './reports' }]]
// String name (resolved via initializePlugin)
reporters: ['allure']
// String name with options
reporters: [['junit', { outputDir: './junit-reports', outputFileFormat: (opts) => `results-${opts.cid}.xml` }]]
Usage Examples
Async reporter with delayed shutdown:
import WDIOReporter from '@wdio/reporter'
export default class AsyncReporter extends WDIOReporter {
constructor(options) {
super(options)
this._pendingUploads = 0
}
get isSynchronised() {
// Delay process exit until all uploads complete
return this._pendingUploads === 0
}
onTestFail(testStats) {
this._pendingUploads++
fetch('https://my-dashboard.example.com/api/failures', {
method: 'POST',
body: JSON.stringify({
test: testStats.fullTitle,
error: testStats.errors?.[0]?.message,
duration: testStats.duration
})
}).finally(() => {
this._pendingUploads--
})
}
onRunnerEnd(runnerStats) {
this.write(JSON.stringify({
specs: this.specs,
passes: this.counts.passes,
failures: this.counts.failures,
skipped: this.counts.skipping
}))
}
}
Reporter that generates a summary file:
import WDIOReporter from '@wdio/reporter'
export default class SummaryReporter extends WDIOReporter {
constructor(options) {
super(options)
this.results = []
}
onTestPass(testStats) {
this.results.push({ title: testStats.title, status: 'passed', duration: testStats.duration })
}
onTestFail(testStats) {
this.results.push({ title: testStats.title, status: 'failed', error: testStats.errors?.[0]?.message })
}
onRunnerEnd() {
this.write(JSON.stringify(this.results, null, 2))
}
}