codeceptjs 4.0.8 → 4.1.0-beta.1-esm-mocha
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/docs/configuration.md +4 -2
- package/docs/installation.md +1 -1
- package/docs/parallel.md +2 -2
- package/docs/typescript.md +7 -5
- package/lib/codecept.js +20 -13
- package/lib/command/check.js +2 -1
- package/lib/command/dryRun.js +2 -1
- package/lib/command/init.js +13 -1
- package/lib/command/workers/runTests.js +7 -12
- package/lib/mocha/factory.js +0 -60
- package/lib/mocha/loadTests.js +69 -0
- package/lib/rerun.js +3 -30
- package/lib/utils/loaderCheck.js +16 -12
- package/lib/utils/typescript.js +1 -3
- package/lib/workers.js +16 -15
- package/package.json +1 -1
package/docs/configuration.md
CHANGED
|
@@ -71,16 +71,18 @@ For TypeScript test files in CodeceptJS 4.x, use the [`tsx`](https://tsx.is) loa
|
|
|
71
71
|
// codecept.conf.ts
|
|
72
72
|
export const config = {
|
|
73
73
|
tests: './**/*_test.ts',
|
|
74
|
-
require: ['tsx/
|
|
74
|
+
require: ['tsx/esm'],
|
|
75
75
|
helpers: {},
|
|
76
76
|
include: {},
|
|
77
77
|
}
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
+
This requires `"type": "module"` in `package.json` so `.ts` test files are compiled as ES Modules.
|
|
81
|
+
|
|
80
82
|
Combine several modules:
|
|
81
83
|
|
|
82
84
|
```ts
|
|
83
|
-
require: ['tsx/
|
|
85
|
+
require: ['tsx/esm', 'should', './lib/testSetup']
|
|
84
86
|
```
|
|
85
87
|
|
|
86
88
|
The config file itself (`codecept.conf.ts`) and helpers are transpiled automatically — only test files need the loader. See [TypeScript](/typescript) for the full setup.
|
package/docs/installation.md
CHANGED
|
@@ -111,7 +111,7 @@ npm i tsx --save-dev
|
|
|
111
111
|
// codecept.conf.ts
|
|
112
112
|
export const config = {
|
|
113
113
|
tests: './**/*_test.ts',
|
|
114
|
-
require: ['tsx/
|
|
114
|
+
require: ['tsx/esm'], // loads the *_test.ts files as ES Modules (needs "type": "module")
|
|
115
115
|
helpers: {
|
|
116
116
|
Playwright: { url: 'http://localhost', browser: 'chromium' },
|
|
117
117
|
},
|
package/docs/parallel.md
CHANGED
|
@@ -116,7 +116,7 @@ import { Workers, event } from 'codeceptjs'
|
|
|
116
116
|
const workers = new Workers(null, { testConfig: './codecept.conf.js' })
|
|
117
117
|
|
|
118
118
|
// split the suite into 2 groups, run each group on two browsers
|
|
119
|
-
const groups = workers.createGroupsOfSuites(2)
|
|
119
|
+
const groups = await workers.createGroupsOfSuites(2)
|
|
120
120
|
for (const browser of ['chromium', 'firefox']) {
|
|
121
121
|
for (const group of groups) {
|
|
122
122
|
const worker = workers.spawn()
|
|
@@ -139,7 +139,7 @@ try {
|
|
|
139
139
|
Building blocks:
|
|
140
140
|
|
|
141
141
|
- `new Workers(N, { testConfig, options })` — `N` workers; pass `null` to spawn them yourself with `spawn()`.
|
|
142
|
-
- `createGroupsOfTests(n)` / `createGroupsOfSuites(n)` — split the suite into `n` groups.
|
|
142
|
+
- `await createGroupsOfTests(n)` / `await createGroupsOfSuites(n)` — split the suite into `n` groups (async: test files are loaded as ES Modules).
|
|
143
143
|
- `worker.addTests(group)` / `worker.addConfig(partialConfig)` — assign tests and config overrides to a spawned worker.
|
|
144
144
|
- `bootstrapAll()` → `run()` → `teardownAll()` — lifecycle (wrap `run()` in `try/finally` so teardown always runs).
|
|
145
145
|
- Events on the `workers` object: `event.test.passed`, `event.test.failed`, `event.all.result`, plus `'message'` for anything a child worker sends. `printResults()` prints the standard summary; `result.hasFailed()` and `result.stats` give the totals.
|
package/docs/typescript.md
CHANGED
|
@@ -15,7 +15,7 @@ CodeceptJS ships [type declarations](https://github.com/codeceptjs/CodeceptJS/tr
|
|
|
15
15
|
? Do you plan to write tests in TypeScript? Yes
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
It writes `codecept.conf.ts` and `*_test.ts` files. The **config file** and helpers are transpiled automatically. **Test files** need a loader — CodeceptJS 4.x is ESM
|
|
18
|
+
It writes `codecept.conf.ts` and `*_test.ts` files. The **config file** and helpers are transpiled automatically. **Test files** need a loader — CodeceptJS 4.x is ESM and loads test files as ES Modules, so use [`tsx`](https://tsx.is) (fast, esbuild-based, no `tsconfig.json` required):
|
|
19
19
|
|
|
20
20
|
```sh
|
|
21
21
|
npm i tsx --save-dev
|
|
@@ -25,16 +25,16 @@ npm i tsx --save-dev
|
|
|
25
25
|
// codecept.conf.ts
|
|
26
26
|
export const config = {
|
|
27
27
|
tests: './**/*_test.ts',
|
|
28
|
-
require: ['tsx/
|
|
28
|
+
require: ['tsx/esm'], // loads the *_test.ts files as ES Modules
|
|
29
29
|
helpers: {
|
|
30
30
|
Playwright: { url: 'http://localhost', browser: 'chromium' },
|
|
31
31
|
},
|
|
32
32
|
}
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
Set `"type": "module"` in `package.json` so `tsx` compiles your `.ts` test files as ES Modules. Then run the tests with `npx codeceptjs run`.
|
|
36
36
|
|
|
37
|
-
> Adding TypeScript to an existing project: set `"type": "module"` in `package.json`, rename the config to `codecept.conf.ts` with `export const config = {}`, install `tsx`, and add `require: ['tsx/
|
|
37
|
+
> Adding TypeScript to an existing project: set `"type": "module"` in `package.json`, rename the config to `codecept.conf.ts` with `export const config = {}`, install `tsx`, and add `require: ['tsx/esm']`.
|
|
38
38
|
|
|
39
39
|
## Writing tests
|
|
40
40
|
|
|
@@ -59,7 +59,9 @@ Scenario('admin signs in', ({ I }) => {
|
|
|
59
59
|
})
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
-
> **Cannot find module** or **Unexpected token** while running tests means the loader isn't wired up — check that `tsx` is installed and `require: ['tsx/
|
|
62
|
+
> **Cannot find module** or **Unexpected token** while running tests means the loader isn't wired up — check that `tsx` is installed and `require: ['tsx/esm']` is in the config.
|
|
63
|
+
>
|
|
64
|
+
> **`ERR_REQUIRE_CYCLE_MODULE`** means `tsx` is compiling your `.ts` tests as CommonJS — add `"type": "module"` to the nearest `package.json`.
|
|
63
65
|
|
|
64
66
|
## Promise-based typings
|
|
65
67
|
|
package/lib/codecept.js
CHANGED
|
@@ -19,6 +19,7 @@ import ActorFactory from './actor.js'
|
|
|
19
19
|
import output from './output.js'
|
|
20
20
|
import { emptyFolder, resolveImportModulePath } from './utils.js'
|
|
21
21
|
import { initCodeceptGlobals } from './globals.js'
|
|
22
|
+
import loadTests from './mocha/loadTests.js'
|
|
22
23
|
import { validateTypeScriptSetup, getTSNodeESMWarning } from './utils/loaderCheck.js'
|
|
23
24
|
import recorder from './recorder.js'
|
|
24
25
|
import store from './store.js'
|
|
@@ -58,7 +59,11 @@ class Codecept {
|
|
|
58
59
|
*/
|
|
59
60
|
async requireModules(requiringModules) {
|
|
60
61
|
if (requiringModules) {
|
|
61
|
-
for (
|
|
62
|
+
for (let requiredModule of requiringModules) {
|
|
63
|
+
if (requiredModule === 'tsx/cjs') {
|
|
64
|
+
output.print(output.styles.debug('`tsx/cjs` is deprecated for test files. Using `tsx/esm` instead. Update your config `require` to `tsx/esm` and add `"type": "module"` to package.json.'))
|
|
65
|
+
requiredModule = 'tsx/esm'
|
|
66
|
+
}
|
|
62
67
|
let modulePath = requiredModule
|
|
63
68
|
const isLocalFile = existsSync(modulePath) || existsSync(`${modulePath}.js`)
|
|
64
69
|
if (isLocalFile) {
|
|
@@ -295,21 +300,23 @@ class Codecept {
|
|
|
295
300
|
this.testFiles.sort()
|
|
296
301
|
}
|
|
297
302
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
mocha.files = this.testFiles
|
|
303
|
+
const mocha = container.mocha()
|
|
304
|
+
mocha.files = this.testFiles
|
|
301
305
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
}
|
|
306
|
-
const testBasename = fsPath.basename(test, '.js')
|
|
307
|
-
const testFeatureBasename = fsPath.basename(test, '.feature')
|
|
308
|
-
mocha.files = mocha.files.filter(t => {
|
|
309
|
-
return fsPath.basename(t, '.js') === testBasename || fsPath.basename(t, '.feature') === testFeatureBasename || t === test
|
|
310
|
-
})
|
|
306
|
+
if (test) {
|
|
307
|
+
if (!fsPath.isAbsolute(test)) {
|
|
308
|
+
test = fsPath.join(store.codeceptDir, test)
|
|
311
309
|
}
|
|
310
|
+
const testBasename = fsPath.basename(test, '.js')
|
|
311
|
+
const testFeatureBasename = fsPath.basename(test, '.feature')
|
|
312
|
+
mocha.files = mocha.files.filter(t => {
|
|
313
|
+
return fsPath.basename(t, '.js') === testBasename || fsPath.basename(t, '.feature') === testFeatureBasename || t === test
|
|
314
|
+
})
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
await loadTests(mocha)
|
|
312
318
|
|
|
319
|
+
return new Promise((resolve, reject) => {
|
|
313
320
|
const done = async (failures) => {
|
|
314
321
|
event.emit(event.all.result, container.result())
|
|
315
322
|
event.emit(event.all.after, this)
|
package/lib/command/check.js
CHANGED
|
@@ -6,6 +6,7 @@ import Container from '../container.js'
|
|
|
6
6
|
import figures from 'figures'
|
|
7
7
|
import chalk from 'chalk'
|
|
8
8
|
import { createTest } from '../mocha/test.js'
|
|
9
|
+
import loadTests from '../mocha/loadTests.js'
|
|
9
10
|
import { getMachineInfo } from './info.js'
|
|
10
11
|
import definitions from './definitions.js'
|
|
11
12
|
|
|
@@ -73,7 +74,7 @@ export default async function (options) {
|
|
|
73
74
|
const files = codecept.testFiles
|
|
74
75
|
const mocha = Container.mocha()
|
|
75
76
|
mocha.files = files
|
|
76
|
-
mocha
|
|
77
|
+
await loadTests(mocha)
|
|
77
78
|
|
|
78
79
|
for (const suite of mocha.suite.suites) {
|
|
79
80
|
if (suite && suite.tests) {
|
package/lib/command/dryRun.js
CHANGED
|
@@ -6,6 +6,7 @@ import output from '../output.js'
|
|
|
6
6
|
import event from '../event.js'
|
|
7
7
|
import store from '../store.js'
|
|
8
8
|
import Container from '../container.js'
|
|
9
|
+
import loadTests from '../mocha/loadTests.js'
|
|
9
10
|
|
|
10
11
|
export default async function (test, options) {
|
|
11
12
|
if (options.grep) process.env.grep = options.grep
|
|
@@ -74,7 +75,7 @@ async function printTests(files) {
|
|
|
74
75
|
|
|
75
76
|
const mocha = Container.mocha()
|
|
76
77
|
mocha.files = files
|
|
77
|
-
mocha
|
|
78
|
+
await loadTests(mocha)
|
|
78
79
|
|
|
79
80
|
let numOfTests = 0
|
|
80
81
|
let numOfSuites = 0
|
package/lib/command/init.js
CHANGED
|
@@ -165,7 +165,7 @@ export default async function (initPath, options = {}) {
|
|
|
165
165
|
config.tests = result.tests
|
|
166
166
|
if (isTypeScript) {
|
|
167
167
|
config.tests = `${config.tests.replace(/\.js$/, `.${extension}`)}`
|
|
168
|
-
config.require = ['tsx/
|
|
168
|
+
config.require = ['tsx/esm']
|
|
169
169
|
}
|
|
170
170
|
|
|
171
171
|
const matchResults = config.tests.match(/[^*.]+/)
|
|
@@ -260,6 +260,18 @@ export default async function (initPath, options = {}) {
|
|
|
260
260
|
}
|
|
261
261
|
|
|
262
262
|
if (isTypeScript) {
|
|
263
|
+
try {
|
|
264
|
+
const pkgPath = path.join(process.cwd(), 'package.json')
|
|
265
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
|
|
266
|
+
if (pkg.type !== 'module') {
|
|
267
|
+
pkg.type = 'module'
|
|
268
|
+
fs.writeFileSync(pkgPath, beautify(JSON.stringify(pkg)))
|
|
269
|
+
print('Set "type": "module" in package.json so TypeScript tests load as ES Modules')
|
|
270
|
+
}
|
|
271
|
+
} catch (err) {
|
|
272
|
+
print(colors.bold.yellow('Could not set "type": "module" in package.json. Add it manually so TypeScript tests load as ES Modules.'))
|
|
273
|
+
}
|
|
274
|
+
|
|
263
275
|
const tsconfigJson = beautify(JSON.stringify(tsconfig))
|
|
264
276
|
const tsconfigFile = path.join(testsPath, 'tsconfig.json')
|
|
265
277
|
if (fileExists(tsconfigFile)) {
|
|
@@ -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, fixErrorStack
|
|
14
|
+
let event, container, Codecept, getConfig, tryOrDefault, deepMerge, fixErrorStack, loadTests
|
|
15
15
|
|
|
16
16
|
let stdout = ''
|
|
17
17
|
|
|
@@ -143,6 +143,7 @@ initPromise = (async function () {
|
|
|
143
143
|
const coreUtilsModule = await import('../../utils.js')
|
|
144
144
|
const CodeceptModule = await import('../../codecept.js')
|
|
145
145
|
const typescriptModule = await import('../../utils/typescript.js')
|
|
146
|
+
const loadTestsModule = await import('../../mocha/loadTests.js')
|
|
146
147
|
|
|
147
148
|
event = eventModule.default
|
|
148
149
|
container = containerModule.default
|
|
@@ -151,6 +152,7 @@ initPromise = (async function () {
|
|
|
151
152
|
deepMerge = coreUtilsModule.deepMerge
|
|
152
153
|
Codecept = CodeceptModule.default
|
|
153
154
|
fixErrorStack = typescriptModule.fixErrorStack
|
|
155
|
+
loadTests = loadTestsModule.default
|
|
154
156
|
|
|
155
157
|
const overrideConfigs = tryOrDefault(() => JSON.parse(options.override), {})
|
|
156
158
|
|
|
@@ -200,7 +202,7 @@ initPromise = (async function () {
|
|
|
200
202
|
// We'll reload test files fresh for each test request
|
|
201
203
|
} else {
|
|
202
204
|
// Legacy mode - filter tests upfront
|
|
203
|
-
filterTests()
|
|
205
|
+
await filterTests()
|
|
204
206
|
}
|
|
205
207
|
|
|
206
208
|
// run tests
|
|
@@ -290,20 +292,13 @@ async function runPoolTests() {
|
|
|
290
292
|
|
|
291
293
|
// Load only the assigned test file
|
|
292
294
|
mocha.files = [testIdentifier]
|
|
293
|
-
mocha
|
|
294
|
-
|
|
295
|
-
try {
|
|
296
|
-
require('fs').appendFileSync('/tmp/config_listener_debug.log', `${new Date().toISOString()} [POOL] Loaded ${testIdentifier}, tests: ${mocha.suite.total()}\n`)
|
|
297
|
-
} catch (e) { /* ignore */ }
|
|
295
|
+
await loadTests(mocha)
|
|
298
296
|
|
|
299
297
|
if (mocha.suite.total() > 0) {
|
|
300
298
|
// Run only the tests in the current mocha suite
|
|
301
299
|
// Don't use codecept.run() as it overwrites mocha.files with ALL test files
|
|
302
300
|
await new Promise((resolve, reject) => {
|
|
303
301
|
mocha.run(() => {
|
|
304
|
-
try {
|
|
305
|
-
require('fs').appendFileSync('/tmp/config_listener_debug.log', `${new Date().toISOString()} [POOL] Finished ${testIdentifier}\n`)
|
|
306
|
-
} catch (e) { /* ignore */ }
|
|
307
302
|
resolve()
|
|
308
303
|
})
|
|
309
304
|
})
|
|
@@ -429,10 +424,10 @@ function filterTestById(testUid) {
|
|
|
429
424
|
}
|
|
430
425
|
}
|
|
431
426
|
|
|
432
|
-
function filterTests() {
|
|
427
|
+
async function filterTests() {
|
|
433
428
|
const files = codecept.testFiles
|
|
434
429
|
mocha.files = files
|
|
435
|
-
mocha
|
|
430
|
+
await loadTests(mocha)
|
|
436
431
|
|
|
437
432
|
// Recursively filter tests in all suites (including nested ones)
|
|
438
433
|
const filterSuiteTests = (suite) => {
|
package/lib/mocha/factory.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import Mocha from 'mocha'
|
|
2
2
|
import fsPath from 'path'
|
|
3
|
-
import fs from 'fs'
|
|
4
3
|
import { fileURLToPath } from 'url'
|
|
5
4
|
import reporter from './cli.js'
|
|
6
|
-
import gherkinParser, { loadTranslations } from './gherkin.js'
|
|
7
5
|
import output from '../output.js'
|
|
8
6
|
import scenarioUiFunction from './ui.js'
|
|
9
7
|
import { initMochaGlobals } from '../globals.js'
|
|
@@ -52,64 +50,6 @@ class MochaFactory {
|
|
|
52
50
|
process.exit(1)
|
|
53
51
|
}
|
|
54
52
|
|
|
55
|
-
// Override loadFiles to handle feature files
|
|
56
|
-
const originalLoadFiles = Mocha.prototype.loadFiles
|
|
57
|
-
mocha.loadFiles = function (fn) {
|
|
58
|
-
// load features
|
|
59
|
-
const featureFiles = this.files.filter(file => file.match(/\.feature$/))
|
|
60
|
-
if (featureFiles.length > 0) {
|
|
61
|
-
// Load translations for Gherkin features
|
|
62
|
-
loadTranslations().catch(() => {
|
|
63
|
-
// Ignore if translations can't be loaded
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
for (const file of featureFiles) {
|
|
67
|
-
const suite = gherkinParser(fs.readFileSync(file, 'utf8'), file)
|
|
68
|
-
this.suite.addSuite(suite)
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// remove feature files
|
|
72
|
-
const jsFiles = this.files.filter(file => !file.match(/\.feature$/))
|
|
73
|
-
this.files = this.files.filter(file => !file.match(/\.feature$/))
|
|
74
|
-
|
|
75
|
-
// Load JavaScript test files using original loadFiles
|
|
76
|
-
if (jsFiles.length > 0) {
|
|
77
|
-
originalLoadFiles.call(this, fn)
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// add ids for each test and check uniqueness
|
|
81
|
-
const dupes = []
|
|
82
|
-
let missingFeatureInFile = []
|
|
83
|
-
const seenTests = []
|
|
84
|
-
this.suite.eachTest(test => {
|
|
85
|
-
if (!test) {
|
|
86
|
-
return // Skip undefined tests
|
|
87
|
-
}
|
|
88
|
-
const name = test.fullTitle()
|
|
89
|
-
if (seenTests.includes(test.uid)) {
|
|
90
|
-
dupes.push(name)
|
|
91
|
-
}
|
|
92
|
-
seenTests.push(test.uid)
|
|
93
|
-
|
|
94
|
-
if (name.slice(0, name.indexOf(':')) === '') {
|
|
95
|
-
missingFeatureInFile.push(test.file)
|
|
96
|
-
}
|
|
97
|
-
})
|
|
98
|
-
if (dupes.length) {
|
|
99
|
-
// ideally this should be no-op and throw (breaking change)...
|
|
100
|
-
output.error(`Duplicate test names detected - Feature + Scenario name should be unique:\n${dupes.join('\n')}`)
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (missingFeatureInFile.length) {
|
|
104
|
-
missingFeatureInFile = [...new Set(missingFeatureInFile)]
|
|
105
|
-
output.error(`Missing Feature section in:\n${missingFeatureInFile.join('\n')}`)
|
|
106
|
-
}
|
|
107
|
-
} else {
|
|
108
|
-
// Use original for non-feature files
|
|
109
|
-
originalLoadFiles.call(this, fn)
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
53
|
const presetReporter = opts.reporter || config.reporter
|
|
114
54
|
// use standard reporter
|
|
115
55
|
if (!presetReporter) {
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import fsPath from 'path'
|
|
3
|
+
import gherkinParser, { loadTranslations } from './gherkin.js'
|
|
4
|
+
import output from '../output.js'
|
|
5
|
+
import { resolveImportModulePath } from '../utils.js'
|
|
6
|
+
|
|
7
|
+
export default async function loadTests(mocha) {
|
|
8
|
+
mocha.lazyLoadFiles(true)
|
|
9
|
+
|
|
10
|
+
const featureFiles = mocha.files.filter(file => file.match(/\.feature$/))
|
|
11
|
+
const testFiles = mocha.files.filter(file => !file.match(/\.feature$/))
|
|
12
|
+
|
|
13
|
+
if (featureFiles.length > 0) {
|
|
14
|
+
await loadTranslations()
|
|
15
|
+
for (const file of featureFiles) {
|
|
16
|
+
mocha.suite.addSuite(gherkinParser(fs.readFileSync(file, 'utf8'), file))
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
for (const file of testFiles) {
|
|
21
|
+
const resolvedPath = resolveImportModulePath(fsPath.resolve(file))
|
|
22
|
+
mocha.suite.emit('pre-require', global, file, mocha)
|
|
23
|
+
try {
|
|
24
|
+
const module = await import(resolvedPath)
|
|
25
|
+
mocha.suite.emit('require', module, file, mocha)
|
|
26
|
+
} catch (err) {
|
|
27
|
+
throw enrichLoaderError(err, file)
|
|
28
|
+
}
|
|
29
|
+
mocha.suite.emit('post-require', global, file, mocha)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
validateLoadedTests(mocha)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function validateLoadedTests(mocha) {
|
|
36
|
+
const dupes = []
|
|
37
|
+
let missingFeatureInFile = []
|
|
38
|
+
const seenTests = []
|
|
39
|
+
mocha.suite.eachTest(test => {
|
|
40
|
+
if (!test) {
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
const name = test.fullTitle()
|
|
44
|
+
if (seenTests.includes(test.uid)) {
|
|
45
|
+
dupes.push(name)
|
|
46
|
+
}
|
|
47
|
+
seenTests.push(test.uid)
|
|
48
|
+
|
|
49
|
+
if (name.slice(0, name.indexOf(':')) === '') {
|
|
50
|
+
missingFeatureInFile.push(test.file)
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
if (dupes.length) {
|
|
55
|
+
output.error(`Duplicate test names detected - Feature + Scenario name should be unique:\n${dupes.join('\n')}`)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (missingFeatureInFile.length) {
|
|
59
|
+
missingFeatureInFile = [...new Set(missingFeatureInFile)]
|
|
60
|
+
output.error(`Missing Feature section in:\n${missingFeatureInFile.join('\n')}`)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function enrichLoaderError(err, file) {
|
|
65
|
+
if (err && err.code === 'ERR_REQUIRE_CYCLE_MODULE') {
|
|
66
|
+
err.message = `${err.message}\n\nFailed to load test file as ES Module: ${file}\nAdd "type": "module" to the nearest package.json so TypeScript files are compiled as ES Modules.\nSee https://codecept.io/typescript`
|
|
67
|
+
}
|
|
68
|
+
return err
|
|
69
|
+
}
|
package/lib/rerun.js
CHANGED
|
@@ -4,23 +4,12 @@ import container from './container.js'
|
|
|
4
4
|
import event from './event.js'
|
|
5
5
|
import BaseCodecept from './codecept.js'
|
|
6
6
|
import output from './output.js'
|
|
7
|
-
import
|
|
8
|
-
import { resolveImportModulePath } from './utils.js'
|
|
9
|
-
|
|
10
|
-
const require = createRequire(import.meta.url)
|
|
7
|
+
import loadTests from './mocha/loadTests.js'
|
|
11
8
|
|
|
12
9
|
class CodeceptRerunner extends BaseCodecept {
|
|
13
10
|
async runOnce(test) {
|
|
14
11
|
await container.started()
|
|
15
12
|
|
|
16
|
-
// Ensure translations are loaded for Gherkin features
|
|
17
|
-
try {
|
|
18
|
-
const { loadTranslations } = await import('./mocha/gherkin.js')
|
|
19
|
-
await loadTranslations()
|
|
20
|
-
} catch (e) {
|
|
21
|
-
// Ignore if gherkin module not available
|
|
22
|
-
}
|
|
23
|
-
|
|
24
13
|
return new Promise(async (resolve, reject) => {
|
|
25
14
|
try {
|
|
26
15
|
// Create a fresh Mocha instance for each run
|
|
@@ -40,24 +29,8 @@ class CodeceptRerunner extends BaseCodecept {
|
|
|
40
29
|
mocha.suite.suites = []
|
|
41
30
|
mocha.suite.tests = []
|
|
42
31
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
try {
|
|
46
|
-
// Clear CommonJS cache if available (for mixed environments)
|
|
47
|
-
try {
|
|
48
|
-
delete require.cache[file]
|
|
49
|
-
} catch (e) {
|
|
50
|
-
// ESM modules don't have require.cache, ignore
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Force reload the module by using a cache-busting query parameter
|
|
54
|
-
const fileUrl = `${fsPath.resolve(file)}`
|
|
55
|
-
const resolvedPath = resolveImportModulePath(fileUrl)
|
|
56
|
-
await import(resolvedPath)
|
|
57
|
-
} catch (e) {
|
|
58
|
-
console.error(`Error loading test file ${file}:`, e)
|
|
59
|
-
}
|
|
60
|
-
}
|
|
32
|
+
mocha.files = filesToRun
|
|
33
|
+
await loadTests(mocha)
|
|
61
34
|
|
|
62
35
|
const done = () => {
|
|
63
36
|
event.emit(event.all.result, container.result())
|
package/lib/utils/loaderCheck.js
CHANGED
|
@@ -50,18 +50,23 @@ CodeceptJS 4.x uses ES Modules (ESM) and requires a loader to run TypeScript tes
|
|
|
50
50
|
npm install --save-dev tsx
|
|
51
51
|
|
|
52
52
|
Configuration:
|
|
53
|
-
Add to your codecept.conf.ts or codecept.conf.js:
|
|
53
|
+
1. Add to your codecept.conf.ts or codecept.conf.js:
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
55
|
+
export const config = {
|
|
56
|
+
tests: './**/*_test.ts',
|
|
57
|
+
require: ['tsx/esm'], // ← Add this line
|
|
58
|
+
helpers: { /* ... */ }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
2. Add "type": "module" to your package.json so TypeScript test files
|
|
62
|
+
are compiled as ES Modules:
|
|
63
|
+
|
|
64
|
+
{ "type": "module" }
|
|
60
65
|
|
|
61
66
|
Why tsx?
|
|
62
67
|
⚡ Fast: Built on esbuild
|
|
63
68
|
🎯 Zero config: No tsconfig.json required
|
|
64
|
-
✅ Works with Mocha:
|
|
69
|
+
✅ Works with Mocha: Test files are loaded as ES Modules
|
|
65
70
|
✅ Complete: Handles all TypeScript features
|
|
66
71
|
|
|
67
72
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
@@ -69,12 +74,11 @@ CodeceptJS 4.x uses ES Modules (ESM) and requires a loader to run TypeScript tes
|
|
|
69
74
|
└─────────────────────────────────────────────────────────────────────────────┘
|
|
70
75
|
|
|
71
76
|
⚠️ ts-node/esm has significant limitations and is not recommended:
|
|
72
|
-
- Doesn't work with "type": "module" in package.json
|
|
73
77
|
- Module resolution doesn't work like standard TypeScript ESM
|
|
74
78
|
- Import statements must use explicit file paths
|
|
75
|
-
|
|
76
|
-
We strongly recommend using tsx/
|
|
77
|
-
|
|
79
|
+
|
|
80
|
+
We strongly recommend using tsx/esm instead.
|
|
81
|
+
|
|
78
82
|
If you still want to use ts-node/esm:
|
|
79
83
|
|
|
80
84
|
Installation:
|
|
@@ -119,7 +123,7 @@ export function getTSNodeESMWarning(requiredModules = []) {
|
|
|
119
123
|
return `
|
|
120
124
|
⚠️ Warning: ts-node/esm with "module": "esnext" requires explicit file extensions in all imports.
|
|
121
125
|
|
|
122
|
-
This is a known limitation. Use tsx/
|
|
126
|
+
This is a known limitation. Use tsx/esm instead to write imports without extensions.
|
|
123
127
|
|
|
124
128
|
Examples:
|
|
125
129
|
|
package/lib/utils/typescript.js
CHANGED
|
@@ -385,9 +385,7 @@ const __dirname = __dirname_fn(__filename);
|
|
|
385
385
|
)
|
|
386
386
|
|
|
387
387
|
// Write the transpiled file with updated imports
|
|
388
|
-
|
|
389
|
-
// don't write to and delete each other's temp files (see issue #5642).
|
|
390
|
-
const tempFile = filePath.replace(/\.ts$/, `.${process.pid}.${Math.random().toString(36).slice(2, 10)}.temp.mjs`)
|
|
388
|
+
const tempFile = filePath.replace(/\.ts$/, '.temp.mjs')
|
|
391
389
|
fs.writeFileSync(tempFile, jsContent)
|
|
392
390
|
transpiledFiles.set(filePath, tempFile)
|
|
393
391
|
}
|
package/lib/workers.js
CHANGED
|
@@ -11,6 +11,7 @@ const __filename = fileURLToPath(import.meta.url)
|
|
|
11
11
|
const __dirname = dirname(__filename)
|
|
12
12
|
import Codecept from './codecept.js'
|
|
13
13
|
import MochaFactory from './mocha/factory.js'
|
|
14
|
+
import loadTests from './mocha/loadTests.js'
|
|
14
15
|
import Container from './container.js'
|
|
15
16
|
import { getTestRoot } from './command/utils.js'
|
|
16
17
|
import { isFunction, fileExists, replaceValueDeep, deepClone } from './utils.js'
|
|
@@ -169,12 +170,12 @@ const indexOfSmallestElement = groups => {
|
|
|
169
170
|
return i
|
|
170
171
|
}
|
|
171
172
|
|
|
172
|
-
const convertToMochaTests = testGroup => {
|
|
173
|
+
const convertToMochaTests = async testGroup => {
|
|
173
174
|
const group = []
|
|
174
175
|
if (testGroup instanceof Array) {
|
|
175
176
|
const mocha = MochaFactory.create({}, {})
|
|
176
177
|
mocha.files = testGroup
|
|
177
|
-
mocha
|
|
178
|
+
await loadTests(mocha)
|
|
178
179
|
mocha.suite.eachTest(test => {
|
|
179
180
|
group.push(test.uid)
|
|
180
181
|
})
|
|
@@ -247,8 +248,8 @@ class WorkerObject {
|
|
|
247
248
|
this.options.override = JSON.stringify(newConfig)
|
|
248
249
|
}
|
|
249
250
|
|
|
250
|
-
addTestFiles(testGroup) {
|
|
251
|
-
this.addTests(convertToMochaTests(testGroup))
|
|
251
|
+
async addTestFiles(testGroup) {
|
|
252
|
+
this.addTests(await convertToMochaTests(testGroup))
|
|
252
253
|
}
|
|
253
254
|
|
|
254
255
|
addTests(tests) {
|
|
@@ -304,13 +305,13 @@ class Workers extends EventEmitter {
|
|
|
304
305
|
const shouldAutoInit = this.workers.length === 0 && ((Number.isInteger(this.numberOfWorkersRequested) && this.numberOfWorkersRequested > 0) || (this.numberOfWorkersRequested < 0 && isFunction(this.config.by)))
|
|
305
306
|
|
|
306
307
|
if (shouldAutoInit) {
|
|
307
|
-
this._initWorkers(this.numberOfWorkersRequested, this.config)
|
|
308
|
+
await this._initWorkers(this.numberOfWorkersRequested, this.config)
|
|
308
309
|
}
|
|
309
310
|
}
|
|
310
311
|
}
|
|
311
312
|
|
|
312
|
-
_initWorkers(numberOfWorkers, config) {
|
|
313
|
-
this.splitTestsByGroups(numberOfWorkers, config)
|
|
313
|
+
async _initWorkers(numberOfWorkers, config) {
|
|
314
|
+
await this.splitTestsByGroups(numberOfWorkers, config)
|
|
314
315
|
// For function-based grouping, use the actual number of test groups created
|
|
315
316
|
const actualNumberOfWorkers = isFunction(config.by) ? this.testGroups.length : numberOfWorkers
|
|
316
317
|
this.workers = createWorkerObjects(this.testGroups, this.codecept.config, getTestRoot(config.testConfig), config.options, config.selectedRuns)
|
|
@@ -330,7 +331,7 @@ class Workers extends EventEmitter {
|
|
|
330
331
|
*
|
|
331
332
|
* This method can be overridden for a better split.
|
|
332
333
|
*/
|
|
333
|
-
splitTestsByGroups(numberOfWorkers, config) {
|
|
334
|
+
async splitTestsByGroups(numberOfWorkers, config) {
|
|
334
335
|
if (isFunction(config.by)) {
|
|
335
336
|
const createTests = config.by
|
|
336
337
|
const testGroups = createTests(numberOfWorkers)
|
|
@@ -338,13 +339,13 @@ class Workers extends EventEmitter {
|
|
|
338
339
|
throw new Error('Test group should be an array')
|
|
339
340
|
}
|
|
340
341
|
for (const testGroup of testGroups) {
|
|
341
|
-
this.testGroups.push(convertToMochaTests(testGroup))
|
|
342
|
+
this.testGroups.push(await convertToMochaTests(testGroup))
|
|
342
343
|
}
|
|
343
344
|
} else if (typeof numberOfWorkers === 'number' && numberOfWorkers > 0) {
|
|
344
345
|
if (config.by === 'pool') {
|
|
345
346
|
this.createTestPool(numberOfWorkers)
|
|
346
347
|
} else {
|
|
347
|
-
this.testGroups = config.by === 'suite' ? this.createGroupsOfSuites(numberOfWorkers) : this.createGroupsOfTests(numberOfWorkers)
|
|
348
|
+
this.testGroups = config.by === 'suite' ? await this.createGroupsOfSuites(numberOfWorkers) : await this.createGroupsOfTests(numberOfWorkers)
|
|
348
349
|
}
|
|
349
350
|
}
|
|
350
351
|
}
|
|
@@ -364,16 +365,16 @@ class Workers extends EventEmitter {
|
|
|
364
365
|
/**
|
|
365
366
|
* @param {Number} numberOfWorkers
|
|
366
367
|
*/
|
|
367
|
-
createGroupsOfTests(numberOfWorkers) {
|
|
368
|
+
async createGroupsOfTests(numberOfWorkers) {
|
|
368
369
|
// If Codecept isn't initialized yet, return empty groups as a safe fallback
|
|
369
370
|
if (!this.codecept) return populateGroups(numberOfWorkers)
|
|
370
371
|
const files = this.codecept.testFiles
|
|
371
|
-
|
|
372
|
+
|
|
372
373
|
// Create a fresh mocha instance to avoid state pollution
|
|
373
374
|
Container.createMocha(this.codecept.config.mocha || {}, this.options)
|
|
374
375
|
const mocha = Container.mocha()
|
|
375
376
|
mocha.files = files
|
|
376
|
-
mocha
|
|
377
|
+
await loadTests(mocha)
|
|
377
378
|
|
|
378
379
|
const groups = populateGroups(numberOfWorkers)
|
|
379
380
|
let groupCounter = 0
|
|
@@ -451,7 +452,7 @@ class Workers extends EventEmitter {
|
|
|
451
452
|
/**
|
|
452
453
|
* @param {Number} numberOfWorkers
|
|
453
454
|
*/
|
|
454
|
-
createGroupsOfSuites(numberOfWorkers) {
|
|
455
|
+
async createGroupsOfSuites(numberOfWorkers) {
|
|
455
456
|
// If Codecept isn't initialized yet, return empty groups as a safe fallback
|
|
456
457
|
if (!this.codecept) return populateGroups(numberOfWorkers)
|
|
457
458
|
const files = this.codecept.testFiles
|
|
@@ -461,7 +462,7 @@ class Workers extends EventEmitter {
|
|
|
461
462
|
Container.createMocha(this.codecept.config.mocha || {}, this.options)
|
|
462
463
|
const mocha = Container.mocha()
|
|
463
464
|
mocha.files = files
|
|
464
|
-
mocha
|
|
465
|
+
await loadTests(mocha)
|
|
465
466
|
|
|
466
467
|
mocha.suite.suites.forEach(suite => {
|
|
467
468
|
const i = indexOfSmallestElement(groups)
|