codeceptjs 4.0.0-beta.7.esm-aria → 4.0.0-beta.8.esm-aria

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +46 -3
  2. package/bin/codecept.js +9 -0
  3. package/bin/test-server.js +64 -0
  4. package/docs/webapi/click.mustache +5 -1
  5. package/lib/ai.js +66 -102
  6. package/lib/codecept.js +99 -24
  7. package/lib/command/generate.js +33 -1
  8. package/lib/command/init.js +7 -3
  9. package/lib/command/run-workers.js +31 -2
  10. package/lib/command/run.js +15 -0
  11. package/lib/command/workers/runTests.js +331 -58
  12. package/lib/config.js +16 -5
  13. package/lib/container.js +15 -13
  14. package/lib/effects.js +1 -1
  15. package/lib/element/WebElement.js +327 -0
  16. package/lib/event.js +10 -1
  17. package/lib/helper/AI.js +11 -11
  18. package/lib/helper/ApiDataFactory.js +34 -6
  19. package/lib/helper/Appium.js +156 -42
  20. package/lib/helper/GraphQL.js +3 -3
  21. package/lib/helper/GraphQLDataFactory.js +4 -4
  22. package/lib/helper/JSONResponse.js +48 -40
  23. package/lib/helper/Mochawesome.js +24 -2
  24. package/lib/helper/Playwright.js +841 -153
  25. package/lib/helper/Puppeteer.js +263 -67
  26. package/lib/helper/REST.js +21 -0
  27. package/lib/helper/WebDriver.js +105 -16
  28. package/lib/helper/clientscripts/PollyWebDriverExt.js +1 -1
  29. package/lib/helper/extras/PlaywrightReactVueLocator.js +52 -0
  30. package/lib/helper/extras/PlaywrightRestartOpts.js +12 -1
  31. package/lib/helper/network/actions.js +8 -6
  32. package/lib/listener/config.js +11 -3
  33. package/lib/listener/enhancedGlobalRetry.js +110 -0
  34. package/lib/listener/globalTimeout.js +19 -4
  35. package/lib/listener/helpers.js +8 -2
  36. package/lib/listener/retryEnhancer.js +85 -0
  37. package/lib/listener/steps.js +12 -0
  38. package/lib/mocha/asyncWrapper.js +13 -3
  39. package/lib/mocha/cli.js +1 -1
  40. package/lib/mocha/factory.js +3 -0
  41. package/lib/mocha/gherkin.js +1 -1
  42. package/lib/mocha/test.js +6 -0
  43. package/lib/mocha/ui.js +13 -0
  44. package/lib/output.js +62 -18
  45. package/lib/plugin/coverage.js +16 -3
  46. package/lib/plugin/enhancedRetryFailedStep.js +99 -0
  47. package/lib/plugin/htmlReporter.js +3648 -0
  48. package/lib/plugin/retryFailedStep.js +1 -0
  49. package/lib/plugin/stepByStepReport.js +1 -1
  50. package/lib/recorder.js +28 -3
  51. package/lib/result.js +100 -23
  52. package/lib/retryCoordinator.js +207 -0
  53. package/lib/step/base.js +1 -1
  54. package/lib/step/comment.js +2 -2
  55. package/lib/step/meta.js +1 -1
  56. package/lib/template/heal.js +1 -1
  57. package/lib/template/prompts/generatePageObject.js +31 -0
  58. package/lib/template/prompts/healStep.js +13 -0
  59. package/lib/template/prompts/writeStep.js +9 -0
  60. package/lib/test-server.js +334 -0
  61. package/lib/utils/mask_data.js +47 -0
  62. package/lib/utils.js +87 -6
  63. package/lib/workerStorage.js +2 -1
  64. package/lib/workers.js +179 -23
  65. package/package.json +58 -47
  66. package/typings/index.d.ts +19 -7
  67. package/typings/promiseBasedTypes.d.ts +5525 -3759
  68. package/typings/types.d.ts +5791 -3781
