codeceptjs 4.0.2-beta.1 → 4.0.2-beta.11

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
@@ -26,6 +49,10 @@ if (poolMode && !options.debug) {
26
49
  // In pool mode without debug, allow test names and important output but suppress verbose details
27
50
  const originalWrite = process.stdout.write
28
51
  process.stdout.write = string => {
52
+ // Always allow Worker logs
53
+ if (string.includes('[Worker')) {
54
+ return originalWrite.call(process.stdout, string)
55
+ }
29
56
  // Allow test names (✔ or ✖), Scenario Steps, failures, and important markers
30
57
  if (
31
58
  string.includes('✔') ||
@@ -45,7 +72,12 @@ if (poolMode && !options.debug) {
45
72
  return originalWrite.call(process.stdout, string)
46
73
  }
47
74
  } else if (!poolMode && !options.debug && !options.verbose) {
75
+ const originalWrite = process.stdout.write
48
76
  process.stdout.write = string => {
77
+ // Always allow Worker logs
78
+ if (string.includes('[Worker')) {
79
+ return originalWrite.call(process.stdout, string)
80
+ }
49
81
  stdout += string
50
82
  return true
51
83
  }
@@ -82,6 +114,8 @@ let config
82
114
  // Load test and run
83
115
  initPromise = (async function () {
84
116
  try {
117
+ console.log(`[Worker ${workerIndex}] Starting initialization...`)
118
+
85
119
  // Import modules dynamically to avoid ES Module loader race conditions in Node 22.x
86
120
  const eventModule = await import('../../event.js')
87
121
  const containerModule = await import('../../container.js')
@@ -89,6 +123,8 @@ initPromise = (async function () {
89
123
  const coreUtilsModule = await import('../../utils.js')
90
124
  const CodeceptModule = await import('../../codecept.js')
91
125
 
126
+ console.log(`[Worker ${workerIndex}] Modules imported`)
127
+
92
128
  event = eventModule.default
93
129
  container = containerModule.default
94
130
  getConfig = utilsModule.getConfig
@@ -98,16 +134,24 @@ initPromise = (async function () {
98
134
 
99
135
  const overrideConfigs = tryOrDefault(() => JSON.parse(options.override), {})
100
136
 
137
+ console.log(`[Worker ${workerIndex}] Loading config...`)
138
+
101
139
  // IMPORTANT: await is required here since getConfig is async
102
140
  const baseConfig = await getConfig(options.config || testRoot)
103
141
 
142
+ console.log(`[Worker ${workerIndex}] Config loaded, creating Codecept...`)
143
+
104
144
  // important deep merge so dynamic things e.g. functions on config are not overridden
105
145
  config = deepMerge(baseConfig, overrideConfigs)
106
146
 
107
147
  // Pass workerIndex as child option for output.process() to display worker prefix
108
148
  const optsWithChild = { ...options, child: workerIndex }
109
149
  codecept = new Codecept(config, optsWithChild)
150
+
151
+ console.log(`[Worker ${workerIndex}] Initializing Codecept...`)
110
152
  await codecept.init(testRoot)
153
+
154
+ console.log(`[Worker ${workerIndex}] Loading tests...`)
111
155
  codecept.loadTests()
112
156
  mocha = container.mocha()
113
157
 
@@ -116,9 +160,14 @@ initPromise = (async function () {
116
160
  // We'll reload test files fresh for each test request
117
161
  } else {
118
162
  // Legacy mode - filter tests upfront
163
+ console.log(`[Worker ${workerIndex}] Starting test filtering. Assigned ${tests.length} test UIDs`)
119
164
  filterTests()
165
+ const finalCount = mocha.suite.total()
166
+ console.log(`[Worker ${workerIndex}] After filtering: ${finalCount} tests to run`)
120
167
  }
121
168
 
169
+ console.log(`[Worker ${workerIndex}] Initialization complete, starting tests...`)
170
+
122
171
  // run tests
123
172
  if (poolMode) {
124
173
  await runPoolTests()
@@ -126,10 +175,12 @@ initPromise = (async function () {
126
175
  await runTests()
127
176
  } else {
128
177
  // No tests to run, close the worker
178
+ console.error(`[Worker ${workerIndex}] ERROR: No tests found after filtering! Assigned ${tests.length} UIDs but none matched.`)
129
179
  parentPort?.close()
130
180
  }
131
181
  } catch (err) {
132
- console.error('Error in worker initialization:', err)
182
+ console.error(`[Worker ${workerIndex}] FATAL ERROR:`, err.message)
183
+ console.error(err.stack)
133
184
  process.exit(1)
134
185
  }
135
186
  })()
@@ -140,6 +191,7 @@ async function runTests() {
140
191
  try {
141
192
  await codecept.bootstrap()
142
193
  } catch (err) {
194
+ console.error(`[Worker ${workerIndex}] Bootstrap error:`, err.message)
143
195
  throw new Error(`Error while running bootstrap file :${err}`)
144
196
  }
145
197
  listenToParentThread()
@@ -147,8 +199,15 @@ async function runTests() {
147
199
  disablePause()
148
200
  try {
149
201
  await codecept.run()
202
+ } catch (err) {
203
+ console.error(`[Worker ${workerIndex}] Runtime error:`, err.message)
204
+ throw err
150
205
  } finally {
151
- await codecept.teardown()
206
+ try {
207
+ await codecept.teardown()
208
+ } catch (err) {
209
+ console.error(`[Worker ${workerIndex}] Teardown error:`, err.message)
210
+ }
152
211
  }
153
212
  }
154
213
 
@@ -336,6 +395,26 @@ function filterTests() {
336
395
  mocha.files = files
337
396
  mocha.loadFiles()
338
397
 
398
+ // Collect all loaded tests for debugging
399
+ const allLoadedTests = [];
400
+ mocha.suite.eachTest(test => {
401
+ if (test) {
402
+ allLoadedTests.push({ uid: test.uid, title: test.fullTitle() });
403
+ }
404
+ });
405
+
406
+ console.log(`[Worker ${workerIndex}] Loaded ${allLoadedTests.length} tests from ${files.length} files`);
407
+ console.log(`[Worker ${workerIndex}] Expecting ${tests.length} test UIDs`);
408
+
409
+ const loadedUids = new Set(allLoadedTests.map(t => t.uid));
410
+ const missingTests = tests.filter(uid => !loadedUids.has(uid));
411
+
412
+ if (missingTests.length > 0) {
413
+ console.error(`[Worker ${workerIndex}] ERROR: ${missingTests.length} assigned tests NOT FOUND in loaded files!`);
414
+ console.error(`[Worker ${workerIndex}] Missing UIDs:`, missingTests);
415
+ console.error(`[Worker ${workerIndex}] Available UIDs:`, Array.from(loadedUids).slice(0, 5), '...');
416
+ }
417
+
339
418
  // Recursively filter tests in all suites (including nested ones)
340
419
  const filterSuiteTests = (suite) => {
341
420
  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.11",
4
4
  "type": "module",
5
5
  "description": "Supercharged End 2 End Testing Framework for NodeJS",
6
6
  "keywords": [