Architecture
How CodeceptJS runs a test, and the internal modules you build plugins, listeners, and helpers against.
How a Test Runs
Section titled “How a Test Runs”CodeceptJS is built on top of Mocha. A run goes through these stages:
- Load. CodeceptJS reads the config, builds the container (helpers, support objects, plugins), and runs the
bootstraphook.event.all.beforefires. - Suite. For each suite,
event.suite.beforefires. Helper_beforeSuitehooks run. - Test. For each test:
event.test.startedfires;Beforehooks from helpers (_before) and from the suite run, thenevent.test.beforefires; the scenario function runs;event.test.passedorevent.test.failedfires;Afterhooks run;event.test.afterand thenevent.test.finishedfire. - Step. Each
I.*call inside a scenario becomes a step. It is scheduled onto the recorder —event.step.beforefires — then executed:event.step.started,event.step.passedorevent.step.failed,event.step.after,event.step.finished. - Finish.
event.suite.afterfires after each suite,event.all.afterafter the last one, andevent.all.resultwhen results are printed. Theteardownhook 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.
The Internal API
Section titled “The Internal API”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
Section titled “The Recorder”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: 'john@example.com' }) })})recorder.add(name, fn)— appendfn(async, or returning a promise) to the chain. The name shows up in--verboseoutput.recorder.startUnlessRunning()— start a chain if none is running. Call it beforeadd()from a listener that may fire outside a running chain, such asevent.all.before.recorder.retry({ retries, when })— retry failing steps that matchwhen. See conditional retries.
Run tests with --verbose to watch the recorder schedule and execute each entry.
Container
Section titled “Container”The container resolves helpers and support objects by name:
import { container } from 'codeceptjs'
const helpers = container.helpers() // every helper, keyed by nameconst { Playwright } = container.helpers() // one helperconst support = container.support() // every support objectconst { UserPage } = container.support() // one page objectconst plugins = container.plugins() // enabled pluginsconst mocha = container.mocha() // the current Mocha instanceAdd 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 },})Events
Section titled “Events”event.dispatcher is a Node EventEmitter. Attach listeners to it from a plugin or bootstrap script.
Events are sync or async:
- sync — fires the moment the action happens. Do synchronous work only.
- async — fires when the action is scheduled. To do async work in the right order, queue it with
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 object
Section titled “Test object”Test events pass a test object with these fields:
title— the test titlebody— the test function as a stringopts— test options such asretries(see test options)pending—truewhile scheduled,falseonce 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 ontest.passed,test.failed,test.finished)skipInfo— present when the test was skipped:{ message, description }
Step object
Section titled “Step object”Step events pass a step object with these fields:
name— the step name, such asseeorclickactor— the current actor, usuallyIhelper— the helper instance that executes this stephelperMethod— the helper method, usually the same asnamestatus—passedorfailedprefix— for a step inside awithinblock, the within text (e.g.Within .js-signup-form)args— the arguments passed to the step
Config
Section titled “Config”import { config } from 'codeceptjs'
config.get() // the full config objectconfig.get('myKey') // one valueconfig.get('myKey', 'fallback') // one value, with a defaultOutput
Section titled “Output”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.
Helpers and the Actor
Section titled “Helpers and the Actor”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.
Running CodeceptJS from Code
Section titled “Running CodeceptJS from Code”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