Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,16 +71,18 @@ For TypeScript test files in CodeceptJS 4.x, use the [`tsx`](https://tsx.is) loa
// codecept.conf.ts
export const config = {
tests: './**/*_test.ts',
require: ['tsx/cjs'],
require: ['tsx/esm'],
helpers: {},
include: {},
}
```

This requires `"type": "module"` in `package.json` so `.ts` test files are compiled as ES Modules.

Combine several modules:

```ts
require: ['tsx/cjs', 'should', './lib/testSetup']
require: ['tsx/esm', 'should', './lib/testSetup']
```

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.
Expand Down
2 changes: 1 addition & 1 deletion docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ npm i tsx --save-dev
// codecept.conf.ts
export const config = {
tests: './**/*_test.ts',
require: ['tsx/cjs'], // loads the *_test.ts files
require: ['tsx/esm'], // loads the *_test.ts files as ES Modules (needs "type": "module")
helpers: {
Playwright: { url: 'http://localhost', browser: 'chromium' },
},
Expand Down
4 changes: 2 additions & 2 deletions docs/parallel.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ import { Workers, event } from 'codeceptjs'
const workers = new Workers(null, { testConfig: './codecept.conf.js' })

// split the suite into 2 groups, run each group on two browsers
const groups = workers.createGroupsOfSuites(2)
const groups = await workers.createGroupsOfSuites(2)
for (const browser of ['chromium', 'firefox']) {
for (const group of groups) {
const worker = workers.spawn()
Expand All @@ -139,7 +139,7 @@ try {
Building blocks:

- `new Workers(N, { testConfig, options })` — `N` workers; pass `null` to spawn them yourself with `spawn()`.
- `createGroupsOfTests(n)` / `createGroupsOfSuites(n)` — split the suite into `n` groups.
- `await createGroupsOfTests(n)` / `await createGroupsOfSuites(n)` — split the suite into `n` groups (async: test files are loaded as ES Modules).
- `worker.addTests(group)` / `worker.addConfig(partialConfig)` — assign tests and config overrides to a spawned worker.
- `bootstrapAll()` → `run()` → `teardownAll()` — lifecycle (wrap `run()` in `try/finally` so teardown always runs).
- 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.
Expand Down
12 changes: 7 additions & 5 deletions docs/typescript.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ CodeceptJS ships [type declarations](https://github.com/codeceptjs/CodeceptJS/tr
? Do you plan to write tests in TypeScript? Yes
```

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 Mocha loads test files through CommonJS hooks, so use [`tsx`](https://tsx.is) (fast, esbuild-based, no `tsconfig.json` required):
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):

```sh
npm i tsx --save-dev
Expand All @@ -25,16 +25,16 @@ npm i tsx --save-dev
// codecept.conf.ts
export const config = {
tests: './**/*_test.ts',
require: ['tsx/cjs'], // loads the *_test.ts files
require: ['tsx/esm'], // loads the *_test.ts files as ES Modules
helpers: {
Playwright: { url: 'http://localhost', browser: 'chromium' },
},
}
```

Run the tests with `npx codeceptjs run`.
Set `"type": "module"` in `package.json` so `tsx` compiles your `.ts` test files as ES Modules. Then run the tests with `npx codeceptjs run`.

> 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/cjs']`.
> 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']`.

## Writing tests

Expand All @@ -59,7 +59,9 @@ Scenario('admin signs in', ({ I }) => {
})
```

> **Cannot find module** or **Unexpected token** while running tests means the loader isn't wired up — check that `tsx` is installed and `require: ['tsx/cjs']` is in the config.
> **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.
>
> **`ERR_REQUIRE_CYCLE_MODULE`** means `tsx` is compiling your `.ts` tests as CommonJS — add `"type": "module"` to the nearest `package.json`.

## Promise-based typings

Expand Down
33 changes: 20 additions & 13 deletions lib/codecept.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import ActorFactory from './actor.js'
import output from './output.js'
import { emptyFolder, resolveImportModulePath } from './utils.js'
import { initCodeceptGlobals } from './globals.js'
import loadTests from './mocha/loadTests.js'
import { validateTypeScriptSetup, getTSNodeESMWarning } from './utils/loaderCheck.js'
import recorder from './recorder.js'
import store from './store.js'
Expand Down Expand Up @@ -58,7 +59,11 @@ class Codecept {
*/
async requireModules(requiringModules) {
if (requiringModules) {
for (const requiredModule of requiringModules) {
for (let requiredModule of requiringModules) {
if (requiredModule === 'tsx/cjs') {
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.'))
requiredModule = 'tsx/esm'
}
let modulePath = requiredModule
const isLocalFile = existsSync(modulePath) || existsSync(`${modulePath}.js`)
if (isLocalFile) {
Expand Down Expand Up @@ -295,21 +300,23 @@ class Codecept {
this.testFiles.sort()
}

return new Promise((resolve, reject) => {
const mocha = container.mocha()
mocha.files = this.testFiles
const mocha = container.mocha()
mocha.files = this.testFiles

if (test) {
if (!fsPath.isAbsolute(test)) {
test = fsPath.join(store.codeceptDir, test)
}
const testBasename = fsPath.basename(test, '.js')
const testFeatureBasename = fsPath.basename(test, '.feature')
mocha.files = mocha.files.filter(t => {
return fsPath.basename(t, '.js') === testBasename || fsPath.basename(t, '.feature') === testFeatureBasename || t === test
})
if (test) {
if (!fsPath.isAbsolute(test)) {
test = fsPath.join(store.codeceptDir, test)
}
const testBasename = fsPath.basename(test, '.js')
const testFeatureBasename = fsPath.basename(test, '.feature')
mocha.files = mocha.files.filter(t => {
return fsPath.basename(t, '.js') === testBasename || fsPath.basename(t, '.feature') === testFeatureBasename || t === test
})
}

await loadTests(mocha)

return new Promise((resolve, reject) => {
const done = async (failures) => {
event.emit(event.all.result, container.result())
event.emit(event.all.after, this)
Expand Down
3 changes: 2 additions & 1 deletion lib/command/check.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Container from '../container.js'
import figures from 'figures'
import chalk from 'chalk'
import { createTest } from '../mocha/test.js'
import loadTests from '../mocha/loadTests.js'
import { getMachineInfo } from './info.js'
import definitions from './definitions.js'

Expand Down Expand Up @@ -73,7 +74,7 @@ export default async function (options) {
const files = codecept.testFiles
const mocha = Container.mocha()
mocha.files = files
mocha.loadFiles()
await loadTests(mocha)

for (const suite of mocha.suite.suites) {
if (suite && suite.tests) {
Expand Down
3 changes: 2 additions & 1 deletion lib/command/dryRun.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import output from '../output.js'
import event from '../event.js'
import store from '../store.js'
import Container from '../container.js'
import loadTests from '../mocha/loadTests.js'

export default async function (test, options) {
if (options.grep) process.env.grep = options.grep
Expand Down Expand Up @@ -74,7 +75,7 @@ async function printTests(files) {

const mocha = Container.mocha()
mocha.files = files
mocha.loadFiles()
await loadTests(mocha)

let numOfTests = 0
let numOfSuites = 0
Expand Down
14 changes: 13 additions & 1 deletion lib/command/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ export default async function (initPath, options = {}) {
config.tests = result.tests
if (isTypeScript) {
config.tests = `${config.tests.replace(/\.js$/, `.${extension}`)}`
config.require = ['tsx/cjs']
config.require = ['tsx/esm']
}

const matchResults = config.tests.match(/[^*.]+/)
Expand Down Expand Up @@ -260,6 +260,18 @@ export default async function (initPath, options = {}) {
}

if (isTypeScript) {
try {
const pkgPath = path.join(process.cwd(), 'package.json')
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
if (pkg.type !== 'module') {
pkg.type = 'module'
fs.writeFileSync(pkgPath, beautify(JSON.stringify(pkg)))
print('Set "type": "module" in package.json so TypeScript tests load as ES Modules')
}
} catch (err) {
print(colors.bold.yellow('Could not set "type": "module" in package.json. Add it manually so TypeScript tests load as ES Modules.'))
}

const tsconfigJson = beautify(JSON.stringify(tsconfig))
const tsconfigFile = path.join(testsPath, 'tsconfig.json')
if (fileExists(tsconfigFile)) {
Expand Down
19 changes: 7 additions & 12 deletions lib/command/workers/runTests.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { parentPort, workerData } from 'worker_threads'

// Delay imports to avoid ES Module loader race conditions in Node 22.x worker threads
// These will be imported dynamically when needed
let event, container, Codecept, getConfig, tryOrDefault, deepMerge, fixErrorStack
let event, container, Codecept, getConfig, tryOrDefault, deepMerge, fixErrorStack, loadTests

let stdout = ''

Expand Down Expand Up @@ -143,6 +143,7 @@ initPromise = (async function () {
const coreUtilsModule = await import('../../utils.js')
const CodeceptModule = await import('../../codecept.js')
const typescriptModule = await import('../../utils/typescript.js')
const loadTestsModule = await import('../../mocha/loadTests.js')

event = eventModule.default
container = containerModule.default
Expand All @@ -151,6 +152,7 @@ initPromise = (async function () {
deepMerge = coreUtilsModule.deepMerge
Codecept = CodeceptModule.default
fixErrorStack = typescriptModule.fixErrorStack
loadTests = loadTestsModule.default

const overrideConfigs = tryOrDefault(() => JSON.parse(options.override), {})

Expand Down Expand Up @@ -200,7 +202,7 @@ initPromise = (async function () {
// We'll reload test files fresh for each test request
} else {
// Legacy mode - filter tests upfront
filterTests()
await filterTests()
}

// run tests
Expand Down Expand Up @@ -290,20 +292,13 @@ async function runPoolTests() {

// Load only the assigned test file
mocha.files = [testIdentifier]
mocha.loadFiles()

try {
require('fs').appendFileSync('/tmp/config_listener_debug.log', `${new Date().toISOString()} [POOL] Loaded ${testIdentifier}, tests: ${mocha.suite.total()}\n`)
} catch (e) { /* ignore */ }
await loadTests(mocha)

if (mocha.suite.total() > 0) {
// Run only the tests in the current mocha suite
// Don't use codecept.run() as it overwrites mocha.files with ALL test files
await new Promise((resolve, reject) => {
mocha.run(() => {
try {
require('fs').appendFileSync('/tmp/config_listener_debug.log', `${new Date().toISOString()} [POOL] Finished ${testIdentifier}\n`)
} catch (e) { /* ignore */ }
resolve()
})
})
Expand Down Expand Up @@ -429,10 +424,10 @@ function filterTestById(testUid) {
}
}

function filterTests() {
async function filterTests() {
const files = codecept.testFiles
mocha.files = files
mocha.loadFiles()
await loadTests(mocha)

// Recursively filter tests in all suites (including nested ones)
const filterSuiteTests = (suite) => {
Expand Down
60 changes: 0 additions & 60 deletions lib/mocha/factory.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import Mocha from 'mocha'
import fsPath from 'path'
import fs from 'fs'
import { fileURLToPath } from 'url'
import reporter from './cli.js'
import gherkinParser, { loadTranslations } from './gherkin.js'
import output from '../output.js'
import scenarioUiFunction from './ui.js'
import { initMochaGlobals } from '../globals.js'
Expand Down Expand Up @@ -52,64 +50,6 @@ class MochaFactory {
process.exit(1)
}

// Override loadFiles to handle feature files
const originalLoadFiles = Mocha.prototype.loadFiles
mocha.loadFiles = function (fn) {
// load features
const featureFiles = this.files.filter(file => file.match(/\.feature$/))
if (featureFiles.length > 0) {
// Load translations for Gherkin features
loadTranslations().catch(() => {
// Ignore if translations can't be loaded
})

for (const file of featureFiles) {
const suite = gherkinParser(fs.readFileSync(file, 'utf8'), file)
this.suite.addSuite(suite)
}

// remove feature files
const jsFiles = this.files.filter(file => !file.match(/\.feature$/))
this.files = this.files.filter(file => !file.match(/\.feature$/))

// Load JavaScript test files using original loadFiles
if (jsFiles.length > 0) {
originalLoadFiles.call(this, fn)
}

// add ids for each test and check uniqueness
const dupes = []
let missingFeatureInFile = []
const seenTests = []
this.suite.eachTest(test => {
if (!test) {
return // Skip undefined tests
}
const name = test.fullTitle()
if (seenTests.includes(test.uid)) {
dupes.push(name)
}
seenTests.push(test.uid)

if (name.slice(0, name.indexOf(':')) === '') {
missingFeatureInFile.push(test.file)
}
})
if (dupes.length) {
// ideally this should be no-op and throw (breaking change)...
output.error(`Duplicate test names detected - Feature + Scenario name should be unique:\n${dupes.join('\n')}`)
}

if (missingFeatureInFile.length) {
missingFeatureInFile = [...new Set(missingFeatureInFile)]
output.error(`Missing Feature section in:\n${missingFeatureInFile.join('\n')}`)
}
} else {
// Use original for non-feature files
originalLoadFiles.call(this, fn)
}
}

const presetReporter = opts.reporter || config.reporter
// use standard reporter
if (!presetReporter) {
Expand Down
Loading