docs: 4.x migration guide and ESM-only code samples#5538
Merged
Conversation
- Add docs/migration-4.md covering ESM switch, removed helpers (Nightmare/Protractor/TestCafe/AI/SoftExpect), removed plugins (autoLogin/tryTo/retryTo/eachElement/allure/htmlReporter/wdio/etc.), Vercel AI SDK, Joi → Zod, restart=browser removal, Custom Locator Strategy removal, within → effect, noGlobals: true default, wait* relative URL resolution, strict mode, elementIndex, CLI plugin args, workers events, TypeScript loader changes. - Document hopeThat (soft assertions) in docs/effects.md as the replacement for SoftExpectHelper; clarify it works with any assertion library. - Convert ~70 CJS code samples across 19 docs to ESM (require/module.exports/exports.config → import/export default). - Fix dynamic require in pageobjects.md → static import. - Update Anthropic example in ai.md to claude-sonnet-4-6 and OpenAI example to gpt-5. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- data.md: faker is a named export; restore `import { faker }`
and update the install hint from the legacy `faker` package
to `@faker-js/faker`.
- internal-api.md: lead with the named-import form and demote
the `codeceptjs` global to a footnote (it only exists when
`noGlobals: false`).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* `@codeceptjs/configure` and `@codeceptjs/expect-helper` both do
`require('codeceptjs')` from inside their own packages. End users
installing codeceptjs into a project's node_modules resolve fine —
but in this repo (where the project IS codeceptjs) Node has nothing
at node_modules/codeceptjs to resolve to, so those imports throw and
helper/plugin loading fails on every CI run. Add `npm link && npm
link codeceptjs` after install in playwright/puppeteer/webdriver/
plugin/dtslint/test workflows. Documented in CLAUDE.md.
* `lib/plugin/browser.js`: route through `setBrowserConfig` from
`@codeceptjs/configure` instead of duplicating the helper-mutation
logic inline. The configure dep is the right primitive — removing
the duplicate, single source of truth.
* `typings/index.d.ts`: drop `/// <reference types="joi" />`. joi is
not used in any public type, the directive was leftover from a prior
incarnation, and it broke `dtslint` (which looks for `@types/joi`,
doesn't exist) and the def-runner test (failed-resolution alignment
produced an undefined `resolutionDiagnostics` access in newer TS).
* Docs: `docs/configuration.md` setCommonPlugins table aligned with
4.x plugin set (no eachElement, pauseOn instead of pauseOnFail);
`docs/plugins.md` regen with aiTrace section.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* `@codeceptjs/configure` is now imported dynamically. If it's not
installed (e.g. user pinned a stripped-down dep set), the plugin
prints a one-line hint and skips the override instead of crashing.
* Arg parsing rewritten as small composable functions: `parseArgs` →
`parseArg` → `parseValue`. Uses `String.split('=')` instead of
manual `indexOf`/`slice`. The hot loop is now a `reduce`.
* Drop number coercion — values stay as strings; helpers (and
setBrowserConfig's regex parsers) coerce as needed. `true`/`false`
still become real booleans for boolean-typed helper options.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both `@codeceptjs/configure@^4.0.0-beta.2` and `@codeceptjs/expect-helper@^4.0.0-beta.3` are now native ESM with codeceptjs declared as an optional peer dep, matching CodeceptJS 4.x. Caret ranges pick up future 4.x betas, RCs, and the eventual stable release without further package.json edits. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reframe semantic locators (`I.click('Save', '.header')`) as the
recommended default for stable scenarios, not a prototyping
shortcut. Combined with a context, they read like prose, survive
ARIA/CSS refactors, and disambiguate duplicate labels — so they're
more precise than ARIA or CSS used alone.
- Intro: lead with the "semantic + context" recipe.
- Locator-types table: split semantic into "with context" (default)
and "no context" (unique label / prototyping); document the
combined pattern as a first-class type.
- Semantic section: front-load the "pair with a context" guidance
and drop the "switch to strict locators once stable" line.
- Context section: explain why scoping every action is the
default, not the special case.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The workaround was needed only while @codeceptjs/configure and
@codeceptjs/expect-helper did `require('codeceptjs')` at load time.
After the configure import in lib/plugin/browser.js was made
optional and @codeceptjs/expect-helper was pinned to ^4.0.0-beta.3,
nothing in the repo's runtime depends on a self-resolving
node_modules/codeceptjs entry — `npm link` is dead code.
- Remove the `- run: npm link && npm link codeceptjs` step (and
surrounding comment block) from dtslint, playwright, plugin,
puppeteer, webdriver workflows, and both jobs in test.yml.
- Drop the matching "Local Development Setup" section from CLAUDE.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…kage
@codeceptjs/configure and @codeceptjs/expect-helper both do top-level
`import 'codeceptjs'`. Both passing through the test config (e.g. the
`Expect: { require: '@codeceptjs/expect-helper' }` line in the
acceptance configs) means CodeceptJS itself can't start in CI without
node_modules/codeceptjs resolving — and the runtime deps load before
any helper or test does, so disabling `codecept check` alone wasn't
enough.
Replace the previous `npm link && npm link codeceptjs` workaround with
a single `ln -sfn .. node_modules/codeceptjs`. One symlink, no global
npm state.
Workflows updated: test, dtslint, playwright, puppeteer, webdriver, plugin.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@codeceptjs/configure and @codeceptjs/expect-helper currently do top-level
\`import 'codeceptjs'\`, which can't resolve inside this repo's CI (the
project IS the codeceptjs package, so npm doesn't drop a node_modules
entry to resolve to). The CI symlink works around it; the proper fix is
to give those packages an in-process handle so they don't need to look
up codeceptjs through Node's bare-specifier resolution.
This change is the codeceptjs-side prep:
- lib/host.js — sets globalThis.codeceptjs = { config, container, event,
output, recorder, Helper } as a side-effect on import. Idempotent;
matches the global @codeceptjs/helper already consults
(\`global.codeceptjs || require('codeceptjs')\`).
- lib/codecept.js + lib/plugin/browser.js — eager import of host.js so
the registry is populated before the user's codecept.conf.js loads
any companion package.
Once @codeceptjs/configure and @codeceptjs/expect-helper ship versions
that read from globalThis.codeceptjs (instead of \`import 'codeceptjs'\`),
the CI symlink can be removed. Drop-in replacements for both packages
are in scripts/upstream-patches/ — apply them upstream and bump versions
in package.json.
End-user projects are unaffected: globalThis.codeceptjs is set in normal
runs too, so the new code path is the same code path everyone takes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…This @codeceptjs/configure@4.0.0-beta.4 and @codeceptjs/expect-helper@4.0.0-beta.5 no longer do top-level `import 'codeceptjs'` — they read from the globalThis.codeceptjs registry the runner sets up in lib/host.js. The self-symlink that fed them a resolvable codeceptjs package in CI is no longer needed. - Bump @codeceptjs/configure to ^4.0.0-beta.4 - Bump @codeceptjs/expect-helper to ^4.0.0-beta.5 - Remove the `ln -sfn .. node_modules/codeceptjs` step from dtslint, playwright, plugin, puppeteer, webdriver, and test workflows - Delete scripts/upstream-patches/ — the patches it documented are now released in the companion packages Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The old API exposed two coupled primitives — hooksCount() / runHooksFrom(N) —
that pushed positional bookkeeping onto the caller. Container.create snapshotted
the count before plugin boot and replayed everything past that index after.
Brittle: any future change that reorders or removes hooks would silently
re-run the wrong ones.
Replace with a per-hook { fn, ran } record and a single runPendingHooks(cfg)
that fires anything not yet applied to the current config. Caller no longer
deals with indices:
if (Config.runPendingHooks(config)) {
// re-feed helper config…
}
Behavior is unchanged for the existing flow:
- create() rebuilds config and re-runs every hook (marks all ran).
- A hook registered after create() is pending until runPendingHooks() picks it up.
- Hooks registered while runPendingHooks() is running are picked up in the
same pass (loop re-checks length each iteration).
Internal API only — no other call sites in the repo or test suite.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
lib/host.js was a separate module that did one thing: assign
globalThis.codeceptjs = { config, container, event, output, recorder, Helper }.
Imported as a side effect from lib/codecept.js (the runner) and
lib/plugin/browser.js (so the browser-plugin unit test would also get
the registry, since it imports the plugin directly without going through
the runner).
The indirection wasn't earning its keep — and it read like we were
quietly re-introducing the user-facing globals we just deprecated.
- Inline the assignment at the top of lib/codecept.js. The runner is the
one place that should own this; everything that goes through the CLI
hits this module first.
- Drop the import from lib/plugin/browser.js. Plugins shouldn't be
responsible for installing framework registries.
- The browser-plugin unit test bypasses the runner, so it now installs
globalThis.codeceptjs in its setup — same pattern we use in the
@codeceptjs/configure test suite.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…t setup Same idea as codeceptjs's own \`config.require\` option: list a setup script in mocha's config and let it run once before any test. The unit-test world is the only place that needs to fake \`globalThis.codeceptjs\` (unit tests bypass the runner that normally installs it), so park the five lines there and stop sprinkling them into individual test files. - test/support/setup.mjs — was just chai.should(); now also installs the framework registry (Config, container, event, output, recorder, Helper) on globalThis.codeceptjs the same way lib/codecept.js does for the live runner. - .mocharc.mjs → .mocharc.cjs — mocha's config auto-discovery is inconsistent with .mocharc.mjs across CLI shapes (recursive runs picked it up, single-file runs didn't). The CJS form auto-loads reliably whether you point mocha at a directory or a single file. The exposed config is unchanged: \`require\` + \`extension\`. - test/unit/plugin/browser_test.js — drop the per-test inline \`globalThis.codeceptjs = ...\`; setup.mjs now covers it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Array-of-object-literal form /** @type {Array<{ fn: (cfg: object) => void, ran: boolean }>} */ is invalid JSDoc syntax for the version of jsdoc this repo's `npm run def` runs through (it doesn't parse object-literal type expressions inside generic params). Result: jsdoc bailed, def generation failed, both "test (20.x)" and the runner test that calls `npm run def` went red. The annotation was decoration only — the hook list is internal state. Replace with a plain comment, ship the doc-regen of plugins.md the def task produced. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…yHook Three changes asked for on the previous hook refactor: - Replace the for-i loops in Config.create and Config.runPendingHooks with for-of. Both rely on Array iterator semantics — newly pushed hooks (e.g. a hook that registers another hook) are still picked up in the same pass because the iterator re-checks length on each step. - Centralize hook execution in a single applyHook() helper that wraps the call in try/finally. A broken hook now logs through globalThis.codeceptjs.output (or stderr when the runner isn't up yet) and is marked ran=true regardless, so a poison entry can't block the rest of the pass or get retried by runPendingHooks. Callers don't touch hook.ran themselves. - Tighten the comment over the globalThis.codeceptjs block in lib/codecept.js. The previous wording implied end-user projects didn't need it — they do, because @codeceptjs/configure is typically imported at the top of a user's codecept.conf.js and registers hooks via the in-process registry before Config.load even returns. setup.mjs handles the same contract for mocha tests via .mocharc require. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drop the side-effect block from lib/codecept.js. The framework now lets each test suite own the registry install via its own bootstrap script: - mocha-driven tests (unit, runner, rest): .mocharc → test/support/setup.mjs already runs before every file. setup.mjs calls installCodeceptjs(). - codeceptjs-runner-driven acceptance suites: each codecept.*.js config references installCodeceptjs as its `bootstrap` field. The runner invokes bootstrap after container.create and before tests start, so globalThis.codeceptjs is in place by the time @codeceptjs/expect-helper's Proxy is read at I.expect*() call time. The shared installer lives in test/support/install-codeceptjs.js — five lines, one job, idempotent. WebDriver's existing 5s selenium-wait bootstrap calls installCodeceptjs() then waits, in that order. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
docs/migration-4.md— a complete 3.x → 4.x migration guide covering the ESM switch, removed helpers/plugins, AI-config replacement (Vercel AI SDK), Joi → Zod,restart: 'browser'removal,withinbecoming an effect,noGlobals: trueas the new default,wait*URL resolution, strict mode, CLI plugin args, workers events, and TypeScript loader changes.hopeThatindocs/effects.mdas the soft-assertion replacement for the removedSoftExpectHelper. Clarifies that it works with any assertion library (built-inI.see*,assert, chai, jest matchers, etc.).require()→import,module.exports→export default,exports.config→export const config. Fixed one dynamic-import-at-top-level regression inpageobjects.md.import 'dotenv/config'inbest.md,import electron from 'electron'inplaywright.md, prose aroundrequire('codeceptjs')updated inhooks.md/internal-api.md.Files
docs/migration-4.mdai,api,basics,bdd,best,commands,configuration,custom-helpers,data,effects,hooks,index,installation,internal-api,pageobjects,parallel,playwright,plugins,puppeteer,reports,secrets,translation,tutorial,webdriverdocs/migration-4.mdintentionally keepsrequire()/module.exportsin its 3.x: before-snippets so users can see what they had.docs/api.md's Joi-migration note and theFor CommonJS Projects (CodeceptJS 3.x)block indocs/configuration.mdare left as CJS for the same reason.Test plan
npm run docsor whichever pipeline) and confirm the new migration guide appears in the sidebar.docs/custom-helpers.md,docs/internal-api.md) by copying it into a freshnpm init-style ESM project and confirming the import resolves.docs/effects.mdrenders thehopeThatexample correctly./effects#hopethat,/locators#aria-locators,/auth,/aitrace,/mcp, etc.) resolve.🤖 Generated with Claude Code