@@ -288,7 +288,7 @@ export async function heal(genPath) {
288
288
  import './heal.js'
289
289
 
290
290
  export const config = {
291
- // ...
291
+ // ...
292
292
  plugins: {
293
293
  heal: {
294
294
  enabled: true
@@ -301,3 +301,35 @@ export const config = {
301
301
  if (!safeFileWrite(healFile, healTemplate)) return
302
302
  output.success(`Heal recipes were created in ${healFile}`)
303
303
  }
304
+
305
+ export async function prompt(promptName, genPath) {
306
+ if (!promptName) {
307
+ output.error('Please specify prompt name: writeStep, healStep, or generatePageObject')
308
+ output.print('Usage: npx codeceptjs generate:prompt <promptName>')
309
+ return
310
+ }
311
+
312
+ const validPrompts = ['writeStep', 'healStep', 'generatePageObject']
313
+ if (!validPrompts.includes(promptName)) {
314
+ output.error(`Invalid prompt name: ${promptName}`)
315
+ output.print(`Valid prompts: ${validPrompts.join(', ')}`)
316
+ return
317
+ }
318
+
319
+ const testsPath = getTestRoot(genPath)
320
+
321
+ const promptsDir = path.join(testsPath, 'prompts')
322
+ if (!fileExists(promptsDir)) {
323
+ mkdirp.sync(promptsDir)
324
+ }
325
+
326
+ const templatePath = path.join(__dirname, `../template/prompts/${promptName}.js`)
327
+ const promptContent = fs.readFileSync(templatePath, 'utf8')
328
+
329
+ const promptFile = path.join(promptsDir, `${promptName}.${extension}`)
330
+ if (!safeFileWrite(promptFile, promptContent)) return
331
+
332
+ output.success(`Prompt ${promptName} was created in ${promptFile}`)
333
+ output.print('Customize this prompt to fit your needs.')
334
+ output.print('This prompt will be automatically loaded when AI features are enabled.')
335
+ }
@@ -19,6 +19,11 @@ const defaultConfig = {
19
19
  output: '',
20
20
  helpers: {},
21
21
  include: {},
22
+ plugins: {
23
+ htmlReporter: {
24
+ enabled: true,
25
+ },
26
+ },
22
27
  }
23
28
 
24
29
  const helpers = ['Playwright', 'WebDriver', 'Puppeteer', 'REST', 'GraphQL', 'Appium']
@@ -35,7 +40,6 @@ const packages = []
35
40
  let isTypeScript = false
36
41
  let extension = 'js'
37
42
 
38
- const requireCodeceptConfigure = "const { setHeadlessWhen, setCommonPlugins } = require('@codeceptjs/configure');"
39
43
  const importCodeceptConfigure = "import { setHeadlessWhen, setCommonPlugins } from '@codeceptjs/configure';"
40
44
 
41
45
  const configHeader = `
@@ -232,9 +236,9 @@ export default async function (initPath) {
232
236
  fs.writeFileSync(typeScriptconfigFile, configSource, 'utf-8')
233
237
  print(`Config created at ${typeScriptconfigFile}`)
234
238
  } else {
235
- configSource = beautify(`/** @type {CodeceptJS.MainConfig} */\nexports.config = ${inspect(config, false, 4, false)}`)
239
+ configSource = beautify(`/** @type {CodeceptJS.MainConfig} */\nexport const config = ${inspect(config, false, 4, false)}`)
236
240
 
237
- if (hasConfigure) configSource = requireCodeceptConfigure + configHeader + configSource
241
+ if (hasConfigure) configSource = importCodeceptConfigure + configHeader + configSource
238
242
 
239
243
  fs.writeFileSync(configFile, configSource, 'utf-8')
240
244
  print(`Config created at ${configFile}`)
@@ -12,7 +12,22 @@ export default async function (workerCount, selectedRuns, options) {
12
12
 
13
13
  const { config: testConfig, override = '' } = options
14
14
  const overrideConfigs = tryOrDefault(() => JSON.parse(override), {})
15
- const by = options.suites ? 'suite' : 'test'
15
+
16
+ // Determine test split strategy
17
+ let by = 'test' // default
18
+ if (options.by) {
19
+ // Explicit --by option takes precedence
20
+ by = options.by
21
+ } else if (options.suites) {
22
+ // Legacy --suites option
23
+ by = 'suite'
24
+ }
25
+
26
+ // Validate the by option
27
+ const validStrategies = ['test', 'suite', 'pool']
28
+ if (!validStrategies.includes(by)) {
29
+ throw new Error(`Invalid --by strategy: ${by}. Valid options are: ${validStrategies.join(', ')}`)
30
+ }
16
31
  delete options.parent
17
32
  const config = {
18
33
  by,
@@ -57,8 +72,22 @@ export default async function (workerCount, selectedRuns, options) {
57
72
  await workers.run()
58
73
  } catch (err) {
59
74
  output.error(err)
60
- process.exit(1)
75
+ process.exitCode = 1
61
76
  } finally {
62
77
  await workers.teardownAll()
78
+
79
+ // Force exit if event loop doesn't clear naturally
80
+ // This is needed because worker threads may leave handles open
81
+ // even after proper cleanup, preventing natural process termination
82
+ if (!options.noExit) {
83
+ // Use beforeExit to ensure we run after all other exit handlers
84
+ // have set the correct exit code
85
+ process.once('beforeExit', (code) => {
86
+ // Give cleanup a moment to complete, then force exit with the correct code
87
+ setTimeout(() => {
88
+ process.exit(code || process.exitCode || 0)
89
+ }, 100)
90
+ })
91
+ }
63
92
  }
64
93
  }
@@ -2,6 +2,7 @@ import { getConfig, printError, getTestRoot, createOutputDir } from './utils.js'
2
2
  import Config from '../config.js'
3
3
  import store from '../store.js'
4
4
  import Codecept from '../codecept.js'
5
+ import container from '../container.js'
5
6
 
6
7
  export default async function (test, options) {
7
8
  // registering options globally to use in config
@@ -42,5 +43,19 @@ export default async function (test, options) {
42
43
  process.exitCode = 1
43
44
  } finally {
44
45
  await codecept.teardown()
46
+
47
+ // Schedule a delayed exit to prevent process hanging due to browser helper event loops
48
+ // Only needed for Playwright/Puppeteer which keep the event loop alive
49
+ // Wait 1 second to allow final cleanup and output to complete
50
+ if (!process.env.CODECEPT_DISABLE_AUTO_EXIT) {
51
+ const helpers = container.helpers()
52
+ const hasBrowserHelper = helpers && (helpers.Playwright || helpers.Puppeteer || helpers.WebDriver)
53
+
54
+ if (hasBrowserHelper) {
55
+ setTimeout(() => {
56
+ process.exit(process.exitCode || 0)
57
+ }, 1000).unref()
58
+ }
59
+ }
45
60
  }
46
61
  }
@@ -8,72 +8,336 @@ if (!tty.getWindowSize) {
8
8
  }
9
9
 
10
10
  import { parentPort, workerData } from 'worker_threads'
11
- import event from '../../event.js'
12
- import container from '../../container.js'
13
- import { getConfig } from '../utils.js'
14
- import { tryOrDefault, deepMerge } from '../../utils.js'
11
+
12
+ // Delay imports to avoid ES Module loader race conditions in Node 22.x worker threads
13
+ // These will be imported dynamically when needed
14
+ let event, container, Codecept, getConfig, tryOrDefault, deepMerge
15
15
 
16
16
  let stdout = ''
17
17
 
18
18
  const stderr = ''
19
19
 
20
- // Importing of Codecept need to be after tty.getWindowSize is available.
21
- import Codecept from '../../codecept.js'
22
-
23
- const { options, tests, testRoot, workerIndex } = workerData
20
+ const { options, tests, testRoot, workerIndex, poolMode } = workerData
24
21
 
25
22
  // hide worker output
26
- if (!options.debug && !options.verbose)
23
+ // In pool mode, only suppress output if debug is NOT enabled
24
+ // In regular mode, hide result output but allow step output in verbose/debug
25
+ if (poolMode && !options.debug) {
26
+ // In pool mode without debug, allow test names and important output but suppress verbose details
27
+ const originalWrite = process.stdout.write
28
+ process.stdout.write = string => {
29
+ // Allow test names (✔ or ✖), Scenario Steps, failures, and important markers
30
+ if (
31
+ string.includes('✔') ||
32
+ string.includes('✖') ||
33
+ string.includes('Scenario Steps:') ||
34
+ string.includes('◯ Scenario Steps:') ||
35
+ string.includes('-- FAILURES:') ||
36
+ string.includes('AssertionError:') ||
37
+ string.includes('Feature(')
38
+ ) {
39
+ return originalWrite.call(process.stdout, string)
40
+ }
41
+ // Suppress result summaries to avoid duplicates
42
+ if (string.includes(' FAIL |') || string.includes(' OK |') || string.includes('◯ File:')) {
43
+ return true
44
+ }
45
+ return originalWrite.call(process.stdout, string)
46
+ }
47
+ } else if (!poolMode && !options.debug && !options.verbose) {
27
48
  process.stdout.write = string => {
28
49
  stdout += string
29
50
  return true
30
51
  }
52
+ } else {
53
+ // In verbose/debug mode for test/suite modes, show step details
54
+ // but suppress individual worker result summaries to avoid duplicate output
55
+ const originalWrite = process.stdout.write
56
+ const originalConsoleLog = console.log
57
+
58
+ process.stdout.write = string => {
59
+ // Suppress individual worker result summaries and failure reports
60
+ if (string.includes(' FAIL |') || string.includes(' OK |') || string.includes('-- FAILURES:') || string.includes('AssertionError:') || string.includes('◯ File:') || string.includes('◯ Scenario Steps:')) {
61
+ return true
62
+ }
63
+ return originalWrite.call(process.stdout, string)
64
+ }
31
65
 
32
- const overrideConfigs = tryOrDefault(() => JSON.parse(options.override), {})
66
+ // Override console.log to catch result summaries
67
+ console.log = (...args) => {
68
+ const fullMessage = args.join(' ')
69
+ if (fullMessage.includes(' FAIL |') || fullMessage.includes(' OK |') || fullMessage.includes('-- FAILURES:')) {
70
+ return
71
+ }
72
+ return originalConsoleLog.apply(console, args)
73
+ }
74
+ }
33
75
 
34
- // important deep merge so dynamic things e.g. functions on config are not overridden
35
- const config = deepMerge(getConfig(options.config || testRoot), overrideConfigs)
76
+ // Declare codecept and mocha at module level so they can be accessed by functions
77
+ let codecept
78
+ let mocha
79
+ let initPromise
80
+ let config
36
81
 
37
82
  // Load test and run
38
- ;(async function () {
39
- const codecept = new Codecept(config, options)
40
- await codecept.init(testRoot)
41
- codecept.loadTests()
42
- const mocha = container.mocha()
83
+ initPromise = (async function () {
84
+ try {
85
+ // Import modules dynamically to avoid ES Module loader race conditions in Node 22.x
86
+ const eventModule = await import('../../event.js')
87
+ const containerModule = await import('../../container.js')
88
+ const utilsModule = await import('../utils.js')
89
+ const coreUtilsModule = await import('../../utils.js')
90
+ const CodeceptModule = await import('../../codecept.js')
91
+
92
+ event = eventModule.default
93
+ container = containerModule.default
94
+ getConfig = utilsModule.getConfig
95
+ tryOrDefault = coreUtilsModule.tryOrDefault
96
+ deepMerge = coreUtilsModule.deepMerge
97
+ Codecept = CodeceptModule.default
98
+
99
+ const overrideConfigs = tryOrDefault(() => JSON.parse(options.override), {})
100
+
101
+ // IMPORTANT: await is required here since getConfig is async
102
+ const baseConfig = await getConfig(options.config || testRoot)
103
+
104
+ // important deep merge so dynamic things e.g. functions on config are not overridden
105
+ config = deepMerge(baseConfig, overrideConfigs)
106
+
107
+ codecept = new Codecept(config, options)
108
+ await codecept.init(testRoot)
109
+ codecept.loadTests()
110
+ mocha = container.mocha()
43
111
 
44
- function filterTests() {
45
- const files = codecept.testFiles
46
- mocha.files = files
47
- mocha.loadFiles()
112
+ if (poolMode) {
113
+ // In pool mode, don't filter tests upfront - wait for assignments
114
+ // We'll reload test files fresh for each test request
115
+ } else {
116
+ // Legacy mode - filter tests upfront
117
+ filterTests()
118
+ }
48
119
 
49
- for (const suite of mocha.suite.suites) {
50
- suite.tests = suite.tests.filter(test => tests.indexOf(test.uid) >= 0)
120
+ // run tests
121
+ if (poolMode) {
122
+ await runPoolTests()
123
+ } else if (mocha.suite.total()) {
124
+ await runTests()
125
+ } else {
126
+ // No tests to run, close the worker
127
+ parentPort?.close()
51
128
  }
129
+ } catch (err) {
130
+ console.error('Error in worker initialization:', err)
131
+ process.exit(1)
52
132
  }
133
+ })()
53
134
 
54
- async function runTests() {
55
- try {
56
- await codecept.bootstrap()
57
- } catch (err) {
58
- throw new Error(`Error while running bootstrap file :${err}`)
135
+ let globalStats = { passes: 0, failures: 0, tests: 0, pending: 0, failedHooks: 0 }
136
+
137
+ async function runTests() {
138
+ try {
139
+ await codecept.bootstrap()
140
+ } catch (err) {
141
+ throw new Error(`Error while running bootstrap file :${err}`)
142
+ }
143
+ listenToParentThread()
144
+ initializeListeners()
145
+ disablePause()
146
+ try {
147
+ await codecept.run()
148
+ } finally {
149
+ await codecept.teardown()
150
+ }
151
+ }
152
+
153
+ async function runPoolTests() {
154
+ try {
155
+ await codecept.bootstrap()
156
+ } catch (err) {
157
+ throw new Error(`Error while running bootstrap file :${err}`)
158
+ }
159
+
160
+ initializeListeners()
161
+ disablePause()
162
+
163
+ // Emit event.all.before once at the start of pool mode
164
+ event.dispatcher.emit(event.all.before, codecept)
165
+
166
+ // Accumulate results across all tests in pool mode
167
+ let consolidatedStats = { passes: 0, failures: 0, tests: 0, pending: 0, failedHooks: 0 }
168
+ let allTests = []
169
+ let allFailures = []
170
+ let previousStats = { passes: 0, failures: 0, tests: 0, pending: 0, failedHooks: 0 }
171
+
172
+ // Keep requesting tests until no more available
173
+ while (true) {
174
+ // Request a test assignment and wait for response
175
+ const testResult = await new Promise((resolve, reject) => {
176
+ // Set up pool mode message handler FIRST before sending request
177
+ const messageHandler = async eventData => {
178
+ // Remove handler immediately to prevent duplicate processing
179
+ parentPort?.off('message', messageHandler)
180
+
181
+ if (eventData.type === 'TEST_ASSIGNED') {
182
+ // In pool mode with ESM, we receive test FILE paths instead of UIDs
183
+ // because UIDs are not stable across different mocha instances
184
+ const testIdentifier = eventData.test
185
+
186
+ try {
187
+ // Create a fresh Mocha instance for each test file
188
+ container.createMocha()
189
+ const mocha = container.mocha()
190
+
191
+ // Load only the assigned test file
192
+ mocha.files = [testIdentifier]
193
+ mocha.loadFiles()
194
+
195
+ try {
196
+ require('fs').appendFileSync('/tmp/config_listener_debug.log', `${new Date().toISOString()} [POOL] Loaded ${testIdentifier}, tests: ${mocha.suite.total()}\n`)
197
+ } catch (e) { /* ignore */ }
198
+
199
+ if (mocha.suite.total() > 0) {
200
+ // Run only the tests in the current mocha suite
201
+ // Don't use codecept.run() as it overwrites mocha.files with ALL test files
202
+ await new Promise((resolve, reject) => {
203
+ mocha.run(() => {
204
+ try {
205
+ require('fs').appendFileSync('/tmp/config_listener_debug.log', `${new Date().toISOString()} [POOL] Finished ${testIdentifier}\n`)
206
+ } catch (e) { /* ignore */ }
207
+ resolve()
208
+ })
209
+ })
210
+
211
+ // Get the results from this specific test run
212
+ const result = container.result()
213
+ const currentStats = result.stats || {}
214
+
215
+ // Calculate the difference from previous accumulated stats
216
+ const newPasses = Math.max(0, (currentStats.passes || 0) - previousStats.passes)
217
+ const newFailures = Math.max(0, (currentStats.failures || 0) - previousStats.failures)
218
+ const newTests = Math.max(0, (currentStats.tests || 0) - previousStats.tests)
219
+ const newPending = Math.max(0, (currentStats.pending || 0) - previousStats.pending)
220
+ const newFailedHooks = Math.max(0, (currentStats.failedHooks || 0) - previousStats.failedHooks)
221
+
222
+ // Add only the new results
223
+ consolidatedStats.passes += newPasses
224
+ consolidatedStats.failures += newFailures
225
+ consolidatedStats.tests += newTests
226
+ consolidatedStats.pending += newPending
227
+ consolidatedStats.failedHooks += newFailedHooks
228
+
229
+ // Update previous stats for next comparison
230
+ previousStats = { ...currentStats }
231
+
232
+ // Add new failures to consolidated collections
233
+ if (result.failures && result.failures.length > allFailures.length) {
234
+ const newFailures = result.failures.slice(allFailures.length)
235
+ allFailures.push(...newFailures)
236
+ }
237
+ }
238
+
239
+ // Signal test completed
240
+ resolve('TEST_COMPLETED')
241
+ } catch (err) {
242
+ reject(err)
243
+ }
244
+ } else if (eventData.type === 'NO_MORE_TESTS') {
245
+ // No tests available, exit worker
246
+ resolve('NO_MORE_TESTS')
247
+ } else {
248
+ // Handle other message types (support messages, etc.)
249
+ container.append({ support: eventData.data })
250
+ // Don't re-add handler - each test request creates its own one-time handler
251
+ }
252
+ }
253
+
254
+ // Set up handler BEFORE sending request to avoid race condition
255
+ parentPort?.on('message', messageHandler)
256
+
257
+ // Now send the request
258
+ sendToParentThread({ type: 'REQUEST_TEST', workerIndex })
259
+ })
260
+
261
+ // Exit if no more tests
262
+ if (testResult === 'NO_MORE_TESTS') {
263
+ break
59
264
  }
60
- listenToParentThread()
61
- initializeListeners()
62
- disablePause()
63
- try {
64
- await codecept.run()
65
- } finally {
66
- await codecept.teardown()
265
+ }
266
+
267
+ // Emit event.all.after once at the end of pool mode
268
+ event.dispatcher.emit(event.all.after, codecept)
269
+
270
+ try {
271
+ await codecept.teardown()
272
+ } catch (err) {
273
+ // Log teardown errors but don't fail
274
+ console.error('Teardown error:', err)
275
+ }
276
+
277
+ // Send final consolidated results for the entire worker
278
+ const finalResult = {
279
+ hasFailed: consolidatedStats.failures > 0,
280
+ stats: consolidatedStats,
281
+ duration: 0, // Pool mode doesn't track duration per worker
282
+ tests: [], // Keep tests empty to avoid serialization issues - stats are sufficient
283
+ failures: allFailures, // Include all failures for error reporting
284
+ }
285
+
286
+ sendToParentThread({ event: event.all.after, workerIndex, data: finalResult })
287
+ sendToParentThread({ event: event.all.result, workerIndex, data: finalResult })
288
+
289
+ // Add longer delay to ensure messages are delivered before closing
290
+ await new Promise(resolve => setTimeout(resolve, 100))
291
+
292
+ // Close worker thread when pool mode is complete
293
+ parentPort?.close()
294
+ }
295
+
296
+ function filterTestById(testUid) {
297
+ // In pool mode with ESM, test files are already loaded once at initialization
298
+ // We just need to filter the existing mocha suite to only include the target test
299
+
300
+ // Get the existing mocha instance
301
+ const mocha = container.mocha()
302
+
303
+ // Save reference to all suites before clearing
304
+ const allSuites = [...mocha.suite.suites]
305
+
306
+ // Clear suites and tests but preserve other mocha settings
307
+ mocha.suite.suites = []
308
+ mocha.suite.tests = []
309
+
310
+ // Find and add only the suite containing our target test
311
+ let foundTest = false
312
+ for (const suite of allSuites) {
313
+ const originalTests = [...suite.tests]
314
+
315
+ // Check if this suite has our target test
316
+ const targetTest = originalTests.find(test => test.uid === testUid)
317
+
318
+ if (targetTest) {
319
+ // Create a filtered suite with only the target test
320
+ suite.tests = [targetTest]
321
+ mocha.suite.suites.push(suite)
322
+ foundTest = true
323
+ break // Only include one test
67
324
  }
68
325
  }
69
326
 
70
- filterTests()
327
+ if (!foundTest) {
328
+ console.error(`WARNING: Test with UID ${testUid} not found in mocha suites`)
329
+ }
330
+ }
71
331
 
72
- // run tests
73
- if (mocha.suite.total()) {
74
- await runTests()
332
+ function filterTests() {
333
+ const files = codecept.testFiles
334
+ mocha.files = files
335
+ mocha.loadFiles()
336
+
337
+ for (const suite of mocha.suite.suites) {
338
+ suite.tests = suite.tests.filter(test => tests.indexOf(test.uid) >= 0)
75
339
  }
76
- })()
340
+ }
77
341
 
78
342
  function initializeListeners() {
79
343
  // suite
@@ -92,10 +356,11 @@ function initializeListeners() {
92
356
  const serializableErr = serializeError(err)
93
357
  safelySendToParent({ event: event.test.finished, workerIndex, data: { ...simplifiedData, err: serializableErr } })
94
358
  })
95
- event.dispatcher.on(event.test.failed, (test, err) => {
359
+ event.dispatcher.on(event.test.failed, (test, err, hookName) => {
96
360
  const simplifiedData = test.simplify()
97
361
  const serializableErr = serializeError(err)
98
- safelySendToParent({ event: event.test.failed, workerIndex, data: { ...simplifiedData, err: serializableErr } })
362
+ // Include hookName to identify hook failures
363
+ safelySendToParent({ event: event.test.failed, workerIndex, data: { ...simplifiedData, err: serializableErr, hookName } })
99
364
  })
100
365
  event.dispatcher.on(event.test.passed, (test, err) => safelySendToParent({ event: event.test.passed, workerIndex, data: { ...test.simplify(), err } }))
101
366
  event.dispatcher.on(event.test.started, test => safelySendToParent({ event: event.test.started, workerIndex, data: test.simplify() }))
@@ -107,19 +372,24 @@ function initializeListeners() {
107
372
  event.dispatcher.on(event.step.passed, step => safelySendToParent({ event: event.step.passed, workerIndex, data: step.simplify() }))
108
373
  event.dispatcher.on(event.step.failed, step => safelySendToParent({ event: event.step.failed, workerIndex, data: step.simplify() }))
109
374
 
110
- event.dispatcher.on(event.hook.failed, (hook, err) => {
111
- const serializableErr = serializeError(err)
112
- safelySendToParent({ event: event.hook.failed, workerIndex, data: { ...hook.simplify(), err: serializableErr } })
113
- })
114
- event.dispatcher.on(event.hook.passed, hook => safelySendToParent({ event: event.hook.passed, workerIndex, data: hook.simplify() }))
115
- event.dispatcher.on(event.hook.finished, hook => safelySendToParent({ event: event.hook.finished, workerIndex, data: hook.simplify() }))
116
-
117
- event.dispatcher.once(event.all.after, () => safelySendToParent({ event: event.all.after, workerIndex, data: container.result().simplify() }))
118
- // all
119
- event.dispatcher.once(event.all.result, () => {
120
- safelySendToParent({ event: event.all.result, workerIndex, data: container.result().simplify() })
121
- parentPort?.close()
122
- })
375
+ event.dispatcher.on(event.hook.failed, (hook, err) => sendToParentThread({ event: event.hook.failed, workerIndex, data: { ...hook.simplify(), err } }))
376
+ event.dispatcher.on(event.hook.passed, hook => sendToParentThread({ event: event.hook.passed, workerIndex, data: hook.simplify() }))
377
+ event.dispatcher.on(event.hook.finished, hook => sendToParentThread({ event: event.hook.finished, workerIndex, data: hook.simplify() }))
378
+
379
+ if (!poolMode) {
380
+ // In regular mode, close worker after all tests are complete
381
+ event.dispatcher.once(event.all.after, () => {
382
+ sendToParentThread({ event: event.all.after, workerIndex, data: container.result().simplify() })
383
+ })
384
+ // all
385
+ event.dispatcher.once(event.all.result, () => {
386
+ sendToParentThread({ event: event.all.result, workerIndex, data: container.result().simplify() })
387
+ parentPort?.close()
388
+ })
389
+ } else {
390
+ // In pool mode, don't send result events for individual tests
391
+ // Results will be sent once when the worker completes all tests
392
+ }
123
393
  }
124
394
 
125
395
  function disablePause() {
@@ -175,7 +445,10 @@ function sendToParentThread(data) {
175
445
  }
176
446
 
177
447
  function listenToParentThread() {
178
- parentPort?.on('message', eventData => {
179
- container.append({ support: eventData.data })
180
- })
448
+ if (!poolMode) {
449
+ parentPort?.on('message', eventData => {
450
+ container.append({ support: eventData.data })
451
+ })
452
+ }
453
+ // In pool mode, message handling is done in runPoolTests()
181
454
  }
package/lib/config.js CHANGED
@@ -34,7 +34,7 @@ const defaultConfig = {
34
34
  let hooks = []
35
35
  let config = {}
36
36
 
37
- const configFileNames = ['codecept.config.js', 'codecept.conf.js', 'codecept.js', 'codecept.config.ts', 'codecept.conf.ts']
37
+ const configFileNames = ['codecept.config.js', 'codecept.conf.js', 'codecept.js', 'codecept.config.cjs', 'codecept.conf.cjs', 'codecept.config.ts', 'codecept.conf.ts']
38
38
 
39
39
  /**
40
40
  * Current configuration
@@ -69,9 +69,20 @@ class Config {
69
69
  configFile = path.resolve(configFile || '.')
70
70
 
71
71
  if (!fileExists(configFile)) {
72
- configFile = configFile.replace('.js', '.ts')
73
-
74
- if (!fileExists(configFile)) {
72
+ // Try different extensions if the file doesn't exist
73
+ const extensions = ['.ts', '.cjs', '.mjs']
74
+ let found = false
75
+
76
+ for (const ext of extensions) {
77
+ const altConfig = configFile.replace(/\.js$/, ext)
78
+ if (fileExists(altConfig)) {
79
+ configFile = altConfig
80
+ found = true
81
+ break
82
+ }
83
+ }
84
+
85
+ if (!found) {
75
86
  throw new Error(`Config file ${configFile} does not exist. Execute 'codeceptjs init' to create config`)
76
87
  }
77
88
  }
@@ -198,7 +209,7 @@ async function loadConfigFile(configFile) {
198
209
  }
199
210
  }
200
211
 
201
- const rawConfig = configModule.config || configModule.default?.config || configModule
212
+ const rawConfig = configModule.config || configModule.default?.config || configModule.default || configModule
202
213
 
203
214
  // Process helpers to extract imported classes
204
215
  if (rawConfig.helpers) {