codeceptjs 4.0.1-beta.9 → 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.
package/bin/codecept.js CHANGED
@@ -174,7 +174,7 @@ program
174
174
  .option('-R, --reporter <name>', 'specify the reporter to use')
175
175
  .option('-S, --sort', 'sort test files')
176
176
  .option('-b, --bail', 'bail after first test failure')
177
- .option('-d, --debug', "enable node's debugger, synonym for node --debug")
177
+ .option('--inspec', "enable node's debugger, synonym for node --debug")
178
178
  .option('-g, --grep <pattern>', 'only run tests matching <pattern>')
179
179
  .option('-f, --fgrep <string>', 'only run tests containing <string>')
180
180
  .option('-i, --invert', 'inverts --grep and --fgrep matches')
@@ -276,7 +276,7 @@ program
276
276
  .option('-R, --reporter <name>', 'specify the reporter to use')
277
277
  .option('-S, --sort', 'sort test files')
278
278
  .option('-b, --bail', 'bail after first test failure')
279
- .option('-d, --debug', "enable node's debugger, synonym for node --debug")
279
+ .option('--inspect', "enable node's debugger, synonym for node --debug")
280
280
  .option('-g, --grep <pattern>', 'only run tests matching <pattern>')
281
281
  .option('-f, --fgrep <string>', 'only run tests containing <string>')
282
282
  .option('-i, --invert', 'inverts --grep and --fgrep matches')
