codeceptjs 4.0.0-beta.9.esm-aria → 4.0.0-rc.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.
Files changed (69) hide show
  1. package/README.md +39 -27
  2. package/bin/codecept.js +2 -2
  3. package/bin/mcp-server.js +610 -0
  4. package/docs/webapi/appendField.mustache +5 -0
  5. package/docs/webapi/attachFile.mustache +12 -0
  6. package/docs/webapi/checkOption.mustache +1 -1
  7. package/docs/webapi/clearField.mustache +5 -0
  8. package/docs/webapi/dontSeeCurrentPathEquals.mustache +10 -0
  9. package/docs/webapi/dontSeeElement.mustache +4 -0
  10. package/docs/webapi/dontSeeInField.mustache +5 -0
  11. package/docs/webapi/fillField.mustache +5 -0
  12. package/docs/webapi/moveCursorTo.mustache +5 -1
  13. package/docs/webapi/seeCurrentPathEquals.mustache +10 -0
  14. package/docs/webapi/seeElement.mustache +4 -0
  15. package/docs/webapi/seeInField.mustache +5 -0
  16. package/docs/webapi/selectOption.mustache +5 -0
  17. package/docs/webapi/uncheckOption.mustache +1 -1
  18. package/lib/actor.js +12 -8
  19. package/lib/codecept.js +51 -18
  20. package/lib/command/definitions.js +14 -7
  21. package/lib/command/init.js +2 -4
  22. package/lib/command/run-workers.js +13 -2
  23. package/lib/command/workers/runTests.js +121 -9
  24. package/lib/config.js +24 -33
  25. package/lib/container.js +177 -28
  26. package/lib/element/WebElement.js +81 -2
  27. package/lib/els.js +12 -6
  28. package/lib/helper/Appium.js +8 -8
  29. package/lib/helper/GraphQL.js +6 -4
  30. package/lib/helper/JSONResponse.js +3 -4
  31. package/lib/helper/Playwright.js +339 -505
  32. package/lib/helper/Puppeteer.js +324 -89
  33. package/lib/helper/REST.js +15 -9
  34. package/lib/helper/WebDriver.js +311 -81
  35. package/lib/helper/errors/ElementNotFound.js +5 -2
  36. package/lib/helper/errors/MultipleElementsFound.js +52 -0
  37. package/lib/helper/extras/elementSelection.js +58 -0
  38. package/lib/helper/scripts/dropFile.js +11 -0
  39. package/lib/html.js +14 -1
  40. package/lib/listener/config.js +11 -3
  41. package/lib/listener/globalRetry.js +32 -6
  42. package/lib/listener/helpers.js +2 -14
  43. package/lib/locator.js +32 -0
  44. package/lib/mocha/cli.js +16 -0
  45. package/lib/mocha/factory.js +7 -27
  46. package/lib/mocha/gherkin.js +4 -4
  47. package/lib/mocha/test.js +4 -2
  48. package/lib/output.js +2 -2
  49. package/lib/plugin/aiTrace.js +464 -0
  50. package/lib/plugin/auth.js +2 -1
  51. package/lib/plugin/retryFailedStep.js +28 -19
  52. package/lib/plugin/stepByStepReport.js +5 -1
  53. package/lib/step/base.js +14 -1
  54. package/lib/step/config.js +15 -2
  55. package/lib/step/meta.js +18 -1
  56. package/lib/step/record.js +9 -1
  57. package/lib/utils/loaderCheck.js +162 -0
  58. package/lib/utils/typescript.js +449 -0
  59. package/lib/utils.js +48 -0
  60. package/lib/workers.js +163 -54
  61. package/package.json +43 -32
  62. package/typings/index.d.ts +120 -4
  63. package/lib/helper/extras/PlaywrightLocator.js +0 -110
  64. package/lib/listener/enhancedGlobalRetry.js +0 -110
  65. package/lib/plugin/enhancedRetryFailedStep.js +0 -99
  66. package/lib/plugin/htmlReporter.js +0 -3648
  67. package/lib/retryCoordinator.js +0 -207
  68. package/typings/promiseBasedTypes.d.ts +0 -11011
  69. package/typings/types.d.ts +0 -13073
@@ -1,11 +1,16 @@
1
1
  Checks that value of input field or textarea doesn't equal to given value
2
2
  Opposite to `seeInField`.
3
3
 
4
+ The third parameter is an optional context (CSS or XPath locator) to narrow the search.
5
+
4
6
  ```js
5
7
  I.dontSeeInField('email', 'user@user.com'); // field by name
6
8
  I.dontSeeInField({ css: 'form input.email' }, 'user@user.com'); // field by CSS
9
+ // within a context
10
+ I.dontSeeInField('Name', 'old_value', '.form-container');
7
11
  ```
8
12
 
9
13
  @param {CodeceptJS.LocatorOrString} field located by label|name|CSS|XPath|strict locator.
10
14
  @param {CodeceptJS.StringOrSecret} value value to check.
15
+ @param {?CodeceptJS.LocatorOrString} [context=null] (optional, `null` by default) element located by CSS | XPath | strict locator.
11
16
  @returns {void} automatically synchronized promise through #recorder
@@ -1,6 +1,8 @@
1
1
  Fills a text field or textarea, after clearing its value, with the given string.
2
2
  Field is located by name, label, CSS, or XPath.
3
3
 
4
+ The third parameter is an optional context (CSS or XPath locator) to narrow the search.
5
+
4
6
  ```js
5
7
  // by label
6
8
  I.fillField('Email', 'hello@world.com');
@@ -10,7 +12,10 @@ I.fillField('password', secret('123456'));
10
12
  I.fillField('form#login input[name=username]', 'John');
11
13
  // or by strict locator
12
14
  I.fillField({css: 'form#login input[name=username]'}, 'John');
15
+ // within a context
16
+ I.fillField('Name', 'John', '#section2');
13
17
  ```
14
18
  @param {CodeceptJS.LocatorOrString} field located by label|name|CSS|XPath|strict locator.
15
19
  @param {CodeceptJS.StringOrSecret} value text value to fill.
20
+ @param {?CodeceptJS.LocatorOrString} [context=null] (optional, `null` by default) element located by CSS | XPath | strict locator.
16
21
  @returns {void} automatically synchronized promise through #recorder
@@ -1,12 +1,16 @@
1
1
  Moves cursor to element matched by locator.
2
2
  Extra shift can be set with offsetX and offsetY options.
3
3
 
4
+ An optional `context` (as a second parameter) can be specified to narrow the search to an element within a parent.
5
+ When the second argument is a non-number (string or locator object), it is treated as context.
6
+
4
7
  ```js
5
8
  I.moveCursorTo('.tooltip');
6
9
  I.moveCursorTo('#submit', 5,5);
10
+ I.moveCursorTo('#submit', '.container');
7
11
  ```
8
12
 
9
13
  @param {CodeceptJS.LocatorOrString} locator located by CSS|XPath|strict locator.
10
- @param {number} [offsetX=0] (optional, `0` by default) X-axis offset.
14
+ @param {number|CodeceptJS.LocatorOrString} [offsetX=0] (optional, `0` by default) X-axis offset or context locator.
11
15
  @param {number} [offsetY=0] (optional, `0` by default) Y-axis offset.
12
16
  @returns {void} automatically synchronized promise through #recorder
@@ -0,0 +1,10 @@
1
+ Checks that current URL path matches the expected path.
2
+ Query strings and URL fragments are ignored.
3
+
4
+ ```js
5
+ I.seeCurrentPathEquals('/info'); // passes for '/info', '/info?user=1', '/info#section'
6
+ I.seeCurrentPathEquals('/'); // passes for '/', '/?user=ok', '/#top'
7
+ ```
8
+
9
+ @param {string} path value to check.
10
+ @returns {void} automatically synchronized promise through #recorder
@@ -1,8 +1,12 @@
1
1
  Checks that a given Element is visible
2
2
  Element is located by CSS or XPath.
3
3
 
4
+ The second parameter is a context (CSS or XPath locator) to narrow the search.
5
+
4
6
  ```js
5
7
  I.seeElement('#modal');
8
+ I.seeElement('#modal', '#container');
6
9
  ```
