# Migrate Cypress To Codeceptjs

> Port a Cypress test suite to CodeceptJS 4. Trigger when the project contains `cypress.config.{js,ts,mjs}`, a `cypress/` directory (`cypress/e2e/`, `cypress/support/{commands,e2e}.*`, `cypress/fixtures/`), `cypress` in `devDependencies`, or test code calling `cy.*` (`cy.visit`, `cy.get`, `cy.contains`, `cy.session`, `cy.intercept`, `cy.request`, `cy.task`, `cy.fixture`, `cy.origin`, `cy.mount`), `Cypress.Commands.add(...)`, or `Cypress.env(...)`.

- Skill: `codeceptjs/migrate-cypress-to-codeceptjs` (Agent Skill)
- Install (CLI): `npx skillmds@latest add codeceptjs/migrate-cypress-to-codeceptjs`
- Raw SKILL.md: https://api.skillmd.com/api/skills/codeceptjs/migrate-cypress-to-codeceptjs/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Productivity
- Author: codeceptjs (https://skillmd.com/u/codeceptjs)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/codeceptjs/migrate-cypress-to-codeceptjs

---


# Migrate Cypress → CodeceptJS 4

Cypress and CodeceptJS share a goal — browser end-to-end testing — but differ in three foundational ways:

1. **Step queueing vs command chains.** CodeceptJS auto-queues every `I.*` call onto an internal recorder; tests look synchronous and `await` is only needed for grabs (`await I.grabTextFrom(...)`). There is no `.then()` chain to thread state through.
2. **Helpers, not a bundled browser.** `I.*` dispatches to a configured helper. Cypress is single-browser by design; CodeceptJS lets you pick **Playwright** (recommended for Cypress migrators — Chromium parity plus cross-browser), Puppeteer, or WebDriver, and the test code stays the same.
3. **First-class abstractions.** Page objects, multi-user `session(...)`, the `auth` plugin, and custom helpers are built in. Cypress projects accumulate ad-hoc versions of these; the migration consolidates them onto the framework's idioms.

Authoritative reference: `node_modules/codeceptjs/docs/` (basics, locators, playwright, custom-helpers, pageobjects).

## When to trigger

Any of:

- `cypress.config.{js,ts,mjs}` at the repo root.
- A `cypress/` directory with `e2e/`, `support/`, `fixtures/`, `plugins/`, or `component/` subdirs.
- `cypress` listed in `devDependencies`.
- Test code calls `cy.*` (`cy.visit`, `cy.get`, `cy.contains`, `cy.session`, `cy.intercept`, `cy.request`, `cy.task`, `cy.fixture`, `cy.origin`, `cy.mount`), uses `Cypress.Commands.add(...)`, or reads `Cypress.env(...)`.
- The user says "migrate / port / convert from Cypress".

## What does not migrate

Be honest up-front:

- **Component tests** (`cy.mount`, `cypress/component/`) — CodeceptJS is E2E only. Keep Cypress for components, or move them to Playwright Component Testing / Vitest + Testing Library.
- **`cy.intercept('POST', '/api').as('save')` → `cy.wait('@save')`** — the closest equivalent is Playwright's `I.mockRoute()` (no alias, no `cy.wait('@x')`). Anchor waits on UI outcomes (`I.waitForText('Saved')`) instead of network events.
- **Cypress Cloud / time-travel debugger** — replaced by the `aiTrace` plugin's per-step artifacts and `@testomatio/reporter` for dashboards.
- **`cy.origin()` multi-origin flows** — limited support; document the gap and plan around it.

## Workflow

Run phases in order. Commit at each boundary so any regression is bisectable.

### 1. Inventory the Cypress project

Before touching anything, build a picture. Two passes.

**Shape of the project** — grep / `wc -l` for cost predictors:

- `cypress.config.{js,ts,mjs}` — which keys are in use
- `cypress/e2e/**/*.cy.{js,ts}` — spec count
- `cypress/fixtures/` — count + filenames
- `cypress/support/{e2e,commands}.{js,ts}` — these always exist; **read in full**
- `cypress/plugins/` — legacy preprocessor / task wiring
- `cypress/component/` + `cy.mount(` — flag for the user (out of scope)
- count occurrences of `cy.intercept(`, `cy.task(`, `cy.session(`, `cy.origin(`, `Cypress.Commands.add(`, `cy.fixture(` — each maps to a known replacement pattern

**Shared logic and shared locators** — Cypress has no built-in page objects, but suites accumulate shared abstractions anyway. Find them before touching test files:

- **Custom commands** — every `Cypress.Commands.add('<name>', fn)` in `cypress/support/commands.{js,ts}`. List name → arguments → body. Almost every suite has them (`cy.login`, `cy.seedData`, `cy.dragRowTo`, …).
- **Page-object-style modules** — look in `cypress/support/`, `cypress/pages/`, `cypress/page-objects/`, `cypress/helpers/`, `cypress/objects/`, `cypress/po/`, and any `pages/` / `pageObjects/` outside the cypress directory. Recognise: modules exporting selector bundles (`{ usernameField: '#user', submitBtn: '[data-cy=submit]' }`), modules exporting methods that call `cy.*` (`login(user, pwd)`, `goToProfile()`), classes with selectors as fields.
- **Shared selector constants** — files named `selectors.{js,ts}` / `locators.{js,ts}`, or modules exporting only strings. Grep specs for repeated `cy.get('[data-cy=...]')` strings — duplicates are abstraction candidates.
- **Utility helpers** — date formatters, URL builders, API wrappers (`api.js`, `helpers.js`, `utils.js`).
- **Global hooks** — `cypress/support/e2e.{js,ts}` `beforeEach` blocks, `Cypress.on('uncaught:exception', ...)`, etc.

Produce a short inventory: every shared abstraction with its current Cypress location and planned CodeceptJS destination (see phase 4's destination table). The user reviews before any code is written.

### 2. Install CodeceptJS alongside Cypress

`npx codeceptjs init` and pick the **Playwright** helper. Do not remove Cypress yet — both run in parallel through the migration, so a half-converted suite still has green coverage.

### 3. Port the config

Map `cypress.config.{js,ts}` keys → `codecept.conf.{js,ts}`:

| Cypress | CodeceptJS 4 (`Playwright` helper) |
|---|---|
| `e2e.baseUrl` | `helpers.Playwright.url` |
| `viewportWidth` / `viewportHeight` | `helpers.Playwright.windowSize: '1280x720'` |
| `defaultCommandTimeout` | `helpers.Playwright.waitForTimeout` |
| `video` | `helpers.Playwright.video: true` |
| `screenshotOnRunFailure` | plugin `screenshot` with `on: 'fail'` |
| `retries` | top-level `retry: N` |
| `env.*` / `Cypress.env('X')` | `process.env.X` |
| `setupNodeEvents` / `cy.task` | custom helper or `bootstrap` / `teardown` |

### 4. Port shared abstractions

This is the bedrock. Do it before any spec rewrite — every spec rewrite shrinks because the verbs it needs (`I.doSmth(...)`) already exist.

**Hard rule for Cypress custom commands.** Every `Cypress.Commands.add('<name>', fn)` becomes a method on a custom helper. **Split commands across two helpers by the kind of operation** — they have different access patterns and different correct APIs:

- **`WebExtra`** (`lib/helpers/WebExtra.js`) for **browser-driven** commands — anything that needs the open page, DOM, `evaluate`, init scripts, storage, network-response waits. Reaches `this.helpers['Playwright'].page` / `.browserContext`.
- **`ApiExtras`** (`lib/helpers/ApiExtras.js`) for **pure HTTP** commands — programmatic login, seed/teardown data, CRUD against an API. Reaches `this.helpers['REST']` (or `GraphQL`). See `node_modules/codeceptjs/docs/api.md` for REST helper configuration.

One async method per Cypress command, named identically, so `cy.doSmth(arg)` → `I.doSmth(arg)`. Register both helpers under `helpers` in `codecept.conf.{js,ts}`.

**Never call `this.helpers['Playwright'].browserContext.request.*` for API work.** That bypasses the REST + `JSONResponse` stack — no step logging, no `I.seeResponseCodeIsSuccessful` assertions, no shared headers, and the same verb ends up split between helpers. If the API needs the same auth as the browser, share cookies once at the top of the config:

```js
import { setSharedCookies } from '@codeceptjs/configure'
setSharedCookies()
```

…or set `defaultHeaders` on the REST helper for token-based auth, or use `I.amBearerAuthenticated(secret(token))` per test. All three patterns are covered in `api.md`.

**WebExtra example** — browser-driven commands (here `login` drives the UI form; the API-driven variant goes to `ApiExtras` below):

```js
import Helper from '@codeceptjs/helper'
import fs from 'node:fs/promises'

export default class WebExtra extends Helper {
  async login(user, password) {
    const { page } = this.helpers['Playwright']
    await page.goto('/login')
    await page.getByLabel('Email').fill(user)
    await page.getByLabel('Password').fill(password)
    await page.getByRole('button', { name: 'Sign In' }).click()
    await page.waitForURL(/\/dashboard/)
  }

  async setLocalStorage(key, value) {
    const { page } = this.helpers['Playwright']
    await page.evaluate(([k, v]) => localStorage.setItem(k, v), [key, value])
  }

  async stubWindowOpen() {
    const { page } = this.helpers['Playwright']
    await page.addInitScript(() => {
      window.__lastOpenUrl = null
      const orig = window.open
      window.open = (url, ...rest) => {
        window.__lastOpenUrl = url
        return orig ? orig.call(window, 'about:blank', ...rest) : null
      }
    })
  }

  async writeJsonFile(filePath, data) {
    await fs.writeFile(filePath, JSON.stringify(data, null, 2))
  }
}
```

**ApiExtras example** — pure HTTP commands routed through the REST helper:

```js
import Helper from '@codeceptjs/helper'

export default class ApiExtras extends Helper {
  async loginViaApi(email, password) {
    const REST = this.helpers['REST']
    await REST.sendPostRequest('/login_ajax', { email, password, remember: false })
  }

  async seedCourse(courseData) {
    const REST = this.helpers['REST']
    const { data } = await REST.sendPostRequest('/course', courseData)
    return data
  }
}
```

**Helper code style** — applies to both:

- All `import` statements at the **top of the file**. Never `const fs = await import('node:fs/promises')` inside a method.
- Use built-in assertions (`I.seeResponseCodeIsSuccessful` for API, `I.seeElement` for browser), `ExpectHelper`, or factories from `codeceptjs/assertions` — **never** `if (cond) throw new Error('...')`. Failures must render as proper assertion errors. See `node_modules/codeceptjs/docs/assertions.md`.
- If your `WebExtra` is growing a session-cache map keyed by user name, you are reimplementing the `auth` plugin — stop and let the `auth` plugin (phase 8) handle session reuse. The helper should expose `loginViaApi` / `login`; the plugin handles caching.

Cypress code that called `cy.window().then(...)`, `cy.wrap(...)`, or imperative DOM tricks translates cleanly into `page.evaluate(...)` inside `WebExtra`. Cypress code that called `cy.request(...)` translates to `REST.sendXxxRequest(...)` inside `ApiExtras`.

**Other destinations** from the phase 1 inventory:

- **Cypress page-object-style module** → CodeceptJS **page object class** under `pages/`. **Port conservatively** — keep only the methods the original module had; do not invent new wrappers during migration. Selector bundles become `this.fields = { ... }`; methods rewrite with `const { I } = inject()` at the top, calling `I.fillField`, `I.click`, and any `I.*` verb the `WebExtra` / `ApiExtras` helpers now contribute. Register under `include` in `codecept.conf.{js,ts}` so the page object auto-injects into Scenarios.

  Page-object anti-patterns to avoid (unless the original Cypress code already had them):
  - **Assertion methods** (`checkTitle() { I.seeElement(...) }`) — page objects are action verbs (`fillForm`, `submitOrder`); let assertions live in the test.
  - **One-liner wrappers** around a single `I.click` / `I.see*` / `I.grabTextFrom` — the wrapper buys nothing over calling `I.*` from the test.
  - **Methods used by only one test** — leave the steps in the test. Page objects exist for reuse.
  - **`if (cond) throw new Error(...)`** in any method — use `I.see*`, `I.seeNumberOfElements`, `ExpectHelper`, or `codeceptjs/assertions` factories instead.

- **Shared selector constants** → fields on the relevant page object. No free-floating `selectors.js`.
- **Pure utility modules** that don't touch the browser → plain ES modules, imported where needed.
- **Global hooks** → CodeceptJS `Before` / `BeforeSuite` in tests, or `bootstrap` / `teardown` in config for one-off setup.

Sanity-check before moving on: `npx codeceptjs check -c <config>` must pass, and `npx codeceptjs list -c <config>` must show every Cypress command name as an `I.*` action contributed by `WebExtra` or `ApiExtras` — whichever owns it.

### 5. Convert spec files

One file at a time, leaning on the abstractions from phase 4. Hand off the per-spec work to the **`writing-codeceptjs-tests`** skill — it drives the live browser via MCP and verifies each step before committing.

| Cypress | CodeceptJS 4 |
|---|---|
| File `*.cy.{js,ts}` | `*_test.{js,ts}` |
| `describe('X', () => { ... })` | `Feature('X')` at top, one Feature per file |
| `it('Y', () => { ... })` | `Scenario('Y', ({ I }) => { ... })` |
| `beforeEach(() => { ... })` | `Before(({ I }) => { ... })` |
| `afterEach(() => { ... })` | `After(({ I }) => { ... })` |
| `before(...)` / `after(...)` | `BeforeSuite(...)` / `AfterSuite(...)` |
| `cy.visit('/x')` | `I.amOnPage('/x')` |
| `cy.login(u, p)` (custom command) | `I.login(u, p)` (from `WebExtra`) |

**Iteration** — in tests, page objects, and helpers, use **`for...of`** for any loop containing `I.*` calls. Never `Array.prototype.forEach`. `.forEach` swallows the iteration callback's return — an `await` inside it does not block the outer function, and the CodeceptJS recorder may queue steps out of order or finish the Scenario before the loop is done. `for...of` keeps the loop sequential and lets you add `await` later without rewriting:

```js
for (const sort of testSort) {
  I.click(locate(this.filterFormLabel).withText(sort))
}
```

```js
for (const row of await I.grabWebElements('.row')) {
  const text = await row.getText()
  I.expectNotEmpty(text)
}
```

**Per batch**: `npx codeceptjs dry-run --steps -c <config>` — loads every Scenario, resolves every `I.*` call, no browser. Surfaces typos, missing imports, page objects not under `include`, and nonexistent verbs in seconds. Fix before anything real.

Then run the batch: `npx codeceptjs run --steps -c <config>`.

- First real runs almost always fail — locator drift, timing the source framework hid behind its own retry, auth/session differences, data assumptions. **Expected; fixing it is part of the migration.**
- Every failure → invoke `debugging-codeceptjs-tests` and fix on the fly (breakpoint, live-page inspection, verified fix). No blind rewrites, no `retry` masking.
- A batch is done when it runs green, not when it dry-runs clean.

### 6. Locators

**Scope every locator with a context.** The last argument of every action narrows the lookup to a region — `I.click('Save', '.toolbar')`, `I.fillField('Email', 'u@t.com', '#login-form')`, `I.click({ role: 'button', name: 'Delete' }, '.modal')`. A short semantic or ARIA locator plus a context beats one long unscoped locator: it reads like the page, disambiguates duplicate labels without growing, and survives markup churn. Apply this to every row of the tables below — the source framework's chain usually splits cleanly into *region* + *what the user sees*.

`cy.get(sel).within(() => ...)` and `cy.get(parent).find(child)` both collapse onto the context argument — that is where a Cypress chain's parent selector belongs.

CodeceptJS priority — pick the highest that fits, then add the context:

1. **Semantic strings** — button text, label, placeholder, link text: `I.click('Save', '.toolbar')`, `I.fillField('Email', 'u@t.com', '#login-form')`. Replaces most `cy.contains(...)` calls.
A plain string already matches `aria-label`, so an icon-only control with `aria-label="Save"` is `I.click('Save', <context>)` — never `'aria-label=Save'` or `{ css: '[aria-label="Save"]' }`.
2. **ARIA roles** — `I.click({ role: 'button', name: 'Sign In' }, '#login-form')`.
3. **`$name` via the `customLocator` plugin** — Cypress users often default to `[data-cy=...]`. Keep those attributes, but enable the plugin so they read as `I.click('$submit', '.checkout')` instead of `{ css: '[data-cy=submit]' }`.
4. **`locate()` builder** — `I.click(locate('button').withText('Edit').inside('tr').withText('Acme'))`; often better split as `I.click('Edit', locate('tr').withText('Acme'))`.
5. **CSS / XPath** — fallback only.

Full guidance in **`writing-codeceptjs-tests`** § Locators.

### 7. Actions, assertions, grabs

| Cypress | CodeceptJS 4 |
|---|---|
| `cy.get(sel).click()` | `I.click(sel)` |
| `cy.get(sel).type('x')` | `I.fillField(sel, 'x')` |
| `cy.get(sel).clear()` | `I.clearField(sel)` |
| `cy.get(sel).check()` / `.uncheck()` | `I.checkOption(sel)` / `I.uncheckOption(sel)` |
| `cy.get(sel).select('A')` | `I.selectOption(sel, 'A')` |
| `cy.get(sel).should('be.visible')` | `I.seeElement(sel)` |
| `cy.get(sel).should('have.text', 'X')` | `I.see('X', sel)` |
| `cy.get(sel).should('have.value', 'X')` | `I.seeInField(sel, 'X')` |
| `cy.get(sel).should('have.length', 5)` | `I.seeNumberOfElements(sel, 5)` |
| `cy.url().should('include', '/x')` | `I.seeInCurrentUrl('/x')` |
| `cy.get(sel).invoke('text').then(t => ...)` | `const t = await I.grabTextFrom(sel)` |
| `cy.getCookie('s')` | `const c = await I.grabCookie('s')` |

`await` only on grabs. Plain actions queue automatically.

### 8. Sessions and auth

`cy.session(id, setup, { validate })` and `cy.request`-based programmatic login → the **`auth` plugin**. Hand off to **`codeceptjs-auth`** for the setup walk-through. If phase 4 already ported `cy.login` into `WebExtra` as `I.login(...)`, the `auth` plugin's role definition just calls `I.login(...)`. For multi-user scenarios (Cypress has no native equivalent) use `session(...)` from `codeceptjs/effects`.

### 9. Fixtures, requests, tasks

| Cypress | CodeceptJS 4 |
|---|---|
| `cy.fixture('users.json')` | `import users from './fixtures/users.json' with { type: 'json' }` |
| `cy.request('POST', '/api/x', body)` | `await I.sendPostRequest('/api/x', body)` via the **REST helper**; for reusable flows wrap in the `ApiExtras` helper from phase 4 |
| `cy.task('seedDB')` | method on `ApiExtras` (if HTTP), a dedicated helper, or `bootstrap` / `teardown` |

REST helper auth: `setSharedCookies()` from `@codeceptjs/configure` shares the browser session with REST so the same user is logged in on both sides; alternatively set `defaultHeaders` for static tokens or `I.amBearerAuthenticated(secret(token))` per test. See `node_modules/codeceptjs/docs/api.md` for the full configuration surface, including `JSONResponse` assertions (`I.seeResponseCodeIsSuccessful`, `I.seeResponseContainsKeys`, `I.seeResponseMatchesJsonSchema` with Zod).

### 10. Network mocking

`cy.intercept(url, handler)` → `I.mockRoute(url, route => route.fulfill({ ... }))` (Playwright). Disable with `I.stopMockingRoute(url)`. There is no `cy.wait('@alias')` equivalent — anchor waits on UI outcomes (`I.waitForText`, `I.seeElement`) instead of network events.

### 11. Decommission Cypress

Only after every spec is ported and CI is green: delete `cypress/`, `cypress.config.*`, drop `cypress` from `devDependencies`, remove the Cypress CI jobs.

## Verify

1. `npx codeceptjs check -c <config>` — config + helper + plugin sanity.
2. `npx codeceptjs list -c <config>` — every ported Cypress command appears as an `I.*` action from `WebExtra` or `ApiExtras`; every page object's methods appear.
3. `npx codeceptjs dry-run --steps -c <config>` — every Scenario loads.
4. Full run: `npx codeceptjs run --steps -c <config>`. Failures are expected on first runs — drive each to a fix via the **`debugging-codeceptjs-tests`** skill (not `retry`, not blind rewrites). The migration is complete only when the whole converted suite is green.
5. Hand off to **`codeceptjs-run-analysis`** to inspect `output/trace_*/` artifacts (requires the `aiTrace` plugin enabled).
6. `grep -r "cy\." cypress/` — empty before deleting `cypress/`.

## Related skills

- `writing-codeceptjs-tests` — per-spec rewrite playbook (MCP-driven, verified steps)
- `debugging-codeceptjs-tests` — use on every failing test from the first full run
- `codeceptjs-auth` — replaces `cy.session()` and programmatic login
- `codeceptjs-fundamentals` — run after migration to confirm wiring
- Reference docs: `node_modules/codeceptjs/docs/` (basics, playwright, locators, custom-helpers, api, assertions, pageobjects, data, sessions, effects)