@@ -41,7 +41,7 @@ const getDefinitionsFileContent = ({ hasCustomHelper, hasCustomStepsFile, helper
41
41
 
42
42
  const importPathsFragment = importPaths.join('\n')
43
43
  const supportObjectsTypeFragment = convertMapToType(supportObject)
44
- const methodsTypeFragment = helperNames.length > 0 ? `interface Methods extends ${helperNames.join(', ')} {}` : ''
44
+ const methodsTypeFragment = helperNames.length > 0 ? `interface Methods extends ${helperNames.join(', ')} {}` : 'interface Methods {}'
45
45
  const translatedActionsFragment = JSON.stringify(translations.vocabulary.actions, null, 2)
46
46
 
47
47
  return generateDefinitionsContent({
@@ -239,8 +239,13 @@ function getImportString(testsPath, targetFolderPath, pathsToType, pathsToValue)
239
239
  }
240
240
 
241
241
  for (const name in pathsToValue) {
242
- const relativePath = getPath(pathsToValue[name], targetFolderPath, testsPath)
243
- importStrings.push(`type ${name} = import('${relativePath}');`)
242
+ const originalPath = pathsToValue[name]
243
+ const relativePath = getPath(originalPath, targetFolderPath, testsPath)
244
+ if (originalPath.endsWith('.js') || originalPath.endsWith('.ts')) {
245
+ importStrings.push(`type ${name} = InstanceType<typeof import('${relativePath}').default>;`)
246
+ } else {
247
+ importStrings.push(`type ${name} = import('${relativePath}');`)
248
+ }
244
249
  }
245
250
 
246
251
  return importStrings
@@ -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,14 +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
- codecept = new Codecept(config, options)
138
+ // Pass workerIndex as child option for output.process() to display worker prefix
139
+ const optsWithChild = { ...options, child: workerIndex }
140
+ codecept = new Codecept(config, optsWithChild)
141
+
142
+ console.log(`[Worker ${workerIndex}] Initializing Codecept...`)
108
143
  await codecept.init(testRoot)
144
+
145
+ console.log(`[Worker ${workerIndex}] Loading tests...`)
109
146
  codecept.loadTests()
110
147
  mocha = container.mocha()
111
148
 
@@ -114,9 +151,14 @@ initPromise = (async function () {
114
151
  // We'll reload test files fresh for each test request
115
152
  } else {
116
153
  // Legacy mode - filter tests upfront
154
+ console.log(`[Worker ${workerIndex}] Starting test filtering. Assigned ${tests.length} test UIDs`)
117
155
  filterTests()
156
+ const finalCount = mocha.suite.total()
157
+ console.log(`[Worker ${workerIndex}] After filtering: ${finalCount} tests to run`)
118
158
  }
119
159
 
160
+ console.log(`[Worker ${workerIndex}] Initialization complete, starting tests...`)
161
+
120
162
  // run tests
121
163
  if (poolMode) {
122
164
  await runPoolTests()
@@ -124,10 +166,12 @@ initPromise = (async function () {
124
166
  await runTests()
125
167
  } else {
126
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.`)
127
170
  parentPort?.close()
128
171
  }
129
172
  } catch (err) {
130
- console.error('Error in worker initialization:', err)
173
+ console.error(`[Worker ${workerIndex}] FATAL ERROR:`, err.message)
174
+ console.error(err.stack)
131
175
  process.exit(1)
132
176
  }
133
177
  })()
@@ -138,6 +182,7 @@ async function runTests() {
138
182
  try {
139
183
  await codecept.bootstrap()
140
184
  } catch (err) {
185
+ console.error(`[Worker ${workerIndex}] Bootstrap error:`, err.message)
141
186
  throw new Error(`Error while running bootstrap file :${err}`)
142
187
  }
143
188
  listenToParentThread()
@@ -145,8 +190,15 @@ async function runTests() {
145
190
  disablePause()
146
191
  try {
147
192
  await codecept.run()
193
+ } catch (err) {
194
+ console.error(`[Worker ${workerIndex}] Runtime error:`, err.message)
195
+ throw err
148
196
  } finally {
149
- await codecept.teardown()
197
+ try {
198
+ await codecept.teardown()
199
+ } catch (err) {
200
+ console.error(`[Worker ${workerIndex}] Teardown error:`, err.message)
201
+ }
150
202
  }
151
203
  }
152
204
 
@@ -334,8 +386,36 @@ function filterTests() {
334
386
  mocha.files = files
335
387
  mocha.loadFiles()
336
388
 
337
- for (const suite of mocha.suite.suites) {
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
+
409
+ // Recursively filter tests in all suites (including nested ones)
410
+ const filterSuiteTests = (suite) => {
338
411
  suite.tests = suite.tests.filter(test => tests.indexOf(test.uid) >= 0)
412
+ for (const childSuite of suite.suites) {
413
+ filterSuiteTests(childSuite)
414
+ }
415
+ }
416
+
417
+ for (const suite of mocha.suite.suites) {
418
+ filterSuiteTests(suite)
339
419
  }
340
420
  }
341
421
 
package/lib/config.js CHANGED
@@ -2,7 +2,7 @@ import fs from 'fs'
2
2
  import path from 'path'
3
3
  import { createRequire } from 'module'
4
4
  import { fileExists, isFile, deepMerge, deepClone } from './utils.js'
5
- import { transpileTypeScript, cleanupTempFiles } from './utils/typescript.js'
5
+ import { transpileTypeScript, cleanupTempFiles, fixErrorStack } from './utils/typescript.js'
6
6
 
7
7
  const defaultConfig = {
8
8
  output: './_output',
@@ -159,12 +159,13 @@ async function loadConfigFile(configFile) {
159
159
  try {
160
160
  // Use the TypeScript transpilation utility
161
161
  const typescript = require('typescript')
162
- const { tempFile, allTempFiles } = await transpileTypeScript(configFile, typescript)
162
+ const { tempFile, allTempFiles, fileMapping } = await transpileTypeScript(configFile, typescript)
163
163
 
164
164
  try {
165
165
  configModule = await import(tempFile)
166
166
  cleanupTempFiles(allTempFiles)
167
167
  } catch (err) {
168
+ fixErrorStack(err, fileMapping)
168
169
  cleanupTempFiles(allTempFiles)
169
170
  throw err
170
171
  }
package/lib/container.js CHANGED
@@ -5,7 +5,7 @@ import debugModule from 'debug'
5
5
  const debug = debugModule('codeceptjs:container')
6
6
  import { MetaStep } from './step.js'
7
7
  import { methodsOfObject, fileExists, isFunction, isAsyncFunction, installedLocally, deepMerge } from './utils.js'
8
- import { transpileTypeScript, cleanupTempFiles } from './utils/typescript.js'
8
+ import { transpileTypeScript, cleanupTempFiles, fixErrorStack } from './utils/typescript.js'
9
9
  import Translation from './translation.js'
10
10
  import MochaFactory from './mocha/factory.js'
11
11
  import recorder from './recorder.js'
@@ -34,6 +34,7 @@ let container = {
34
34
  /** @type {Result | null} */
35
35
  result: null,
36
36
  sharedKeys: new Set(), // Track keys shared via share() function
37
+ tsFileMapping: null, // TypeScript file mapping for error stack fixing
37
38
  }
38
39
 
39
40
  /**
@@ -88,7 +89,7 @@ class Container {
88
89
  container.support.I = mod
89
90
  }
90
91
  } catch (e) {
91
- throw new Error(`Could not include object I: ${e.message}`)
92
+ throw e
92
93
  }
93
94
  } else {
94
95
  // Create default actor - this sets up the callback in asyncHelperPromise
@@ -176,6 +177,15 @@ class Container {
176
177
  return container.translation
177
178
  }
178
179
 
180
+ /**
181
+ * Get TypeScript file mapping for error stack fixing
182
+ *
183
+ * @api
184
+ */
185
+ static tsFileMapping() {
186
+ return container.tsFileMapping
187
+ }
188
+
179
189
  /**
180
190
  * Get Mocha instance
181
191
  *
@@ -398,20 +408,66 @@ async function requireHelperFromModule(helperName, config, HelperClass) {
398
408
  throw err
399
409
  }
400
410
  } else {
411
+ // Handle TypeScript files
412
+ let importPath = moduleName
413
+ let tempJsFile = null
414
+ let fileMapping = null
415
+ const ext = path.extname(moduleName)
416
+
417
+ if (ext === '.ts') {
418
+ try {
419
+ // Use the TypeScript transpilation utility
420
+ const typescript = await import('typescript')
421
+ const { tempFile, allTempFiles, fileMapping: mapping } = await transpileTypeScript(importPath, typescript)
422
+
423
+ debug(`Transpiled TypeScript helper: ${importPath} -> ${tempFile}`)
424
+
425
+ importPath = tempFile
426
+ tempJsFile = allTempFiles
427
+ fileMapping = mapping
428
+ // Store file mapping in container for runtime error fixing (merge with existing)
429
+ if (!container.tsFileMapping) {
430
+ container.tsFileMapping = new Map()
431
+ }
432
+ for (const [key, value] of mapping.entries()) {
433
+ container.tsFileMapping.set(key, value)
434
+ }
435
+ } catch (tsError) {
436
+ throw new Error(`Failed to load TypeScript helper ${importPath}: ${tsError.message}. Make sure 'typescript' package is installed.`)
437
+ }
438
+ }
439
+
401
440
  // check if the new syntax export default HelperName is used and loads the Helper, otherwise loads the module that used old syntax export = HelperName.
402
441
  try {
403
442
  // Try dynamic import for both CommonJS and ESM modules
404
- const mod = await import(moduleName)
443
+ const mod = await import(importPath)
405
444
  if (!mod && !mod.default) {
406
445
  throw new Error(`Helper module '${moduleName}' was not found. Make sure you have installed the package correctly.`)
407
446
  }
408
447
  HelperClass = mod.default || mod
448
+
449
+ // Clean up temp files if created
450
+ if (tempJsFile) {
451
+ const filesToClean = Array.isArray(tempJsFile) ? tempJsFile : [tempJsFile]
452
+ cleanupTempFiles(filesToClean)
453
+ }
409
454
  } catch (err) {
455
+ // Fix error stack to point to original .ts files
456
+ if (fileMapping) {
457
+ fixErrorStack(err, fileMapping)
458
+ }
459
+
460
+ // Clean up temp files before rethrowing
461
+ if (tempJsFile) {
462
+ const filesToClean = Array.isArray(tempJsFile) ? tempJsFile : [tempJsFile]
463
+ cleanupTempFiles(filesToClean)
464
+ }
465
+
410
466
  if (err.code === 'ERR_REQUIRE_ESM' || (err.message && err.message.includes('ES module'))) {
411
467
  // This is an ESM module, use dynamic import
412
468
  try {
413
469
  const pathModule = await import('path')
414
- const absolutePath = pathModule.default.resolve(moduleName)
470
+ const absolutePath = pathModule.default.resolve(importPath)
415
471
  const mod = await import(absolutePath)
416
472
  HelperClass = mod.default || mod
417
473
  debug(`helper ${helperName} loaded via ESM import`)
@@ -699,6 +755,7 @@ async function loadSupportObject(modulePath, supportObjectName) {
699
755
  // Use dynamic import for both ESM and CJS modules
700
756
  let importPath = modulePath
701
757
  let tempJsFile = null
758
+ let fileMapping = null
702
759
 
703
760
  if (typeof importPath === 'string') {
704
761
  const ext = path.extname(importPath)
@@ -708,7 +765,7 @@ async function loadSupportObject(modulePath, supportObjectName) {
708
765
  try {
709
766
  // Use the TypeScript transpilation utility
710
767
  const typescript = await import('typescript')
711
- const { tempFile, allTempFiles } = await transpileTypeScript(importPath, typescript)
768
+ const { tempFile, allTempFiles, fileMapping: mapping } = await transpileTypeScript(importPath, typescript)
712
769
 
713
770
  debug(`Transpiled TypeScript file: ${importPath} -> ${tempFile}`)
714
771
 
@@ -716,6 +773,14 @@ async function loadSupportObject(modulePath, supportObjectName) {
716
773
  importPath = tempFile
717
774
  // Store temp files list in a way that cleanup can access them
718
775
  tempJsFile = allTempFiles
776
+ fileMapping = mapping
777
+ // Store file mapping in container for runtime error fixing (merge with existing)
778
+ if (!container.tsFileMapping) {
779
+ container.tsFileMapping = new Map()
780
+ }
781
+ for (const [key, value] of mapping.entries()) {
782
+ container.tsFileMapping.set(key, value)
783
+ }
719
784
  } catch (tsError) {
720
785
  throw new Error(`Failed to load TypeScript file ${importPath}: ${tsError.message}. Make sure 'typescript' package is installed.`)
721
786
  }
@@ -729,6 +794,11 @@ async function loadSupportObject(modulePath, supportObjectName) {
729
794
  try {
730
795
  obj = await import(importPath)
731
796
  } catch (importError) {
797
+ // Fix error stack to point to original .ts files
798
+ if (fileMapping) {
799
+ fixErrorStack(importError, fileMapping)
800
+ }
801
+
732
802
  // Clean up temp files if created before rethrowing
733
803
  if (tempJsFile) {
734
804
  const filesToClean = Array.isArray(tempJsFile) ? tempJsFile : [tempJsFile]
@@ -777,7 +847,9 @@ async function loadSupportObject(modulePath, supportObjectName) {
777
847
 
778
848
  throw new Error(`Support object "${supportObjectName}" should be an object, class, or function, but got ${typeof actualObj}`)
779
849
  } catch (err) {
780
- throw new Error(`Could not include object ${supportObjectName} from module '${modulePath}'\n${err.message}\n${err.stack}`)
850
+ const newErr = new Error(`Could not include object ${supportObjectName} from module '${modulePath}': ${err.message}`)
851
+ newErr.stack = err.stack
852
+ throw newErr
781
853
  }
782
854
  }
783
855