UNPKG

@wdio/cli

Version:
286 lines (285 loc) 11 kB
import { EventEmitter } from 'node:events'; import chalk, { supportsColor } from 'chalk'; import logger from '@wdio/logger'; import { SnapshotManager } from '@vitest/snapshot/manager'; import { HookError } from './utils.js'; import { getRunnerName } from './utils.js'; const log = logger('@wdio/cli'); const EVENT_FILTER = ['sessionStarted', 'sessionEnded', 'finishedCommand', 'ready', 'workerResponse', 'workerEvent']; export default class WDIOCLInterface extends EventEmitter { _config; totalWorkerCnt; _isWatchMode; #snapshotManager = new SnapshotManager({ updateSnapshot: 'new' // ignored in this context }); hasAnsiSupport; result = { finished: 0, passed: 0, retries: 0, failed: 0 }; _jobs = new Map(); _specFileRetries; _specFileRetriesDelay; _skippedSpecs = 0; _inDebugMode = false; _start = new Date(); _messages = { reporter: {}, debugger: {} }; constructor(_config, totalWorkerCnt, _isWatchMode = false) { super(); this._config = _config; this.totalWorkerCnt = totalWorkerCnt; this._isWatchMode = _isWatchMode; /** * Colors can be forcibly enabled/disabled with env variable `FORCE_COLOR` * `FORCE_COLOR=1` - forcibly enable colors * `FORCE_COLOR=0` - forcibly disable colors */ this.hasAnsiSupport = supportsColor && supportsColor.hasBasic; this.totalWorkerCnt = totalWorkerCnt; this._isWatchMode = _isWatchMode; this._specFileRetries = _config.specFileRetries || 0; this._specFileRetriesDelay = _config.specFileRetriesDelay || 0; this.on('job:start', this.addJob.bind(this)); this.on('job:end', this.clearJob.bind(this)); this.setup(); this.onStart(); } #hasShard() { return this._config.shard && this._config.shard.total !== 1; } setup() { this._jobs = new Map(); this._start = new Date(); /** * The relationship between totalWorkerCnt and these counters are as follows: * totalWorkerCnt - retries = finished = passed + failed */ this.result = { finished: 0, passed: 0, retries: 0, failed: 0 }; this._messages = { reporter: {}, debugger: {} }; } onStart() { const shardNote = this.#hasShard() ? ` (Shard ${this._config.shard.current} of ${this._config.shard.total})` : ''; this.log(chalk.bold(`\nExecution of ${chalk.blue(this.totalWorkerCnt)} workers${shardNote} started at`), this._start.toISOString()); if (this._inDebugMode) { this.log(chalk.bgYellow(chalk.black('DEBUG mode enabled!'))); } if (this._isWatchMode) { this.log(chalk.bgYellow(chalk.black('WATCH mode enabled!'))); } this.log(''); } onSpecRunning(rid) { this.onJobComplete(rid, this._jobs.get(rid), 0, chalk.bold(chalk.cyan('RUNNING'))); } onSpecRetry(rid, job, retries = 0) { const delayMsg = this._specFileRetriesDelay > 0 ? ` after ${this._specFileRetriesDelay}s` : ''; this.onJobComplete(rid, job, retries, chalk.bold(chalk.yellow('RETRYING') + delayMsg)); } onSpecPass(rid, job, retries = 0) { this.onJobComplete(rid, job, retries, chalk.bold(chalk.green('PASSED'))); } onSpecFailure(rid, job, retries = 0) { this.onJobComplete(rid, job, retries, chalk.bold(chalk.red('FAILED'))); } onSpecSkip(rid, job) { this.onJobComplete(rid, job, 0, 'SKIPPED', log.info); } onJobComplete(cid, job, retries = 0, message = '', _logger = this.log) { const details = [`[${cid}]`, message]; if (job) { details.push('in', getRunnerName(job.caps), this.getFilenames(job.specs)); } if (retries > 0) { details.push(`(${retries} retries)`); } return _logger(...details); } onTestError(payload) { const error = { type: payload.error?.type || 'Error', message: payload.error?.message || (typeof payload.error === 'string' ? payload.error : 'Unknown error.'), stack: payload.error?.stack }; return this.log(`[${payload.cid}]`, `${chalk.red(error.type)} in "${payload.fullTitle}"\n${chalk.red(error.stack || error.message)}`); } getFilenames(specs = []) { if (specs.length > 0) { return '- ' + specs.join(', ').replace(new RegExp(`${process.cwd()}`, 'g'), ''); } return ''; } /** * add job to interface */ addJob({ cid, caps, specs, hasTests }) { this._jobs.set(cid, { caps, specs, hasTests }); if (hasTests) { this.onSpecRunning(cid); } else { this._skippedSpecs++; } } /** * clear job from interface */ clearJob({ cid, passed, retries }) { const job = this._jobs.get(cid); this._jobs.delete(cid); const retryAttempts = this._specFileRetries - retries; const retry = !passed && retries > 0; if (!retry) { this.result.finished++; } if (job && job.hasTests === false) { return this.onSpecSkip(cid, job); } if (passed) { this.result.passed++; this.onSpecPass(cid, job, retryAttempts); } else if (retry) { this.totalWorkerCnt++; this.result.retries++; this.onSpecRetry(cid, job, retryAttempts); } else { this.result.failed++; this.onSpecFailure(cid, job, retryAttempts); } } /** * for testing purposes call console log in a static method */ log(...args) { // eslint-disable-next-line no-console console.log(...args); return args; } logHookError(error) { if (error instanceof HookError) { return this.log(`${chalk.red(error.name)} in "${error.origin}"\n${chalk.red(error.stack || error.message)}`); } return this.log(`${chalk.red(error.name)}: ${chalk.red(error.stack || error.message)}`); } /** * event handler that is triggered when runner sends up events */ onMessage(event) { if (event.name === 'reporterRealTime') { this.log(event.content); return; } if (event.origin === 'debugger' && event.name === 'start') { this.log(chalk.yellow(event.params.introMessage)); this._inDebugMode = true; return this._inDebugMode; } if (event.origin === 'debugger' && event.name === 'stop') { this._inDebugMode = false; return this._inDebugMode; } if (event.name === 'testFrameworkInit') { return this.emit('job:start', event.content); } if (event.name === 'snapshot') { const snapshotResults = event.content; return snapshotResults.forEach((snapshotResult) => { this.#snapshotManager.add(snapshotResult); }); } if (event.name === 'error') { return this.log(`[${event.cid}]`, chalk.white(chalk.bgRed(chalk.bold(' Error: '))), event.content ? (event.content.message || event.content.stack || event.content) : ''); } if (event.origin !== 'reporter' && event.origin !== 'debugger') { /** * filter certain events though */ if (EVENT_FILTER.includes(event.name)) { return; } return this.log(event.cid, event.origin, event.name, event.content); } if (event.name === 'printFailureMessage') { return this.onTestError(event.content); } if (!this._messages[event.origin][event.name]) { this._messages[event.origin][event.name] = []; } this._messages[event.origin][event.name].push(event.content); } sigintTrigger() { /** * allow to exit repl mode via Ctrl+C */ if (this._inDebugMode) { return false; } const isRunning = this._jobs.size !== 0 || this._isWatchMode; const shutdownMessage = isRunning ? 'Ending WebDriver sessions gracefully ...\n' + '(press ctrl+c again to hard kill the runner)' : 'Ended WebDriver sessions gracefully after a SIGINT signal was received!'; return this.log('\n\n' + shutdownMessage); } printReporters() { /** * print reporter output */ const reporter = this._messages.reporter; this._messages.reporter = {}; for (const [reporterName, messages] of Object.entries(reporter)) { this.log('\n', chalk.bold(chalk.magenta(`"${reporterName}" Reporter:`))); this.log(messages.join('')); } } printSummary() { const totalJobs = this.totalWorkerCnt - this.result.retries; const elapsed = (new Date(Date.now() - this._start.getTime())).toUTCString().match(/(\d\d:\d\d:\d\d)/)[0]; const retries = this.result.retries ? chalk.yellow(this.result.retries, 'retries') + ', ' : ''; const failed = this.result.failed ? chalk.red(this.result.failed, 'failed') + ', ' : ''; const skipped = this._skippedSpecs > 0 ? chalk.gray(this._skippedSpecs, 'skipped') + ', ' : ''; const percentCompleted = totalJobs ? Math.round(this.result.finished / totalJobs * 100) : 0; const snapshotSummary = this.#snapshotManager.summary; const snapshotNotes = []; if (snapshotSummary.added > 0) { snapshotNotes.push(chalk.green(`${snapshotSummary.added} snapshot(s) added.`)); } if (snapshotSummary.updated > 0) { snapshotNotes.push(chalk.yellow(`${snapshotSummary.updated} snapshot(s) updated.`)); } if (snapshotSummary.unmatched > 0) { snapshotNotes.push(chalk.red(`${snapshotSummary.unmatched} snapshot(s) unmatched.`)); } if (snapshotSummary.unchecked > 0) { snapshotNotes.push(chalk.gray(`${snapshotSummary.unchecked} snapshot(s) unchecked.`)); } if (snapshotNotes.length > 0) { this.log('\nSnapshot Summary:'); snapshotNotes.forEach((note) => this.log(note)); } return this.log('\nSpec Files:\t', chalk.green(this.result.passed, 'passed') + ', ' + retries + failed + skipped + totalJobs, 'total', `(${percentCompleted}% completed)`, 'in', elapsed, this.#hasShard() ? `\nShard:\t\t ${this._config.shard.current} / ${this._config.shard.total}` : '', '\n'); } finalise() { this.printReporters(); this.printSummary(); } }