7
10
  @param {CodeceptJS.LocatorOrString} locator located by CSS|XPath|strict locator.
11
+ @param {?CodeceptJS.LocatorOrString} [context=null] (optional, `null` by default) element located by CSS | XPath | strict locator.
8
12
  @returns {void} automatically synchronized promise through #recorder
@@ -1,12 +1,17 @@
1
1
  Checks that the given input field or textarea equals to given value.
2
2
  For fuzzy locators, fields are matched by label text, the "name" attribute, CSS, and XPath.
3
3
 
4
+ The third parameter is an optional context (CSS or XPath locator) to narrow the search.
5
+
4
6
  ```js
5
7
  I.seeInField('Username', 'davert');
6
8
  I.seeInField({css: 'form textarea'},'Type your comment here');
7
9
  I.seeInField('form input[type=hidden]','hidden_value');
8
10
  I.seeInField('#searchform input','Search');
11
+ // within a context
12
+ I.seeInField('Name', 'John', '.form-container');
9
13
  ```
10
14
  @param {CodeceptJS.LocatorOrString} field located by label|name|CSS|XPath|strict locator.
11
15
  @param {CodeceptJS.StringOrSecret} value value to check.
16
+ @param {?CodeceptJS.LocatorOrString} [context=null] (optional, `null` by default) element located by CSS | XPath | strict locator.
12
17
  @returns {void} automatically synchronized promise through #recorder
@@ -2,6 +2,8 @@ Selects an option in a drop-down select.
2
2
  Field is searched by label | name | CSS | XPath.
3
3
  Option is selected by visible text or by value.
4
4
 
5
+ The third parameter is an optional context (CSS or XPath locator) to narrow the search.
6
+
5
7
  ```js
6
8
  I.selectOption('Choose Plan', 'Monthly'); // select by label
7
9
  I.selectOption('subscription', 'Monthly'); // match option by text
@@ -9,6 +11,8 @@ I.selectOption('subscription', '0'); // or by value
9
11
  I.selectOption('//form/select[@name=account]','Premium');
10
12
  I.selectOption('form select[name=account]', 'Premium');
11
13
  I.selectOption({css: 'form select[name=account]'}, 'Premium');
14
+ // within a context
15
+ I.selectOption('age', '21-60', '#section2');
12
16
  ```
13
17
 
14
18
  Provide an array for the second argument to select multiple options.
@@ -18,4 +22,5 @@ I.selectOption('Which OS do you use?', ['Android', 'iOS']);
18
22
  ```
19
23
  @param {LocatorOrString} select field located by label|name|CSS|XPath|strict locator.
20
24
  @param {string|Array<*>} option visible text or value of option.
25
+ @param {?CodeceptJS.LocatorOrString} [context=null] (optional, `null` by default) element located by CSS | XPath | strict locator.
21
26
  @returns {void} automatically synchronized promise through #recorder
@@ -1,7 +1,7 @@
1
1
  Unselects a checkbox or radio button.
2
2
  Element is located by label or name or CSS or XPath.
3
3
 
4
- The second parameter is a context (CSS or XPath locator) to narrow the search.
4
+ The second parameter is an optional context (CSS or XPath locator) to narrow the search.
5
5
 
6
6
  ```js
7
7
  I.uncheckOption('#agree');
package/lib/actor.js CHANGED
@@ -75,7 +75,8 @@ export default function (obj = {}, container) {
75
75
  if (!container) {
76
76
  container = Container
77
77
  }
78
-
78
+
79
+ // Get existing actor or create a new one
79
80
  const actor = container.actor() || new Actor()
80
81
 
81
82
  // load all helpers once container initialized
@@ -111,14 +112,17 @@ export default function (obj = {}, container) {
111
112
  }
112
113
  })
113
114
 
114
- container.append({
115
- support: {
116
- I: actor,
117
- },
118
- })
115
+ // Update container.support.I to ensure it has the latest actor reference
116
+ if (!container.actor() || container.actor() !== actor) {
117
+ container.append({
118
+ support: {
119
+ I: actor,
120
+ },
121
+ })
122
+ }
119
123
  })
