codeceptjs 4.0.2-beta.1 → 4.0.2-beta.10

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.
@@ -40,11 +40,22 @@ export default async function (workerCount, selectedRuns, options) {
40
40
 
41
41
  output.print(`CodeceptJS v${Codecept.version()} ${output.standWithUkraine()}`)
42
42
  output.print(`Running tests in ${output.styles.bold(numberOfWorkers)} workers...`)
43
- output.print()
44
43
  store.hasWorkers = true
45
44
 
46
45
  const workers = new Workers(numberOfWorkers, config)
47
46
  workers.overrideConfig(overrideConfigs)
47
+
48
+ // Show test distribution after workers are initialized
49
+ await workers.bootstrapAll()
50
+
51
+ const workerObjects = workers.getWorkers()
52
+ output.print()
53
+ output.print('Test distribution:')
54
+ workerObjects.forEach((worker, index) => {
55
+ const testCount = worker.tests.length
56
+ output.print(` Worker ${index + 1}: ${testCount} test${testCount !== 1 ? 's' : ''}`)
57
+ })
58
+ output.print()
48
59
 
49
60
  workers.on(event.test.failed, test => {
50
61
  output.test.failed(test)
@@ -68,7 +79,6 @@ export default async function (workerCount, selectedRuns, options) {
68
79
  if (options.verbose) {
69
80
  await getMachineInfo()
70
81
  }
71
- await workers.bootstrapAll()
72
82
  await workers.run()
73
83
  } catch (err) {
74
84
  output.error(err)
@@ -19,6 +19,29 @@ const stderr = ''
19
19
 
20
20
  const { options, tests, testRoot, workerIndex, poolMode } = workerData
21
21
 
22
+ // Global error handlers to catch critical errors but not test failures
23
+ process.on('uncaughtException', (err) => {
24
+ // Don't exit on test assertion errors - those are handled by mocha
25
+ if (err.name === 'AssertionError' || err.message?.includes('expected')) {
26
+ console.error(`[Worker ${workerIndex}] Test assertion error (handled by mocha):`, err.message)
27
+ return
28
+ }
29
+ console.error(`[Worker ${workerIndex}] Uncaught exception:`, err.message)
30
+ console.error(err.stack)
31
+ process.exit(1)
32
+ })
33
+
34
+ process.on('unhandledRejection', (reason, promise) => {
35
+ // Don't exit on test-related rejections
36
+ const msg = reason?.message || String(reason)
37
+ if (msg.includes('expected') || msg.includes('AssertionError')) {
38
+ console.error(`[Worker ${workerIndex}] Test rejection (handled by mocha):`, msg)
39
+ return
40
+ }
41
+ console.error(`[Worker ${workerIndex}] Unhandled rejection:`, reason)
42
+ process.exit(1)
43
+ })
44
+
22
45
  // hide worker output
23
46
  // In pool mode, only suppress output if debug is NOT enabled
24
47
  // In regular mode, hide result output but allow step output in verbose/debug
@@ -82,6 +105,8 @@ let config
82
105
  // Load test and run
83
106
  initPromise = (async function () {
84
107
  try {
108
+ console.log(`[Worker ${workerIndex}] Starting initialization...`)
109
+
85
110
  // Import modules dynamically to avoid ES Module loader race conditions in Node 22.x
86
111
  const eventModule = await import('../../event.js')
87
112
  const containerModule = await import('../../container.js')
@@ -89,6 +114,8 @@ initPromise = (async function () {
89
114
  const coreUtilsModule = await import('../../utils.js')
90
115
  const CodeceptModule = await import('../../codecept.js')
91
116
 
117
+ console.log(`[Worker ${workerIndex}] Modules imported`)
118
+
92
119
  event = eventModule.default
93
120
  container = containerModule.default
94
121
  getConfig = utilsModule.getConfig
@@ -98,16 +125,24 @@ initPromise = (async function () {
98
125
 
99
126
  const overrideConfigs = tryOrDefault(() => JSON.parse(options.override), {})
100
127
 
128
+ console.log(`[Worker ${workerIndex}] Loading config...`)
129
+
101
130
  // IMPORTANT: await is required here since getConfig is async
102
131
  const baseConfig = await getConfig(options.config || testRoot)
103
132
 
133
+ console.log(`[Worker ${workerIndex}] Config loaded, creating Codecept...`)
134
+
104
135
  // important deep merge so dynamic things e.g. functions on config are not overridden
105
136
  config = deepMerge(baseConfig, overrideConfigs)
106
137
 
107
138
  // Pass workerIndex as child option for output.process() to display worker prefix
108
139
  const optsWithChild = { ...options, child: workerIndex }
109
140
  codecept = new Codecept(config, optsWithChild)
141
+
142
+ console.log(`[Worker ${workerIndex}] Initializing Codecept...`)
110
143
  await codecept.init(testRoot)
144
+
145
+ console.log(`[Worker ${workerIndex}] Loading tests...`)
111
146
  codecept.loadTests()
112
147
  mocha = container.mocha()
113
148
 
@@ -116,9 +151,14 @@ initPromise = (async function () {
116
151
  // We'll reload test files fresh for each test request
117
152
  } else {
118
153
  // Legacy mode - filter tests upfront
154
+ console.log(`[Worker ${workerIndex}] Starting test filtering. Assigned ${tests.length} test UIDs`)
119
155
  filterTests()
156
+ const finalCount = mocha.suite.total()
157
+ console.log(`[Worker ${workerIndex}] After filtering: ${finalCount} tests to run`)
120
158
  }
121
159
 
160
+ console.log(`[Worker ${workerIndex}] Initialization complete, starting tests...`)
161
+
122
162
  // run tests
123
163
  if (poolMode) {
124
164
  await runPoolTests()
@@ -126,10 +166,12 @@ initPromise = (async function () {
126
166
  await runTests()
127
167
  } else {
128
168
  // No tests to run, close the worker
169
+ console.error(`[Worker ${workerIndex}] ERROR: No tests found after filtering! Assigned ${tests.length} UIDs but none matched.`)
129
170
  parentPort?.close()
130
171
  }
131
172
  } catch (err) {
132
- console.error('Error in worker initialization:', err)
173
+ console.error(`[Worker ${workerIndex}] FATAL ERROR:`, err.message)
174
+ console.error(err.stack)
133
175
  process.exit(1)
134
176
  }
135
177
  })()
@@ -140,6 +182,7 @@ async function runTests() {
140
182
  try {
141
183
  await codecept.bootstrap()
142
184
  } catch (err) {
185
+ console.error(`[Worker ${workerIndex}] Bootstrap error:`, err.message)
143
186
  throw new Error(`Error while running bootstrap file :${err}`)
144
187
  }
145
188
  listenToParentThread()
@@ -147,8 +190,15 @@ async function runTests() {
147
190
  disablePause()
148
191
  try {
149
192
  await codecept.run()
193
+ } catch (err) {
194
+ console.error(`[Worker ${workerIndex}] Runtime error:`, err.message)
195
+ throw err
150
196
  } finally {
151
- await codecept.teardown()
197
+ try {
198
+ await codecept.teardown()
199
+ } catch (err) {
200
+ console.error(`[Worker ${workerIndex}] Teardown error:`, err.message)
201
+ }
152
202
  }
153
203
  }
154
204
 
@@ -336,6 +386,26 @@ function filterTests() {
336
386
  mocha.files = files
337
387
  mocha.loadFiles()
338
388
 
389
+ // Collect all loaded tests for debugging
390
+ const allLoadedTests = [];
391
+ mocha.suite.eachTest(test => {
392
+ if (test) {
393
+ allLoadedTests.push({ uid: test.uid, title: test.fullTitle() });
394
+ }
395
+ });
396
+
397
+ console.log(`[Worker ${workerIndex}] Loaded ${allLoadedTests.length} tests from ${files.length} files`);
398
+ console.log(`[Worker ${workerIndex}] Expecting ${tests.length} test UIDs`);
399
+
400
+ const loadedUids = new Set(allLoadedTests.map(t => t.uid));
401
+ const missingTests = tests.filter(uid => !loadedUids.has(uid));
402
+
403
+ if (missingTests.length > 0) {
404
+ console.error(`[Worker ${workerIndex}] ERROR: ${missingTests.length} assigned tests NOT FOUND in loaded files!`);
405
+ console.error(`[Worker ${workerIndex}] Missing UIDs:`, missingTests);
406
+ console.error(`[Worker ${workerIndex}] Available UIDs:`, Array.from(loadedUids).slice(0, 5), '...');
407
+ }
408
+
339
409
  // Recursively filter tests in all suites (including nested ones)
340
410
  const filterSuiteTests = (suite) => {
341
411
  suite.tests = suite.tests.filter(test => tests.indexOf(test.uid) >= 0)
package/lib/workers.js CHANGED
@@ -370,6 +370,9 @@ class Workers extends EventEmitter {
370
370
  // If Codecept isn't initialized yet, return empty groups as a safe fallback
371
371
  if (!this.codecept) return populateGroups(numberOfWorkers)
372
372
  const files = this.codecept.testFiles
373
+
374
+ // Create a fresh mocha instance to avoid state pollution
375
+ Container.createMocha(this.codecept.config.mocha || {}, this.options)
373
376
  const mocha = Container.mocha()
374
377
  mocha.files = files
375
378
  mocha.loadFiles()
@@ -384,6 +387,10 @@ class Workers extends EventEmitter {
384
387
  groupCounter++
385
388
  }
386
389
  })
390
+
391
+ // Clean up after collecting test UIDs
392
+ mocha.unloadFiles()
393
+
387
394
  return groups
388
395
  }
389
396
 
@@ -452,9 +459,12 @@ class Workers extends EventEmitter {
452
459
  const files = this.codecept.testFiles
453
460
  const groups = populateGroups(numberOfWorkers)
454
461
 
462
+ // Create a fresh mocha instance to avoid state pollution
463
+ Container.createMocha(this.codecept.config.mocha || {}, this.options)
455
464
  const mocha = Container.mocha()
456
465
  mocha.files = files
457
466
  mocha.loadFiles()
467
+
458
468
  mocha.suite.suites.forEach(suite => {
459
469
  const i = indexOfSmallestElement(groups)
460
470
  suite.tests.forEach(test => {
@@ -463,6 +473,10 @@ class Workers extends EventEmitter {
463
473
  }
464
474
  })
465
475
  })
476
+
477
+ // Clean up after collecting test UIDs
478
+ mocha.unloadFiles()
479
+
466
480
  return groups
467
481
  }
468
482
 
@@ -504,8 +518,24 @@ class Workers extends EventEmitter {
504
518
  // Workers are already running, this is just a placeholder step
505
519
  })
506
520
 
521
+ // Add overall timeout to prevent infinite hanging
522
+ const overallTimeout = setTimeout(() => {
523
+ console.error('[Main] Overall timeout reached (10 minutes). Force terminating remaining workers...')
524
+ workerThreads.forEach(w => {
525
+ try {
526
+ w.terminate()
527
+ } catch (e) {
528
+ // ignore
529
+ }
530
+ })
531
+ this._finishRun()
532
+ }, 600000) // 10 minutes
533
+
507
534
  return new Promise(resolve => {
508
- this.on('end', resolve)
535
+ this.on('end', () => {
536
+ clearTimeout(overallTimeout)
537
+ resolve()
538
+ })
509
539
  })
510
540
  }
511
541
 
@@ -528,8 +558,37 @@ class Workers extends EventEmitter {
528
558
  if (this.isPoolMode) {
529
559
  this.activeWorkers.set(worker, { available: true, workerIndex: null })
530
560
  }
561
+
562
+ // Track last activity time to detect hanging workers
563
+ let lastActivity = Date.now()
564
+ let currentTest = null
565
+ const workerTimeout = 300000 // 5 minutes
566
+
567
+ const timeoutChecker = setInterval(() => {
568
+ const elapsed = Date.now() - lastActivity
569
+ if (elapsed > workerTimeout) {
570
+ console.error(`[Main] Worker appears to be hanging (no activity for ${Math.floor(elapsed/1000)}s). Terminating...`)
571
+ if (currentTest) {
572
+ console.error(`[Main] Last test: ${currentTest}`)
573
+ }
574
+ clearInterval(timeoutChecker)
575
+ worker.terminate()
576
+ }
577
+ }, 30000) // Check every 30 seconds
531
578
 
532
579
  worker.on('message', message => {
580
+ lastActivity = Date.now() // Update activity timestamp
581
+
582
+ // Track current test
583
+ if (message.event === event.test.started && message.data) {
584
+ currentTest = message.data.title || message.data.fullTitle
585
+ console.log(`[Worker ${message.workerIndex}] Started: ${currentTest}`)
586
+ }
587
+
588
+ if (message.event === event.test.finished && message.data) {
589
+ console.log(`[Worker ${message.workerIndex}] Finished: ${message.data.title || currentTest}`)
590
+ }
591
+
533
592
  output.process(message.workerIndex)
534
593
 
535
594
  // Handle test requests for pool mode
@@ -646,11 +705,27 @@ class Workers extends EventEmitter {
646
705
  })
647
706
 
648
707
  worker.on('error', err => {
708
+ console.error(`[Main] Worker error:`, err.message || err)
709
+ if (currentTest) {
710
+ console.error(`[Main] Failed during test: ${currentTest}`)
711
+ }
649
712
  this.errors.push(err)
650
713
  })
651
714
 
652
- worker.on('exit', () => {
715
+ worker.on('exit', (code) => {
716
+ clearInterval(timeoutChecker)
653
717
  this.closedWorkers += 1
718
+
719
+ if (code !== 0) {
720
+ console.error(`[Main] Worker exited with code ${code}`)
721
+ if (currentTest) {
722
+ console.error(`[Main] Last test running: ${currentTest}`)
723
+ }
724
+ // Mark as failed
725
+ process.exitCode = 1
726
+ }
727
+
728
+ console.log(`[Main] Workers closed: ${this.closedWorkers}/${this.numberOfWorkers}`)
654
729
 
655
730
  if (this.isPoolMode) {
656
731
  // Pool mode: finish when all workers have exited and no more tests
@@ -665,8 +740,9 @@ class Workers extends EventEmitter {
665
740
  }
666
741
 
667
742
  _finishRun() {
743
+ console.log('[Main] Finishing test run...')
668
744
  event.dispatcher.emit(event.workers.after, { tests: this.workers.map(worker => worker.tests) })
669
- if (Container.result().hasFailed) {
745
+ if (Container.result().hasFailed || this.errors.length > 0) {
670
746
  process.exitCode = 1
671
747
  } else {
672
748
  process.exitCode = 0
@@ -706,8 +782,10 @@ class Workers extends EventEmitter {
706
782
  this._testStates.clear()
707
783
  }
708
784
 
785
+ console.log('[Main] Emitting final results...')
709
786
  this.emit(event.all.result, Container.result())
710
787
  event.dispatcher.emit(event.workers.result, Container.result())
788
+ console.log('[Main] Emitting end event...')
711
789
  this.emit('end') // internal event
712
790
  }
713
791
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeceptjs",
3
- "version": "4.0.2-beta.1",
3
+ "version": "4.0.2-beta.10",
4
4
  "type": "module",
5
5
  "description": "Supercharged End 2 End Testing Framework for NodeJS",
6
6
  "keywords": [