fix(@wdio/reporter): propagate failing session creation to report (#14223)

* fix(@wdio/reporter): propagate failing session creation to report

* add unit tests
This commit is contained in:
Christian Bromann 2025-02-24 10:50:20 -08:00 committed by GitHub
parent 06aab2b51f
commit 10fe0271bd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 62 additions and 26 deletions

View file

@ -1,11 +0,0 @@
declare module "junit-report-builder" {
interface Builder {
build: () => void
}
const cssValue: {
newBuilder: () => Builder
}
export = cssValue
}

View file

@ -4,7 +4,7 @@ import os from 'node:os'
import type { RunnerStats, SuiteStats, TestStats } from '@wdio/reporter'
import WDIOReporter from '@wdio/reporter'
import junit from 'junit-report-builder'
import junitReportBuilder, { type Builder as JUnitReportBuilder } from 'junit-report-builder'
import type { JUnitReporterOptions } from './types.js'
import { limit } from './utils.js'
@ -276,7 +276,7 @@ class JunitReporter extends WDIOReporter {
}
private _buildJunitXml(runner: RunnerStats) {
const builder = junit.newBuilder()
const builder = (junitReportBuilder as unknown as JUnitReportBuilder).newBuilder()
if (runner.config.hostname !== undefined && runner.config.hostname.indexOf('browserstack') > -1) {
// NOTE: deviceUUID is used to build sanitizedCapabilities resulting in a ever-changing package name in runner.sanitizedCapabilities when running Android tests under Browserstack. (i.e. ht79v1a03938.android.9)
// NOTE: platformVersion is used to build sanitizedCapabilities which can be incorrect and includes a minor version for iOS which is not guaranteed to be the same under Browserstack.
@ -317,7 +317,14 @@ class JunitReporter extends WDIOReporter {
return builder.build() as unknown as string
}
private _buildOrderedReport(builder: any, runner: RunnerStats, specFileName: string, type: string, isCucumberFrameworkRunner: boolean) {
private _buildOrderedReport(builder: JUnitReportBuilder, runner: RunnerStats, specFileName: string, type: string, isCucumberFrameworkRunner: boolean) {
const suiteKeys = Object.keys(this.suites)
if (suiteKeys.length === 0) {
const error = this.runnerStat?.error ?? 'No tests found'
return builder.testCase().failure(error)
}
for (const suiteKey of Object.keys(this.suites)) {
/**
* ignore root before all

View file

@ -209,6 +209,7 @@ export default class WDIOReporter extends EventEmitter {
if (this.runnerStat) {
this.runnerStat.failures = runner.failures
this.runnerStat.retries = runner.retries
this.runnerStat.error = runner.error
this.runnerStat.complete()
this.onRunnerEnd(this.runnerStat)
}

View file

@ -19,6 +19,7 @@ export default class RunnerStats extends RunnableStats {
retry?: number
failures?: number
retries?: number
error?: string
constructor (runner: Options.RunnerStart) {
super('runner')

View file

@ -33,6 +33,7 @@ export default class Runner extends EventEmitter {
private _cid?: string
private _specs?: string[]
private _caps?: Capabilities.RequestedStandaloneCapabilities | Capabilities.RequestedMultiremoteCapabilities
private _sessionInitError?: Error
/**
* run test suite
@ -272,7 +273,9 @@ export default class Runner extends EventEmitter {
const browser = await this._startSession(config, caps) as WebdriverIO.Browser
// return null if session couldn't get established
if (!browser) { return }
if (!browser) {
return
}
/**
* register global helper method to fetch elements
@ -366,8 +369,9 @@ export default class Runner extends EventEmitter {
if (this._isMultiremote) {
_setGlobal('multiremotebrowser', this._browser, config.injectGlobals)
}
} catch (err: any) {
log.error(err)
} catch (error: any) {
log.error(error)
this._sessionInitError = error
return
}
@ -393,7 +397,7 @@ export default class Runner extends EventEmitter {
config: this._config,
isMultiremote: this._isMultiremote,
instanceOptions: {},
capabilities: { ...this._configParser!.getCapabilities() },
capabilities: this._caps,
retry: this._specFileRetryAttempts
})
}
@ -401,7 +405,8 @@ export default class Runner extends EventEmitter {
this._reporter!.emit('runner:end', {
failures,
cid: this._cid,
retries
retries,
...(this._sessionInitError instanceof Error ? { error: this._sessionInitError.message } : {})
} as Options.RunnerEnd)
try {
await this._reporter!.waitForSync()

View file

@ -49,13 +49,13 @@ export default class BaseReporter {
uid?: string
file?: string
title?: string
error?: Error
error?: string
sessionId?: string
config?: unknown
isMultiremote?: boolean
instanceOptions?: Options.Testrunner
capabilities?: unknown
retry?: number
retry?: number,
}) {
payload.cid = this._cid

View file

@ -476,6 +476,7 @@ describe('wdio-runner', () => {
const runner = new WDIORunner()
runner['_configParser'] = { getCapabilities: vi.fn().mockReturnValue([{ browserName: 'safari' }]) } as any
runner['_browser'] = {} as unknown as BrowserObject
runner['_caps'] = { 'browserName': 'safari' }
runner['_reporter'] = {
waitForSync: vi.fn().mockReturnValue(Promise.resolve()),
emit: vi.fn()
@ -485,9 +486,7 @@ describe('wdio-runner', () => {
const args = [
['runner:start', {
'capabilities': {
'0': { 'browserName': 'safari' }
},
'capabilities': { 'browserName': 'safari' },
'cid': undefined,
'config': undefined,
'instanceOptions': {},

View file

@ -213,6 +213,14 @@ export default class SpecReporter extends WDIOReporter {
// Get the results
const results = this.getResultDisplay(preface)
if (results.length === 0 && runner.error) {
results.push(
this.setMessageColor(`${this.getSymbol(State.FAILED)} Failed to create a session:`, State.FAILED),
runner.error,
''
)
}
// If there are no test results then return nothing
if (results.length === 0) {
return

View file

@ -273,6 +273,21 @@ exports[`SpecReporter > onlyFailures > true > 1 failure 1`] = `
]
`;
exports[`SpecReporter > printReport > should print a report even if session could not be created 1`] = `
[
[
"------------------------------------------------------------------
[loremipsum 50 Windows 10 #0-0] Running: loremipsum (v50) on Windows 10
[loremipsum 50 Windows 10 #0-0] Session ID: foobar
[loremipsum 50 Windows 10 #0-0]
[loremipsum 50 Windows 10 #0-0] red ✖ Failed to create a session:
[loremipsum 50 Windows 10 #0-0] No tests found
[loremipsum 50 Windows 10 #0-0]
",
],
]
`;
exports[`SpecReporter > printReport > with disabled sharable Sauce report links > should print the default Sauce Labs job details page link 1`] = `[]`;
exports[`SpecReporter > printReport > with normal setup > should print jobs of all instance when run with multiremote 1`] = `

View file

@ -341,6 +341,16 @@ describe('SpecReporter', () => {
expect(printReporter.write.mock.calls.length).toBe(0)
})
it('should print a report even if session could not be created', () => {
printReporter['_suiteUids'] = []
printReporter.suites = {}
const runner = getRunnerConfig()
runner.error = 'No tests found'
printReporter.printReport(runner)
expect(printReporter.write.mock.calls).toMatchSnapshot()
})
})
describe('getHeaderDisplay', () => {

View file

@ -358,4 +358,5 @@ export interface RunnerEnd {
failures: number
cid: string
retries: number
error?: string
}

View file

@ -91,7 +91,7 @@ export async function startWebDriverSession (params: RemoteConfig): Promise<{ se
} catch (err) {
log.error(err)
const message = getSessionError(err as Error, params)
throw new Error('Failed to create session.\n' + message)
throw new Error(message)
}
const sessionId = response.value.sessionId || response.sessionId

View file

@ -331,7 +331,7 @@ describe('utils', () => {
logLevel: 'warn',
capabilities: {}
}).catch((err) => err)
expect(error.message).toContain('Failed to create session')
expect(error.message).toContain('Invalid URL')
})
it('should break if JSONWire and WebDriver caps are mixed together', async () => {