diff --git a/docs/configuration.md b/docs/configuration.md index 21ce18d01..6bc7733fb 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -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. diff --git a/docs/installation.md b/docs/installation.md index 531c9de9d..725c5d3be 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -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' }, }, diff --git a/docs/parallel.md b/docs/parallel.md index 39746e4ba..e75429764 100644 --- a/docs/parallel.md +++ b/docs/parallel.md @@ -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() @@ -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. diff --git a/docs/typescript.md b/docs/typescript.md index cae3d970d..442a21b8a 100644 --- a/docs/typescript.md +++ b/docs/typescript.md @@ -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 @@ -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 @@ -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 diff --git a/lib/codecept.js b/lib/codecept.js index 4c28afc6d..c630abfab 100644 --- a/lib/codecept.js +++ b/lib/codecept.js @@ -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' @@ -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) { @@ -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) diff --git a/lib/command/check.js b/lib/command/check.js index cb59aba92..eab6848bb 100644 --- a/lib/command/check.js +++ b/lib/command/check.js @@ -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' @@ -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) { diff --git a/lib/command/dryRun.js b/lib/command/dryRun.js index b44183b8b..dc1b58182 100644 --- a/lib/command/dryRun.js +++ b/lib/command/dryRun.js @@ -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 @@ -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 diff --git a/lib/command/init.js b/lib/command/init.js index c768dbf41..8aa310ed7 100644 --- a/lib/command/init.js +++ b/lib/command/init.js @@ -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(/[^*.]+/) @@ -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)) { diff --git a/lib/command/workers/runTests.js b/lib/command/workers/runTests.js index 85511e271..a564e26c3 100644 --- a/lib/command/workers/runTests.js +++ b/lib/command/workers/runTests.js @@ -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 = '' @@ -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 @@ -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), {}) @@ -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 @@ -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() }) }) @@ -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) => { diff --git a/lib/mocha/factory.js b/lib/mocha/factory.js index 6e288a2ce..8b492f1d8 100644 --- a/lib/mocha/factory.js +++ b/lib/mocha/factory.js @@ -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' @@ -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) { diff --git a/lib/mocha/loadTests.js b/lib/mocha/loadTests.js new file mode 100644 index 000000000..f2265e39f --- /dev/null +++ b/lib/mocha/loadTests.js @@ -0,0 +1,69 @@ +import fs from 'fs' +import fsPath from 'path' +import gherkinParser, { loadTranslations } from './gherkin.js' +import output from '../output.js' +import { resolveImportModulePath } from '../utils.js' + +export default async function loadTests(mocha) { + mocha.lazyLoadFiles(true) + + const featureFiles = mocha.files.filter(file => file.match(/\.feature$/)) + const testFiles = mocha.files.filter(file => !file.match(/\.feature$/)) + + if (featureFiles.length > 0) { + await loadTranslations() + for (const file of featureFiles) { + mocha.suite.addSuite(gherkinParser(fs.readFileSync(file, 'utf8'), file)) + } + } + + for (const file of testFiles) { + const resolvedPath = resolveImportModulePath(fsPath.resolve(file)) + mocha.suite.emit('pre-require', global, file, mocha) + try { + const module = await import(resolvedPath) + mocha.suite.emit('require', module, file, mocha) + } catch (err) { + throw enrichLoaderError(err, file) + } + mocha.suite.emit('post-require', global, file, mocha) + } + + validateLoadedTests(mocha) +} + +function validateLoadedTests(mocha) { + const dupes = [] + let missingFeatureInFile = [] + const seenTests = [] + mocha.suite.eachTest(test => { + if (!test) { + return + } + 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) { + 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')}`) + } +} + +function enrichLoaderError(err, file) { + if (err && err.code === 'ERR_REQUIRE_CYCLE_MODULE') { + 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` + } + return err +} diff --git a/lib/rerun.js b/lib/rerun.js index 4d296338f..29fba27c5 100644 --- a/lib/rerun.js +++ b/lib/rerun.js @@ -4,23 +4,12 @@ import container from './container.js' import event from './event.js' import BaseCodecept from './codecept.js' import output from './output.js' -import { createRequire } from 'module' -import { resolveImportModulePath } from './utils.js' - -const require = createRequire(import.meta.url) +import loadTests from './mocha/loadTests.js' class CodeceptRerunner extends BaseCodecept { async runOnce(test) { await container.started() - // Ensure translations are loaded for Gherkin features - try { - const { loadTranslations } = await import('./mocha/gherkin.js') - await loadTranslations() - } catch (e) { - // Ignore if gherkin module not available - } - return new Promise(async (resolve, reject) => { try { // Create a fresh Mocha instance for each run @@ -40,24 +29,8 @@ class CodeceptRerunner extends BaseCodecept { mocha.suite.suites = [] mocha.suite.tests = [] - // Manually load each test file by importing it - for (const file of filesToRun) { - try { - // Clear CommonJS cache if available (for mixed environments) - try { - delete require.cache[file] - } catch (e) { - // ESM modules don't have require.cache, ignore - } - - // Force reload the module by using a cache-busting query parameter - const fileUrl = `${fsPath.resolve(file)}` - const resolvedPath = resolveImportModulePath(fileUrl) - await import(resolvedPath) - } catch (e) { - console.error(`Error loading test file ${file}:`, e) - } - } + mocha.files = filesToRun + await loadTests(mocha) const done = () => { event.emit(event.all.result, container.result()) diff --git a/lib/utils/loaderCheck.js b/lib/utils/loaderCheck.js index 147161790..bfec25718 100644 --- a/lib/utils/loaderCheck.js +++ b/lib/utils/loaderCheck.js @@ -50,18 +50,23 @@ CodeceptJS 4.x uses ES Modules (ESM) and requires a loader to run TypeScript tes npm install --save-dev tsx Configuration: - Add to your codecept.conf.ts or codecept.conf.js: + 1. Add to your codecept.conf.ts or codecept.conf.js: - export const config = { - tests: './**/*_test.ts', - require: ['tsx/cjs'], // ← Add this line - helpers: { /* ... */ } - } + export const config = { + tests: './**/*_test.ts', + require: ['tsx/esm'], // ← Add this line + helpers: { /* ... */ } + } + + 2. Add "type": "module" to your package.json so TypeScript test files + are compiled as ES Modules: + + { "type": "module" } Why tsx? ⚡ Fast: Built on esbuild 🎯 Zero config: No tsconfig.json required - ✅ Works with Mocha: Uses CommonJS hooks + ✅ Works with Mocha: Test files are loaded as ES Modules ✅ Complete: Handles all TypeScript features ┌─────────────────────────────────────────────────────────────────────────────┐ @@ -69,12 +74,11 @@ CodeceptJS 4.x uses ES Modules (ESM) and requires a loader to run TypeScript tes └─────────────────────────────────────────────────────────────────────────────┘ ⚠️ ts-node/esm has significant limitations and is not recommended: - - Doesn't work with "type": "module" in package.json - Module resolution doesn't work like standard TypeScript ESM - Import statements must use explicit file paths - - We strongly recommend using tsx/cjs instead. - + + We strongly recommend using tsx/esm instead. + If you still want to use ts-node/esm: Installation: @@ -119,7 +123,7 @@ export function getTSNodeESMWarning(requiredModules = []) { return ` ⚠️ Warning: ts-node/esm with "module": "esnext" requires explicit file extensions in all imports. -This is a known limitation. Use tsx/cjs instead to write imports without extensions. +This is a known limitation. Use tsx/esm instead to write imports without extensions. Examples: diff --git a/lib/workers.js b/lib/workers.js index 4542fb8e1..aba291665 100644 --- a/lib/workers.js +++ b/lib/workers.js @@ -11,6 +11,7 @@ const __filename = fileURLToPath(import.meta.url) const __dirname = dirname(__filename) import Codecept from './codecept.js' import MochaFactory from './mocha/factory.js' +import loadTests from './mocha/loadTests.js' import Container from './container.js' import { getTestRoot } from './command/utils.js' import { isFunction, fileExists, replaceValueDeep, deepClone } from './utils.js' @@ -169,12 +170,12 @@ const indexOfSmallestElement = groups => { return i } -const convertToMochaTests = testGroup => { +const convertToMochaTests = async testGroup => { const group = [] if (testGroup instanceof Array) { const mocha = MochaFactory.create({}, {}) mocha.files = testGroup - mocha.loadFiles() + await loadTests(mocha) mocha.suite.eachTest(test => { group.push(test.uid) }) @@ -247,8 +248,8 @@ class WorkerObject { this.options.override = JSON.stringify(newConfig) } - addTestFiles(testGroup) { - this.addTests(convertToMochaTests(testGroup)) + async addTestFiles(testGroup) { + this.addTests(await convertToMochaTests(testGroup)) } addTests(tests) { @@ -304,13 +305,13 @@ class Workers extends EventEmitter { const shouldAutoInit = this.workers.length === 0 && ((Number.isInteger(this.numberOfWorkersRequested) && this.numberOfWorkersRequested > 0) || (this.numberOfWorkersRequested < 0 && isFunction(this.config.by))) if (shouldAutoInit) { - this._initWorkers(this.numberOfWorkersRequested, this.config) + await this._initWorkers(this.numberOfWorkersRequested, this.config) } } } - _initWorkers(numberOfWorkers, config) { - this.splitTestsByGroups(numberOfWorkers, config) + async _initWorkers(numberOfWorkers, config) { + await this.splitTestsByGroups(numberOfWorkers, config) // For function-based grouping, use the actual number of test groups created const actualNumberOfWorkers = isFunction(config.by) ? this.testGroups.length : numberOfWorkers this.workers = createWorkerObjects(this.testGroups, this.codecept.config, getTestRoot(config.testConfig), config.options, config.selectedRuns) @@ -330,7 +331,7 @@ class Workers extends EventEmitter { * * This method can be overridden for a better split. */ - splitTestsByGroups(numberOfWorkers, config) { + async splitTestsByGroups(numberOfWorkers, config) { if (isFunction(config.by)) { const createTests = config.by const testGroups = createTests(numberOfWorkers) @@ -338,13 +339,13 @@ class Workers extends EventEmitter { throw new Error('Test group should be an array') } for (const testGroup of testGroups) { - this.testGroups.push(convertToMochaTests(testGroup)) + this.testGroups.push(await convertToMochaTests(testGroup)) } } else if (typeof numberOfWorkers === 'number' && numberOfWorkers > 0) { if (config.by === 'pool') { this.createTestPool(numberOfWorkers) } else { - this.testGroups = config.by === 'suite' ? this.createGroupsOfSuites(numberOfWorkers) : this.createGroupsOfTests(numberOfWorkers) + this.testGroups = config.by === 'suite' ? await this.createGroupsOfSuites(numberOfWorkers) : await this.createGroupsOfTests(numberOfWorkers) } } } @@ -364,16 +365,16 @@ class Workers extends EventEmitter { /** * @param {Number} numberOfWorkers */ - createGroupsOfTests(numberOfWorkers) { + async createGroupsOfTests(numberOfWorkers) { // If Codecept isn't initialized yet, return empty groups as a safe fallback if (!this.codecept) return populateGroups(numberOfWorkers) const files = this.codecept.testFiles - + // Create a fresh mocha instance to avoid state pollution Container.createMocha(this.codecept.config.mocha || {}, this.options) const mocha = Container.mocha() mocha.files = files - mocha.loadFiles() + await loadTests(mocha) const groups = populateGroups(numberOfWorkers) let groupCounter = 0 @@ -451,7 +452,7 @@ class Workers extends EventEmitter { /** * @param {Number} numberOfWorkers */ - createGroupsOfSuites(numberOfWorkers) { + async createGroupsOfSuites(numberOfWorkers) { // If Codecept isn't initialized yet, return empty groups as a safe fallback if (!this.codecept) return populateGroups(numberOfWorkers) const files = this.codecept.testFiles @@ -461,7 +462,7 @@ class Workers extends EventEmitter { Container.createMocha(this.codecept.config.mocha || {}, this.options) const mocha = Container.mocha() mocha.files = files - mocha.loadFiles() + await loadTests(mocha) mocha.suite.suites.forEach(suite => { const i = indexOfSmallestElement(groups) diff --git a/test/data/sandbox/import-config-proof/codecept.conf.js b/test/data/sandbox/import-config-proof/codecept.conf.js new file mode 100644 index 000000000..5a2f5a2db --- /dev/null +++ b/test/data/sandbox/import-config-proof/codecept.conf.js @@ -0,0 +1,12 @@ +export const config = { + tests: './*_test.js', + timeout: 10000, + output: './output', + helpers: { + FileSystem: {}, + }, + include: {}, + bootstrap: false, + mocha: {}, + name: 'import-config-proof', +} diff --git a/test/data/sandbox/import-config-proof/config_import_test.js b/test/data/sandbox/import-config-proof/config_import_test.js new file mode 100644 index 000000000..855ff91a3 --- /dev/null +++ b/test/data/sandbox/import-config-proof/config_import_test.js @@ -0,0 +1,9 @@ +import { config } from 'codeceptjs' +import assert from 'assert' + +Feature('import config') + +Scenario('config imported from codeceptjs is the live running instance', () => { + const name = config.get('name') + assert.strictEqual(name, 'import-config-proof', `expected live config.name "import-config-proof" but got "${name}" (second module copy?)`) +}) diff --git a/test/data/typescript-esm-exports/package.json b/test/data/typescript-esm-exports/package.json index e160f5318..bedf0f89d 100644 --- a/test/data/typescript-esm-exports/package.json +++ b/test/data/typescript-esm-exports/package.json @@ -1,5 +1,6 @@ { "name": "container-esm", "version": "1.0.0", - "private": true + "private": true, + "type": "module" } diff --git a/test/runner/codecept_test.js b/test/runner/codecept_test.js index 7266797e7..fa99feed9 100644 --- a/test/runner/codecept_test.js +++ b/test/runner/codecept_test.js @@ -54,6 +54,16 @@ describe('CodeceptJS Runner', () => { }) }) + it('should expose the live config to a test that imports { config } from codeceptjs', done => { + process.chdir(codecept_dir) + exec(`${codecept_run} --config ${codecept_dir}/import-config-proof/codecept.conf.js`, (err, stdout) => { + stdout.should.include('import config') // feature + stdout.should.include('1 passed') // Scenario asserts config.get('name') resolves to the running config + assert(!err) // a second module copy would make config.get('name') undefined and fail the run + done() + }) + }) + it('should show failures and exit with 1 on fail', done => { exec(codecept_run_config('codecept.failed.js'), (err, stdout) => { stdout.should.include('Not-A-Filesystem') diff --git a/test/unit/worker_test.js b/test/unit/worker_test.js index 6c0a51978..44d73f340 100644 --- a/test/unit/worker_test.js +++ b/test/unit/worker_test.js @@ -133,26 +133,27 @@ describe('Workers', function () { const workers = new Workers(-1, workerConfig) const workerOne = workers.spawn() - workerOne.addTestFiles([path.join(codecept_dir, '/custom-worker/base_test.worker.js')]) - const workerTwo = workers.spawn() - workerTwo.addTestFiles([path.join(codecept_dir, '/custom-worker/custom_test.worker.js')]) - for (const worker of workers.getWorkers()) { - worker.addConfig({ - helpers: { - FileSystem: {}, - Workers: { - require: './workers_helper', - }, - CustomWorkers: { - require: './custom_worker_helper', - }, - }, - }) - } + Promise.all([workerOne.addTestFiles([path.join(codecept_dir, '/custom-worker/base_test.worker.js')]), workerTwo.addTestFiles([path.join(codecept_dir, '/custom-worker/custom_test.worker.js')])]) + .then(() => { + for (const worker of workers.getWorkers()) { + worker.addConfig({ + helpers: { + FileSystem: {}, + Workers: { + require: './workers_helper', + }, + CustomWorkers: { + require: './custom_worker_helper', + }, + }, + }) + } - workers.run() + workers.run() + }) + .catch(done) workers.on(event.all.result, result => { expect(workers.getWorkers().length).equal(2) @@ -168,29 +169,32 @@ describe('Workers', function () { } const workers = new Workers(-1, workerConfig) - const testGroups = workers.createGroupsOfSuites(2) - - const workerOne = workers.spawn() - workerOne.addTests(testGroups[0]) - - const workerTwo = workers.spawn() - workerTwo.addTests(testGroups[1]) + workers + .createGroupsOfSuites(2) + .then(testGroups => { + const workerOne = workers.spawn() + workerOne.addTests(testGroups[0]) + + const workerTwo = workers.spawn() + workerTwo.addTests(testGroups[1]) + + for (const worker of workers.getWorkers()) { + worker.addConfig({ + helpers: { + FileSystem: {}, + Workers: { + require: './workers_helper', + }, + CustomWorkers: { + require: './custom_worker_helper', + }, + }, + }) + } - for (const worker of workers.getWorkers()) { - worker.addConfig({ - helpers: { - FileSystem: {}, - Workers: { - require: './workers_helper', - }, - CustomWorkers: { - require: './custom_worker_helper', - }, - }, + workers.run() }) - } - - workers.run() + .catch(done) workers.on(event.all.result, result => { expect(workers.getWorkers().length).equal(2)