import path from 'node:path'
import logger from '@wdio/logger'
import DotReporter from '@wdio/dot-reporter'
import { initializePlugin } from '@wdio/utils'
import type { Options, Capabilities, Reporters } from '@wdio/types'
const log = logger('@wdio/runner')
const mochaAllHooks = ['"before all" hook', '"after all" hook']
* BaseReporter
* responsible for initializing reporters for every testrun and propagating events
* to all these reporters
*/
export default class BaseReporter {
private _reporters: Reporters.ReporterInstance[] = []
private listeners: ((ev: unknown) => void)[] = []
constructor(
private _config: Options.Testrunner,
private _cid: string,
public caps: Capabilities.RequestedStandaloneCapabilities | Capabilities.RequestedMultiremoteCapabilities
) {
* make sure there is at least on default reporter set up (dot reporter is default)
*/
this._config.reporters = this._config.reporters || []
if (this._config.reporters.length === 0) {
this._config.reporters.push([DotReporter, {}])
}
}
async initReporters () {
this._reporters = await Promise.all(
this._config.reporters!.map(this._loadReporter.bind(this))
)
}
* emit events to all registered reporter and wdio launcer
*
* @param {string} e event name
* @param {object} payload event payload
*/
emit (e: string, payload: {
cid?: string
specs?: string[]
uid?: string
file?: string
title?: string
error?: string
sessionId?: string
config?: unknown
isMultiremote?: boolean
instanceOptions?: Options.Testrunner
capabilities?: unknown
retry?: number,
}) {
payload.cid = this._cid
* Send failure message (only once) in case of test or hook failure
*/
const isTestError = e === 'test:fail'
const isHookError = (
e === 'hook:end' &&
payload.error &&
mochaAllHooks.some(hook => payload.title?.startsWith(hook))
)
if (isTestError || isHookError) {
this.#emitData({
origin: 'reporter',
name: 'printFailureMessage',
content: payload
})
}
this._reporters.forEach((reporter) => {
try {
reporter.emit(e, payload)
} catch (err) {
const error = err instanceof Error ? err : new Error(`An unknown error occurred: ${err}`)
* When reporter throws an exception, log the error and continue with the next reporter
*/
this.#emitData({
origin: 'reporter',
name: 'printFailureMessage',
content: {
cid: this._cid,
error: { message: error.message, stack: error.stack },
fullTitle: `reporter ${reporter.constructor.name}`,
}
})
}
})
}
onMessage (listener: (ev: unknown) => void) {
this.listeners.push(listener)
}
getLogFile (name: string) {
const options = Object.assign({}, this._config) as Options.Testrunner & {
cid: string
capabilities: Capabilities.RequestedStandaloneCapabilities | Capabilities.RequestedMultiremoteCapabilities
}
let filename = `wdio-${this._cid}-${name}-reporter.log`
const reporterOptions = this._config.reporters!.find((reporter) => (
Array.isArray(reporter) &&
(
reporter[0] === name ||
typeof reporter[0] === 'function' && reporter[0].name === name
)
))
if (reporterOptions && Array.isArray(reporterOptions)) {
const fileformat = reporterOptions[1].outputFileFormat
options.cid = this._cid
options.capabilities = this.caps
Object.assign(options, reporterOptions[1])
if (fileformat) {
if (typeof fileformat !== 'function') {
throw new Error('outputFileFormat must be a function')
}
filename = fileformat(options)
}
}
if (!options.outputDir) {
return
}
return path.join(options.outputDir, filename)
}
* return write stream object based on reporter name
*/
getWriteStreamObject (reporter: string) {
return {
write: (content: unknown) => this.#emitData({
origin: 'reporter',
name: reporter,
content
})
}
}
* emit data either through process or listener
*/
#emitData (payload: unknown) {
if (typeof process.send === 'function') {
return process.send!(payload)
}
this.listeners.forEach((fn) => fn(payload))
return true
}
* wait for reporter to finish synchronization, e.g. when sending data asynchronous
* to a server (e.g. sumo reporter)
*/
waitForSync() {
const startTime = Date.now()
return new Promise((resolve, reject) => {
const interval = setInterval(() => {
const unsyncedReporter = this._reporters
.filter((reporter) => !reporter.isSynchronised)
.map((reporter) => reporter.constructor.name)
if ((Date.now() - startTime) > this._config.reporterSyncTimeout! && unsyncedReporter.length) {
clearInterval(interval)
return reject(new Error(`Some reporters are still unsynced: ${unsyncedReporter.join(', ')}`))
}
* no reporter are in need to sync anymore, continue
*/
if (!unsyncedReporter.length) {
clearInterval(interval)
return resolve(true)
}
log.info(`Wait for ${unsyncedReporter.length} reporter to synchronize`)
}, this._config.reporterSyncInterval)
})
}
* initialize reporters
*/
private async _loadReporter (reporter: Reporters.ReporterEntry) {
let ReporterClass: Reporters.ReporterClass
let options: Partial<Reporters.Options> = {}
* check if reporter has custom options
*/
if (Array.isArray(reporter)) {
options = Object.assign({}, options, reporter[1])
reporter = reporter[0]
}
* check if reporter was passed in from a file, e.g.
*
* ```js
* import MyCustomReporter from '/some/path/MyCustomReporter.js'
* export const config = {
* //...
* reporters: [
* MyCustomReporter, // or
* [MyCustomReporter, { custom: 'option' }]
* ]
* //...
* }
* ```
*/
if (typeof reporter === 'function') {
ReporterClass = reporter as Reporters.ReporterClass
options.logFile = options.setLogFile
? options.setLogFile(this._cid, ReporterClass.name)
: typeof options.logFile === 'string'
? options.logFile
: this.getLogFile(ReporterClass.name)
options.writeStream = this.getWriteStreamObject(ReporterClass.name)
return new ReporterClass(options)
}
* check if reporter is a node package, e.g. wdio-dot reporter
*
* ```js
* export const config = {
* //...
* reporters: [
* 'dot', // or
* ['dot', { custom: 'option' }]
* ]
* //...
* }
* ```
*/
if (typeof reporter === 'string') {
ReporterClass = (await initializePlugin(reporter, 'reporter')).default as Reporters.ReporterClass
options.logFile = options.setLogFile
? options.setLogFile(this._cid, reporter)
: typeof options.logFile === 'string'
? options.logFile
: this.getLogFile(reporter)
options.writeStream = this.getWriteStreamObject(reporter)
return new ReporterClass(options)
}
* throw error if reporter property was invalid
*/
throw new Error('Invalid reporters config')
}
}