Migrating from 3.x to 4.x
CodeceptJS 4.x is a major release. It moves the codebase from CommonJS to native ESM, drops several long-deprecated helpers and plugins, replaces legacy plugins with first-class APIs, and bumps most third-party dependencies.
This guide tells you exactly what to change in your project to upgrade.
1. Update Node and Package
Section titled “1. Update Node and Package”CodeceptJS 4.x supports Node 16+, but Node 20 or newer is recommended.
npm install codeceptjs@4If you write tests in TypeScript, install tsx:
npm install --save-dev tsx4.x replaces
ts-node/esmwithtsx.ts-node/esmis no longer recommended and emits a warning.
2. Switch Your Project to ESM
Section titled “2. Switch Your Project to ESM”CodeceptJS 4.x ships as native ESM ("type": "module"). Convert your project to ESM.
Add to your package.json:
{ "type": "module"}Then convert your config, page objects, and custom helpers to ESM (sections below).
Convert Custom Helpers
Section titled “Convert Custom Helpers”3.x:
const Helper = require('@codeceptjs/helper')
class MyHelper extends Helper { doSomething() { /* ... */ }}
module.exports = MyHelper4.x:
import Helper from '@codeceptjs/helper'
class MyHelper extends Helper { doSomething() { /* ... */ }}
export default MyHelperConvert Page Objects
Section titled “Convert Page Objects”Replace module.exports = { ... } with export default { ... }.
Page objects gain new lifecycle hooks in 4.x: _before, _after, _afterSuite. They run automatically around suites that include the page object.
Convert Programmatic Usage
Section titled “Convert Programmatic Usage”3.x:
const { codecept, container, event } = require('codeceptjs')4.x:
import codeceptjs, { container, event } from 'codeceptjs'Container.create() and Config.load() are now async. Await them:
const config = await Config.load('./codecept.conf.js')await Container.create(config, opts)3. Remove Helpers That No Longer Exist
Section titled “3. Remove Helpers That No Longer Exist”| Removed helper | What to do |
|---|---|
Nightmare | Switch to Playwright, Puppeteer, or WebDriver. |
Protractor | Switch to Playwright or WebDriver. |
TestCafe | Switch to Playwright. |
AI | Use the top-level ai: config option and the new aiTrace plugin. |
SoftExpectHelper | Use the hopeThat effect instead — see below. |
Container.STANDARD_ACTING_HELPERS no longer lists TestCafe.
SoftExpectHelper → hopeThat
Section titled “SoftExpectHelper → hopeThat”3.x shipped a SoftExpectHelper (I.softAssert, I.softExpectEqual, I.flushSoftAssertions, etc.) for soft assertions. It is gone in 4.x. Use the hopeThat effect — it works with any assertion that throws (built-in I.see*, your custom helper, expect from chai/jest, Node’s assert).
3.x:
helpers: { SoftExpectHelper: {} }
// in scenarioI.softExpectEqual(user.name, 'jon')I.softExpectContain(emails, 'jon@doe.com')I.flushSoftAssertions()4.x:
import { hopeThat } from 'codeceptjs/effects'
await hopeThat(() => assert.strictEqual(user.name, 'jon'))await hopeThat(() => assert.ok(emails.includes('jon@doe.com')))hopeThat.noErrors()Each hopeThat() call records the failure as a note on the test and lets the scenario continue; hopeThat.noErrors() throws once at the end with every recorded failure if any happened. See Effects: hopeThat.
Custom Assertion Libraries
Section titled “Custom Assertion Libraries”No code changes are required for chai, expect, jest-style matchers, or Node’s assert — just import them in your test files. With noGlobals: true, they work the same as before.
Heads up on chai: 3.x pinned chai@4; 4.x devDep is chai@6, which is ESM-only and drops some legacy APIs. If you import chai in your tests, switch to import { expect } from 'chai' and verify your matchers still resolve.
4. Replace or Remove Plugins
Section titled “4. Replace or Remove Plugins”| Removed plugin | Replacement |
|---|---|
autoLogin | auth plugin — see Authorization. |
tryTo | import { tryTo } from 'codeceptjs/effects' |
retryTo | import { retryTo } from 'codeceptjs/effects' |
eachElement | import { eachElement } from 'codeceptjs/els' |
commentStep | import step from 'codeceptjs/steps' then step.section('name') / step.endSection() |
fakerTransform | Import @faker-js/faker directly in tests. |
enhancedRetryFailedStep | Merged into retryFailedStep. Rename in config. |
allure | Use @testomatio/reporter or Mochawesome. |
htmlReporter | Use an external reporter. |
wdio | Configure WebdriverIO services directly in helpers.WebDriver. |
selenoid | Run Selenoid externally. |
standardActingHelpers | No longer needed; the list lives in core. |
autoLogin → auth
Section titled “autoLogin → auth”3.x:
plugins: { autoLogin: { enabled: true, saveToFile: true, inject: 'login', users: { admin: { login, check, fetch } }, },}4.x:
plugins: { auth: { enabled: true, users: { admin: { login: (I) => { /* ... */ }, check: (I) => { /* ... */ }, }, }, },}Inject login and call login('admin') — same as before.
Renamed Plugins
Section titled “Renamed Plugins”4.x unifies four plugins (screenshot, pause, aiTrace, heal) under a shared on= parameter. The old names live on as deprecated aliases that emit a warning and forward to the new plugin.
| Old plugin | New plugin | Notes |
|---|---|---|
screenshotOnFail | screenshot | Default on='fail', same behavior |
pauseOnFail | pause | Default on='fail', same behavior |
stepByStepReport | screenshot with slides: true | Use on=step to capture every step |
New Plugins You Can Enable
Section titled “New Plugins You Can Enable”aiTrace— captures failure traces (DOM, console, network, screenshots) for AI debugging. See AI Trace.pause— pauses execution on a chosen event or on failure. See Debugging.heal— self-heals failing steps with AI; narrow withon=file|url.
5. Update Removed and Changed APIs
Section titled “5. Update Removed and Changed APIs”AI Config Now Uses Vercel AI SDK
Section titled “AI Config Now Uses Vercel AI SDK”3.x required a hand-written request function that called your provider’s SDK directly. 4.x replaces this with Vercel AI SDK — pass a model and CodeceptJS handles the calls.
Install the SDK and the provider package you want:
npm install ai @ai-sdk/openai# or @ai-sdk/anthropic, @ai-sdk/google, @ai-sdk/mistral, @ai-sdk/groq, @ai-sdk/xai, @ai-sdk/azure, @ai-sdk/cohere3.x:
ai: { request: async messages => { const OpenAI = require('openai') const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }) const completion = await openai.chat.completions.create({ model: 'gpt-3.5-turbo', messages, }) return completion?.choices[0]?.message?.content },}4.x:
import { openai } from '@ai-sdk/openai'
export default { ai: { model: openai('gpt-5'), },}The same shape works for every supported provider — swap openai('gpt-5') for anthropic('claude-sonnet-4-6'), google('gemini-1.5-flash'), etc. API keys still come from environment variables (OPENAI_API_KEY, ANTHROPIC_API_KEY, GOOGLE_GENERATIVE_AI_API_KEY, …).
The request function is no longer supported. Delete it from your config.
See Testing with AI for the full provider list and prompt customization.
JSON Schema Validation: Joi → Zod
Section titled “JSON Schema Validation: Joi → Zod”I.seeResponseMatchesJsonSchema() (from the JSONResponse helper) now validates with Zod instead of Joi. Joi is gone from the dependency tree; Zod is bundled.
Rewrite your schemas:
3.x:
const Joi = require('joi')
I.seeResponseMatchesJsonSchema(Joi.object().keys({ name: Joi.string().required(), email: Joi.string().email().required(), age: Joi.number().integer().min(0),}))4.x:
import { z } from 'zod'
I.seeResponseMatchesJsonSchema(z.object({ name: z.string(), email: z.string().email(), age: z.number().int().min(0),}))Or pass a callback that receives z:
I.seeResponseMatchesJsonSchema(z => z.object({ name: z.string(), id: z.number(),}))Common rewrites:
| Joi | Zod |
|---|---|
Joi.object().keys({...}) | z.object({...}) |
Joi.string().required() | z.string() (required by default) |
Joi.string().email() | z.string().email() |
Joi.number().integer() | z.number().int() |
Joi.array().items(...) | z.array(...) |
Joi.string().optional() | z.string().optional() |
Joi.date() | z.string().datetime() or z.date() |
Joi.alternatives().try(a, b) | z.union([a, b]) |
Uninstall joi from your project if you only used it for CodeceptJS schemas:
npm uninstall joirestart: 'browser' removed (Playwright)
Section titled “restart: 'browser' removed (Playwright)”Use one of:
restart: 'session'— reset session per test (default)restart: 'context'— new browser context per testrestart: 'keep'— keep one browser across tests
Custom Locator Strategy removed (Playwright)
Section titled “Custom Locator Strategy removed (Playwright)”The customLocators strategy registration in Playwright config is removed. Use the customLocator plugin or built-in ARIA locators ({ role: 'button', name: 'Submit' }).
I.retry() is deprecated
Section titled “I.retry() is deprecated”Use the step options API:
import step from 'codeceptjs/steps'
I.click('Submit', step.retry(3))I.fillField('Email', 'a@b.c', step.timeout(10))I.click('Add', step.opts({ elementIndex: 2 }))within Is Now an Effect
Section titled “within Is Now an Effect”In 3.x, within(...) was a global statement available everywhere. In 4.x it’s an effect alongside tryTo, retryTo, and hopeThat. Under noGlobals: true you must import it:
import { within } from 'codeceptjs/effects'
await within('.signup-form', () => { I.fillField('Email', 'a@b.c') I.click('Submit')})within returns a Promise — await it whenever you need its return value or want subsequent steps to wait for it. The same applies to session, which moved from global to a regular import (import { session } from 'codeceptjs').
Effects and Assertions Are Subpath Imports
Section titled “Effects and Assertions Are Subpath Imports”import { within, tryTo, retryTo, hopeThat } from 'codeceptjs/effects'import { hopeThat } from 'codeceptjs/assertions'import { eachElement, element, expectElement } from 'codeceptjs/els'import step from 'codeceptjs/steps'import store from 'codeceptjs/store'tryTo and hopeThat now return Promise<boolean>. The 3.x generic Promise<T | false> signature is gone.
hopeThat.noErrors() is new — call it once at the end of a scenario to fail the test if any soft assertion failed.
Globals Are Deprecated — noGlobals: true Is the New Default
Section titled “Globals Are Deprecated — noGlobals: true Is the New Default”Up to 3.x, almost everything was global: Feature, Scenario, Before, pause, within, session, secret, Helper, actor, inject, share, locate, DataTable, Given/When/Then, codecept_dir, output_dir.
In 4.x:
-
npx codeceptjs initwritesnoGlobals: trueinto new configs. -
Projects without
noGlobalsset keep the old behavior but print a deprecation warning on every run:Global functions are deprecated. Use
import { Helper, pause, within, session } from "codeceptjs"instead. SetnoGlobals: truein config to disable globals.
To silence the warning, set noGlobals: true:
export const config = { noGlobals: true, // ...}What changes when noGlobals: true:
| Symbol | With noGlobals: true |
|---|---|
Feature, Scenario, xFeature, xScenario, BeforeSuite, AfterSuite, Before, After, Background, BeforeAll, AfterAll | Still work in test files — Mocha injects these into the test context. No import needed. |
inject(), share() | Still global. No package export — keep using them as globals. |
codecept_dir, output_dir | Still global (kept for backward compatibility with external plugins). |
pause, within, session, secret, locate, dataTable, actor, codeceptjs | Import from codeceptjs. |
Helper (base class) | Import from @codeceptjs/helper. |
Given, When, Then, And, DefineParameterType (BDD step definitions) | Available as globals inside Gherkin step definition files (CodeceptJS scope-injects them while loading the step files). No import needed. |
Imports for the new style:
import { pause, within, session, secret, locate, dataTable, actor } from 'codeceptjs'import Helper from '@codeceptjs/helper'Test files written for 3.x keep working until you flip the flag.
wait* Methods Resolve Relative URLs
Section titled “wait* Methods Resolve Relative URLs”waitInUrl, waitUrlEquals, and waitCurrentPathEquals now resolve a relative path against the helper’s configured url before comparing. In 3.x a literal substring match against window.location.href would fail for relative paths.
// helpers: { Playwright: { url: 'https://app.example.com' } }
I.waitUrlEquals('/dashboard') // matches https://app.example.com/dashboardI.waitInUrl('/users') // matches any URL containing /userswaitUrlEquals error messages now include the actual URL the page was on when the wait timed out — easier to diagnose /dashboard vs /dashboard?session=expired.
6. Adopt New Behaviors
Section titled “6. Adopt New Behaviors”Strict Mode
Section titled “Strict Mode”Playwright, Puppeteer, and WebDriver helpers support strict: true. Any locator that matches more than one element throws MultipleElementsFound instead of silently picking the first match.
helpers: { Playwright: { url: '...', strict: true },}Per-step alternative: I.click('a', step.opts({ exact: true })).
The error includes a fetchDetails() method that prints XPaths and HTML for every match.
Element Index
Section titled “Element Index”Pick a specific match without writing a more specific locator:
I.click('a', step.opts({ elementIndex: 2 }))I.click('a', step.opts({ elementIndex: 'last' }))I.fillField('input', 'x', step.opts({ elementIndex: -1 }))Unfocused Element Detection
Section titled “Unfocused Element Detection”I.type() and I.pressKey() throw NonFocusedType if no element has focus. Click or focus the field first.
Context Parameter on Form Methods
Section titled “Context Parameter on Form Methods”appendField, clearField, attachFile, and moveCursorTo accept an optional second context argument, matching fillField and click.
Other New Methods
Section titled “Other New Methods”I.seeCurrentPathEquals(path)/I.dontSeeCurrentPathEquals(path)— compare the path ignoring query strings.I.waitCurrentPathEquals(path, sec?)— wait until the path matches.I.seeFileDownloaded(name)I.clickXY(locator?, x, y)— click at coordinates, either page-relative or element-relative.I.grabAriaSnapshot(locator?)— capture an accessibility-tree snapshot for the page or a region (Playwright).I.grabWebElement(locator)/I.grabWebElements(locator)— return helper-agnosticWebElementwrappers.attachFile— supports drag-and-drop dropzones.fillField— supports rich text editors (CKEditor, ProseMirror, etc.).- BDD:
Butkeyword is recognized.
CLI Plugin Arguments
Section titled “CLI Plugin Arguments”-p accepts colon-chained arguments, so plugins can be enabled and configured from the command line without editing config:
npx codeceptjs run -p pause # pause on every failurenpx codeceptjs run -p pause:on=url:pattern=/checkout/* # pause when URL matchesnpx codeceptjs run -p screenshot:on=step # screenshot every stepnpx codeceptjs run -p browser:show # force visible browsernpx codeceptjs run -p browser:browser=firefox:windowSize=1024x768npx codeceptjs run -p plugin1,plugin2:arg # multiple pluginsEach argument after the plugin name is a key=value pair. : separates pairs. ; is an inline alternative for visually grouping related pairs (e.g. path=...;line=...). Reserved keys: on, path, line, pattern.
The browser plugin is new in 4.x — it overrides the active browser helper (Playwright, Puppeteer, WebDriver, Appium) from the CLI, useful for ad-hoc local runs and CI matrices. See Commands.
The old -p all magic keyword is gone (it conflicted with the colon syntax). Enable specific plugins explicitly: -p pluginA,pluginB.
Workers: Events and Plugin Scope
Section titled “Workers: Events and Plugin Scope”Two notable changes for parallel runs:
- Event dispatcher fires inside workers. In 3.x, listeners attached to
event.dispatcheronly saw events from the main process. In 4.x, plugins and listeners observe per-test events inside each worker, so things like custom reporters and screenshot hooks work the same in single-process and worker modes (#5464). runInParent/runInMainplugin option. Set tofalseon plugins that should only run inside worker children (default istrue). Useful for plugins that aggregate per-worker state from the parent.
plugins: { myReporter: { enabled: true, runInParent: false, // only run in worker children },}TypeScript Improvements
Section titled “TypeScript Improvements”If you write tests in TypeScript, 4.x is significantly better:
tsxloader instead ofts-node/esm— faster startup, better ESM compatibility. Installtsx(optional peer dep).ts-node/esmstill works but emits a deprecation warning.- Error stack traces point at
.tssource lines, not transpiled output. __dirname/__filenameare injected for TypeScript files that use them (ESM normally hides these globals).- Path aliases from
tsconfig.json(paths) are resolved at runtime —import x from '@/utils'works without extra runtime config. codecept.conf.tssupports top-levelawaitvia dynamic imports.steps_file.tsand TypeScript support objects load correctly across files.
7. Update Dependency Versions
Section titled “7. Update Dependency Versions”If your project depends on these directly, check for breakage:
| Package | 3.x | 4.x |
|---|---|---|
chai | ^4 | ^6 (ESM-only) |
chai-as-promised | 7 | 8 (ESM-only) |
@cucumber/gherkin | 35 | 38 |
@cucumber/messages | 29 | 32 |
chokidar | 4 | 5 |
commander | 11 | 14 |
@faker-js/faker | 9 | 10 |
webdriverio | 9.12 | 9.23 |
puppeteer | 24.15 | 24.36 |
electron | 38 | 40 |
typescript | 5.8 | 5.9 |
testcafe | 3.7.2 | removed |
inquirer-test | 2.0.1 | removed |
joi | 18 | removed — use zod |
zod | — | added (^4) — schema validation in JSONResponse |
tsx | — | added as optional peer |
@modelcontextprotocol/sdk | — | added |
@testomatio/reporter | — | added |
8. New Capabilities Worth Knowing
Section titled “8. New Capabilities Worth Knowing”You don’t need these to upgrade, but they unlock new workflows:
- MCP server —
bin/mcp-server.js(also installed ascodeceptjs-mcp) exposes CodeceptJS to AI agents through Model Context Protocol. See MCP. - WebElement wrapper —
grabWebElements()returns helper-agnosticWebElementinstances with a unified API. - ARIA-first locators —
{ role: 'button', name: 'Submit' }works in Playwright, Puppeteer, and WebDriver. Theroletype is now first-class inLocator. See Locators. - Locator DSL —
locate(...)gains.withClass(),.not()negation, raw-predicate helpers, and aroleselector type. - Workers — the
eventdispatcher fires inside worker processes, so listeners and plugins observe parallel runs the same way they observe single-process runs. - Path normalization — file-path handling is normalized cross-platform; tests authored on Windows run unchanged on Linux/CI.
- Test metadata — the
Scenariocallback receives atestobject withtest.tags,test.artifacts,test.meta, andtest.notesfor custom reporting. - Security — the
emptyFolderutility (used by output cleanup) no longer shells out viarm -rf, closing a command-injection vector (#5191).
9. Verify the Upgrade
Section titled “9. Verify the Upgrade”npx codeceptjs check— surfaces config issues.npx codeceptjs run --debugon a small smoke suite. Confirm the run starts and steps execute.npx codeceptjs run --workers 2— confirm parallel execution.- TypeScript users: run with
tsxinstalled and confirm error stack traces point at.tsfiles. - If you removed
autoLogin: confirm sessions restore under theauthplugin. - If you used
tryTo/retryTo/eachElementplugins: grep your tests for the old globals and switch to subpath imports. - CI: bump the Node version to 20+ if you were on 18 or below.