关于 migrate-cypress-to-codeceptjs
这项迁移技能指导将 Cypress 端到端测试项目迁至 CodeceptJS 4。它涵盖项目盘点、配置与共享代码转换,再逐步迁移测试文件,并在过渡期间保留 Cypress。
主要功能
- 01盘点 Cypress 测试与共享代码
- 02映射 Cypress 和 CodeceptJS 配置
- 03迁移自定义 helper 与页面对象
- 04转换测试文件的写法
- 05标明不支持或有限支持的对应功能
适用场景
- 01规划 Cypress 项目迁移
- 02移植 Cypress 自定义命令和页面对象
- 03转换 Cypress 端到端测试文件
- 04识别迁移范围之外的组件测试
MCP 相关
该技能将单个测试文件的迁移交由 `writing-codeceptjs-tests` 技能处理;后者通过 MCP 操作实时浏览器并验证步骤。
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ codeceptjs/ skills --skill migrate-cypress-to-codeceptjs安装后在 Claude Code 中输入/migrate-cypress-to-codeceptjs确认已加载。
详细介绍
迁移:Cypress → CodeceptJS 4
Cypress 和 CodeceptJS 的目标相同——浏览器端到端测试——但在三个根本方面有所不同:
- 步骤排队与命令链。 CodeceptJS 会自动将每次
I.*调用加入内部记录器队列;测试看起来是同步的,只有获取内容时才需要await(await I.grabTextFrom(...))。无需通过.then()链传递状态。 - 使用 Helpers,而非内置浏览器。
I.*会调用已配置的 helper。Cypress 按设计仅支持单一浏览器;CodeceptJS 则允许选择 Playwright(推荐用于 Cypress 迁移——与 Chromium 保持一致,并支持跨浏览器)、Puppeteer 或 WebDriver,而且测试代码保持不变。 - 内置的一流抽象。 页面对象、多用户
session(...)、auth插件和自定义 helpers 都已内置。Cypress 项目往往会积累这些功能的临时实现;迁移可将它们统一为框架惯用的方式。
权威参考资料:node_modules/codeceptjs/docs/(basics、locators、playwright、custom-helpers、pageobjects)。
何时触发
满足以下任一条件:
- 仓库根目录中存在
cypress.config.{js,ts,mjs}。 - 存在
cypress/目录,且其中包含e2e/、support/、fixtures/、plugins/或component/子目录。 devDependencies中列出了cypress。- 测试代码调用
cy.*(cy.visit、cy.get、cy.contains、cy.session、cy.intercept、cy.request、cy.task、cy.fixture、cy.origin、cy.mount)、使用Cypress.Commands.add(...),或读取Cypress.env(...)。 - 用户提出“从 Cypress 迁移/移植/转换”。
无法迁移的内容
请一开始就如实说明:
- 组件测试(
cy.mount、cypress/component/)——CodeceptJS 仅用于端到端测试。组件测试可以继续使用 Cypress,也可以迁移到 Playwright Component Testing / Vitest + Testing Library。 cy.intercept('POST', '/api').as('save')→cy.wait('@save')——最接近的等效方式是 Playwright 的I.mockRoute()(没有别名,也没有cy.wait('@x'))。请根据界面结果(I.waitForText('Saved'))而非网络事件设置等待条件。- Cypress Cloud / 时间穿梭调试器——由
aiTrace插件提供的逐步产物和用于仪表板的@testomatio/reporter取代。 cy.origin()多来源流程——支持有限;请记录这一差距并提前规划应对方案。
工作流
按顺序执行各阶段。在每个阶段边界都提交更改,以便对任何回归进行二分排查。
1. 清点 Cypress 项目
动手前,先全面了解项目。分两轮进行。
项目概况 — 使用 grep / wc -l 估算工作量:
cypress.config.{js,ts,mjs}— 当前使用了哪些配置项cypress/e2e/**/*.cy.{js,ts}— 测试规格文件数量cypress/fixtures/— 数量与文件名cypress/support/{e2e,commands}.{js,ts}— 这些文件始终存在;完整阅读cypress/plugins/— 旧版预处理器 / 任务配置cypress/component/+cy.mount(— 标记并告知用户(不在范围内)- 统计
cy.intercept(、cy.task(、cy.session(、cy.origin(、Cypress.Commands.add(、cy.fixture(的出现次数 — 每一项都对应已知的替换模式
共享逻辑与共享定位器 — Cypress 没有内置页面对象,但测试套件仍会逐渐积累共享抽象。修改测试文件前先找出它们:
- 自定义命令 — 列出
cypress/support/commands.{js,ts}中每个Cypress.Commands.add('<name>', fn),包括名称 → 参数 → 函数体。几乎每个测试套件都有这些命令(cy.login、cy.seedData、cy.dragRowTo等)。 - 页面对象风格的模块 — 查看
cypress/support/、cypress/pages/、cypress/page-objects/、cypress/helpers/、cypress/objects/、cypress/po/,以及 cypress 目录外的任何pages//pageObjects/。识别以下内容:导出选择器集合的模块({ usernameField: '#user', submitBtn: '[data-cy=submit]' })、导出调用cy.*方法的模块(login(user, pwd)、goToProfile())、将选择器作为字段的类。 - 共享选择器常量 — 查找名为
selectors.{js,ts}/locators.{js,ts}的文件,或仅导出字符串的模块。搜索测试规格中重复出现的cy.get('[data-cy=...]')字符串 — 重复项可作为抽象候选。 - 工具函数 — 日期格式化程序、URL 构造器、API 封装(
api.js、helpers.js、utils.js)。 - 全局钩子 —
cypress/support/e2e.{js,ts}中的beforeEach块、Cypress.on('uncaught:exception', ...)等。
整理一份简短清单:列出每个共享抽象当前在 Cypress 中的位置,以及计划迁移到的 CodeceptJS 目标位置(参见第 4 阶段的目标位置表)。开始编写任何代码前,先让用户审核。
2. 在 Cypress 旁安装 CodeceptJS
运行 npx codeceptjs init 并选择 Playwright helper。暂时不要移除 Cypress — 迁移期间两者并行运行,确保尚未完全转换的测试套件仍能保持覆盖且通过测试。
3. 迁移配置
将 cypress.config.{js,ts} 配置项映射到 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 | 使用 on: 'fail' 的 screenshot 插件 |
retries | 顶层配置 retry: N |
env.* / Cypress.env('X') | process.env.X |
setupNodeEvents / cy.task | 自定义 helper 或 bootstrap / teardown |
4. 迁移共享抽象
这是基础工作。先于任何测试规格重写完成 — 每个测试规格都能因此缩短,因为它需要的操作方法(I.doSmth(...))已经存在。
Cypress 自定义命令的硬性规则。 每个 Cypress.Commands.add('<name>', fn) 都要变成自定义 helper 上的方法。按操作类型将命令拆分到两个 helper 中 — 它们的访问方式不同,正确的 API 也不同:
WebExtra(lib/helpers/WebExtra.js)用于由浏览器驱动的命令 — 任何需要打开的页面、DOM、evaluate、初始化脚本、存储或等待网络响应的命令。通过this.helpers['Playwright'].page/.browserContext访问。ApiExtras(lib/helpers/ApiExtras.js)用于纯 HTTP 命令 — 通过程序化方式登录、准备 / 清理数据,以及对 API 执行 CRUD 操作。通过this.helpers['REST'](或GraphQL)访问。REST helper 配置请参阅node_modules/codeceptjs/docs/api.md。 每个 Cypress 命令对应一个异步方法,方法名保持一致,因此cy.doSmth(arg)→I.doSmth(arg)。在codecept.conf.{js,ts}的helpers下注册这两个辅助模块。
绝不要调用 this.helpers['Playwright'].browserContext.request.* 执行 API 操作。 这样会绕过 REST + JSONResponse 调用链——没有步骤日志、没有 I.seeResponseCodeIsSuccessful 断言、没有共享标头,而且相同的 HTTP 方法最终会分散在不同辅助模块中。如果 API 需要与浏览器使用相同的身份验证,请在配置顶部共享 Cookie,一次即可:
import { setSharedCookies } from '@codeceptjs/configure'
setSharedCookies()
……或者为基于令牌的身份验证在 REST 辅助模块上设置 defaultHeaders,也可以在每个测试中使用 I.amBearerAuthenticated(secret(token))。这三种模式都在 api.md 中有介绍。
WebExtra 示例——由浏览器驱动的命令(此处的 login 驱动 UI 表单;由 API 驱动的变体见下方 ApiExtras):
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 示例——通过 REST 辅助模块路由的纯 HTTP 命令:
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
}
}
辅助模块代码风格——适用于两者:
- 所有
import语句都放在文件顶部。绝不要在方法内部使用const fs = await import('node:fs/promises')。 - 使用内置断言(API 使用
I.seeResponseCodeIsSuccessful,浏览器使用I.seeElement)、ExpectHelper,或codeceptjs/assertions中的工厂函数——绝不要使用if (cond) throw new Error('...')。失败必须显示为正确的断言错误。参见node_modules/codeceptjs/docs/assertions.md。 - 如果你的
WebExtra正在按用户名添加会话缓存映射,那你就是在重新实现auth插件——请停止,让auth插件(阶段 8)处理会话复用。辅助模块应提供loginViaApi/login;缓存由插件处理。
调用了 cy.window().then(...)、cy.wrap(...) 或命令式 DOM 操作技巧的 Cypress 代码,可直接转换为 WebExtra 中的 page.evaluate(...)。调用了 cy.request(...) 的 Cypress 代码,可转换为 ApiExtras 中的 REST.sendXxxRequest(...)。
阶段 1 清单中的其他迁移目标:
-
Cypress 页面对象风格模块 →
pages/下的 CodeceptJS 页面对象类。谨慎迁移——只保留原模块已有的方法;迁移时不要自行添加新的封装。选择器集合转换为this.fields = { ... };方法改写时,在顶部使用const { I } = inject(),调用I.fillField、I.click,以及WebExtra/ApiExtras辅助模块新增的任何I.*动词。在codecept.conf.{js,ts}的include下注册页面对象,使其自动注入到场景中。应避免的页面对象反模式(除非原始 Cypress 代码中已有):
- 断言方法(
checkTitle() { I.seeElement(...) })——页面对象应封装操作动词(fillForm、submitOrder);断言应放在测试中。 - 单行封装,只包装一次
I.click/I.see*/I.grabTextFrom调用——与其封装,不如直接在测试中调用I.*。 - 仅在一个测试中使用的方法——将步骤留在测试中。页面对象的用途是复用。
- 任何方法中的
if (cond) throw new Error(...)——改用I.see*、I.seeNumberOfElements、ExpectHelper或codeceptjs/assertions工厂函数。
- 断言方法(
-
共享选择器常量 → 放到相关页面对象的字段中。不要创建独立的
selectors.js。 -
不操作浏览器的纯工具模块 → 普通 ES 模块,在需要的地方导入。
-
全局钩子 → 测试中的 CodeceptJS
Before/BeforeSuite,或者配置中的bootstrap/teardown,用于一次性设置。
继续之前请先做健全性检查:npx codeceptjs check -c <config> 必须通过,并且 npx codeceptjs list -c <config> 必须显示每个 Cypress 命令名称都是由 WebExtra 或 ApiExtras(视其归属而定)提供的 I.* 操作。
5. 转换 spec 文件
一次处理一个文件,依托第 4 阶段的抽象。将每个 spec 的工作交给 writing-codeceptjs-tests 技能处理——它通过 MCP 驱动实时浏览器,并在提交前验证每个步骤。
| Cypress | CodeceptJS 4 |
|---|---|
文件 *.cy.{js,ts} | *_test.{js,ts} |
describe('X', () => { ... }) | 文件顶部的 Feature('X'),每个文件一个 Feature |
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)(自定义命令) | I.login(u, p)(来自 WebExtra) |
迭代——在测试、页面对象和辅助函数中,任何包含 I.* 调用的循环都使用 for...of。绝不要使用 Array.prototype.forEach。.forEach 会吞掉迭代回调的返回值——其中的 await 不会阻塞外层函数,CodeceptJS 记录器可能会将步骤排错顺序,或在循环结束前就完成 Scenario。for...of 能保持循环顺序执行,并且以后添加 await 时无需重写:
for (const sort of testSort) {
I.click(locate(this.filterFormLabel).withText(sort))
}
for (const row of await I.grabWebElements('.row')) {
const text = await row.getText()
I.expectNotEmpty(text)
}
每批次:运行 npx codeceptjs dry-run --steps -c <config>——加载每个 Scenario,解析每个 I.* 调用,无需浏览器。几秒内即可发现拼写错误、缺失的导入、未放入 include 的页面对象,以及不存在的动词。先修复,再进行实际运行。
然后运行该批次:npx codeceptjs run --steps -c <config>。
- 首次实际运行几乎总会失败——定位器偏移、源框架通过自身重试机制掩盖的时序问题、身份验证/会话差异、数据假设。这是预期情况;修复问题本就是迁移的一部分。
- 每次失败 → 调用
debugging-codeceptjs-tests并当场修复(断点、实时页面检查、验证修复结果)。不要盲目重写,也不要用retry掩盖问题。 - 一批次成功运行才算完成,而不是 dry-run 通过就算完成。
6. 定位器
为每个定位器限定上下文。 每个操作的最后一个参数都会将查找范围缩小到某个区域——I.click('Save', '.toolbar')、I.fillField('Email', '[email protected]', '#login-form')、I.click({ role: 'button', name: 'Delete' }, '.modal')。简短的语义或 ARIA 定位器加上上下文,胜过一个冗长且未限定范围的定位器:它读起来就像页面内容,能消除重复标签的歧义而不增加长度,也能适应标记变化。对下表中的每一行都这样处理——源框架中的链式调用通常可以清晰拆分为区域 + 用户可见内容。
cy.get(sel).within(() => ...) 和 cy.get(parent).find(child) 都会折叠到上下文参数中——Cypress 链的父选择器就放在这里。
CodeceptJS 优先级——选择最合适的最高优先级定位器,然后添加上下文:
- 语义字符串——按钮文本、标签、占位符、链接文本:
I.click('Save', '.toolbar')、I.fillField('Email', '[email protected]', '#login-form')。可替代大多数cy.contains(...)调用。 普通字符串本身就能匹配aria-label,所以带有aria-label="Save"的纯图标控件应使用I.click('Save', <context>)——绝不要使用'aria-label=Save'或{ css: '[aria-label="Save"]' }。 - ARIA 角色——
I.click({ role: 'button', name: 'Sign In' }, '#login-form')。 - 通过
customLocator插件使用$name——Cypress 用户通常会默认使用[data-cy=...]。保留这些属性,但启用该插件,这样就能写成I.click('$submit', '.checkout'),而不是{ css: '[data-cy=submit]' }。 locate()构建器——I.click(locate('button').withText('Edit').inside('tr').withText('Acme'));通常拆分为I.click('Edit', locate('tr').withText('Acme'))更好。- CSS / XPath——仅作为后备方案。
完整指南见 writing-codeceptjs-tests § 定位器。
7. 操作、断言和抓取
| 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。普通操作会自动排队。
8. 会话和身份验证
cy.session(id, setup, { validate }) 和基于 cy.request 的程序化登录 → 使用 auth 插件。将设置步骤交由 codeceptjs-auth 完成。如果第 4 阶段已将 cy.login 移植到 WebExtra,并实现为 I.login(...),则 auth 插件的角色定义只需调用 I.login(...)。对于多用户场景(Cypress 没有原生等效功能),请使用 codeceptjs/effects 中的 session(...)。
9. 固定数据、请求和任务
| Cypress | CodeceptJS 4 |
|---|---|
cy.fixture('users.json') | import users from './fixtures/users.json' with { type: 'json' } |
cy.request('POST', '/api/x', body) | 通过 REST helper 执行 await I.sendPostRequest('/api/x', body);对于可复用的流程,可将其封装到第 4 阶段的 ApiExtras helper 中 |
cy.task('seedDB') | 使用 ApiExtras 中的方法(如果是 HTTP)、专用 helper,或 bootstrap / teardown |
REST helper 身份验证:@codeceptjs/configure 中的 setSharedCookies() 会将浏览器会话共享给 REST,使同一用户同时登录两端;也可以为静态令牌设置 defaultHeaders,或在每个测试中使用 I.amBearerAuthenticated(secret(token))。完整配置选项(包括 JSONResponse 断言,如 I.seeResponseCodeIsSuccessful、I.seeResponseContainsKeys、搭配 Zod 使用的 I.seeResponseMatchesJsonSchema)请参阅 node_modules/codeceptjs/docs/api.md。
10. 网络模拟
cy.intercept(url, handler) → I.mockRoute(url, route => route.fulfill({ ... }))(Playwright)。使用 I.stopMockingRoute(url) 禁用。没有与 cy.wait('@alias') 等效的功能——应通过 UI 结果(I.waitForText、I.seeElement)而非网络事件来设置等待锚点。
11. 移除 Cypress
仅在所有 spec 均已移植且 CI 全部通过后:删除 cypress/、cypress.config.*,从 devDependencies 中移除 cypress,并删除 Cypress CI 任务。
验证
npx codeceptjs check -c <config>— 检查配置、helper 和插件是否正常。npx codeceptjs list -c <config>— 每条已移植的 Cypress 命令都应作为WebExtra或ApiExtras中的I.*操作出现;每个页面对象的方法也应出现。npx codeceptjs dry-run --steps -c <config>— 加载每个 Scenario。- 完整运行:
npx codeceptjs run --steps -c <config>。首次运行时出现失败是预期的——使用debugging-codeceptjs-testsskill 逐一定位并修复(不要用retry,也不要盲目重写)。只有整个转换后的套件全部通过,迁移才算完成。 - 将检查
output/trace_*/产物的工作交由codeceptjs-run-analysis完成(需启用aiTrace插件)。 grep -r "cy\." cypress/— 删除cypress/前,此命令的结果应为空。
相关技能
writing-codeceptjs-tests— 按规范重写的操作手册(由 MCP 驱动,步骤经过验证)debugging-codeceptjs-tests— 从首次完整运行开始,用于每个失败的测试codeceptjs-auth— 替代cy.session()和编程式登录codeceptjs-fundamentals— 迁移后运行,以确认连接配置- 参考文档:
node_modules/codeceptjs/docs/(基础知识、playwright、定位器、自定义辅助工具、API、断言、页面对象、数据、会话、效果)