diff --git a/lib/codecept.js b/lib/codecept.js index 05203d241..97f257f50 100644 --- a/lib/codecept.js +++ b/lib/codecept.js @@ -120,23 +120,26 @@ class Codecept { * Executes hooks. */ async runHooks() { - // default hooks - dynamic imports for ESM - const listenerModules = [ - './listener/store.js', - './listener/steps.js', - './listener/config.js', - './listener/result.js', - './listener/helpers.js', - './listener/globalTimeout.js', - './listener/globalRetry.js', - './listener/retryEnhancer.js', - './listener/exit.js', - './listener/emptyRun.js', - ] - - for (const modulePath of listenerModules) { - const module = await import(modulePath) - runHook(module.default || module) + // For workers parent process we only need plugins/hooks. + // Core listeners are executed inside worker threads. + if (!this.opts?.skipDefaultListeners) { + const listenerModules = [ + './listener/store.js', + './listener/steps.js', + './listener/config.js', + './listener/result.js', + './listener/helpers.js', + './listener/globalTimeout.js', + './listener/globalRetry.js', + './listener/retryEnhancer.js', + './listener/exit.js', + './listener/emptyRun.js', + ] + + for (const modulePath of listenerModules) { + const module = await import(modulePath) + runHook(module.default || module) + } } // custom hooks (previous iteration of plugins) diff --git a/lib/command/run-workers.js b/lib/command/run-workers.js index fdbf84510..695a33365 100644 --- a/lib/command/run-workers.js +++ b/lib/command/run-workers.js @@ -41,6 +41,7 @@ export default async function (workerCount, selectedRuns, options) { output.print(`CodeceptJS v${Codecept.version()} ${output.standWithUkraine()}`) output.print(`Running tests in ${output.styles.bold(numberOfWorkers)} workers...`) store.hasWorkers = true + process.env.RUNS_WITH_WORKERS = 'true' const workers = new Workers(numberOfWorkers, config) workers.overrideConfig(overrideConfigs) diff --git a/lib/container.js b/lib/container.js index fc5bfb2e1..6bf89ba92 100644 --- a/lib/container.js +++ b/lib/container.js @@ -657,13 +657,28 @@ async function createPlugins(config, options = {}) { const enabledPluginsByOptions = (options.plugins || '').split(',') for (const pluginName in config) { if (!config[pluginName]) config[pluginName] = {} - if (!config[pluginName].enabled && enabledPluginsByOptions.indexOf(pluginName) < 0) { + const pluginConfig = config[pluginName] + if (!pluginConfig.enabled && enabledPluginsByOptions.indexOf(pluginName) < 0) { continue // plugin is disabled } + + // Generic workers gate: + // - runInWorker / runInWorkers controls plugin execution inside worker threads. + // - runInParent / runInMain can disable plugin in workers parent process. + const runInWorker = pluginConfig.runInWorker ?? pluginConfig.runInWorkers ?? (pluginName === 'testomatio' ? false : true) + const runInParent = pluginConfig.runInParent ?? pluginConfig.runInMain ?? true + + if (options.child && !runInWorker) { + continue + } + + if (!options.child && process.env.RUNS_WITH_WORKERS === 'true' && !runInParent) { + continue + } let module try { - if (config[pluginName].require) { - module = config[pluginName].require + if (pluginConfig.require) { + module = pluginConfig.require if (module.startsWith('.')) { // local module = path.resolve(global.codecept_dir, module) // custom plugin @@ -673,7 +688,7 @@ async function createPlugins(config, options = {}) { } // Use async loading for all plugins (ESM and CJS) - plugins[pluginName] = await loadPluginAsync(module, config[pluginName]) + plugins[pluginName] = await loadPluginAsync(module, pluginConfig) debug(`plugin ${pluginName} loaded via async import`) } catch (err) { throw new Error(`Could not load plugin ${pluginName} from module '${module}':\n${err.message}\n${err.stack}`) diff --git a/lib/workers.js b/lib/workers.js index 0ed3a71b3..367939e90 100644 --- a/lib/workers.js +++ b/lib/workers.js @@ -28,7 +28,7 @@ const pathToWorker = path.join(__dirname, 'command', 'workers', 'runTests.js') const initializeCodecept = async (configPath, options = {}) => { const config = await mainConfig.load(configPath || '.') - const codecept = new Codecept(config, options) + const codecept = new Codecept(config, { ...options, skipDefaultListeners: true }) await codecept.init(getTestRoot(configPath)) codecept.loadTests() @@ -625,13 +625,32 @@ class Workers extends EventEmitter { break case event.suite.before: - this.emit(event.suite.before, deserializeSuite(message.data)) + { + const suite = deserializeSuite(message.data) + this.emit(event.suite.before, suite) + event.dispatcher.emit(event.suite.before, suite) + } + break + case event.suite.after: + { + const suite = deserializeSuite(message.data) + this.emit(event.suite.after, suite) + event.dispatcher.emit(event.suite.after, suite) + } break case event.test.before: - this.emit(event.test.before, deserializeTest(message.data)) + { + const test = deserializeTest(message.data) + this.emit(event.test.before, test) + event.dispatcher.emit(event.test.before, test) + } break case event.test.started: - this.emit(event.test.started, deserializeTest(message.data)) + { + const test = deserializeTest(message.data) + this.emit(event.test.started, test) + event.dispatcher.emit(event.test.started, test) + } break case event.test.failed: // For hook failures, emit immediately as there won't be a test.finished event @@ -645,7 +664,11 @@ class Workers extends EventEmitter { // Skip individual passed events - we'll emit based on finished state break case event.test.skipped: - this.emit(event.test.skipped, deserializeTest(message.data)) + { + const test = deserializeTest(message.data) + this.emit(event.test.skipped, test) + event.dispatcher.emit(event.test.skipped, test) + } break case event.test.finished: // Handle different types of test completion properly @@ -674,28 +697,47 @@ class Workers extends EventEmitter { } } - this.emit(event.test.finished, deserializeTest(data)) + const test = deserializeTest(data) + this.emit(event.test.finished, test) + event.dispatcher.emit(event.test.finished, test) } break case event.test.after: - this.emit(event.test.after, deserializeTest(message.data)) + { + const test = deserializeTest(message.data) + this.emit(event.test.after, test) + event.dispatcher.emit(event.test.after, test) + } break case event.step.finished: this.emit(event.step.finished, message.data) + event.dispatcher.emit(event.step.finished, message.data) break case event.step.started: this.emit(event.step.started, message.data) + event.dispatcher.emit(event.step.started, message.data) break case event.step.passed: this.emit(event.step.passed, message.data) + event.dispatcher.emit(event.step.passed, message.data) break case event.step.failed: this.emit(event.step.failed, message.data, message.data.error) + event.dispatcher.emit(event.step.failed, message.data, message.data.error) break case event.hook.failed: // Hook failures are already reported as test failures by the worker // Just emit the hook.failed event for listeners this.emit(event.hook.failed, message.data) + event.dispatcher.emit(event.hook.failed, message.data) + break + case event.hook.passed: + this.emit(event.hook.passed, message.data) + event.dispatcher.emit(event.hook.passed, message.data) + break + case event.hook.finished: + this.emit(event.hook.finished, message.data) + event.dispatcher.emit(event.hook.finished, message.data) break } }) diff --git a/test/helper/webapi.js b/test/helper/webapi.js index 584689bd4..2f69a036c 100644 --- a/test/helper/webapi.js +++ b/test/helper/webapi.js @@ -1580,7 +1580,8 @@ export function tests() { }, ) } catch (e) { - expect(e.message).to.include('expected all elements ({css: a[href="/codeceptjs/CodeceptJS"]}) to have attributes {"disable":true} "0" to equal "3"') + expect(e.message).to.include('expected all elements ({css: a[href="/codeceptjs/CodeceptJS"]}) to have attributes {"disable":true}') + expect(e.message).to.match(/"0" to equal "\d+"/) } })