CodeceptJS Fundamentals
Two jobs, in order: learn the rules below, then discover what this project has configured.
Gate
- CodeceptJS 4 is ESM/TypeScript only; tests, configs, page objects, helpers use
import/export.
- No
"type": "module" in package.json → add it before anything else.
- TypeScript: config
codecept.conf.ts, TS loader entry in require: [...].
- Project on 3.x or CommonJS (
require(), removed plugins like autoLogin, helper Nightmare) → stop, recommend migrate-codeceptjs-4. Don't patch files piecemeal — migration is whole-project.
Main rule
- Tests are written from the user's perspective: a linear scenario of actions, readable as prose.
- Good:
I.click('Login'), I.fillField('Email', ...), I.see('Welcome')
- Tests are declarative, helpers imperative — recommended layering:
- Scenario shows what the user does via
I.*; implementation details live below
- Low-level access (
this.helpers['Playwright'].page, fetch, filesystem) works fine inside a Scenario, but it's recommended to push it into a helper and expose one I.* action instead
- Keep tests short: repeated sequences → actor method, page object, or step object.
- Prefer semantic locators over selectors so tests survive markup churn.
Where things go (recommended placement)
- Site-wide actions (
login, dropdowns, rich text editors) → actor file (custom steps)
- Page/screen actions + locators → page object; SPA screen = one page object
- Site-wide widgets (nav, modals, datepickers) → page fragments / component objects
- Low-level driver access (DB connections, email, filesystem, complex mouse) → helper
- Data creation/cleanup via API → data objects (REST/GraphQL helper +
_after() cleanup), or ApiDataFactory (I.have(...))
- Don't overengineer: no page object until an abstraction is reused across tests.
Shortcuts
- Login needed →
autoLogin plugin or actor method, not inline steps per test
- Test data → create via API before the test, not through the UI
- Long test → break into several; long tests are fragile and hard to follow
- Optional UI element / conditional flow →
await tryTo(...) instead of if (await I.grab...) — keeps scenarios linear
Architecture
- Config:
codecept.conf.{js,ts,mjs,cjs} at repo root; multiple files selected via --config <file>.
- Helpers execute;
I delegates. Every I.<method> is routed to whichever active helper implements it (Playwright, WebDriver, Puppeteer, Appium share one API surface). Active helpers = keys under helpers. Tests call the actor, never the engine — backends stay swappable.
- DI container — why it exists:
- Everything shared (actor, helpers, page objects, support objects) registers under one container;
include maps names → modules
- Classes are auto-instantiated by the container — no
new, no manual wiring
inject() returns lazy proxies: destructuring at module top resolves at call time, so circular page-object references work where plain import would give undefined
- Access: destructure in Scenario signature (
Scenario('...', ({ I, loginPage }) => ...)) or const { I } = inject() once per file
- Custom helpers extend
Helper, register under helpers, add new I.* methods.
I does not exist inside a helper. Compose via this.helpers['<HelperName>'] (e.g. this.helpers['Playwright'].page, this.helpers['REST'].sendGetRequest(...)).
- Plugins are event listeners on lifecycle events (
suite.*, test.*, step.*, hook.*, multiple.*). Full list: node_modules/codeceptjs/lib/event.js. Register under plugins with enabled: true.
Config mutation trap
@codeceptjs/configure mutates resolved config at load time (setHeadlessWhen, setBrowser, ...). Static values can lie — grep for its import before trusting show: / browser: fields.
setCommonPlugins(): enables retryFailedStep + screenshot; registers (off until -p) pause, browser, aiTrace, heal.
Plugins worth knowing
retryFailedStep — retries transient step failures
screenshot — screenshots on failure; slides: true → output/records.html slideshow
pageInfo — dumps URL/HTML/console on failure
auth — session reuse for login (see codeceptjs-auth skill)
aiTrace — per-step screenshots/HTML/ARIA/console for AI debugging
pause — interactive pause
heal — AI-suggested fixes for broken steps (off in --debug)
screencast — video of the run
customLocator — maps $name prefix to team's test attribute (data-testid, data-qa)
browser — CLI-only override of browser helper config (see below)
Note: tryTo, retryTo, eachElement are not plugins in 4.x — import them from codeceptjs/effects.
Plugins from CLI
Any plugin can be enabled/reconfigured per-run with -p <plugin>, args chained with ::
npx codeceptjs run -p aiTrace # enable for this run
npx codeceptjs run -p screenshot:on=step # reconfigure inline
npx codeceptjs run -p pause:on=file:path=tests/login_test.js;line=43
screenshot, pause, aiTrace, heal share an on= trigger: fail (default except aiTrace) | step | test | file:path=...;line=N | url:pattern=<glob>
browser plugin overrides without touching config — CI matrix legs, one-off env variants:
-p browser:hide / -p browser:show / -p browser:browser=firefox / -p browser:windowSize=1280x800
- Requires
@codeceptjs/configure
Effects (codeceptjs/effects)
Flow-control functions imported from codeceptjs/effects. In 4.x these are no longer plugins/globals.
tryTo(() => ...) — runs steps that may fail without stopping the test; returns boolean.
retryTo(() => ..., maxTries, pollInterval = 200) — retries a step block until it succeeds (flaky elements, animations); callback receives the current attempt count.
hopeThat(() => ...) — soft assertions (see Assertions); end with hopeThat.noErrors().
within(locator | { frame }, fn) — scopes resolution to subtree or iframe; can return values (await). Prefer the context parameter of individual actions (I.click('Save', '.toolbar')) when possible — reserve within for genuinely scoped blocks.
All effects return Promises — await them.
Element-based API (codeceptjs/els)
Hybrid style: mix I.* with direct element access. Import { element, eachElement, expectElement, expectAnyElement, expectAllElements } from 'codeceptjs/els'.
element(locator, async el => { ... }) — scoped access to one element; chain el.$(locator) into children without re-querying
eachElement(locator, async (el, index) => ...) — iterate collections
expectElement / expectAnyElement / expectAllElements(locator, fn) — custom conditions
- Elements are
WebElement wrappers — same API on all helpers: getText(), getAttribute(), isVisible(), isEnabled(), getBoundingBox(), exists(), $$()
- Optional purpose string improves debug logs:
element('verify discount applied', '.price', ...)
- Use when built-ins don't cover it: collections, layout checks (
getBoundingBox), per-element loops, chaining ops on one element. Prefer I.* for readability otherwise.
Writing tests
- Structure: one
Feature(...) per file, one or more Scenario(...) inside. No nested suites, no multiple Features per file.
- Hooks:
Before, After, BeforeSuite, AfterSuite, Fail(...).
- Page object lifecycle hooks:
_before() (lazy, once per test, on first use), _after() (skipped if unused), _beforeSuite(), _afterSuite().
await required for: grab* methods, imported functions, page-object methods containing async ops (elsewhere: unhandled rejections). Never for plain action steps — the recorder chains them.
- Secrets:
I.fillField('Password', secret(process.env.PASSWORD)) — masks logs, traces, AI prompts. Import from codeceptjs.
- Sessions:
session(name, fn) — parallel browser context for multi-user Scenarios (chat, multi-tenant).
Locators
- ARIA locators are strongest — resilient to CSS refactors, describe what the user sees:
I.click({ role: 'button', name: 'Save' })
- Actions accept plain strings (visible text, label, placeholder,
name, aria-label) or objects ({ css }, { xpath }, { id }).
- Plain string already matches
aria-label — no 'aria-label=...' prefix needed.
- Pass context as last argument — scoped semantic locator beats long unscoped one:
I.click('Save', '.toolbar') not I.click('#toolbar .btn-save')
- Avoid style-based class names (
.bg-green); prefer semantic ones (.btn-save).
data-testid/data-qa apps → enable customLocator, write $name.
- No semantic name fits →
locate(...) builder (.withClass, .withText, .inside, .and): locate('.button').withText('Click me').
Waiting
- Action steps auto-wait for existence + interactability. Add explicit
waitFor* only when the condition isn't tied to an interaction (modal after network call, spinner hiding).
- Avoid
I.wait(N) — last resort.
Assertions
Built-in browser assertions come first: I.see, I.seeTextEquals, I.seeElement, I.seeInField, I.seeNumberOfElements, I.seeInCurrentUrl (+ dontSee* counterparts). Clear failures, recorder-integrated. see matches visible text; hidden DOM content needs seeInSource / seeElementInDOM.
For what built-ins don't cover, in order of preference:
- Reusable custom assertion in a helper —
I.seeTableIsOrdered('Price', 'desc'); name positives see*, negatives dontSee*; use codeceptjs/assertions inside, never raw throw new Error()
- ExpectHelper (
@codeceptjs/expect-helper) — chai matchers on I: I.expectEqual, I.expectDeepEqualExcluding, I.expectMatchesPattern, I.expectJsonSchema; appears in step log like other steps
codeceptjs/assertions directly — dependency-free factories: equals(subject).assert(actual, expected) / .negate(...); failure messages match I.see formatting
- Any library on grabbed data (
grab* always needs await) — chai/jest/node:assert; fails the test but won't show as a step
Soft assertions: hopeThat(() => I.see(...)) from codeceptjs/effects — logs each failure and continues; end with hopeThat.noErrors() to fail if any were recorded.
Parallel runs
run-workers <N> — splits Scenarios across worker threads
run-multiple <profile> — profiles via multiple block in config (browsers, viewports)
Config organization (recommended)
- Multiple config files per environment (
codecept.conf.js, codecept.ci.conf.js, ...); share parts via modules in a config/ dir
.env files + dotenv for secrets/env-specific values
- Bulk-register page objects/components by spreading exported maps into
include
- Pass data from config/bootstrap into tests via
codeceptjs.container.append({ testUser }) — injectable by name
Discover this project
In order; skipping steps produces wrong guesses:
- Verify setup loads:
npx codeceptjs check -c <config> — validates everything; output doubles as inventory. Fix failures before continuing.
- Read the active config: helpers (+ browser/baseURL/viewport/env-driven values), plugins (incl. anything
setCommonPlugins() injects), AI provider + required env var, env selection mechanism, page objects from include, custom helpers.
- List actions:
npx codeceptjs list -c <config> (--docs adds JSDoc; --action <name> for one). The actual I.* surface differs from built-ins when custom helpers exist — always check before suggesting a method.
- List tests:
npx codeceptjs dry-run -c <config> — --steps shows queued actions, --grep filters, --numbers gives per-test step indices matching MCP pauseAt.
- ⚠
dry-run --grep and run --grep do not select the same set (4.1.0): run --grep matches Feature + Scenario, dry-run --grep matches the Scenario title only. dry-run --grep 'Dialogs' lists 0 tests where run --grep 'Dialogs' executes all 11. Never size a run from a dry-run's grep, and target a whole Feature by file path (run tests/foo_test.ts) when the selection must be exact.
Gherkin projects: npx codeceptjs gherkin:steps -c <config>.
Reference docs live under node_modules/codeceptjs/docs/ — read them instead of guessing APIs.
Report
Short prose summary. Must include:
- Env-driven values flagged as env-driven (
process.env.BROWSER || 'chromium', not just 'chromium')
- Conflicts flagged (static
show: true vs setHeadlessWhen(CI); auth configured but credential env vars missing)
- No config at root and no
--config referenced → recommend npx codeceptjs init ., stop
- 3.x/CommonJS detected → recommend
migrate-codeceptjs-4, stop
1---2name: codeceptjs-fundamentals3description: Run first when working with any CodeceptJS 4 project — before writing, debugging, refactoring, or migrating tests. Teaches the framework's non-obvious rules and runs four-step discovery (`check` → read config → `list` → `dry-run`) reporting which helpers, plugins, page objects, custom actions, and tests are active. Other CodeceptJS skills depend on this output.4---56# CodeceptJS Fundamentals78Two jobs, in order: learn the rules below, then discover what *this* project has configured.910## Gate1112- CodeceptJS 4 is **ESM/TypeScript only**; tests, configs, page objects, helpers use `import`/`export`.13- No `"type": "module"` in package.json → add it before anything else.14- TypeScript: config `codecept.conf.ts`, TS loader entry in `require: [...]`.15- Project on 3.x or CommonJS (`require()`, removed plugins like `autoLogin`, helper `Nightmare`) → stop, recommend `migrate-codeceptjs-4`. Don't patch files piecemeal — migration is whole-project.1617## Main rule1819- Tests are written from the user's perspective: a linear scenario of actions, readable as prose.20 - Good: `I.click('Login')`, `I.fillField('Email', ...)`, `I.see('Welcome')`21- **Tests are declarative, helpers imperative — recommended layering:**22 - Scenario shows *what* the user does via `I.*`; implementation details live below23 - Low-level access (`this.helpers['Playwright'].page`, fetch, filesystem) works fine inside a Scenario, but it's recommended to push it into a helper and expose one `I.*` action instead24- Keep tests short: repeated sequences → actor method, page object, or step object.25- Prefer semantic locators over selectors so tests survive markup churn.2627## Where things go (recommended placement)2829- Site-wide actions (`login`, dropdowns, rich text editors) → **actor file** (custom steps)30- Page/screen actions + locators → **page object**; SPA screen = one page object31- Site-wide widgets (nav, modals, datepickers) → **page fragments / component objects**32- Low-level driver access (DB connections, email, filesystem, complex mouse) → **helper**33- Data creation/cleanup via API → **data objects** (REST/GraphQL helper + `_after()` cleanup), or `ApiDataFactory` (`I.have(...)`)34- Don't overengineer: no page object until an abstraction is reused across tests.3536## Shortcuts3738- Login needed → `autoLogin` plugin or actor method, not inline steps per test39- Test data → create via API before the test, not through the UI40- Long test → break into several; long tests are fragile and hard to follow41- Optional UI element / conditional flow → `await tryTo(...)` instead of `if (await I.grab...)` — keeps scenarios linear4243## Architecture4445- **Config**: `codecept.conf.{js,ts,mjs,cjs}` at repo root; multiple files selected via `--config <file>`.46- **Helpers execute; `I` delegates.** Every `I.<method>` is routed to whichever active helper implements it (Playwright, WebDriver, Puppeteer, Appium share one API surface). Active helpers = keys under `helpers`. Tests call the actor, never the engine — backends stay swappable.47- **DI container** — why it exists:48 - Everything shared (actor, helpers, page objects, support objects) registers under one container; `include` maps names → modules49 - Classes are auto-instantiated by the container — no `new`, no manual wiring50 - **`inject()` returns lazy proxies**: destructuring at module top resolves at call time, so circular page-object references work where plain `import` would give `undefined`51 - Access: destructure in Scenario signature (`Scenario('...', ({ I, loginPage }) => ...)`) or `const { I } = inject()` once per file52- **Custom helpers** extend `Helper`, register under `helpers`, add new `I.*` methods.53 - **`I` does not exist inside a helper.** Compose via `this.helpers['<HelperName>']` (e.g. `this.helpers['Playwright'].page`, `this.helpers['REST'].sendGetRequest(...)`).54- **Plugins** are event listeners on lifecycle events (`suite.*`, `test.*`, `step.*`, `hook.*`, `multiple.*`). Full list: `node_modules/codeceptjs/lib/event.js`. Register under `plugins` with `enabled: true`.5556## Config mutation trap5758- `@codeceptjs/configure` mutates resolved config at load time (`setHeadlessWhen`, `setBrowser`, ...). Static values can lie — grep for its import before trusting `show:` / `browser:` fields.59- `setCommonPlugins()`: **enables** `retryFailedStep` + `screenshot`; **registers** (off until `-p`) `pause`, `browser`, `aiTrace`, `heal`.6061## Plugins worth knowing6263- `retryFailedStep` — retries transient step failures64- `screenshot` — screenshots on failure; `slides: true` → `output/records.html` slideshow65- `pageInfo` — dumps URL/HTML/console on failure66- `auth` — session reuse for login (see `codeceptjs-auth` skill)67- `aiTrace` — per-step screenshots/HTML/ARIA/console for AI debugging68- `pause` — interactive pause69- `heal` — AI-suggested fixes for broken steps (off in `--debug`)70- `screencast` — video of the run71- `customLocator` — maps `$name` prefix to team's test attribute (`data-testid`, `data-qa`)72- `browser` — CLI-only override of browser helper config (see below)7374Note: `tryTo`, `retryTo`, `eachElement` are not plugins in 4.x — import them from `codeceptjs/effects`.7576## Plugins from CLI7778Any plugin can be enabled/reconfigured per-run with `-p <plugin>`, args chained with `:`:7980```sh81npx codeceptjs run -p aiTrace # enable for this run82npx codeceptjs run -p screenshot:on=step # reconfigure inline83npx codeceptjs run -p pause:on=file:path=tests/login_test.js;line=4384```8586- `screenshot`, `pause`, `aiTrace`, `heal` share an `on=` trigger: `fail` (default except aiTrace) | `step` | `test` | `file:path=...;line=N` | `url:pattern=<glob>`87- `browser` plugin overrides without touching config — CI matrix legs, one-off env variants:88 - `-p browser:hide` / `-p browser:show` / `-p browser:browser=firefox` / `-p browser:windowSize=1280x800`89 - Requires `@codeceptjs/configure`9091## Effects (`codeceptjs/effects`)9293Flow-control functions imported from `codeceptjs/effects`. In 4.x these are no longer plugins/globals.9495- **`tryTo(() => ...)`** — runs steps that may fail without stopping the test; returns `boolean`.96 - **Prefer `tryTo` over `if`:** scenarios should stay linear — instead of branching on a grabbed value to decide whether a UI state exists, attempt the optional steps and branch on the boolean result:97 ```js98 const banner = await tryTo(() => { I.see('Cookie banner'); I.click('Accept cookies') })99 if (!banner) I.say('No cookie banner')100 ```101 - Auto-retries are disabled inside `tryTo` blocks.102- **`retryTo(() => ..., maxTries, pollInterval = 200)`** — retries a step block until it succeeds (flaky elements, animations); callback receives the current attempt count.103- **`hopeThat(() => ...)`** — soft assertions (see Assertions); end with `hopeThat.noErrors()`.104- **`within(locator | { frame }, fn)`** — scopes resolution to subtree or iframe; can return values (`await`). Prefer the context parameter of individual actions (`I.click('Save', '.toolbar')`) when possible — reserve `within` for genuinely scoped blocks.105106All effects return Promises — `await` them.107108## Element-based API (`codeceptjs/els`)109110Hybrid style: mix `I.*` with direct element access. Import `{ element, eachElement, expectElement, expectAnyElement, expectAllElements } from 'codeceptjs/els'`.111112- `element(locator, async el => { ... })` — scoped access to one element; chain `el.$(locator)` into children without re-querying113- `eachElement(locator, async (el, index) => ...)` — iterate collections114- `expectElement` / `expectAnyElement` / `expectAllElements(locator, fn)` — custom conditions115- Elements are `WebElement` wrappers — same API on all helpers: `getText()`, `getAttribute()`, `isVisible()`, `isEnabled()`, `getBoundingBox()`, `exists()`, `$$()`116- Optional purpose string improves debug logs: `element('verify discount applied', '.price', ...)`117- Use when built-ins don't cover it: collections, layout checks (`getBoundingBox`), per-element loops, chaining ops on one element. Prefer `I.*` for readability otherwise.118119## Writing tests120121- Structure: one `Feature(...)` per file, one or more `Scenario(...)` inside. No nested suites, no multiple Features per file.122- Hooks: `Before`, `After`, `BeforeSuite`, `AfterSuite`, `Fail(...)`.123- Page object lifecycle hooks: `_before()` (lazy, once per test, on first use), `_after()` (skipped if unused), `_beforeSuite()`, `_afterSuite()`.124- **`await` required for**: `grab*` methods, imported functions, page-object methods containing async ops (elsewhere: unhandled rejections). Never for plain action steps — the recorder chains them.125- Secrets: `I.fillField('Password', secret(process.env.PASSWORD))` — masks logs, traces, AI prompts. Import from `codeceptjs`.126- Sessions: `session(name, fn)` — parallel browser context for multi-user Scenarios (chat, multi-tenant).127128## Locators129130- **ARIA locators are strongest** — resilient to CSS refactors, describe what the user sees:131 - `I.click({ role: 'button', name: 'Save' })`132- Actions accept plain strings (visible text, label, placeholder, `name`, `aria-label`) or objects (`{ css }`, `{ xpath }`, `{ id }`).133- Plain string already matches `aria-label` — no `'aria-label=...'` prefix needed.134- **Pass context as last argument** — scoped semantic locator beats long unscoped one:135 - `I.click('Save', '.toolbar')` not `I.click('#toolbar .btn-save')`136- Avoid style-based class names (`.bg-green`); prefer semantic ones (`.btn-save`).137- `data-testid`/`data-qa` apps → enable `customLocator`, write `$name`.138- No semantic name fits → `locate(...)` builder (`.withClass`, `.withText`, `.inside`, `.and`): `locate('.button').withText('Click me')`.139140## Waiting141142- Action steps auto-wait for existence + interactability. Add explicit `waitFor*` only when the condition isn't tied to an interaction (modal after network call, spinner hiding).143- Avoid `I.wait(N)` — last resort.144145## Assertions146147Built-in browser assertions come first: `I.see`, `I.seeTextEquals`, `I.seeElement`, `I.seeInField`, `I.seeNumberOfElements`, `I.seeInCurrentUrl` (+ `dontSee*` counterparts). Clear failures, recorder-integrated. `see` matches *visible* text; hidden DOM content needs `seeInSource` / `seeElementInDOM`.148149For what built-ins don't cover, in order of preference:1501511. **Reusable custom assertion** in a helper — `I.seeTableIsOrdered('Price', 'desc')`; name positives `see*`, negatives `dontSee*`; use `codeceptjs/assertions` inside, never raw `throw new Error()`1522. **ExpectHelper** (`@codeceptjs/expect-helper`) — chai matchers on `I`: `I.expectEqual`, `I.expectDeepEqualExcluding`, `I.expectMatchesPattern`, `I.expectJsonSchema`; appears in step log like other steps1533. **`codeceptjs/assertions`** directly — dependency-free factories: `equals(subject).assert(actual, expected)` / `.negate(...)`; failure messages match `I.see` formatting1544. **Any library** on grabbed data (`grab*` always needs `await`) — chai/jest/`node:assert`; fails the test but won't show as a step155156Soft assertions: `hopeThat(() => I.see(...))` from `codeceptjs/effects` — logs each failure and continues; end with `hopeThat.noErrors()` to fail if any were recorded.157158## Parallel runs159160- `run-workers <N>` — splits Scenarios across worker threads161- `run-multiple <profile>` — profiles via `multiple` block in config (browsers, viewports)162163## Config organization (recommended)164165- Multiple config files per environment (`codecept.conf.js`, `codecept.ci.conf.js`, ...); share parts via modules in a `config/` dir166- `.env` files + `dotenv` for secrets/env-specific values167- Bulk-register page objects/components by spreading exported maps into `include`168- Pass data from config/bootstrap into tests via `codeceptjs.container.append({ testUser })` — injectable by name169170## Discover this project171172In order; skipping steps produces wrong guesses:1731741. **Verify setup loads**: `npx codeceptjs check -c <config>` — validates everything; output doubles as inventory. Fix failures before continuing.1752. **Read the active config**: helpers (+ browser/baseURL/viewport/env-driven values), plugins (incl. anything `setCommonPlugins()` injects), AI provider + required env var, env selection mechanism, page objects from `include`, custom helpers.1763. **List actions**: `npx codeceptjs list -c <config>` (`--docs` adds JSDoc; `--action <name>` for one). The actual `I.*` surface differs from built-ins when custom helpers exist — always check before suggesting a method.1774. **List tests**: `npx codeceptjs dry-run -c <config>` — `--steps` shows queued actions, `--grep` filters, `--numbers` gives per-test step indices matching MCP `pauseAt`.178 - ⚠ `dry-run --grep` and `run --grep` do **not** select the same set (4.1.0): `run --grep` matches `Feature` + `Scenario`, `dry-run --grep` matches the Scenario title only. `dry-run --grep 'Dialogs'` lists 0 tests where `run --grep 'Dialogs'` executes all 11. Never size a run from a dry-run's grep, and target a whole Feature by file path (`run tests/foo_test.ts`) when the selection must be exact.179180Gherkin projects: `npx codeceptjs gherkin:steps -c <config>`.181182Reference docs live under `node_modules/codeceptjs/docs/` — read them instead of guessing APIs.183184## Report185186Short prose summary. Must include:187188- Env-driven values flagged as env-driven (`process.env.BROWSER || 'chromium'`, not just `'chromium'`)189- Conflicts flagged (static `show: true` vs `setHeadlessWhen(CI)`; `auth` configured but credential env vars missing)190- No config at root and no `--config` referenced → recommend `npx codeceptjs init .`, stop191- 3.x/CommonJS detected → recommend `migrate-codeceptjs-4`, stop