Skip to content

Commit 77fc33b

Browse files
DavertMikclaude
andcommitted
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) <noreply@anthropic.com>
1 parent c8a7be8 commit 77fc33b

File tree

5 files changed

+166
-146
lines changed

5 files changed

+166
-146
lines changed

docs/internal-api.md

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -282,14 +282,14 @@ Each test contains:
282282
283283
### Executing Suites
284284
285-
Use `executeSuite()` to run all tests within a suite:
285+
Use `runSuite()` to run all tests within a suite:
286286
287287
```js
288288
await codecept.bootstrap();
289289

290290
const suites = codecept.getSuites();
291291
for (const suite of suites) {
292-
await codecept.executeSuite(suite);
292+
await codecept.runSuite(suite);
293293
}
294294

295295
const result = container.result();
@@ -302,14 +302,14 @@ await codecept.teardown();
302302
303303
### Executing Individual Tests
304304
305-
Use `executeTest()` to run a single test:
305+
Use `runTest()` to run a single test:
306306
307307
```js
308308
await codecept.bootstrap();
309309

310310
const suites = codecept.getSuites();
311311
for (const test of suites[0].tests) {
312-
await codecept.executeTest(test);
312+
await codecept.runTest(test);
313313
}
314314

315315
const result = container.result();
@@ -350,18 +350,42 @@ try {
350350
}
351351
```
352352
353+
### Runner
354+
355+
All Mocha interactions are handled by the `Runner` class (`lib/runner.js`). After calling `init()`, it's available as `codecept.runner`:
356+
357+
```js
358+
const runner = codecept.runner;
359+
360+
// Same as codecept.getSuites() / runSuite() / runTest()
361+
const suites = runner.getSuites();
362+
await runner.runSuite(suites[0]);
363+
await runner.runTest(suites[0].tests[0]);
364+
```
365+
366+
The `Codecept` methods (`getSuites`, `runSuite`, `runTest`, `run`) delegate to the Runner.
367+
353368
### Codecept Methods Reference
354369
355370
| Method | Description |
356371
|--------|-------------|
357-
| `new Codecept(config, opts)` | Create runner instance |
358-
| `await init(dir)` | Initialize globals, container, helpers, plugins |
372+
| `new Codecept(config, opts)` | Create codecept instance |
373+
| `await init(dir)` | Initialize globals, container, helpers, plugins, runner |
359374
| `loadTests(pattern?)` | Find test files by glob pattern |
360375
| `getSuites(pattern?)` | Load and return parsed suites with tests |
361376
| `await bootstrap()` | Execute bootstrap hook |
362377
| `await run(test?)` | Run all loaded tests (or filter by file path) |
363-
| `await executeSuite(suite)` | Run a specific suite from `getSuites()` |
364-
| `await executeTest(test)` | Run a specific test from `getSuites()` |
378+
| `await runSuite(suite)` | Run a specific suite from `getSuites()` |
379+
| `await runTest(test)` | Run a specific test from `getSuites()` |
365380
| `await teardown()` | Execute teardown hook |
366381
382+
### Runner Methods Reference
383+
384+
| Method | Description |
385+
|--------|-------------|
386+
| `getSuites(pattern?)` | Parse suites using a temporary Mocha instance |
387+
| `run(test?)` | Run tests, optionally filtering by file path |
388+
| `runSuite(suite)` | Run all tests in a suite |
389+
| `runTest(test)` | Run a single test by fullTitle |
390+
367391
> Also, you can run tests inside workers in a custom script. Please refer to the [parallel execution](/parallel) guide for more details.

lib/codecept.js

Lines changed: 10 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,13 @@ const __filename = fileURLToPath(import.meta.url)
1111
const __dirname = dirname(__filename)
1212

1313
import Helper from '@codeceptjs/helper'
14-
import MochaFactory from './mocha/factory.js'
14+
import Runner from './runner.js'
1515
import container from './container.js'
1616
import Config from './config.js'
17-
import event from './event.js'
1817
import runHook from './hooks.js'
1918
import ActorFactory from './actor.js'
20-
import output from './output.js'
2119
import { emptyFolder } from './utils.js'
2220
import { initCodeceptGlobals } from './globals.js'
23-
import { validateTypeScriptSetup, getTSNodeESMWarning } from './utils/loaderCheck.js'
24-
import recorder from './recorder.js'
2521
import store from './store.js'
2622

2723
import storeListener from './listener/store.js'
@@ -104,6 +100,7 @@ class Codecept {
104100
await this.requireModules(this.requiringModules)
105101
// initializing listeners
106102
await container.create(this.config, this.opts)
103+
this.runner = new Runner(this)
107104
await this.runHooks()
108105
}
109106

@@ -266,87 +263,29 @@ class Codecept {
266263
* @returns {Array<{title: string, file: string, tags: string[], tests: Array<{title: string, uid: string, tags: string[], fullTitle: string}>}>}
267264
*/
268265
getSuites(pattern) {
269-
if (this.testFiles.length === 0) {
270-
this.loadTests(pattern)
271-
}
272-
273-
const tempMocha = MochaFactory.create(this.config.mocha || {}, this.opts || {})
274-
tempMocha.files = this.testFiles
275-
tempMocha.loadFiles()
276-
277-
const suites = []
278-
for (const suite of tempMocha.suite.suites) {
279-
suites.push({
280-
...suite.simplify(),
281-
file: suite.file || '',
282-
tests: suite.tests.map(test => ({
283-
...test.simplify(),
284-
fullTitle: test.fullTitle(),
285-
})),
286-
})
287-
}
288-
289-
tempMocha.unloadFiles()
290-
return suites
266+
return this.runner.getSuites(pattern)
291267
}
292268

293269
/**
294-
* Execute all tests in a suite.
270+
* Run all tests in a suite.
295271
* Must be called after init() and bootstrap().
296272
*
297273
* @param {{file: string}} suite - suite object returned by getSuites()
298274
* @returns {Promise<void>}
299275
*/
300-
async executeSuite(suite) {
301-
return this.run(suite.file)
276+
async runSuite(suite) {
277+
return this.runner.runSuite(suite)
302278
}
303279

304280
/**
305-
* Execute a single test by its fullTitle.
281+
* Run a single test by its fullTitle.
306282
* Must be called after init() and bootstrap().
307283
*
308284
* @param {{fullTitle: string}} test - test object returned by getSuites()
309285
* @returns {Promise<void>}
310286
*/
311-
async executeTest(test) {
312-
await container.started()
313-
314-
const tsValidation = validateTypeScriptSetup(this.testFiles, this.requiringModules || [])
315-
if (tsValidation.hasError) {
316-
output.error(tsValidation.message)
317-
process.exit(1)
318-
}
319-
320-
try {
321-
const { loadTranslations } = await import('./mocha/gherkin.js')
322-
await loadTranslations()
323-
} catch (e) {
324-
// Ignore if gherkin module not available
325-
}
326-
327-
return new Promise((resolve, reject) => {
328-
const mocha = container.mocha()
329-
mocha.files = this.testFiles
330-
mocha.grep(test.fullTitle)
331-
332-
const done = async (failures) => {
333-
event.emit(event.all.result, container.result())
334-
event.emit(event.all.after, this)
335-
await recorder.promise()
336-
if (failures) {
337-
process.exitCode = 1
338-
}
339-
resolve()
340-
}
341-
342-
try {
343-
event.emit(event.all.before, this)
344-
mocha.run(async (failures) => await done(failures))
345-
} catch (e) {
346-
output.error(e.stack)
347-
reject(e)
348-
}
349-
})
287+
async runTest(test) {
288+
return this.runner.runTest(test)
350289
}
351290

352291
/**
@@ -356,64 +295,7 @@ class Codecept {
356295
* @returns {Promise<void>}
357296
*/
358297
async run(test) {
359-
await container.started()
360-
361-
// Check TypeScript loader configuration before running tests
362-
const tsValidation = validateTypeScriptSetup(this.testFiles, this.requiringModules || [])
363-
if (tsValidation.hasError) {
364-
output.error(tsValidation.message)
365-
process.exit(1)
366-
}
367-
368-
// Show warning if ts-node/esm is being used
369-
const tsWarning = getTSNodeESMWarning(this.requiringModules || [])
370-
if (tsWarning) {
371-
output.print(output.colors.yellow(tsWarning))
372-
}
373-
374-
// Ensure translations are loaded for Gherkin features
375-
try {
376-
const { loadTranslations } = await import('./mocha/gherkin.js')
377-
await loadTranslations()
378-
} catch (e) {
379-
// Ignore if gherkin module not available
380-
}
381-
382-
return new Promise((resolve, reject) => {
383-
const mocha = container.mocha()
384-
mocha.files = this.testFiles
385-
386-
if (test) {
387-
if (!fsPath.isAbsolute(test)) {
388-
test = fsPath.join(store.codeceptDir, test)
389-
}
390-
const testBasename = fsPath.basename(test, '.js')
391-
const testFeatureBasename = fsPath.basename(test, '.feature')
392-
mocha.files = mocha.files.filter(t => {
393-
return fsPath.basename(t, '.js') === testBasename || fsPath.basename(t, '.feature') === testFeatureBasename || t === test
394-
})
395-
}
396-
397-
const done = async (failures) => {
398-
event.emit(event.all.result, container.result())
399-
event.emit(event.all.after, this)
400-
// Wait for any recorder tasks added by event.all.after handlers
401-
await recorder.promise()
402-
// Set exit code based on test failures
403-
if (failures) {
404-
process.exitCode = 1
405-
}
406-
resolve()
407-
}
408-
409-
try {
410-
event.emit(event.all.before, this)
411-
mocha.run(async (failures) => await done(failures))
412-
} catch (e) {
413-
output.error(e.stack)
414-
reject(e)
415-
}
416-
})
298+
return this.runner.run(test)
417299
}
418300

419301
/**

lib/command/check.js

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -70,15 +70,10 @@ export default async function (options) {
7070
if (codecept) {
7171
try {
7272
codecept.loadTests()
73-
const files = codecept.testFiles
74-
const mocha = Container.mocha()
75-
mocha.files = files
76-
mocha.loadFiles()
77-
78-
for (const suite of mocha.suite.suites) {
79-
if (suite && suite.tests) {
80-
numTests += suite.tests.length
81-
}
73+
const suites = codecept.getSuites()
74+
75+
for (const suite of suites) {
76+
numTests += suite.tests.length
8277
}
8378

8479
if (numTests > 0) {

lib/index.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import ai from './ai.js'
2424
import Workers from './workers.js'
2525
import Secret, { secret } from './secret.js'
2626
import Result from './result.js'
27+
import Runner from './runner.js'
2728

2829
export default {
2930
/** @type {typeof CodeceptJS.Codecept} */
@@ -71,7 +72,9 @@ export default {
7172

7273
/** @type {typeof Result} */
7374
Result,
75+
76+
Runner,
7477
}
7578

7679
// Named exports for ESM compatibility
77-
export { codecept, output, container, event, recorder, config, actor, helper, pause, within, dataTable, dataTableArgument, store, locator, heal, ai, Workers, Secret, secret, Result }
80+
export { codecept, output, container, event, recorder, config, actor, helper, pause, within, dataTable, dataTableArgument, store, locator, heal, ai, Workers, Secret, secret, Result, Runner }

0 commit comments

Comments
 (0)