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.
- package/README.md +39 -27
- package/bin/codecept.js +2 -2
- package/bin/mcp-server.js +610 -0
- package/docs/webapi/appendField.mustache +5 -0
- package/docs/webapi/attachFile.mustache +12 -0
- package/docs/webapi/checkOption.mustache +1 -1
- package/docs/webapi/clearField.mustache +5 -0
- package/docs/webapi/dontSeeCurrentPathEquals.mustache +10 -0
- package/docs/webapi/dontSeeElement.mustache +4 -0
- package/docs/webapi/dontSeeInField.mustache +5 -0
- package/docs/webapi/fillField.mustache +5 -0
- package/docs/webapi/moveCursorTo.mustache +5 -1
- package/docs/webapi/seeCurrentPathEquals.mustache +10 -0
- package/docs/webapi/seeElement.mustache +4 -0
- package/docs/webapi/seeInField.mustache +5 -0
- package/docs/webapi/selectOption.mustache +5 -0
- package/docs/webapi/uncheckOption.mustache +1 -1
- package/lib/actor.js +12 -8
- package/lib/codecept.js +51 -18
- package/lib/command/definitions.js +14 -7
- package/lib/command/init.js +2 -4
- package/lib/command/run-workers.js +13 -2
- package/lib/command/workers/runTests.js +121 -9
- package/lib/config.js +24 -33
- package/lib/container.js +177 -28
- package/lib/element/WebElement.js +81 -2
- package/lib/els.js +12 -6
- package/lib/helper/Appium.js +8 -8
- package/lib/helper/GraphQL.js +6 -4
- package/lib/helper/JSONResponse.js +3 -4
- package/lib/helper/Playwright.js +339 -505
- package/lib/helper/Puppeteer.js +324 -89
- package/lib/helper/REST.js +15 -9
- package/lib/helper/WebDriver.js +311 -81
- package/lib/helper/errors/ElementNotFound.js +5 -2
- package/lib/helper/errors/MultipleElementsFound.js +52 -0
- package/lib/helper/extras/elementSelection.js +58 -0
- package/lib/helper/scripts/dropFile.js +11 -0
- package/lib/html.js +14 -1
- package/lib/listener/config.js +11 -3
- package/lib/listener/globalRetry.js +32 -6
- package/lib/listener/helpers.js +2 -14
- package/lib/locator.js +32 -0
- package/lib/mocha/cli.js +16 -0
- package/lib/mocha/factory.js +7 -27
- package/lib/mocha/gherkin.js +4 -4
- package/lib/mocha/test.js +4 -2
- package/lib/output.js +2 -2
- package/lib/plugin/aiTrace.js +464 -0
- package/lib/plugin/auth.js +2 -1
- package/lib/plugin/retryFailedStep.js +28 -19
- package/lib/plugin/stepByStepReport.js +5 -1
- package/lib/step/base.js +14 -1
- package/lib/step/config.js +15 -2
- package/lib/step/meta.js +18 -1
- package/lib/step/record.js +9 -1
- package/lib/utils/loaderCheck.js +162 -0
- package/lib/utils/typescript.js +449 -0
- package/lib/utils.js +48 -0
- package/lib/workers.js +163 -54
- package/package.json +43 -32
- package/typings/index.d.ts +120 -4
- package/lib/helper/extras/PlaywrightLocator.js +0 -110
- package/lib/listener/enhancedGlobalRetry.js +0 -110
- package/lib/plugin/enhancedRetryFailedStep.js +0 -99
- package/lib/plugin/htmlReporter.js +0 -3648
- package/lib/retryCoordinator.js +0 -207
- package/typings/promiseBasedTypes.d.ts +0 -11011
- 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
|
|
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.
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
|
243
|
-
|
|
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
|
package/lib/command/init.js
CHANGED
|
@@ -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('
|
|
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
|
-
|
|
102
|
-
|
|
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
|
-
|
|
108
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
160
|
-
const
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
187
|
-
|
|
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
|