From c8a7be8bbd040d92669cfd2482291aa5b5dce55f Mon Sep 17 00:00:00 2001 From: DavertMik Date: Sun, 12 Apr 2026 22:03:39 +0300 Subject: [PATCH 1/5] feat: add programmatic API with getSuites(), executeSuite(), executeTest() Add clean programmatic API to Codecept class that wraps Mocha internals, eliminating duplicated boilerplate across dryRun, workers, and custom scripts. - getSuites(pattern?) returns parsed suites with tests as plain objects - executeSuite(suite) runs all tests in a suite - executeTest(test) runs a single test by fullTitle - Refactor workers.js to use getSuites() (removes ~30 lines of Mocha boilerplate) - Refactor dryRun.js to use getSuites() (removes Container.mocha() dependency) - Export Result class from lib/index.js - Rewrite docs/internal-api.md with full programmatic API reference Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/internal-api.md | 165 ++++++++++++++++++++++++++++++++++-------- lib/codecept.js | 93 ++++++++++++++++++++++++ lib/command/dryRun.js | 17 ++--- lib/index.js | 6 +- lib/workers.js | 50 ++++--------- 5 files changed, 251 insertions(+), 80 deletions(-) diff --git a/docs/internal-api.md b/docs/internal-api.md index aa0846036..ed95db293 100644 --- a/docs/internal-api.md +++ b/docs/internal-api.md @@ -225,42 +225,143 @@ Step events provide step objects with following fields: Whenever you execute tests with `--verbose` option you will see registered events and promises executed by a recorder. -## Custom Runner +## Programmatic API -You can run CodeceptJS tests from your script. +CodeceptJS can be imported and used programmatically from your scripts. The main entry point is the `Codecept` class, which provides methods to list and execute tests. + +### Setup ```js -const { codecept: Codecept } = require('codeceptjs'); - -// define main config -const config = { - helpers: { - WebDriver: { - browser: 'chrome', - url: 'http://localhost' - } - } +import { Codecept, container } from 'codeceptjs'; + +const config = { + helpers: { + Playwright: { browser: 'chromium', url: 'http://localhost' } + }, + tests: './*_test.js', }; -const opts = { steps: true }; - -// run CodeceptJS inside async function -(async () => { - const codecept = new Codecept(config, options); - codecept.init(__dirname); - - try { - await codecept.bootstrap(); - codecept.loadTests('**_test.js'); - // run tests - await codecept.run(test); - } catch (err) { - printError(err); - process.exitCode = 1; - } finally { - await codecept.teardown(); - } -})(); +const codecept = new Codecept(config, { steps: true }); +await codecept.init(__dirname); +``` + +### Listing Tests + +Use `getSuites()` to get all parsed suites with their tests without executing them: + +```js +const suites = codecept.getSuites(); + +for (const suite of suites) { + console.log(suite.title, suite.tags); + for (const test of suite.tests) { + console.log(' -', test.title, test.tags); + } +} +``` + +`getSuites()` accepts an optional glob pattern. If `loadTests()` hasn't been called yet, it will be called internally. + +Each suite contains: + +| Property | Type | Description | +|----------|------|-------------| +| `title` | `string` | Feature/suite title | +| `file` | `string` | Absolute path to the test file | +| `tags` | `string[]` | Tags (e.g. `@smoke`) | +| `tests` | `Array` | Tests in this suite | + +Each test contains: + +| Property | Type | Description | +|----------|------|-------------| +| `title` | `string` | Scenario title | +| `uid` | `string` | Unique test identifier | +| `tags` | `string[]` | Tags from scenario and suite | +| `fullTitle` | `string` | `"Suite: Test"` format | + +### Executing Suites + +Use `executeSuite()` to run all tests within a suite: + +```js +await codecept.bootstrap(); + +const suites = codecept.getSuites(); +for (const suite of suites) { + await codecept.executeSuite(suite); +} + +const result = container.result(); +console.log(result.stats); +console.log(`Passed: ${result.passedTests.length}`); +console.log(`Failed: ${result.failedTests.length}`); + +await codecept.teardown(); +``` + +### Executing Individual Tests + +Use `executeTest()` to run a single test: + +```js +await codecept.bootstrap(); + +const suites = codecept.getSuites(); +for (const test of suites[0].tests) { + await codecept.executeTest(test); +} + +const result = container.result(); +await codecept.teardown(); +``` + +### Result Object + +The `Result` object returned by `container.result()` provides: + +| Property | Type | Description | +|----------|------|-------------| +| `stats` | `object` | `{ passes, failures, tests, pending, failedHooks, duration }` | +| `tests` | `Test[]` | All collected tests | +| `passedTests` | `Test[]` | Tests that passed | +| `failedTests` | `Test[]` | Tests that failed | +| `skippedTests` | `Test[]` | Tests that were skipped | +| `hasFailed` | `boolean` | Whether any test failed | +| `duration` | `number` | Total duration in milliseconds | + +### Full Lifecycle (Low-Level) + +For full control, you can orchestrate the lifecycle manually: + +```js +const codecept = new Codecept(config, opts); +await codecept.init(__dirname); + +try { + await codecept.bootstrap(); + codecept.loadTests('**_test.js'); + await codecept.run(); +} catch (err) { + console.error(err); + process.exitCode = 1; +} finally { + await codecept.teardown(); +} ``` -> Also, you can run tests inside workers in a custom scripts. Please refer to the [parallel execution](/parallel) guide for more details. +### Codecept Methods Reference + +| Method | Description | +|--------|-------------| +| `new Codecept(config, opts)` | Create runner instance | +| `await init(dir)` | Initialize globals, container, helpers, plugins | +| `loadTests(pattern?)` | Find test files by glob pattern | +| `getSuites(pattern?)` | Load and return parsed suites with tests | +| `await bootstrap()` | Execute bootstrap hook | +| `await run(test?)` | Run all loaded tests (or filter by file path) | +| `await executeSuite(suite)` | Run a specific suite from `getSuites()` | +| `await executeTest(test)` | Run a specific test from `getSuites()` | +| `await teardown()` | Execute teardown hook | + +> Also, you can run tests inside workers in a custom script. Please refer to the [parallel execution](/parallel) guide for more details. diff --git a/lib/codecept.js b/lib/codecept.js index e01fe8345..4bc45926f 100644 --- a/lib/codecept.js +++ b/lib/codecept.js @@ -11,6 +11,7 @@ const __filename = fileURLToPath(import.meta.url) const __dirname = dirname(__filename) import Helper from '@codeceptjs/helper' +import MochaFactory from './mocha/factory.js' import container from './container.js' import Config from './config.js' import event from './event.js' @@ -256,6 +257,98 @@ class Codecept { return testFiles.slice(startIndex, endIndex) } + /** + * Returns parsed suites with their tests. + * Creates a temporary Mocha instance to avoid polluting container state. + * Must be called after init(). Calls loadTests() internally if testFiles is empty. + * + * @param {string} [pattern] - glob pattern for test files + * @returns {Array<{title: string, file: string, tags: string[], tests: Array<{title: string, uid: string, tags: string[], fullTitle: string}>}>} + */ + getSuites(pattern) { + if (this.testFiles.length === 0) { + this.loadTests(pattern) + } + + const tempMocha = MochaFactory.create(this.config.mocha || {}, this.opts || {}) + tempMocha.files = this.testFiles + tempMocha.loadFiles() + + const suites = [] + for (const suite of tempMocha.suite.suites) { + suites.push({ + ...suite.simplify(), + file: suite.file || '', + tests: suite.tests.map(test => ({ + ...test.simplify(), + fullTitle: test.fullTitle(), + })), + }) + } + + tempMocha.unloadFiles() + return suites + } + + /** + * Execute all tests in a suite. + * Must be called after init() and bootstrap(). + * + * @param {{file: string}} suite - suite object returned by getSuites() + * @returns {Promise} + */ + async executeSuite(suite) { + return this.run(suite.file) + } + + /** + * Execute a single test by its fullTitle. + * Must be called after init() and bootstrap(). + * + * @param {{fullTitle: string}} test - test object returned by getSuites() + * @returns {Promise} + */ + async executeTest(test) { + await container.started() + + const tsValidation = validateTypeScriptSetup(this.testFiles, this.requiringModules || []) + if (tsValidation.hasError) { + output.error(tsValidation.message) + process.exit(1) + } + + try { + const { loadTranslations } = await import('./mocha/gherkin.js') + await loadTranslations() + } catch (e) { + // Ignore if gherkin module not available + } + + return new Promise((resolve, reject) => { + const mocha = container.mocha() + mocha.files = this.testFiles + mocha.grep(test.fullTitle) + + const done = async (failures) => { + event.emit(event.all.result, container.result()) + event.emit(event.all.after, this) + await recorder.promise() + if (failures) { + process.exitCode = 1 + } + resolve() + } + + try { + event.emit(event.all.before, this) + mocha.run(async (failures) => await done(failures)) + } catch (e) { + output.error(e.stack) + reject(e) + } + }) + } + /** * Run a specific test or all loaded tests. * diff --git a/lib/command/dryRun.js b/lib/command/dryRun.js index 1d46caf85..af1ed1976 100644 --- a/lib/command/dryRun.js +++ b/lib/command/dryRun.js @@ -4,7 +4,6 @@ import Codecept from '../codecept.js' import output from '../output.js' import event from '../event.js' import store from '../store.js' -import Container from '../container.js' export default async function (test, options) { if (options.grep) process.env.grep = options.grep @@ -35,7 +34,7 @@ export default async function (test, options) { store.dryRun = true if (!options.steps && !options.verbose && !options.debug) { - await printTests(codecept.testFiles) + await printTests(codecept) return } event.dispatcher.on(event.all.result, printFooter) @@ -46,16 +45,14 @@ export default async function (test, options) { } } -async function printTests(files) { +async function printTests(codecept) { const { default: figures } = await import('figures') const { default: colors } = await import('chalk') output.print(output.styles.debug(`Tests from ${store.codeceptDir}:`)) output.print() - const mocha = Container.mocha() - mocha.files = files - mocha.loadFiles() + const suites = codecept.getSuites() let numOfTests = 0 let numOfSuites = 0 @@ -65,19 +62,19 @@ async function printTests(files) { let filterRegex if (filterBy) { try { - filterRegex = new RegExp(filterBy, 'i') // Case-insensitive matching + filterRegex = new RegExp(filterBy, 'i') } catch (err) { console.error(`Invalid grep pattern: ${filterBy}`) process.exit(1) } } - for (const suite of mocha.suite.suites) { + for (const suite of suites) { const suiteMatches = filterRegex ? filterRegex.test(suite.title) : true let suiteHasMatchingTests = false if (suiteMatches) { - outputString += `${colors.white.bold(suite.title)} -- ${output.styles.log(suite.file || '')}\n` + outputString += `${colors.white.bold(suite.title)} -- ${output.styles.log(suite.file)}\n` suiteHasMatchingTests = true numOfSuites++ } @@ -87,7 +84,7 @@ async function printTests(files) { if (testMatches) { if (!suiteMatches && !suiteHasMatchingTests) { - outputString += `${colors.white.bold(suite.title)} -- ${output.styles.log(suite.file || '')}\n` + outputString += `${colors.white.bold(suite.title)} -- ${output.styles.log(suite.file)}\n` suiteHasMatchingTests = true numOfSuites++ } diff --git a/lib/index.js b/lib/index.js index 6d427892a..2bdde91f3 100644 --- a/lib/index.js +++ b/lib/index.js @@ -23,6 +23,7 @@ import heal from './heal.js' import ai from './ai.js' import Workers from './workers.js' import Secret, { secret } from './secret.js' +import Result from './result.js' export default { /** @type {typeof CodeceptJS.Codecept} */ @@ -67,7 +68,10 @@ export default { Secret, /** @type {typeof CodeceptJS.secret} */ secret, + + /** @type {typeof Result} */ + Result, } // Named exports for ESM compatibility -export { codecept, output, container, event, recorder, config, actor, helper, pause, within, dataTable, dataTableArgument, store, locator, heal, ai, Workers, Secret, secret } +export { codecept, output, container, event, recorder, config, actor, helper, pause, within, dataTable, dataTableArgument, store, locator, heal, ai, Workers, Secret, secret, Result } diff --git a/lib/workers.js b/lib/workers.js index d97304a69..cbbe4e090 100644 --- a/lib/workers.js +++ b/lib/workers.js @@ -369,30 +369,18 @@ class Workers extends EventEmitter { * @param {Number} numberOfWorkers */ 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() - + const suites = this.codecept.getSuites() const groups = populateGroups(numberOfWorkers) let groupCounter = 0 - mocha.suite.eachTest(test => { - const i = groupCounter % groups.length - if (test) { - groups[i].push(test.uid) + for (const suite of suites) { + for (const test of suite.tests) { + groups[groupCounter % groups.length].push(test.uid) groupCounter++ } - }) - - // Clean up after collecting test UIDs - mocha.unloadFiles() - + } + return groups } @@ -456,29 +444,17 @@ class Workers extends EventEmitter { * @param {Number} numberOfWorkers */ 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 + const suites = this.codecept.getSuites() const groups = populateGroups(numberOfWorkers) - // 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() - - mocha.suite.suites.forEach(suite => { + for (const suite of suites) { const i = indexOfSmallestElement(groups) - suite.tests.forEach(test => { - if (test) { - groups[i].push(test.uid) - } - }) - }) - - // Clean up after collecting test UIDs - mocha.unloadFiles() - + for (const test of suite.tests) { + groups[i].push(test.uid) + } + } + return groups } From 77fc33b56cb00dec4ea8263bc63ab77027105281 Mon Sep 17 00:00:00 2001 From: DavertMik Date: Sun, 12 Apr 2026 22:40:28 +0300 Subject: [PATCH 2/5] refactor: extract Runner class from Codecept for all Mocha interactions - Create lib/runner.js with Runner class that owns all Mocha interactions: getSuites(), run(), runSuite(), runTest() backed by a single _execute() core - Codecept.run/runSuite/runTest/getSuites now delegate to this.runner - Remove duplicated executeTest() that was copy of run() - Refactor check.js to use codecept.getSuites() instead of manual Mocha - Export Runner from lib/index.js Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/internal-api.md | 40 ++++++++++--- lib/codecept.js | 138 ++++--------------------------------------- lib/command/check.js | 13 ++-- lib/index.js | 5 +- lib/runner.js | 116 ++++++++++++++++++++++++++++++++++++ 5 files changed, 166 insertions(+), 146 deletions(-) create mode 100644 lib/runner.js diff --git a/docs/internal-api.md b/docs/internal-api.md index ed95db293..833cc0b5b 100644 --- a/docs/internal-api.md +++ b/docs/internal-api.md @@ -282,14 +282,14 @@ Each test contains: ### Executing Suites -Use `executeSuite()` to run all tests within a suite: +Use `runSuite()` to run all tests within a suite: ```js await codecept.bootstrap(); const suites = codecept.getSuites(); for (const suite of suites) { - await codecept.executeSuite(suite); + await codecept.runSuite(suite); } const result = container.result(); @@ -302,14 +302,14 @@ await codecept.teardown(); ### Executing Individual Tests -Use `executeTest()` to run a single test: +Use `runTest()` to run a single test: ```js await codecept.bootstrap(); const suites = codecept.getSuites(); for (const test of suites[0].tests) { - await codecept.executeTest(test); + await codecept.runTest(test); } const result = container.result(); @@ -350,18 +350,42 @@ try { } ``` +### Runner + +All Mocha interactions are handled by the `Runner` class (`lib/runner.js`). After calling `init()`, it's available as `codecept.runner`: + +```js +const runner = codecept.runner; + +// Same as codecept.getSuites() / runSuite() / runTest() +const suites = runner.getSuites(); +await runner.runSuite(suites[0]); +await runner.runTest(suites[0].tests[0]); +``` + +The `Codecept` methods (`getSuites`, `runSuite`, `runTest`, `run`) delegate to the Runner. + ### Codecept Methods Reference | Method | Description | |--------|-------------| -| `new Codecept(config, opts)` | Create runner instance | -| `await init(dir)` | Initialize globals, container, helpers, plugins | +| `new Codecept(config, opts)` | Create codecept instance | +| `await init(dir)` | Initialize globals, container, helpers, plugins, runner | | `loadTests(pattern?)` | Find test files by glob pattern | | `getSuites(pattern?)` | Load and return parsed suites with tests | | `await bootstrap()` | Execute bootstrap hook | | `await run(test?)` | Run all loaded tests (or filter by file path) | -| `await executeSuite(suite)` | Run a specific suite from `getSuites()` | -| `await executeTest(test)` | Run a specific test from `getSuites()` | +| `await runSuite(suite)` | Run a specific suite from `getSuites()` | +| `await runTest(test)` | Run a specific test from `getSuites()` | | `await teardown()` | Execute teardown hook | +### Runner Methods Reference + +| Method | Description | +|--------|-------------| +| `getSuites(pattern?)` | Parse suites using a temporary Mocha instance | +| `run(test?)` | Run tests, optionally filtering by file path | +| `runSuite(suite)` | Run all tests in a suite | +| `runTest(test)` | Run a single test by fullTitle | + > Also, you can run tests inside workers in a custom script. Please refer to the [parallel execution](/parallel) guide for more details. diff --git a/lib/codecept.js b/lib/codecept.js index 4bc45926f..dfff4cee4 100644 --- a/lib/codecept.js +++ b/lib/codecept.js @@ -11,17 +11,13 @@ const __filename = fileURLToPath(import.meta.url) const __dirname = dirname(__filename) import Helper from '@codeceptjs/helper' -import MochaFactory from './mocha/factory.js' +import Runner from './runner.js' import container from './container.js' import Config from './config.js' -import event from './event.js' import runHook from './hooks.js' import ActorFactory from './actor.js' -import output from './output.js' import { emptyFolder } from './utils.js' import { initCodeceptGlobals } from './globals.js' -import { validateTypeScriptSetup, getTSNodeESMWarning } from './utils/loaderCheck.js' -import recorder from './recorder.js' import store from './store.js' import storeListener from './listener/store.js' @@ -104,6 +100,7 @@ class Codecept { await this.requireModules(this.requiringModules) // initializing listeners await container.create(this.config, this.opts) + this.runner = new Runner(this) await this.runHooks() } @@ -266,87 +263,29 @@ class Codecept { * @returns {Array<{title: string, file: string, tags: string[], tests: Array<{title: string, uid: string, tags: string[], fullTitle: string}>}>} */ getSuites(pattern) { - if (this.testFiles.length === 0) { - this.loadTests(pattern) - } - - const tempMocha = MochaFactory.create(this.config.mocha || {}, this.opts || {}) - tempMocha.files = this.testFiles - tempMocha.loadFiles() - - const suites = [] - for (const suite of tempMocha.suite.suites) { - suites.push({ - ...suite.simplify(), - file: suite.file || '', - tests: suite.tests.map(test => ({ - ...test.simplify(), - fullTitle: test.fullTitle(), - })), - }) - } - - tempMocha.unloadFiles() - return suites + return this.runner.getSuites(pattern) } /** - * Execute all tests in a suite. + * Run all tests in a suite. * Must be called after init() and bootstrap(). * * @param {{file: string}} suite - suite object returned by getSuites() * @returns {Promise} */ - async executeSuite(suite) { - return this.run(suite.file) + async runSuite(suite) { + return this.runner.runSuite(suite) } /** - * Execute a single test by its fullTitle. + * Run a single test by its fullTitle. * Must be called after init() and bootstrap(). * * @param {{fullTitle: string}} test - test object returned by getSuites() * @returns {Promise} */ - async executeTest(test) { - await container.started() - - const tsValidation = validateTypeScriptSetup(this.testFiles, this.requiringModules || []) - if (tsValidation.hasError) { - output.error(tsValidation.message) - process.exit(1) - } - - try { - const { loadTranslations } = await import('./mocha/gherkin.js') - await loadTranslations() - } catch (e) { - // Ignore if gherkin module not available - } - - return new Promise((resolve, reject) => { - const mocha = container.mocha() - mocha.files = this.testFiles - mocha.grep(test.fullTitle) - - const done = async (failures) => { - event.emit(event.all.result, container.result()) - event.emit(event.all.after, this) - await recorder.promise() - if (failures) { - process.exitCode = 1 - } - resolve() - } - - try { - event.emit(event.all.before, this) - mocha.run(async (failures) => await done(failures)) - } catch (e) { - output.error(e.stack) - reject(e) - } - }) + async runTest(test) { + return this.runner.runTest(test) } /** @@ -356,64 +295,7 @@ class Codecept { * @returns {Promise} */ async run(test) { - await container.started() - - // Check TypeScript loader configuration before running tests - const tsValidation = validateTypeScriptSetup(this.testFiles, this.requiringModules || []) - if (tsValidation.hasError) { - output.error(tsValidation.message) - process.exit(1) - } - - // Show warning if ts-node/esm is being used - const tsWarning = getTSNodeESMWarning(this.requiringModules || []) - if (tsWarning) { - output.print(output.colors.yellow(tsWarning)) - } - - // 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((resolve, reject) => { - 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 - }) - } - - const done = async (failures) => { - event.emit(event.all.result, container.result()) - event.emit(event.all.after, this) - // Wait for any recorder tasks added by event.all.after handlers - await recorder.promise() - // Set exit code based on test failures - if (failures) { - process.exitCode = 1 - } - resolve() - } - - try { - event.emit(event.all.before, this) - mocha.run(async (failures) => await done(failures)) - } catch (e) { - output.error(e.stack) - reject(e) - } - }) + return this.runner.run(test) } /** diff --git a/lib/command/check.js b/lib/command/check.js index c07a1f5f3..d4ceb63a8 100644 --- a/lib/command/check.js +++ b/lib/command/check.js @@ -70,15 +70,10 @@ export default async function (options) { if (codecept) { try { codecept.loadTests() - const files = codecept.testFiles - const mocha = Container.mocha() - mocha.files = files - mocha.loadFiles() - - for (const suite of mocha.suite.suites) { - if (suite && suite.tests) { - numTests += suite.tests.length - } + const suites = codecept.getSuites() + + for (const suite of suites) { + numTests += suite.tests.length } if (numTests > 0) { diff --git a/lib/index.js b/lib/index.js index 2bdde91f3..6dc390e0b 100644 --- a/lib/index.js +++ b/lib/index.js @@ -24,6 +24,7 @@ import ai from './ai.js' import Workers from './workers.js' import Secret, { secret } from './secret.js' import Result from './result.js' +import Runner from './runner.js' export default { /** @type {typeof CodeceptJS.Codecept} */ @@ -71,7 +72,9 @@ export default { /** @type {typeof Result} */ Result, + + Runner, } // Named exports for ESM compatibility -export { codecept, output, container, event, recorder, config, actor, helper, pause, within, dataTable, dataTableArgument, store, locator, heal, ai, Workers, Secret, secret, Result } +export { codecept, output, container, event, recorder, config, actor, helper, pause, within, dataTable, dataTableArgument, store, locator, heal, ai, Workers, Secret, secret, Result, Runner } diff --git a/lib/runner.js b/lib/runner.js new file mode 100644 index 000000000..ed2880edc --- /dev/null +++ b/lib/runner.js @@ -0,0 +1,116 @@ +import fsPath from 'path' +import container from './container.js' +import MochaFactory from './mocha/factory.js' +import event from './event.js' +import recorder from './recorder.js' +import output from './output.js' +import store from './store.js' +import { validateTypeScriptSetup, getTSNodeESMWarning } from './utils/loaderCheck.js' + +class Runner { + constructor(codecept) { + this.codecept = codecept + } + + getSuites(pattern) { + if (this.codecept.testFiles.length === 0) { + this.codecept.loadTests(pattern) + } + + const tempMocha = MochaFactory.create(this.codecept.config.mocha || {}, this.codecept.opts || {}) + tempMocha.files = this.codecept.testFiles + tempMocha.loadFiles() + + const suites = [] + for (const suite of tempMocha.suite.suites) { + suites.push({ + ...suite.simplify(), + file: suite.file || '', + tests: suite.tests.map(test => ({ + ...test.simplify(), + fullTitle: test.fullTitle(), + })), + }) + } + + tempMocha.unloadFiles() + return suites + } + + async run(test) { + let files = this.codecept.testFiles + let grep + + if (test) { + if (!fsPath.isAbsolute(test)) { + test = fsPath.join(store.codeceptDir, test) + } + const testBasename = fsPath.basename(test, '.js') + const testFeatureBasename = fsPath.basename(test, '.feature') + files = files.filter(t => { + return fsPath.basename(t, '.js') === testBasename || fsPath.basename(t, '.feature') === testFeatureBasename || t === test + }) + } + + return this._execute({ files, grep }) + } + + async runSuite(suite) { + return this.run(suite.file) + } + + async runTest(test) { + return this._execute({ grep: test.fullTitle }) + } + + async _execute({ files, grep } = {}) { + await container.started() + + const tsValidation = validateTypeScriptSetup(this.codecept.testFiles, this.codecept.requiringModules || []) + if (tsValidation.hasError) { + output.error(tsValidation.message) + process.exit(1) + } + + const tsWarning = getTSNodeESMWarning(this.codecept.requiringModules || []) + if (tsWarning) { + output.print(output.colors.yellow(tsWarning)) + } + + try { + const { loadTranslations } = await import('./mocha/gherkin.js') + await loadTranslations() + } catch (e) { + // Ignore if gherkin module not available + } + + return new Promise((resolve, reject) => { + const mocha = container.mocha() + mocha.files = files || this.codecept.testFiles + + if (grep) { + mocha.grep(grep) + } + + const done = async (failures) => { + event.emit(event.all.result, container.result()) + event.emit(event.all.after, this.codecept) + await recorder.promise() + if (failures) { + process.exitCode = 1 + } + resolve() + } + + try { + event.emit(event.all.before, this.codecept) + mocha.run(async (failures) => await done(failures)) + } catch (e) { + output.error(e.stack) + reject(e) + } + }) + } +} + +export default Runner From cd81b576397f3890e2433dfa67d478b82fd3385a Mon Sep 17 00:00:00 2001 From: DavertMik Date: Mon, 13 Apr 2026 01:39:00 +0300 Subject: [PATCH 3/5] =?UTF-8?q?refactor:=20clean=20up=20workers.js=20?= =?UTF-8?q?=E2=80=94=20remove=20dead=20code,=20extract=20Config.applyRunCo?= =?UTF-8?q?nfig?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove dead code: activeWorkers Map, maxWorkers, empty recorder placeholder - Simplify _initializeTestPool to 4 lines - Remove redundant configWithoutFunctions variable in WorkerObject.addConfig - Remove hardcoded mochawesome/mocha-junit-reporter path manipulation from createWorkerObjects — fragile, incomplete, and redundant with output dir override - Extract getOverridenConfig to Config.applyRunConfig() in lib/config.js, shared by both workers.js and run-multiple.js Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/command/run-multiple.js | 19 +------ lib/config.js | 29 +++++++++- lib/workers.js | 110 ++++-------------------------------- 3 files changed, 40 insertions(+), 118 deletions(-) diff --git a/lib/command/run-multiple.js b/lib/command/run-multiple.js index 6f28af9bc..504d2832e 100644 --- a/lib/command/run-multiple.js +++ b/lib/command/run-multiple.js @@ -9,6 +9,7 @@ import { createRuns } from './run-multiple/collection.js' import { clearString, replaceValueDeep } from '../utils.js' import { getConfig, getTestRoot, fail } from './utils.js' import store from '../store.js' +import Config from '../config.js' const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) @@ -115,17 +116,11 @@ export default async function (selectedRuns, options) { } function executeRun(runName, runConfig) { - // clone config - let overriddenConfig = { ...config } + let overriddenConfig = Config.applyRunConfig(config, runConfig) - // get configuration const browserConfig = runConfig.browser const browserName = browserConfig.browser - for (const key in browserConfig) { - overriddenConfig.helpers = replaceValueDeep(overriddenConfig.helpers, key, browserConfig[key]) - } - let outputDir = `${runName}_` if (browserConfig.outputName) { outputDir += typeof browserConfig.outputName === 'function' ? browserConfig.outputName() : browserConfig.outputName @@ -138,20 +133,10 @@ function executeRun(runName, runConfig) { outputDir = clearString(outputDir) - // tweaking default output directories and for mochawesome overriddenConfig = replaceValueDeep(overriddenConfig, 'output', path.join(config.output, outputDir)) overriddenConfig = replaceValueDeep(overriddenConfig, 'reportDir', path.join(config.output, outputDir)) overriddenConfig = replaceValueDeep(overriddenConfig, 'mochaFile', path.join(config.output, outputDir, `${browserName}_report.xml`)) - // override tests configuration - if (overriddenConfig.tests) { - overriddenConfig.tests = runConfig.tests - } - - if (overriddenConfig.gherkin && runConfig.gherkin && runConfig.gherkin.features) { - overriddenConfig.gherkin.features = runConfig.gherkin.features - } - // override grep param and collect all params const params = ['run', '--child', `${runId++}.${runName}:${browserName}`, '--override', JSON.stringify(overriddenConfig)] diff --git a/lib/config.js b/lib/config.js index 0b3372e32..9c1114b35 100644 --- a/lib/config.js +++ b/lib/config.js @@ -1,7 +1,7 @@ import fs from 'fs' import path from 'path' import { createRequire } from 'module' -import { fileExists, isFile, deepMerge, deepClone } from './utils.js' +import { fileExists, isFile, deepMerge, deepClone, replaceValueDeep } from './utils.js' import { transpileTypeScript, cleanupTempFiles, fixErrorStack } from './utils/typescript.js' const defaultConfig = { @@ -134,6 +134,33 @@ class Config { return (config = deepMerge(config, additionalConfig)) } + /** + * Apply run configuration (browser overrides, tests, gherkin) to a base config. + * Used by workers and run-multiple to create per-run configurations. + * + * @param {Object} baseConfig + * @param {Object} runConfig - must have .browser object, optionally .tests and .gherkin + * @return {Object} + */ + static applyRunConfig(baseConfig, runConfig) { + const overriddenConfig = deepClone(baseConfig) + const browserConfig = runConfig.browser + + for (const key in browserConfig) { + overriddenConfig.helpers = replaceValueDeep(overriddenConfig.helpers, key, browserConfig[key]) + } + + if (overriddenConfig.tests && runConfig.tests) { + overriddenConfig.tests = runConfig.tests + } + + if (overriddenConfig.gherkin && runConfig.gherkin?.features) { + overriddenConfig.gherkin.features = runConfig.gherkin.features + } + + return overriddenConfig + } + /** * Resets config to default * @return {Object} diff --git a/lib/workers.js b/lib/workers.js index cbbe4e090..c6e5d7f7e 100644 --- a/lib/workers.js +++ b/lib/workers.js @@ -13,7 +13,7 @@ import Codecept from './codecept.js' import MochaFactory from './mocha/factory.js' import Container from './container.js' import { getTestRoot } from './command/utils.js' -import { isFunction, fileExists, replaceValueDeep, deepClone } from './utils.js' +import { isFunction, fileExists } from './utils.js' import mainConfig from './config.js' import output from './output.js' import event from './event.js' @@ -117,34 +117,11 @@ const createWorkerObjects = (testGroups, config, testRoot, options, selectedRuns } const workersToExecute = [] - const currentOutputFolder = config.output - let currentMochawesomeReportDir - let currentMochaJunitReporterFile - - if (config.mocha && config.mocha.reporterOptions) { - currentMochawesomeReportDir = config.mocha.reporterOptions?.mochawesome.options.reportDir - currentMochaJunitReporterFile = config.mocha.reporterOptions['mocha-junit-reporter'].options.mochaFile - } - createRuns(selectedRuns, config).forEach(worker => { - const separator = path.sep - const _config = { ...config } - let workerName = worker.name.replace(':', '_') - _config.output = `${currentOutputFolder}${separator}${workerName}` - if (config.mocha && config.mocha.reporterOptions) { - _config.mocha.reporterOptions.mochawesome.options.reportDir = `${currentMochawesomeReportDir}${separator}${workerName}` - - const _tempArray = currentMochaJunitReporterFile.split(separator) - _tempArray.splice( - _tempArray.findIndex(item => item.includes('.xml')), - 0, - workerName, - ) - _config.mocha.reporterOptions['mocha-junit-reporter'].options.mochaFile = _tempArray.join(separator) - } - workerName = worker.getOriginalName() || worker.getName() - const workerConfig = worker.getConfig() - workersToExecute.push(getOverridenConfig(workerName, workerConfig, _config)) + const workerName = worker.name.replace(':', '_') + const _config = mainConfig.applyRunConfig(config, worker.getConfig()) + _config.output = path.join(config.output, workerName) + workersToExecute.push(_config) }) const workers = [] let index = 0 @@ -188,27 +165,6 @@ const convertToMochaTests = testGroup => { return group } -const getOverridenConfig = (workerName, workerConfig, config) => { - // clone config - const overriddenConfig = deepClone(config) - - // get configuration - const browserConfig = workerConfig.browser - - for (const key in browserConfig) { - overriddenConfig.helpers = replaceValueDeep(overriddenConfig.helpers, key, browserConfig[key]) - } - - // override tests configuration - if (overriddenConfig.tests) { - overriddenConfig.tests = workerConfig.tests - } - - if (overriddenConfig.gherkin && workerConfig.gherkin && workerConfig.gherkin.features) { - overriddenConfig.gherkin.features = workerConfig.gherkin.features - } - return overriddenConfig -} class WorkerObject { /** @@ -224,17 +180,11 @@ class WorkerObject { addConfig(config) { const oldConfig = JSON.parse(this.options.override || '{}') - // Remove customLocatorStrategies from both old and new config before JSON serialization - // since functions cannot be serialized and will be lost, causing workers to have empty strategies. - // Note: Only WebDriver helper supports customLocatorStrategies - const configWithoutFunctions = { ...config } - - // Clean both old and new config const cleanConfig = cfg => { if (cfg.helpers) { cfg.helpers = { ...cfg.helpers } Object.keys(cfg.helpers).forEach(helperName => { - if (cfg.helpers[helperName] && cfg.helpers[helperName].customLocatorStrategies !== undefined) { + if (cfg.helpers[helperName]?.customLocatorStrategies !== undefined) { cfg.helpers[helperName] = { ...cfg.helpers[helperName] } delete cfg.helpers[helperName].customLocatorStrategies } @@ -243,11 +193,7 @@ class WorkerObject { return cfg } - const cleanedOldConfig = cleanConfig(oldConfig) - const cleanedNewConfig = cleanConfig(configWithoutFunctions) - - // Deep merge configurations to preserve all helpers from base config - const newConfig = merge({}, cleanedOldConfig, cleanedNewConfig) + const newConfig = merge({}, cleanConfig(oldConfig), cleanConfig(config)) this.options.override = JSON.stringify(newConfig) } @@ -292,8 +238,6 @@ class Workers extends EventEmitter { this.testPool = [] this.testPoolInitialized = false this.isPoolMode = config.by === 'pool' - this.activeWorkers = new Map() - this.maxWorkers = numberOfWorkers // Track original worker count for pool mode createOutputDir(config.testConfig) // Defer worker initialization until codecept is ready @@ -395,36 +339,11 @@ class Workers extends EventEmitter { this.testGroups = populateGroups(numberOfWorkers) } - /** - * Initialize the test pool if not already done - * This is called lazily to avoid state pollution issues during construction - */ _initializeTestPool() { - if (this.testPoolInitialized) { - return - } - - // Ensure codecept is initialized - if (!this.codecept) { - output.log('Warning: codecept not initialized when initializing test pool') - this.testPoolInitialized = true - return - } - - const files = this.codecept.testFiles - if (!files || files.length === 0) { - this.testPoolInitialized = true - return - } - - // In ESM, test UIDs are not stable across different mocha instances - // So instead of using UIDs, we distribute test FILES - // Each file may contain multiple tests - for (const file of files) { - this.testPool.push(file) - } - + if (this.testPoolInitialized) return this.testPoolInitialized = true + if (!this.codecept) return + this.testPool = [...this.codecept.testFiles] } /** @@ -493,10 +412,6 @@ class Workers extends EventEmitter { workerThreads.push(workerThread) } - recorder.add('workers started', () => { - // Workers are already running, this is just a placeholder step - }) - // Add overall timeout to prevent infinite hanging const overallTimeout = setTimeout(() => { console.error('[Main] Overall timeout reached (10 minutes). Force terminating remaining workers...') @@ -533,11 +448,6 @@ class Workers extends EventEmitter { } _listenWorkerEvents(worker) { - // Track worker thread for pool mode - if (this.isPoolMode) { - this.activeWorkers.set(worker, { available: true, workerIndex: null }) - } - // Track last activity time to detect hanging workers let lastActivity = Date.now() let currentTest = null From a15acc38af26c976aaa38d28b26261f4e5843163 Mon Sep 17 00:00:00 2001 From: DavertMik Date: Wed, 22 Apr 2026 21:02:42 +0300 Subject: [PATCH 4/5] fix: address PR feedback on programmatic API and workers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - runner.js: escape regex metacharacters in runTest() grep so titles with brackets/parens match literally and exactly (anchored ^…$) - index.js: add Codecept and Helper as named exports so `import { Codecept } from 'codeceptjs'` works as documented - workers.js: restore per-worker report path overrides for multiple runs, using the generic replaceValueDeep approach already in run-multiple.js. Without this, mochawesome/mocha-junit-reporter overwrite each other across browser workers. Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/index.js | 2 +- lib/runner.js | 3 ++- lib/workers.js | 9 ++++++--- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/index.js b/lib/index.js index 6dc390e0b..1a974bf88 100644 --- a/lib/index.js +++ b/lib/index.js @@ -77,4 +77,4 @@ export default { } // Named exports for ESM compatibility -export { codecept, output, container, event, recorder, config, actor, helper, pause, within, dataTable, dataTableArgument, store, locator, heal, ai, Workers, Secret, secret, Result, Runner } +export { codecept, codecept as Codecept, output, container, event, recorder, config, actor, helper, helper as Helper, pause, within, dataTable, dataTableArgument, store, locator, heal, ai, Workers, Secret, secret, Result, Runner } diff --git a/lib/runner.js b/lib/runner.js index ed2880edc..e2aa411a7 100644 --- a/lib/runner.js +++ b/lib/runner.js @@ -1,4 +1,5 @@ import fsPath from 'path' +import escapeRe from 'escape-string-regexp' import container from './container.js' import MochaFactory from './mocha/factory.js' import event from './event.js' @@ -60,7 +61,7 @@ class Runner { } async runTest(test) { - return this._execute({ grep: test.fullTitle }) + return this._execute({ grep: `^${escapeRe(test.fullTitle)}$` }) } async _execute({ files, grep } = {}) { diff --git a/lib/workers.js b/lib/workers.js index c6e5d7f7e..ff69af3f7 100644 --- a/lib/workers.js +++ b/lib/workers.js @@ -13,7 +13,7 @@ import Codecept from './codecept.js' import MochaFactory from './mocha/factory.js' import Container from './container.js' import { getTestRoot } from './command/utils.js' -import { isFunction, fileExists } from './utils.js' +import { isFunction, fileExists, replaceValueDeep } from './utils.js' import mainConfig from './config.js' import output from './output.js' import event from './event.js' @@ -119,8 +119,11 @@ const createWorkerObjects = (testGroups, config, testRoot, options, selectedRuns createRuns(selectedRuns, config).forEach(worker => { const workerName = worker.name.replace(':', '_') - const _config = mainConfig.applyRunConfig(config, worker.getConfig()) - _config.output = path.join(config.output, workerName) + let _config = mainConfig.applyRunConfig(config, worker.getConfig()) + const workerOutput = path.join(config.output, workerName) + _config = replaceValueDeep(_config, 'output', workerOutput) + _config = replaceValueDeep(_config, 'reportDir', workerOutput) + _config = replaceValueDeep(_config, 'mochaFile', path.join(workerOutput, `${workerName}_report.xml`)) workersToExecute.push(_config) }) const workers = [] From 60087346334ad569df2f2bfd253c7df0fe888dcb Mon Sep 17 00:00:00 2001 From: DavertMik Date: Thu, 23 Apr 2026 03:01:49 +0300 Subject: [PATCH 5/5] refactor: merge Runner class back into Codecept The Runner extraction created a runtime object cycle: Codecept owned a Runner, and Runner held a back-reference to Codecept for config, opts, testFiles, requiringModules, loadTests, and the event.all.before/after payload. Since Runner had one creation site, no independent state, and its only collaborator was the object it pointed back to, the class wasn't carrying its weight. Moving the methods back onto Codecept puts behavior next to its data and eliminates the cycle by construction. - lib/codecept.js: absorbs getSuites, run, runSuite, runTest, _execute - lib/runner.js: deleted - lib/index.js: Runner removed from public exports (unreleased API) Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/codecept.js | 97 ++++++++++++++++++++++++++++++++++++--- lib/index.js | 5 +-- lib/runner.js | 117 ------------------------------------------------ 3 files changed, 92 insertions(+), 127 deletions(-) delete mode 100644 lib/runner.js diff --git a/lib/codecept.js b/lib/codecept.js index dfff4cee4..a08d0603b 100644 --- a/lib/codecept.js +++ b/lib/codecept.js @@ -10,15 +10,20 @@ import { createRequire } from 'module' const __filename = fileURLToPath(import.meta.url) const __dirname = dirname(__filename) +import escapeRe from 'escape-string-regexp' import Helper from '@codeceptjs/helper' -import Runner from './runner.js' +import MochaFactory from './mocha/factory.js' import container from './container.js' import Config from './config.js' +import event from './event.js' +import recorder from './recorder.js' +import output from './output.js' import runHook from './hooks.js' import ActorFactory from './actor.js' import { emptyFolder } from './utils.js' import { initCodeceptGlobals } from './globals.js' import store from './store.js' +import { validateTypeScriptSetup, getTSNodeESMWarning } from './utils/loaderCheck.js' import storeListener from './listener/store.js' import stepsListener from './listener/steps.js' @@ -100,7 +105,6 @@ class Codecept { await this.requireModules(this.requiringModules) // initializing listeners await container.create(this.config, this.opts) - this.runner = new Runner(this) await this.runHooks() } @@ -263,7 +267,26 @@ class Codecept { * @returns {Array<{title: string, file: string, tags: string[], tests: Array<{title: string, uid: string, tags: string[], fullTitle: string}>}>} */ getSuites(pattern) { - return this.runner.getSuites(pattern) + if (this.testFiles.length === 0) this.loadTests(pattern) + + const tempMocha = MochaFactory.create(this.config.mocha || {}, this.opts || {}) + tempMocha.files = this.testFiles + tempMocha.loadFiles() + + const suites = [] + for (const suite of tempMocha.suite.suites) { + suites.push({ + ...suite.simplify(), + file: suite.file || '', + tests: suite.tests.map(test => ({ + ...test.simplify(), + fullTitle: test.fullTitle(), + })), + }) + } + + tempMocha.unloadFiles() + return suites } /** @@ -274,7 +297,7 @@ class Codecept { * @returns {Promise} */ async runSuite(suite) { - return this.runner.runSuite(suite) + return this.run(suite.file) } /** @@ -285,7 +308,7 @@ class Codecept { * @returns {Promise} */ async runTest(test) { - return this.runner.runTest(test) + return this._execute({ grep: `^${escapeRe(test.fullTitle)}$` }) } /** @@ -295,7 +318,69 @@ class Codecept { * @returns {Promise} */ async run(test) { - return this.runner.run(test) + let 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') + files = files.filter(t => { + return fsPath.basename(t, '.js') === testBasename || fsPath.basename(t, '.feature') === testFeatureBasename || t === test + }) + } + + return this._execute({ files }) + } + + async _execute({ files, grep } = {}) { + await container.started() + + const tsValidation = validateTypeScriptSetup(this.testFiles, this.requiringModules || []) + if (tsValidation.hasError) { + output.error(tsValidation.message) + process.exit(1) + } + + const tsWarning = getTSNodeESMWarning(this.requiringModules || []) + if (tsWarning) { + output.print(output.colors.yellow(tsWarning)) + } + + try { + const { loadTranslations } = await import('./mocha/gherkin.js') + await loadTranslations() + } catch (e) { + // Ignore if gherkin module not available + } + + return new Promise((resolve, reject) => { + const mocha = container.mocha() + mocha.files = files || this.testFiles + + if (grep) { + mocha.grep(grep) + } + + const done = async (failures) => { + event.emit(event.all.result, container.result()) + event.emit(event.all.after, this) + await recorder.promise() + if (failures) { + process.exitCode = 1 + } + resolve() + } + + try { + event.emit(event.all.before, this) + mocha.run(async (failures) => await done(failures)) + } catch (e) { + output.error(e.stack) + reject(e) + } + }) } /** diff --git a/lib/index.js b/lib/index.js index 1a974bf88..a125fc94a 100644 --- a/lib/index.js +++ b/lib/index.js @@ -24,7 +24,6 @@ import ai from './ai.js' import Workers from './workers.js' import Secret, { secret } from './secret.js' import Result from './result.js' -import Runner from './runner.js' export default { /** @type {typeof CodeceptJS.Codecept} */ @@ -72,9 +71,7 @@ export default { /** @type {typeof Result} */ Result, - - Runner, } // Named exports for ESM compatibility -export { codecept, codecept as Codecept, output, container, event, recorder, config, actor, helper, helper as Helper, pause, within, dataTable, dataTableArgument, store, locator, heal, ai, Workers, Secret, secret, Result, Runner } +export { codecept, codecept as Codecept, output, container, event, recorder, config, actor, helper, helper as Helper, pause, within, dataTable, dataTableArgument, store, locator, heal, ai, Workers, Secret, secret, Result } diff --git a/lib/runner.js b/lib/runner.js deleted file mode 100644 index e2aa411a7..000000000 --- a/lib/runner.js +++ /dev/null @@ -1,117 +0,0 @@ -import fsPath from 'path' -import escapeRe from 'escape-string-regexp' -import container from './container.js' -import MochaFactory from './mocha/factory.js' -import event from './event.js' -import recorder from './recorder.js' -import output from './output.js' -import store from './store.js' -import { validateTypeScriptSetup, getTSNodeESMWarning } from './utils/loaderCheck.js' - -class Runner { - constructor(codecept) { - this.codecept = codecept - } - - getSuites(pattern) { - if (this.codecept.testFiles.length === 0) { - this.codecept.loadTests(pattern) - } - - const tempMocha = MochaFactory.create(this.codecept.config.mocha || {}, this.codecept.opts || {}) - tempMocha.files = this.codecept.testFiles - tempMocha.loadFiles() - - const suites = [] - for (const suite of tempMocha.suite.suites) { - suites.push({ - ...suite.simplify(), - file: suite.file || '', - tests: suite.tests.map(test => ({ - ...test.simplify(), - fullTitle: test.fullTitle(), - })), - }) - } - - tempMocha.unloadFiles() - return suites - } - - async run(test) { - let files = this.codecept.testFiles - let grep - - if (test) { - if (!fsPath.isAbsolute(test)) { - test = fsPath.join(store.codeceptDir, test) - } - const testBasename = fsPath.basename(test, '.js') - const testFeatureBasename = fsPath.basename(test, '.feature') - files = files.filter(t => { - return fsPath.basename(t, '.js') === testBasename || fsPath.basename(t, '.feature') === testFeatureBasename || t === test - }) - } - - return this._execute({ files, grep }) - } - - async runSuite(suite) { - return this.run(suite.file) - } - - async runTest(test) { - return this._execute({ grep: `^${escapeRe(test.fullTitle)}$` }) - } - - async _execute({ files, grep } = {}) { - await container.started() - - const tsValidation = validateTypeScriptSetup(this.codecept.testFiles, this.codecept.requiringModules || []) - if (tsValidation.hasError) { - output.error(tsValidation.message) - process.exit(1) - } - - const tsWarning = getTSNodeESMWarning(this.codecept.requiringModules || []) - if (tsWarning) { - output.print(output.colors.yellow(tsWarning)) - } - - try { - const { loadTranslations } = await import('./mocha/gherkin.js') - await loadTranslations() - } catch (e) { - // Ignore if gherkin module not available - } - - return new Promise((resolve, reject) => { - const mocha = container.mocha() - mocha.files = files || this.codecept.testFiles - - if (grep) { - mocha.grep(grep) - } - - const done = async (failures) => { - event.emit(event.all.result, container.result()) - event.emit(event.all.after, this.codecept) - await recorder.promise() - if (failures) { - process.exitCode = 1 - } - resolve() - } - - try { - event.emit(event.all.before, this.codecept) - mocha.run(async (failures) => await done(failures)) - } catch (e) { - output.error(e.stack) - reject(e) - } - }) - } -} - -export default Runner