Skip to content
37 changes: 20 additions & 17 deletions lib/codecept.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions lib/command/run-workers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
23 changes: 19 additions & 4 deletions lib/container.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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}`)
Expand Down
56 changes: 49 additions & 7 deletions lib/workers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
}
})
Expand Down
3 changes: 2 additions & 1 deletion test/helper/webapi.js
Original file line number Diff line number Diff line change
Expand Up @@ -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+"/)
}
})

Expand Down