120
- // store.actor = actor;
121
- // add custom steps from actor
124
+
125
+ // add custom steps from actor immediately
122
126
  Object.keys(obj).forEach(key => {
123
127
  const ms = new MetaStep('I', key)
124
128
  ms.setContext(actor)
package/lib/codecept.js CHANGED
@@ -3,8 +3,9 @@ import { globSync } from 'glob'
3
3
  import shuffle from 'lodash.shuffle'
4
4
  import fsPath from 'path'
5
5
  import { resolve } from 'path'
6
- import { fileURLToPath } from 'url'
6
+ import { fileURLToPath, pathToFileURL } from 'url'
7
7
  import { dirname } from 'path'
8
+ import { createRequire } from 'module'
8
9
 
9
10
  const __filename = fileURLToPath(import.meta.url)
10
11
  const __dirname = dirname(__filename)
@@ -18,6 +19,7 @@ import ActorFactory from './actor.js'
18
19
  import output from './output.js'
19
20
  import { emptyFolder } from './utils.js'
20
21
  import { initCodeceptGlobals } from './globals.js'
22
+ import { validateTypeScriptSetup, getTSNodeESMWarning } from './utils/loaderCheck.js'
21
23
  import recorder from './recorder.js'
22
24
 
23
25
  import storeListener from './listener/store.js'
@@ -66,6 +68,21 @@ class Codecept {
66
68
  modulePath = `${modulePath}.js`
67
69
  }
68
70
  }
71
+ } else {
72
+ // For npm packages, resolve from the user's directory
73
+ // This ensures packages like tsx are found in user's node_modules
74
+ const userDir = global.codecept_dir || process.cwd()
75
+
76
+ try {
77
+ // Use createRequire to resolve from user's directory
78
+ const userRequire = createRequire(pathToFileURL(resolve(userDir, 'package.json')).href)
79
+ const resolvedPath = userRequire.resolve(requiredModule)
80
+ modulePath = pathToFileURL(resolvedPath).href
81
+ } catch (resolveError) {
82
+ // If resolution fails, try direct import (will check from CodeceptJS node_modules)
83
+ // This is the fallback for globally installed packages
84
+ modulePath = requiredModule
85
+ }
69
86
  }
70
87
  // Use dynamic import for ESM
71
88
  await import(modulePath)
