mcpField
M
migrate-cypress-to-codeceptjs

migrate-cypress-to-codeceptjs

github.com/codeceptjs测试与质量251 次安装1 周前更新
代码仓库

关于 migrate-cypress-to-codeceptjs

这项迁移技能指导将 Cypress 端到端测试项目迁至 CodeceptJS 4。它涵盖项目盘点、配置与共享代码转换,再逐步迁移测试文件,并在过渡期间保留 Cypress。

主要功能

  1. 01盘点 Cypress 测试与共享代码
  2. 02映射 Cypress 和 CodeceptJS 配置
  3. 03迁移自定义 helper 与页面对象
  4. 04转换测试文件的写法
  5. 05标明不支持或有限支持的对应功能

适用场景

  1. 01规划 Cypress 项目迁移
  2. 02移植 Cypress 自定义命令和页面对象
  3. 03转换 Cypress 端到端测试文件
  4. 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 的目标相同——浏览器端到端测试——但在三个根本方面有所不同:

  1. 步骤排队与命令链。 CodeceptJS 会自动将每次 I.* 调用加入内部记录器队列;测试看起来是同步的,只有获取内容时才需要 await(await I.grabTextFrom(...))。无需通过 .then() 链传递状态。
  2. 使用 Helpers,而非内置浏览器。 I.* 会调用已配置的 helper。Cypress 按设计仅支持单一浏览器;CodeceptJS 则允许选择 Playwright(推荐用于 Cypress 迁移——与 Chromium 保持一致,并支持跨浏览器)、Puppeteer 或 WebDriver,而且测试代码保持不变。
  3. 内置的一流抽象。 页面对象、多用户 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}:

CypressCodeceptJS 4(Playwright helper)
e2e.baseUrlhelpers.Playwright.url
viewportWidth / viewportHeighthelpers.Playwright.windowSize: '1280x720'
defaultCommandTimeouthelpers.Playwright.waitForTimeout
videohelpers.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 驱动实时浏览器,并在提交前验证每个步骤。

CypressCodeceptJS 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 优先级——选择最合适的最高优先级定位器,然后添加上下文:

  1. 语义字符串——按钮文本、标签、占位符、链接文本: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"]' }。
  2. ARIA 角色——I.click({ role: 'button', name: 'Sign In' }, '#login-form')。
  3. 通过 customLocator 插件使用 $name——Cypress 用户通常会默认使用 [data-cy=...]。保留这些属性,但启用该插件,这样就能写成 I.click('$submit', '.checkout'),而不是 { css: '[data-cy=submit]' }。
  4. locate() 构建器——I.click(locate('button').withText('Edit').inside('tr').withText('Acme'));通常拆分为 I.click('Edit', locate('tr').withText('Acme')) 更好。
  5. CSS / XPath——仅作为后备方案。

完整指南见 writing-codeceptjs-tests § 定位器。

7. 操作、断言和抓取
CypressCodeceptJS 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. 固定数据、请求和任务
CypressCodeceptJS 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 任务。

验证

  1. npx codeceptjs check -c <config> — 检查配置、helper 和插件是否正常。
  2. npx codeceptjs list -c <config> — 每条已移植的 Cypress 命令都应作为 WebExtra 或 ApiExtras 中的 I.* 操作出现;每个页面对象的方法也应出现。
  3. npx codeceptjs dry-run --steps -c <config> — 加载每个 Scenario。
  4. 完整运行:npx codeceptjs run --steps -c <config>。首次运行时出现失败是预期的——使用 debugging-codeceptjs-tests skill 逐一定位并修复(不要用 retry,也不要盲目重写)。只有整个转换后的套件全部通过,迁移才算完成。
  5. 将检查 output/trace_*/ 产物的工作交由 codeceptjs-run-analysis 完成(需启用 aiTrace 插件)。
  6. grep -r "cy\." cypress/ — 删除 cypress/ 前,此命令的结果应为空。

相关技能

  • writing-codeceptjs-tests — 按规范重写的操作手册(由 MCP 驱动,步骤经过验证)
  • debugging-codeceptjs-tests — 从首次完整运行开始,用于每个失败的测试
  • codeceptjs-auth — 替代 cy.session() 和编程式登录
  • codeceptjs-fundamentals — 迁移后运行,以确认连接配置
  • 参考文档:node_modules/codeceptjs/docs/(基础知识、playwright、定位器、自定义辅助工具、API、断言、页面对象、数据、会话、效果)

评论

支持 Markdown