Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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