docs/architecture.md
How CodeceptJS runs a test, and the internal modules you build plugins, listeners, and helpers against.
CodeceptJS is built on top of Mocha. A run goes through these stages:
bootstrap hook. event.all.before fires.event.suite.before fires. Helper _beforeSuite hooks run.event.test.started fires; Before hooks from helpers (_before) and from the suite run, then event.test.before fires; the scenario function runs; event.test.passed or event.test.failed fires; After hooks run; event.test.after and then event.test.finished fire.I.* call inside a scenario becomes a step. It is scheduled onto the recorder — event.step.before fires — then executed: event.step.started, event.step.passed or event.step.failed, event.step.after, event.step.finished.event.suite.after fires after each suite, event.all.after after the last one, and event.all.result when results are printed. The teardown hook runs.The key idea is step 4: a scenario doesn't execute its steps as it runs — it queues them. I.click() returns immediately; the recorder runs the queued action later. This is why scenarios rarely need await, and why anything that injects async work has to go through the recorder.
CodeceptJS exposes its internals as named exports of the codeceptjs package. Import only what you need:
import { recorder, event, output, container, config } from 'codeceptjs'
| Export | What it is |
|---|---|
codecept | the test runner class |
config | the loaded configuration |
container | dependency-injection container: helpers, support objects, plugins, the Mocha instance |
recorder | the global promise chain that orders every step |
event | the event dispatcher and the names of all lifecycle events |
output | the printer used for all console output |
store | global state of the run — current test/step, run modes, directories |
helper | the base class every helper extends |
actor | the base class behind the I object |
Older code relied on a global
codeceptjsobject (const { recorder } = codeceptjs). That global only exists undernoGlobals: false, the deprecated 3.x default — prefer named imports.
The API reference on GitHub documents these modules; the source is the final word.
The recorder is a single global promise chain. Every step a scenario "calls" is appended to it, and the chain runs the steps one after another. To run your own async code at the right point in a test, append it to the recorder too:
import { event, recorder } from 'codeceptjs'
event.dispatcher.on(event.test.before, () => {
recorder.add('seed fixture data', async () => {
await api.post('/users', { name: 'john', email: '[email protected]' })
})
})
recorder.add(name, fn) — append fn (async, or returning a promise) to the chain. The name shows up in --verbose output.recorder.startUnlessRunning() — start a chain if none is running. Call it before add() from a listener that may fire outside a running chain, such as event.all.before.recorder.retry({ retries, when }) — retry failing steps that match when. See conditional retries.Run tests with --verbose to watch the recorder schedule and execute each entry.
The container resolves helpers and support objects by name:
import { container } from 'codeceptjs'
const helpers = container.helpers() // every helper, keyed by name
const { Playwright } = container.helpers() // one helper
const support = container.support() // every support object
const { UserPage } = container.support() // one page object
const plugins = container.plugins() // enabled plugins
const mocha = container.mocha() // the current Mocha instance
Add objects at runtime — useful from a bootstrap script:
import { container } from 'codeceptjs'
import UserPage from './pages/user.js'
container.append({
helpers: { MyHelper: new MyHelper({ host: 'http://example.com' }) },
support: { UserPage },
})
event.dispatcher is a Node EventEmitter. Attach listeners to it from a plugin or bootstrap script.
Events are sync or async:
recorder.add().| Event | Kind | When |
|---|---|---|
event.all.before | — | before any test runs |
event.suite.before(suite) | async | before a suite |
event.test.started(test) | sync | at the very start of a test |
event.test.before(test) | async | after Before hooks from helpers and the test are run |
event.test.passed(test) | sync | test passed |
event.test.failed(test, err) | sync | test failed |
event.test.skipped(test) | sync | test skipped |
event.test.after(test) | async | after each test |
event.test.finished(test) | sync | test finished |
event.suite.after(suite) | async | after a suite |
event.step.before(step) | async | step scheduled for execution |
event.step.started(step) | sync | step starts executing |
event.step.passed(step) | sync | step passed |
event.step.failed(step, err) | sync | step failed |
event.step.after(step) | async | after a step |
event.step.finished(step) | sync | step finished |
event.step.comment(step) | sync | a comment such as I.say(...) |
event.bddStep.before(step) / event.bddStep.after(step) | async | around a Gherkin step |
event.hook.started(hook) / event.hook.passed / event.hook.failed / event.hook.finished | sync | around Before / After / BeforeSuite / AfterSuite hooks |
event.all.after | — | after all tests |
event.all.result(result) | — | when results are printed |
event.all.failures(failures) | — | when a run reports failures |
event.workers.before / event.workers.after / event.workers.result(result) | — | around a parallel run (parent process only) |
The built-in listeners are working examples — every reporter and several plugins are listeners.
Test events pass a test object with these fields:
title — the test titlebody — the test function as a stringopts — test options such as retries (see test options)pending — true while scheduled, false once finishedtags — array of tags for this testartifacts — files attached to this test (screenshots, videos, …), shared across reportersfile — path to the test filesteps — executed steps (only on test.passed, test.failed, test.finished)skipInfo — present when the test was skipped: { message, description }Step events pass a step object with these fields:
name — the step name, such as see or clickactor — the current actor, usually Ihelper — the helper instance that executes this stephelperMethod — the helper method, usually the same as namestatus — passed or failedprefix — for a step inside a within block, the within text (e.g. Within .js-signup-form)args — the arguments passed to the stepimport { config } from 'codeceptjs'
config.get() // the full config object
config.get('myKey') // one value
config.get('myKey', 'fallback') // one value, with a default
Output has four verbosity levels, each toggled by a CLI flag:
| Level | Flag | Use |
|---|---|---|
| default | — | output.print — basic information |
| steps | --steps | step execution |
| debug | --debug | steps plus output.debug |
| verbose | --verbose | debug plus output.log (internal logs and recorder activity) |
import { output } from 'codeceptjs'
output.print('basic information')
output.debug('debug information')
output.log('verbose logging information')
Use these instead of console.log so messages respect the chosen verbosity.
store holds the state of the current run — the executing test, suite, and step, the active run modes (dryRun, debugMode, workerMode, …), and the project directories. Listeners, plugins, and helpers read it to know where in the lifecycle they are without that information being passed to them:
import { store } from 'codeceptjs'
event.dispatcher.on(event.step.before, () => {
if (store.dryRun) return // no side effects on a dry run
output.debug(`in ${store.currentTest?.title}`)
})
CodeceptJS keeps the state fields up to date for you. See the Store reference for every field and when to write to it.
The I object is an actor assembled from the enabled helpers. Each I.method() call delegates to the matching helper method and is wrapped as a step. Methods whose names start with _ are private to the helper and not exposed on I. To add your own actions, write a custom helper.
CodeceptJS can be driven from your own script. Create the runner with a config and options, initialize it, then bootstrap, load tests, and run:
import { codecept as Codecept } from 'codeceptjs'
const config = { helpers: { Playwright: { browser: 'chromium', url: 'http://localhost' } } }
const opts = { steps: true }
const codecept = new Codecept(config, opts)
codecept.init(import.meta.dirname) // the test root directory
try {
await codecept.bootstrap()
codecept.loadTests('**/*_test.js')
await codecept.run() // pass a test file path to run only that file
} catch (err) {
console.error(err)
process.exitCode = 1
} finally {
await codecept.teardown()
}
To run tests inside workers from a script, see parallel execution.
See also: Extending CodeceptJS · Custom Helpers · Plugins · Bootstrap & Teardown