import logger from '@wdio/logger'
import type { EventEmitter } from 'node:events'
import type { ReporterOptions, ParentSuite, TestEvent, SuiteEvent, JasmineOpts } from './types.js'
const log = logger('@wdio/jasmine-framework')
const STACKTRACE_FILTER = /(node_modules(\/|\\)(\w+)*|@wdio\/sync\/(build|src)|- - - - -)/g
export default class JasmineReporter {
public startedSuite?: SuiteEvent
private _cid: string
private _specs: string[]
private _jasmineOpts: JasmineOpts
private _shouldCleanStack: boolean
private _parent: ParentSuite[] = []
private _failedCount = 0
private _suiteStart = new Date()
private _testStart = new Date()
constructor (
private _reporter: EventEmitter,
params: ReporterOptions
) {
this._cid = params.cid
this._specs = params.specs
this._jasmineOpts = params.jasmineOpts
this._shouldCleanStack = typeof params.cleanStack === 'boolean' ? params.cleanStack : true
}
suiteStarted (suite: jasmine.SuiteResult) {
this._suiteStart = new Date()
const newSuite: SuiteEvent = {
type: 'suite',
start: this._suiteStart,
...suite
}
this.startedSuite = newSuite
let fullName = suite.description
for (const parent of this._parent.reverse()) {
fullName = parent.description + '.' + fullName
}
newSuite.fullName = fullName
this.emit('suite:start', newSuite)
this._parent.push({
description: suite.description,
id: suite.id,
tests: 0
})
}
specStarted (test: jasmine.SpecResult) {
this._testStart = new Date()
const newTest: TestEvent = {
type: 'test',
start: this._testStart,
...test
}
const parentSuite = this._parent[this._parent.length - 1]
* if jasmine test has no root describe block, create one
*/
if (!parentSuite) {
log.warn(
'No root suite was defined! This can cause reporters to malfunction. ' +
'Please always start a spec file with describe("...", () => { ... }).'
)
} else {
parentSuite.tests++
}
this.emit('test:start', newTest)
}
specDone (test: jasmine.SpecResult) {
const newTest: TestEvent = {
...test,
start: this._testStart,
type: 'test',
duration: Date.now() - this._testStart.getTime()
}
* excluded tests are treated as pending tests
*/
if (test.status === 'excluded') {
newTest.status = 'pending'
}
* mark failing tests due to missing assertion
*/
if (
test.status === 'failed' &&
this._jasmineOpts.failSpecWithNoExpectations &&
test.failedExpectations.length === 0 &&
test.passedExpectations.length === 0
) {
test.failedExpectations.push({
matcherName: 'toHaveAssertion',
expected: ' >1 assertions',
actual: '0 assertions',
passed: false,
message: (
'No assertions found in test! This test is failing because no assertions were found ' +
'but "jasmineOpts" had a "failSpecWithNoExpectations" flag set to "true".\n' +
'\nRead more on this Jasmine option at: https://jasmine.github.io/api/5.0/Configuration#failSpecWithNoExpectations'
),
stack: ''
})
}
if (test.failedExpectations && test.failedExpectations.length) {
let errors = test.failedExpectations
if (this._shouldCleanStack) {
errors = test.failedExpectations.map(x => this.cleanStack(x))
}
newTest.errors = errors
newTest.error = errors[0]
}
const e = 'test:' + (newTest.status ? newTest.status.replace(/ed/, '') : 'unknown')
this.emit(e, newTest)
this._failedCount += test.status === 'failed' ? 1 : 0
this.emit('test:end', newTest)
}
suiteDone (suite: jasmine.SuiteResult) {
const parentSuite = this._parent[this._parent.length - 1]
const newSuite: SuiteEvent = {
...suite,
type: 'suite',
start: this._suiteStart,
duration: Date.now() - this._suiteStart.getTime()
}
* in case there is a runtime error within one of the specs
* create an empty test to attach the error to it
*/
if (parentSuite.tests === 0 && suite.failedExpectations && suite.failedExpectations.length) {
const id = 'spec' + Math.random()
this.specStarted({
id,
description: '<unknown test>',
fullName: '<unknown test>',
duration: null,
properties: {},
failedExpectations: [],
deprecationWarnings: [],
passedExpectations: [],
status: 'unknown',
pendingReason: '',
debugLogs: null,
filename: suite.filename
})
this.specDone({
id,
description: '<unknown test>',
fullName: '<unknown test>',
failedExpectations: suite.failedExpectations,
deprecationWarnings: [],
passedExpectations: [],
status: 'failed',
duration: null,
properties: {},
pendingReason: '',
debugLogs: null,
filename: suite.filename
})
}
this._parent.pop()
this.emit('suite:end', newSuite)
delete this.startedSuite
}
emit (event: string, payload: SuiteEvent | TestEvent) {
const message = {
cid: this._cid,
uid: this.getUniqueIdentifier(payload),
event: event,
title: payload.description,
fullTitle: payload.fullName,
pending: payload.status === 'pending',
pendingReason: (payload as TestEvent).pendingReason,
parent: this._parent.length ? this.getUniqueIdentifier(this._parent[this._parent.length - 1]) : null,
type: payload.type,
error: payload.error,
errors: payload.errors,
duration: payload.duration || 0,
specs: this._specs,
start: payload.start,
file: payload.filename
}
this._reporter.emit(event, message)
}
getFailedCount () {
return this._failedCount
}
getUniqueIdentifier (target: Pick<TestEvent, 'description' | 'id'>) {
return target.description + target.id
}
cleanStack (error: jasmine.FailedExpectation) {
if (!error.stack) {
return error
}
let stack = error.stack.split('\n')
stack = stack.filter((line) => !line.match(STACKTRACE_FILTER))
error.stack = stack.join('\n')
return error
}
}