Skip to content

docs: 4.x migration guide and ESM-only code samples#5538

Merged
DavertMik merged 19 commits into4.xfrom
docs/4.x-migration-guide-and-esm-cleanup
Apr 28, 2026
Merged

docs: 4.x migration guide and ESM-only code samples#5538
DavertMik merged 19 commits into4.xfrom
docs/4.x-migration-guide-and-esm-cleanup

Conversation

@DavertMik
Copy link
Copy Markdown
Contributor

Summary

  • Adds 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, within becoming an effect, noGlobals: true as the new default, wait* URL resolution, strict mode, CLI plugin args, workers events, and TypeScript loader changes.
  • Documents hopeThat in docs/effects.md as the soft-assertion replacement for the removed SoftExpectHelper. Clarifies that it works with any assertion library (built-in I.see*, assert, chai, jest matchers, etc.).
  • Converts ~70 CommonJS code samples to ESM across 19 docs: require()import, module.exportsexport default, exports.configexport const config. Fixed one dynamic-import-at-top-level regression in pageobjects.md.
  • Other small touch-ups: claude-sonnet-4-6 / gpt-5 in AI examples, ESM-friendly import 'dotenv/config' in best.md, import electron from 'electron' in playwright.md, prose around require('codeceptjs') updated in hooks.md / internal-api.md.

Files

  • New: docs/migration-4.md
  • Updated docs (CJS → ESM + various): ai, api, basics, bdd, best, commands, configuration, custom-helpers, data, effects, hooks, index, installation, internal-api, pageobjects, parallel, playwright, plugins, puppeteer, reports, secrets, translation, tutorial, webdriver

docs/migration-4.md intentionally keeps require() / module.exports in its 3.x: before-snippets so users can see what they had. docs/api.md's Joi-migration note and the For CommonJS Projects (CodeceptJS 3.x) block in docs/configuration.md are left as CJS for the same reason.

Test plan

  • Render the docs site locally (npm run docs or whichever pipeline) and confirm the new migration guide appears in the sidebar.
  • Spot-check a converted sample (e.g. docs/custom-helpers.md, docs/internal-api.md) by copying it into a fresh npm init-style ESM project and confirming the import resolves.
  • Verify docs/effects.md renders the hopeThat example correctly.
  • Confirm intra-doc links (/effects#hopethat, /locators#aria-locators, /auth, /aitrace, /mcp, etc.) resolve.

🤖 Generated with Claude Code

DavertMik and others added 19 commits April 26, 2026 22:27
- 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>
@DavertMik DavertMik merged commit fb1a602 into 4.x Apr 28, 2026
10 checks passed
@DavertMik DavertMik deleted the docs/4.x-migration-guide-and-esm-cleanup branch April 28, 2026 23:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant