Skip to content

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.

CodeceptJS 4.x supports Node 16+, but Node 20 or newer is recommended.

Terminal window
npm install codeceptjs@4

If you write tests in TypeScript, install tsx:

Terminal window
npm install --save-dev tsx

4.x replaces ts-node/esm with tsx. ts-node/esm is no longer recommended and emits a warning.

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).

3.x:

const Helper = require('@codeceptjs/helper')
class MyHelper extends Helper {
doSomething() { /* ... */ }
}
module.exports = MyHelper

4.x:

import Helper from '@codeceptjs/helper'
class MyHelper extends Helper {
doSomething() { /* ... */ }
}
export default MyHelper

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.

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)
Removed helperWhat to do
NightmareSwitch to Playwright, Puppeteer, or WebDriver.
ProtractorSwitch to Playwright or WebDriver.
TestCafeSwitch to Playwright.
AIUse the top-level ai: config option and the new aiTrace plugin.
SoftExpectHelperUse the hopeThat effect instead — see below.

Container.STANDARD_ACTING_HELPERS no longer lists TestCafe.

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 scenario
I.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.

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.

Removed pluginReplacement
autoLoginauth plugin — see Authorization.
tryToimport { tryTo } from 'codeceptjs/effects'
retryToimport { retryTo } from 'codeceptjs/effects'
eachElementimport { eachElement } from 'codeceptjs/els'
commentStepimport step from 'codeceptjs/steps' then step.section('name') / step.endSection()
fakerTransformImport @faker-js/faker directly in tests.
enhancedRetryFailedStepMerged into retryFailedStep. Rename in config.
allureUse @testomatio/reporter or Mochawesome.
htmlReporterUse an external reporter.
wdioConfigure WebdriverIO services directly in helpers.WebDriver.
selenoidRun Selenoid externally.
standardActingHelpersNo longer needed; the list lives in core.

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.

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 pluginNew pluginNotes
screenshotOnFailscreenshotDefault on='fail', same behavior
pauseOnFailpauseDefault on='fail', same behavior
stepByStepReportscreenshot with slides: trueUse on=step to capture every step
  • 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 with on=file|url.

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:

Terminal window
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/cohere

3.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.

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:

JoiZod
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:

Terminal window
npm uninstall joi

Use one of:

  • restart: 'session' — reset session per test (default)
  • restart: 'context' — new browser context per test
  • restart: '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' }).

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 }))

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 init writes noGlobals: true into new configs.

  • Projects without noGlobals set 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. Set noGlobals: true in config to disable globals.

To silence the warning, set noGlobals: true:

codecept.conf.js
export const config = {
noGlobals: true,
// ...
}

What changes when noGlobals: true:

SymbolWith noGlobals: true
Feature, Scenario, xFeature, xScenario, BeforeSuite, AfterSuite, Before, After, Background, BeforeAll, AfterAllStill 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_dirStill global (kept for backward compatibility with external plugins).
pause, within, session, secret, locate, dataTable, actor, codeceptjsImport 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.

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/dashboard
I.waitInUrl('/users') // matches any URL containing /users

waitUrlEquals error messages now include the actual URL the page was on when the wait timed out — easier to diagnose /dashboard vs /dashboard?session=expired.

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.

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 }))

I.type() and I.pressKey() throw NonFocusedType if no element has focus. Click or focus the field first.

appendField, clearField, attachFile, and moveCursorTo accept an optional second context argument, matching fillField and click.

  • 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-agnostic WebElement wrappers.
  • attachFile — supports drag-and-drop dropzones.
  • fillField — supports rich text editors (CKEditor, ProseMirror, etc.).
  • BDD: But keyword is recognized.

-p accepts colon-chained arguments, so plugins can be enabled and configured from the command line without editing config:

Terminal window
npx codeceptjs run -p pause # pause on every failure
npx codeceptjs run -p pause:on=url:pattern=/checkout/* # pause when URL matches
npx codeceptjs run -p screenshot:on=step # screenshot every step
npx codeceptjs run -p browser:show # force visible browser
npx codeceptjs run -p browser:browser=firefox:windowSize=1024x768
npx codeceptjs run -p plugin1,plugin2:arg # multiple plugins

Each 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.

Two notable changes for parallel runs:

  • Event dispatcher fires inside workers. In 3.x, listeners attached to event.dispatcher only 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 / runInMain plugin option. Set to false on plugins that should only run inside worker children (default is true). Useful for plugins that aggregate per-worker state from the parent.
plugins: {
myReporter: {
enabled: true,
runInParent: false, // only run in worker children
},
}

If you write tests in TypeScript, 4.x is significantly better:

  • tsx loader instead of ts-node/esm — faster startup, better ESM compatibility. Install tsx (optional peer dep). ts-node/esm still works but emits a deprecation warning.
  • Error stack traces point at .ts source lines, not transpiled output.
  • __dirname / __filename are 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.ts supports top-level await via dynamic imports.
  • steps_file.ts and TypeScript support objects load correctly across files.

If your project depends on these directly, check for breakage:

Package3.x4.x
chai^4^6 (ESM-only)
chai-as-promised78 (ESM-only)
@cucumber/gherkin3538
@cucumber/messages2932
chokidar45
commander1114
@faker-js/faker910
webdriverio9.129.23
puppeteer24.1524.36
electron3840
typescript5.85.9
testcafe3.7.2removed
inquirer-test2.0.1removed
joi18removed — use zod
zodadded (^4) — schema validation in JSONResponse
tsxadded as optional peer
@modelcontextprotocol/sdkadded
@testomatio/reporteradded

You don’t need these to upgrade, but they unlock new workflows:

  • MCP serverbin/mcp-server.js (also installed as codeceptjs-mcp) exposes CodeceptJS to AI agents through Model Context Protocol. See MCP.
  • WebElement wrappergrabWebElements() returns helper-agnostic WebElement instances with a unified API.
  • ARIA-first locators{ role: 'button', name: 'Submit' } works in Playwright, Puppeteer, and WebDriver. The role type is now first-class in Locator. See Locators.
  • Locator DSLlocate(...) gains .withClass(), .not() negation, raw-predicate helpers, and a role selector type.
  • Workers — the event dispatcher 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 Scenario callback receives a test object with test.tags, test.artifacts, test.meta, and test.notes for custom reporting.
  • Security — the emptyFolder utility (used by output cleanup) no longer shells out via rm -rf, closing a command-injection vector (#5191).
  1. npx codeceptjs check — surfaces config issues.
  2. npx codeceptjs run --debug on a small smoke suite. Confirm the run starts and steps execute.
  3. npx codeceptjs run --workers 2 — confirm parallel execution.
  4. TypeScript users: run with tsx installed and confirm error stack traces point at .ts files.
  5. If you removed autoLogin: confirm sessions restore under the auth plugin.
  6. If you used tryTo / retryTo / eachElement plugins: grep your tests for the old globals and switch to subpath imports.
  7. CI: bump the Node version to 20+ if you were on 18 or below.