codeceptjs 4.0.0-beta.9.esm-aria → 4.0.0-rc.2
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 +2 -2
- package/docs/webapi/dontSeeCurrentPathEquals.mustache +10 -0
- package/docs/webapi/seeCurrentPathEquals.mustache +10 -0
- package/lib/actor.js +12 -8
- package/lib/codecept.js +31 -1
- package/lib/command/definitions.js +14 -7
- package/lib/command/init.js +2 -1
- package/lib/command/run-workers.js +12 -2
- package/lib/command/workers/runTests.js +121 -9
- package/lib/config.js +24 -33
- package/lib/container.js +158 -24
- package/lib/helper/GraphQL.js +6 -4
- package/lib/helper/JSONResponse.js +3 -4
- package/lib/helper/Playwright.js +195 -423
- package/lib/helper/Puppeteer.js +155 -38
- package/lib/helper/REST.js +15 -9
- package/lib/helper/WebDriver.js +158 -23
- package/lib/helper/errors/ElementNotFound.js +5 -2
- package/lib/helper/errors/MultipleElementsFound.js +135 -0
- package/lib/listener/config.js +11 -3
- package/lib/listener/helpers.js +2 -14
- package/lib/locator.js +32 -0
- package/lib/mocha/cli.js +6 -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/auth.js +2 -1
- package/lib/plugin/htmlReporter.js +4 -4
- package/lib/step/base.js +14 -1
- package/lib/step/meta.js +18 -1
- package/lib/step/record.js +8 -0
- package/lib/utils/loaderCheck.js +162 -0
- package/lib/utils/typescript.js +449 -0
- package/lib/workers.js +114 -47
- package/package.json +39 -30
- package/typings/index.d.ts +101 -4
- package/typings/promiseBasedTypes.d.ts +3974 -5516
- package/typings/types.d.ts +4146 -5817
- package/lib/helper/extras/PlaywrightLocator.js +0 -110
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('
|
|
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('
|
|
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')
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Checks that current URL path does NOT match the expected path.
|
|
2
|
+
Query strings and URL fragments are ignored.
|
|
3
|
+
|
|
4
|
+
```js
|
|
5
|
+
I.dontSeeCurrentPathEquals('/form'); // fails for '/form', '/form?user=1', '/form#section'
|
|
6
|
+
I.dontSeeCurrentPathEquals('/'); // fails for '/', '/?user=ok', '/#top'
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
@param {string} path value to check.
|
|
10
|
+
@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
|
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)
|
|
@@ -246,6 +263,19 @@ class Codecept {
|
|
|
246
263
|
async run(test) {
|
|
247
264
|
await container.started()
|
|
248
265
|
|
|
266
|
+
// Check TypeScript loader configuration before running tests
|
|
267
|
+
const tsValidation = validateTypeScriptSetup(this.testFiles, this.requiringModules || [])
|
|
268
|
+
if (tsValidation.hasError) {
|
|
269
|
+
output.error(tsValidation.message)
|
|
270
|
+
process.exit(1)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Show warning if ts-node/esm is being used
|
|
274
|
+
const tsWarning = getTSNodeESMWarning(this.requiringModules || [])
|
|
275
|
+
if (tsWarning) {
|
|
276
|
+
output.print(output.colors.yellow(tsWarning))
|
|
277
|
+
}
|
|
278
|
+
|
|
249
279
|
// Ensure translations are loaded for Gherkin features
|
|
250
280
|
try {
|
|
251
281
|
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
|
@@ -161,7 +161,7 @@ export default async function (initPath) {
|
|
|
161
161
|
isTypeScript = true
|
|
162
162
|
extension = isTypeScript === true ? 'ts' : 'js'
|
|
163
163
|
packages.push('typescript')
|
|
164
|
-
packages.push('
|
|
164
|
+
packages.push('tsx') // Add tsx for TypeScript support
|
|
165
165
|
packages.push('@types/node')
|
|
166
166
|
}
|
|
167
167
|
|
|
@@ -172,6 +172,7 @@ export default async function (initPath) {
|
|
|
172
172
|
config.tests = result.tests
|
|
173
173
|
if (isTypeScript) {
|
|
174
174
|
config.tests = `${config.tests.replace(/\.js$/, `.${extension}`)}`
|
|
175
|
+
config.require = ['tsx/cjs'] // Add tsx/cjs loader for TypeScript tests
|
|
175
176
|
}
|
|
176
177
|
|
|
177
178
|
// create a directory tests if it is included in tests path
|
|
@@ -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)
|
|
@@ -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
|