@@ -103,23 +120,26 @@ class Codecept {
103
120
  * Executes hooks.
104
121
  */
105
122
  async runHooks() {
106
- // default hooks - dynamic imports for ESM
107
- const listenerModules = [
108
- './listener/store.js',
109
- './listener/steps.js',
110
- './listener/config.js',
111
- './listener/result.js',
112
- './listener/helpers.js',
113
- './listener/globalTimeout.js',
114
- './listener/globalRetry.js',
115
- './listener/retryEnhancer.js',
116
- './listener/exit.js',
117
- './listener/emptyRun.js',
118
- ]
119
-
120
- for (const modulePath of listenerModules) {
121
- const module = await import(modulePath)
122
- runHook(module.default || module)
123
+ // For workers parent process we only need plugins/hooks.
124
+ // Core listeners are executed inside worker threads.
125
+ if (!this.opts?.skipDefaultListeners) {
126
+ const listenerModules = [
127
+ './listener/store.js',
128
+ './listener/steps.js',
129
+ './listener/config.js',
130
+ './listener/result.js',
131
+ './listener/helpers.js',
132
+ './listener/globalTimeout.js',
133
+ './listener/globalRetry.js',
134
+ './listener/retryEnhancer.js',
135
+ './listener/exit.js',
136
+ './listener/emptyRun.js',
137
+ ]
138
+
139
+ for (const modulePath of listenerModules) {
140
+ const module = await import(modulePath)
141
+ runHook(module.default || module)
142
+ }
123
143
  }
124
144
 
125
145
  // custom hooks (previous iteration of plugins)
@@ -246,6 +266,19 @@ class Codecept {
246
266
  async run(test) {
247
267
  await container.started()
248
268
 
269
+ // Check TypeScript loader configuration before running tests
270
+ const tsValidation = validateTypeScriptSetup(this.testFiles, this.requiringModules || [])
271
+ if (tsValidation.hasError) {
272
+ output.error(tsValidation.message)
273
+ process.exit(1)
274
+ }
275
+
276
+ // Show warning if ts-node/esm is being used
277
+ const tsWarning = getTSNodeESMWarning(this.requiringModules || [])
278
+ if (tsWarning) {
279
+ output.print(output.colors.yellow(tsWarning))
280
+ }
281
+
249
282
  // Ensure translations are loaded for Gherkin features
250
283
  try {
251
284
  const { loadTranslations } = await import('./mocha/gherkin.js')
@@ -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({
@@ -229,18 +229,25 @@ function getImportString(testsPath, targetFolderPath, pathsToType, pathsToValue)
229
229
  const importStrings = []
230
230
 
231
231
  for (const name in pathsToType) {
232
- const relativePath = getPath(pathsToType[name], targetFolderPath, testsPath)
233
- // For ESM modules with default exports, we need to access the default export type
234
- if (relativePath.endsWith('.js')) {
235
- importStrings.push(`type ${name} = typeof import('${relativePath}')['default'];`)
232
+ const originalPath = pathsToType[name]
233
+ const relativePath = getPath(originalPath, targetFolderPath, testsPath)
234
+ // For .js files with plain object exports, access .default to allow TypeScript to extract properties
235
+ // For .ts files, the default export is handled differently by TypeScript
236
+ if (originalPath.endsWith('.js')) {
237
+ importStrings.push(`type ${name} = typeof import('${relativePath}').default;`)
236
238
  } else {
237
239
  importStrings.push(`type ${name} = typeof import('${relativePath}');`)
238
240
  }
239
241
  }
240
242
 
241
243
  for (const name in pathsToValue) {
242
- const relativePath = getPath(pathsToValue[name], targetFolderPath, testsPath)
243
- importStrings.push(`type ${name} = import('${relativePath}');`)
244
+ const originalPath = pathsToValue[name]
245
+ const relativePath = getPath(originalPath, targetFolderPath, testsPath)
246
+ if (originalPath.endsWith('.js') || originalPath.endsWith('.ts')) {
247
+ importStrings.push(`type ${name} = InstanceType<typeof import('${relativePath}').default>;`)
248
+ } else {
249
+ importStrings.push(`type ${name} = import('${relativePath}');`)
250
+ }
244
251
  }
245
252
 
246
253
  return importStrings
@@ -20,9 +20,6 @@ const defaultConfig = {
20
20
  helpers: {},
21
21
  include: {},
22
22
  plugins: {
23
- htmlReporter: {
24
- enabled: true,
25
- },
26
23
  },
27
24
  }
28
25
 
@@ -161,7 +158,7 @@ export default async function (initPath) {
161
158
  isTypeScript = true
162
159
  extension = isTypeScript === true ? 'ts' : 'js'
163
160
  packages.push('typescript')
164
- packages.push('ts-node')
161
+ packages.push('tsx') // Add tsx for TypeScript support
165
162
  packages.push('@types/node')
166
163
  }
167
164
 
@@ -172,6 +169,7 @@ export default async function (initPath) {
172
169
  config.tests = result.tests
173
170
  if (isTypeScript) {
174
171
  config.tests = `${config.tests.replace(/\.js$/, `.${extension}`)}`
172
+ config.require = ['tsx/cjs'] // Add tsx/cjs loader for TypeScript tests
175
173
  }
176
174
 
177
175
  // create a directory tests if it is included in tests path
@@ -40,11 +40,23 @@ 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
44
+ process.env.RUNS_WITH_WORKERS = 'true'
45
45
 
46
46
  const workers = new Workers(numberOfWorkers, config)
47
47
  workers.overrideConfig(overrideConfigs)
48
+
49
+ // Show test distribution after workers are initialized
50
+ await workers.bootstrapAll()
51
+
52
+ const workerObjects = workers.getWorkers()
53
+ output.print()
54
+ output.print('Test distribution:')
55
+ workerObjects.forEach((worker, index) => {
56
+ const testCount = worker.tests.length
57
+ output.print(` Worker ${index + 1}: ${testCount} test${testCount !== 1 ? 's' : ''}`)
58
+ })
59
+ output.print()
48
60
 
49
61
  workers.on(event.test.failed, test => {
50
62
  output.test.failed(test)
@@ -68,7 +80,6 @@ export default async function (workerCount, selectedRuns, options) {
68
80
  if (options.verbose) {
69
81
  await getMachineInfo()
70
82
  }
71
- await workers.bootstrapAll()
72
83
  await workers.run()
73
84
  } catch (err) {
74
85
  output.error(err)
@@ -11,7 +11,7 @@ import { parentPort, workerData } from 'worker_threads'
11
11
 
12
12
  // Delay imports to avoid ES Module loader race conditions in Node 22.x worker threads
13
13
  // These will be imported dynamically when needed
14
- let event, container, Codecept, getConfig, tryOrDefault, deepMerge
14
+ let event, container, Codecept, getConfig, tryOrDefault, deepMerge, fixErrorStack
15
15
 
16
16
  let stdout = ''
17
17
 
@@ -19,6 +19,48 @@ 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
+ if (global.container?.tsFileMapping && fixErrorStack) {
25
+ const fileMapping = global.container.tsFileMapping()
26
+ if (fileMapping) {
27
+ fixErrorStack(err, fileMapping)
28
+ }
29
+ }
30
+
31
+ // Log to stderr to bypass stdout suppression
32
+ process.stderr.write(`[Worker ${workerIndex}] UNCAUGHT EXCEPTION: ${err.message}\n`)
33
+ process.stderr.write(`${err.stack}\n`)
34
+
35
+ // Don't exit on test assertion errors - those are handled by mocha
36
+ if (err.name === 'AssertionError' || err.message?.includes('expected')) {
37
+ return
38
+ }
39
+ process.exit(1)
40
+ })
41
+
42
+ process.on('unhandledRejection', (reason, promise) => {
43
+ if (reason && typeof reason === 'object' && reason.stack && global.container?.tsFileMapping && fixErrorStack) {
44
+ const fileMapping = global.container.tsFileMapping()
45
+ if (fileMapping) {
46
+ fixErrorStack(reason, fileMapping)
47
+ }
48
+ }
49
+
50
+ // Log to stderr to bypass stdout suppression
51
+ const msg = reason?.message || String(reason)
52
+ process.stderr.write(`[Worker ${workerIndex}] UNHANDLED REJECTION: ${msg}\n`)
53
+ if (reason?.stack) {
54
+ process.stderr.write(`${reason.stack}\n`)
55
+ }
56
+
57
+ // Don't exit on test-related rejections
58
+ if (msg.includes('expected') || msg.includes('AssertionError')) {
59
+ return
60
+ }
61
+ process.exit(1)
62
+ })
63
+
22
64
  // hide worker output
23
65
  // In pool mode, only suppress output if debug is NOT enabled
24
66
  // In regular mode, hide result output but allow step output in verbose/debug
@@ -26,6 +68,10 @@ if (poolMode && !options.debug) {
26
68
  // In pool mode without debug, allow test names and important output but suppress verbose details
27
69
  const originalWrite = process.stdout.write
28
70
  process.stdout.write = string => {
71
+ // Always allow Worker logs
72
+ if (string.includes('[Worker')) {
73
+ return originalWrite.call(process.stdout, string)
74
+ }
29
75
  // Allow test names (✔ or ✖), Scenario Steps, failures, and important markers
30
76
  if (
31
77
  string.includes('✔') ||
@@ -45,7 +91,12 @@ if (poolMode && !options.debug) {
45
91
  return originalWrite.call(process.stdout, string)
46
92
  }
47
93
  } else if (!poolMode && !options.debug && !options.verbose) {
94
+ const originalWrite = process.stdout.write
48
95
  process.stdout.write = string => {
96
+ // Always allow Worker logs
97
+ if (string.includes('[Worker')) {
98
+ return originalWrite.call(process.stdout, string)
99
+ }
49
100
  stdout += string
50
101
  return true
51
102
  }
@@ -82,30 +133,69 @@ let config
82
133
  // Load test and run
83
134
  initPromise = (async function () {
84
135
  try {
136
+ // Add staggered delay at the very start to prevent resource conflicts
137
+ // Longer delay for browser initialization conflicts
138
+ const delay = (workerIndex - 1) * 2000 // 0ms, 2s, 4s, etc.
139
+ if (delay > 0) {
140
+ await new Promise(resolve => setTimeout(resolve, delay))
141
+ }
142
+
85
143
  // Import modules dynamically to avoid ES Module loader race conditions in Node 22.x
86
144
  const eventModule = await import('../../event.js')
87
145
  const containerModule = await import('../../container.js')
88
146
  const utilsModule = await import('../utils.js')
89
147
  const coreUtilsModule = await import('../../utils.js')
90
148
  const CodeceptModule = await import('../../codecept.js')
91
-
149
+ const typescriptModule = await import('../../utils/typescript.js')
150
+
92
151
  event = eventModule.default
93
152
  container = containerModule.default
94
153
  getConfig = utilsModule.getConfig
95
154
  tryOrDefault = coreUtilsModule.tryOrDefault
96
155
  deepMerge = coreUtilsModule.deepMerge
97
156
  Codecept = CodeceptModule.default
157
+ fixErrorStack = typescriptModule.fixErrorStack
98
158
 
99
159
  const overrideConfigs = tryOrDefault(() => JSON.parse(options.override), {})
100
160
 
101
- // IMPORTANT: await is required here since getConfig is async
102
- const baseConfig = await getConfig(options.config || testRoot)
161
+ let baseConfig
162
+ try {
163
+ // IMPORTANT: await is required here since getConfig is async
164
+ baseConfig = await getConfig(options.config || testRoot)
165
+ } catch (configErr) {
166
+ if (global.container?.tsFileMapping && fixErrorStack) {
167
+ const fileMapping = global.container.tsFileMapping()
168
+ if (fileMapping) {
169
+ fixErrorStack(configErr, fileMapping)
170
+ }
171
+ }
172
+ process.stderr.write(`[Worker ${workerIndex}] FAILED loading config: ${configErr.message}\n`)
173
+ process.stderr.write(`${configErr.stack}\n`)
174
+ await new Promise(resolve => setTimeout(resolve, 100))
175
+ process.exit(1)
176
+ }
103
177
 
104
178
  // important deep merge so dynamic things e.g. functions on config are not overridden
105
179
  config = deepMerge(baseConfig, overrideConfigs)
106
180
 
107
- codecept = new Codecept(config, options)
108
- await codecept.init(testRoot)
181
+ // Pass workerIndex as child option for output.process() to display worker prefix
182
+ const optsWithChild = { ...options, child: workerIndex }
183
+ codecept = new Codecept(config, optsWithChild)
184
+
185
+ try {
186
+ await codecept.init(testRoot)
187
+ } catch (initErr) {
188
+ if (global.container?.tsFileMapping && fixErrorStack) {
189
+ const fileMapping = global.container.tsFileMapping()
190
+ if (fileMapping) {
191
+ fixErrorStack(initErr, fileMapping)
192
+ }
193
+ }
194
+ process.stderr.write(`[Worker ${workerIndex}] FAILED during codecept.init(): ${initErr.message}\n`)
195
+ process.stderr.write(`${initErr.stack}\n`)
196
+ process.exit(1)
197
+ }
198
+
109
199
  codecept.loadTests()
110
200
  mocha = container.mocha()
111
201
 
@@ -124,10 +214,18 @@ initPromise = (async function () {
124
214
  await runTests()
125
215
  } else {
126
216
  // No tests to run, close the worker
217
+ console.error(`[Worker ${workerIndex}] ERROR: No tests found after filtering! Assigned ${tests.length} UIDs but none matched.`)
127
218
  parentPort?.close()
128
219
  }
129
220
  } catch (err) {
130
- console.error('Error in worker initialization:', err)
221
+ if (global.container?.tsFileMapping && fixErrorStack) {
222
+ const fileMapping = global.container.tsFileMapping()
223
+ if (fileMapping) {
224
+ fixErrorStack(err, fileMapping)
225
+ }
226
+ }
227
+ process.stderr.write(`[Worker ${workerIndex}] FATAL ERROR: ${err.message}\n`)
228
+ process.stderr.write(`${err.stack}\n`)
131
229
  process.exit(1)
132
230
  }
133
231
  })()
@@ -145,8 +243,14 @@ async function runTests() {
145
243
  disablePause()
146
244
  try {
147
245
  await codecept.run()
246
+ } catch (err) {
247
+ throw err
148
248
  } finally {
149
- await codecept.teardown()
249
+ try {
250
+ await codecept.teardown()
251
+ } catch (err) {
252
+ // Ignore teardown errors
253
+ }
150
254
  }
151
255
  }
152
256
 
@@ -334,8 +438,16 @@ function filterTests() {
334
438
  mocha.files = files
335
439
  mocha.loadFiles()
336
440
 
337
- for (const suite of mocha.suite.suites) {
441
+ // Recursively filter tests in all suites (including nested ones)
442
+ const filterSuiteTests = (suite) => {
338
443
  suite.tests = suite.tests.filter(test => tests.indexOf(test.uid) >= 0)
444
+ for (const childSuite of suite.suites) {
445
+ filterSuiteTests(childSuite)
446
+ }
447
+ }
448
+
449
+ for (const suite of mocha.suite.suites) {
450
+ filterSuiteTests(suite)
339
451
  }
340
452
  }
341
453
 
package/lib/config.js CHANGED
@@ -2,6 +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, fixErrorStack } from './utils/typescript.js'
5
6
 
6
7
  const defaultConfig = {
7
8
  output: './_output',
@@ -155,42 +156,32 @@ async function loadConfigFile(configFile) {
155
156
  try {
156
157
  // For .ts files, try to compile and load as JavaScript
157
158
  if (extensionName === '.ts') {
159
+ let transpileError = null
160
+ let tempFile = null
161
+ let allTempFiles = null
162
+ let fileMapping = null
163
+
158
164
  try {
159
- // Try to load ts-node and compile the file
160
- const { transpile } = require('typescript')
161
- const tsContent = fs.readFileSync(configFile, 'utf8')
162
-
163
- // Transpile TypeScript to JavaScript with ES module output
164
- const jsContent = transpile(tsContent, {
165
- module: 99, // ModuleKind.ESNext
166
- target: 99, // ScriptTarget.ESNext
167
- esModuleInterop: true,
168
- allowSyntheticDefaultImports: true,
169
- })
170
-
171
- // Create a temporary JS file with .mjs extension to force ES module treatment
172
- const tempJsFile = configFile.replace('.ts', '.temp.mjs')
173
- fs.writeFileSync(tempJsFile, jsContent)
174
-
175
- try {
176
- configModule = await import(tempJsFile)
177
- // Clean up temp file
178
- fs.unlinkSync(tempJsFile)
179
- } catch (err) {
180
- // Clean up temp file even on error
181
- if (fs.existsSync(tempJsFile)) {
182
- fs.unlinkSync(tempJsFile)
183
- }
184
- throw err
165
+ // Use the TypeScript transpilation utility
166
+ const typescript = require('typescript')
167
+ const result = await transpileTypeScript(configFile, typescript)
168
+ tempFile = result.tempFile
169
+ allTempFiles = result.allTempFiles
170
+ fileMapping = result.fileMapping
171
+
172
+ configModule = await import(tempFile)
173
+ cleanupTempFiles(allTempFiles)
174
+ } catch (err) {
175
+ transpileError = err
176
+ if (fileMapping) {
177
+ fixErrorStack(err, fileMapping)
185
178
  }
186
- } catch (tsError) {
187
- // If TypeScript compilation fails, fallback to ts-node
188
- try {
189
- require('ts-node/register')
190
- configModule = require(configFile)
191
- } catch (tsNodeError) {
192
- throw new Error(`Failed to load TypeScript config: ${tsError.message}`)
179
+ if (allTempFiles) {
180
+ cleanupTempFiles(allTempFiles)
193
181
  }
182
+ // Throw immediately with the actual error - don't fall back to ts-node
183
+ // as it will mask the real error with "Unexpected token 'export'"
184
+ throw err
194
185
  }
195
186
  } else {
196
187
  // Try ESM import first for JS files