mcpField
M
migrate-testcafe-to-codeceptjs

migrate-testcafe-to-codeceptjs

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

关于 migrate-testcafe-to-codeceptjs

这项迁移 Skill 指导将 TestCafe 测试套件转到 CodeceptJS 4,涵盖项目盘点、配置、可复用 Helper 和页面对象,并说明 TestCafe 特有模式如何对应到 CodeceptJS 的替代方案。

主要功能

  1. 01盘点 TestCafe 项目
  2. 02映射到 CodeceptJS 的配置
  3. 03设置 Playwright Helper
  4. 04转换自定义 Helper
  5. 05页面对象迁移指导
  6. 06替换 TestCafe 模式

适用场景

  1. 01规划渐进式 TestCafe 迁移
  2. 02转换 TestCafe 配置
  3. 03迁移可复用浏览器和 HTTP Helper
  4. 04替换 Selector、Role 和请求钩子模式

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/codeceptjs/skills --skill migrate-testcafe-to-codeceptjs

安装后在 Claude Code 中输入/migrate-testcafe-to-codeceptjs确认已加载。

详细介绍

将 TestCafe 迁移到 CodeceptJS 4

TestCafe 和 CodeceptJS 在表面上有很多相似之处——两者都提供一组统一的测试控制器动词(t.* / I.*),都有惰性、可链式调用的选择器,也都内置了基于角色的身份验证以及屏幕截图和视频支持。迁移工作大多是机械性的,但有三个基础差异会影响整个过程:

  1. CodeceptJS 的操作不需要 await。 录制器会自动将每个 I.* 调用加入队列。TestCafe 要求每个操作都使用 await(await t.click(...));CodeceptJS 则禁止在操作上使用 await,只将其用于抓取操作(await I.grabTextFrom(...))。这是转换测试规范时最大的机械性改动——删除 I.click、I.fillField、I.see*、I.waitFor* 前面,以及会返回 void 的页面对象方法调用前面的所有 await。
  2. 使用 Helpers,而非捆绑式代理。 TestCafe 以 HTTP/HTTPS 代理的形式运行,向页面注入自动化代码;CodeceptJS 则将 I.* 调度到已配置的 Helper。推荐使用 Playwright——体验最接近、速度最快,并且与 TestCafe 一样支持三种引擎(Chromium / Firefox / WebKit)。
  3. 内置一流抽象。 页面对象、多用户 session(...)、auth 插件、自定义 Helpers 和 customLocator 插件均为内置功能。TestCafe 项目通常会积累这些功能的临时实现(Selector 属性类、Role 工厂、ClientFunction 工厂);迁移会将它们整合到框架惯用方式中。

权威参考:node_modules/codeceptjs/docs/(basics、locators、playwright、custom-helpers、pageobjects)。

何时触发

符合以下任一条件时:

  • 仓库根目录存在 .testcaferc.{json,js,ts,cjs}。
  • devDependencies 中列出了 testcafe。
  • 从 testcafe 导入(Selector、ClientFunction、Role、RequestMock、RequestHook、RequestLogger)。
  • 测试文件的顶层使用 fixture('X').page(...) 和 test('y', async t => { ... })。
  • 代码使用 Selector(...).withText(...) / .withAttribute(...) / .nth(...) / .find(...) / .filter(...) 链、t.useRole(...)、t.addRequestHooks(...)、t.eval(...) 或 ClientFunction(...)。
  • 存在一个 tests/ 目录,其中的 *.test.{js,ts} 文件内容以 fixture(...) 开头。
  • 用户提出“从 TestCafe 迁移 / 移植 / 转换”。

无法迁移的内容

请提前如实说明:

  • 基于代理的架构——TestCafe 作为中间人代理运行,并重写页面以注入其驱动程序。CodeceptJS 使用 Playwright(CDP)或 WebDriver。取舍是:失去无需驱动程序的设置方式,获得 Playwright 的速度和易用性。依赖代理实现的少见 TLS / CORS 技巧需要重新考虑。
  • TestCafe Studio 录制——需要重新编写通过 UI 录制的测试。使用 writing-codeceptjs-tests MCP 脚手架与暂停模式,针对实时浏览器重新创建这些测试。
  • TestCafe RequestHook / RequestLogger——分情况替换:Hook → I.mockRoute();Logger → 如果确实需要请求记录,则在 WebExtra 中使用 page.on('request' | 'response'),否则以 UI 结果作为判断依据。
  • disablePageCaching、quarantineMode 的精细调优——Playwright 会按上下文处理缓存;quarantine 大致对应 retry: N,但不具备相同的启发式策略。
  • 通过 testcafe-browser-provider-* 包进行移动端测试——使用 Playwright 的移动设备模拟(devices['iPhone 13']),或使用 Appium Helper 测试真实设备。
  • TestCafe Cloud / Dashboard——可替换为 @testomatio/reporter 或其他兼容 CodeceptJS 的报告器。

工作流

按顺序执行各阶段。在每个阶段边界提交更改,以便通过二分法定位任何回归问题。

1. 梳理 TestCafe 项目

动手之前,先全面了解项目。分两步进行。

项目概况 — 使用 grep / wc -l 查找成本预测指标:

  • .testcaferc.{json,js,ts,cjs} — 正在使用哪些键(browsers、src、concurrency、selectorTimeout、assertionTimeout、pageLoadTimeout、screenshots、videoPath、clientScripts、quarantineMode、stopOnFirstFail、reporter)
  • 测试文件数量 + glob(TestCafe 不要求特定后缀;常见形式为 *.test.{js,ts} 或 tests/ 下的任意文件)
  • 统计 Selector(、ClientFunction(、Role(、RequestMock(、t.useRole(、t.eval(、t.addRequestHooks(、.withText(、.withAttribute(、.nth(、.find(、.filter(、.parent(、.child(、.sibling( 的出现次数——每一种都对应已知的替换模式

共享逻辑 — 即使框架不提供支持,TestCafe 项目也会积累四类共享抽象:

  • 页面对象风格的模块 — 属性为 Selector(...) 引用、方法驱动 t.* 的类。通常位于 tests/page-objects/、tests/pages/ 或 <feature>.po.{js,ts}。直接移植为 CodeceptJS 页面对象。
  • Role 定义 — 每个 const admin = Role('https://x/login', async t => { ... })。这是 TestCafe 按会话缓存的登录流程;替代方案是 auth 插件(第 8 阶段)。
  • ClientFunction 工厂 — const getURL = ClientFunction(() => window.location.href)。每个都转换为 WebExtra 上使用 page.evaluate 的方法。
  • RequestMock 工厂 / hook 文件 — RequestMock().onRequestTo('/api/x').respond(...)。每个都转换为 I.mockRoute(...) 调用,可直接写在测试中,也可在大量复用时封装到 WebExtra 上。
  • Fixture hook — fixture(...).beforeEach(...) / .before(...) / .after(...)。转换为对应测试文件中的 CodeceptJS Before / BeforeSuite hook。
  • 自定义 Test Controller 方法 — 有些项目会通过 mixin 扩展 t;将其视为 helper 方法,并按 UI 与 HTTP 分别拆分到 WebExtra / ApiExtras。

生成一份简短清单,列出每个共享抽象的当前位置及计划迁移到的 CodeceptJS 位置。编写代码之前先让用户审核。

2. 在 TestCafe 旁安装 CodeceptJS

运行 npx codeceptjs init 并选择 Playwright helper。Playwright 通过一个配置即可支持与 TestCafe 相同的三个引擎(Chromium / Firefox / WebKit)。暂时不要移除 TestCafe——迁移期间两者并行运行。

3. 移植配置

将 .testcaferc.{json,js} 的键映射到 codecept.conf.{js,ts}:

TestCafeCodeceptJS 4(Playwright helper)
browsers: ['chrome'] / ['firefox'] / ['safari']helpers.Playwright.browser: 'chromium' / 'firefox' / 'webkit'
browsers: ['chrome:headless']helpers.Playwright.show: false(或使用 setHeadlessWhen(CI))
src: ['tests/**/*.test.js']tests: './tests/**/*_test.{js,ts}'
fixture .page('https://x')helpers.Playwright.url: 'https://x'
selectorTimeout / assertionTimeouthelpers.Playwright.waitForTimeout
pageLoadTimeouthelpers.Playwright.timeout
concurrency: NCLI:npx codeceptjs run-workers N
screenshots.path / videoPath顶层 output: './output'
screenshots.takeOnFails: truescreenshot 插件,设置 on: 'fail'
设置了 videoPathhelpers.Playwright.video: true
clientScripts: ['inject.js']使用 page.addInitScript 的 WebExtra 方法,或使用 bootstrap()
quarantineMode顶层 retry: N
stopOnFirstFail: trueCLI:--bail
reporter: 'spec'删除(Mocha 默认行为)或使用插件
hostname / port(代理)删除——Playwright 会自行管理
4. 移植共享抽象

这是基础工作。在重写任何 spec 之前完成——每个 spec 的重写工作量都会减少,因为它所需的动词(I.doSmth(...))已经存在。 共享辅助代码的硬性规则。 每个可复用的浏览器/HTTP 函数都应成为自定义 CodeceptJS 辅助器上的方法。按操作类型拆分到两个辅助器中——它们的访问模式不同,正确的 API 也不同:

  • WebExtra(lib/helpers/WebExtra.js)用于浏览器驱动的操作——任何需要打开的页面、DOM、初始化脚本、存储或等待网络响应的操作。**所有 ClientFunction 和 t.eval 的函数体都放在这里,写成 page.evaluate(...)。**访问 this.helpers['Playwright'].page / .browserContext。
  • ApiExtras(lib/helpers/ApiExtras.js)用于纯 HTTP 操作——程序化登录、准备/清理数据、通过 API 执行 CRUD。访问 this.helpers['REST'](或 GraphQL)。REST 辅助器的配置请参见 node_modules/codeceptjs/docs/api.md。

在 codecept.conf.{js,ts} 的 helpers 下注册这两个辅助器。

绝不要将 this.helpers['Playwright'].browserContext.request.* 用于 API 操作。 这样会绕过 REST + JSONResponse 调用栈——没有步骤日志记录、没有 I.seeResponseCodeIsSuccessful 断言,也没有共享标头。如果 API 需要使用与浏览器相同的身份验证,请在配置顶部共享一次 Cookie:

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

……或者在 REST 辅助器上设置 defaultHeaders 以使用基于令牌的身份验证,也可以在每个测试中使用 I.amBearerAuthenticated(secret(token))。api.md 中介绍了这三种模式。

WebExtra 示例——将 ClientFunction 移植到这里:

import Helper from '@codeceptjs/helper'

export default class WebExtra extends Helper {
  async grabLocationHref() {
    const { page } = this.helpers['Playwright']
    return page.evaluate(() => window.location.href)
  }

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

  async injectClientScript(path) {
    const { page } = this.helpers['Playwright']
    await page.addInitScript({ path })
  }
}

ApiExtras 示例——用于真实 HTTP 调用(准备测试数据,而非模拟响应)的 RequestMock 放在这里:

import Helper from '@codeceptjs/helper'

export default class ApiExtras extends Helper {
  async loginViaApi(email, password) {
    const REST = this.helpers['REST']
    await REST.sendPostRequest('/api/auth/login', { email, password })
  }

  async seedUser(user) {
    const REST = this.helpers['REST']
    const { data } = await REST.sendPostRequest('/api/users', user)
    return data
  }
}

对于请求模拟(RequestMock().onRequestTo(...).respond(...)),请参见阶段 10——这里使用 I.mockRoute(Playwright),而不是 ApiExtras。

辅助器代码风格——适用于两者:

  • 所有 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。

阶段 1 清单中的其他迁移目标:

  • TestCafe 页面对象风格的模块 → 放在 pages/ 下的 CodeceptJS 页面对象类。谨慎移植——仅保留原模块已有的方法;迁移过程中不要自行添加封装。Selector(...) 属性(this.usernameField = Selector('#user'))改为定位器字符串字段(fields = { usernameField: '#user' });方法改写时,应在顶部使用 const { I } = inject(),并调用 I.fillField、I.click 以及 WebExtra / ApiExtras 辅助器新增的任何 I.* 动词。去掉 async t => 这一层封装——方法直接接收参数。在 codecept.conf.{js,ts} 的 include 下注册,以便页面对象自动注入 Scenarios。

    应避免的页面对象反模式(原 TestCafe 模块已有此类做法时除外):

    • 断言方法(checkTitle() { I.seeElement(...) })——页面对象应封装操作动词(fillForm、submitOrder);断言留在测试中。
    • 围绕单个 I.click / I.see* / I.grabTextFrom 编写的单行封装——与在测试中直接调用 I.* 相比,这类封装没有任何好处。
    • 仅被一个测试使用的方法——将这些步骤留在测试中。
    • 任何方法中的 if (cond) throw new Error(...)——改用 I.see*、I.seeNumberOfElements、ExpectHelper 或 codeceptjs/assertions 工厂函数。
  • 共享的 Selector 常量 → 相关页面对象中的字段。不要创建游离的 selectors.{js,ts}。

  • Role 定义 → auth 插件的角色定义(阶段 8)。如果 Role 函数体通过 UI 登录,先将其移植为 WebExtra 方法;如果它访问 API,则移植为 ApiExtras 方法。之后由 auth 插件调用该方法。

  • 不操作浏览器的纯工具模块 → 普通 ES 模块,在需要的位置导入。 继续之前,请先检查:npx codeceptjs check -c <config> 必须通过,且 npx codeceptjs list -c <config> 必须将每个移植的 helper 方法显示为由 WebExtra 或 ApiExtras 提供的 I.* 操作。

5. 转换 spec 文件

一次处理一个文件,尽量使用第 4 阶段的抽象。将每个 spec 的工作交给 writing-codeceptjs-tests 技能——它通过 MCP 驱动实时浏览器,并在提交前验证每个步骤。

TestCafeCodeceptJS 4
文件 *.test.{js,ts}*_test.{js,ts}
fixture('X').page('/x')顶部的 Feature('X') + Before(({ I }) => I.amOnPage('/x'))
test('y', async t => { ... })Scenario('y', ({ I }) => { ... }) — 删除 async t =>,从测试签名中接收 ({ I, … })
.beforeEach(async t => { ... })Before(({ I }) => { ... })
.afterEach(async t => { ... })After(({ I }) => { ... })
.before(...) / .after(...)BeforeSuite(...) / AfterSuite(...)
t.navigateTo('/x')I.amOnPage('/x')
await loginPage.login(u, p)loginPage.login(u, p) — 对返回 void 的页面对象方法不使用 await

移除多余的 await——这是影响最大的机械性修改。TestCafe 要求对每个操作都使用 await;CodeceptJS 禁止对操作使用 await,并将其保留给抓取操作。约定:

I.click('Save')                        // no await
I.fillField('Email', '[email protected]')        // no await
I.see('Saved')                         // no await
I.waitForElement('.toast', 3)          // no await
const text = await I.grabTextFrom('h1')   // await — grabs return data
const ok = await tryTo(() => I.click('Accept'))  // await — effects can return values

如果原始步骤使用 t.ctx.foo = ... 在一次测试中传递状态,请将其存储在 Scenario 回调中声明的普通 let 变量里。t.fixtureCtx(整个套件范围的状态)应改为模块级变量,或存储在由 BeforeSuite 填充的对象中。

迭代——在测试、页面对象和 helpers 中,任何包含 I.* 调用的循环都使用 for...of。绝不要使用 Array.prototype.forEach。.forEach 会吞掉迭代回调的返回值——其中的 await 不会阻塞外层函数,录制器可能会以错误顺序排入步骤,或在循环完成前结束 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 定位器加上上下文,胜过冗长且没有限定范围的定位器:它读起来就像页面本身,能消除重复标签的歧义,又不会越写越长,并且能适应标记变化。将此规则应用于下表的每一行——源框架中的链式调用通常可以清晰地拆分为区域 + 用户看到的内容。

CodeceptJS 优先级——选择最适用的最高优先级定位器,然后添加上下文。TestCafe 的惰性可链式调用 Selector 与 CodeceptJS 的 locate() 构建器十分契合,但多数链式调用都能大幅简化,因为语义字符串加上上下文就能涵盖 .withText + .find 的作用——Selector('.row').withText('Acme').find('.btn') 可改为 I.click('Edit', locate('.row').withText('Acme'))。

  1. 语义字符串 — 按钮文本、标签、占位符、链接文本:I.click('Save', '.toolbar')、I.fillField('Email', '[email protected]', '#login-form')。可简洁地替代 Selector('button').withText('Save')。 普通字符串已经能匹配 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 — 当测试套件使用 data-test / data-qa 属性时。
  4. locate() 构建器 — I.click(locate('.row').withText('Acme').inside('table'))。适用于无法简化为定位器 + 上下文的 TestCafe Selector(...) 链式调用。
  5. CSS / XPath / 属性对象 — { id: 'foo' }、{ name: 'email' }、{ css: '[data-test=submit]' }、{ xpath: '//div[@id="x"]' }。兜底方案。
TestCafe Selector 链式调用CodeceptJS 4
Selector('.btn')'.btn'
Selector('button').withText('Submit')'Submit'(语义定位器)或 locate('button').withText('Submit')
Selector('button').withExactText('Submit')locate('button').withTextEquals('Submit') — 或通过 I.seeTextEquals('Submit', 'button') 断言
Selector('input').withAttribute('name', 'email'){ name: 'email' }
Selector('input').withAttribute('data-test', /^submit/){ css: 'input[data-test^="submit"]' }
Selector('.row').nth(0)step.opts({ elementIndex: 1 })
Selector('.row').nth(-1)step.opts({ elementIndex: 'last' })
Selector('.row').find('.btn')locate('.btn').inside('.row') — 或使用上下文参数 I.click('.btn', '.row')
Selector('.parent').child('.kid')locate('.kid').inside('.parent')
Selector('.row').filter('.active')locate('.row').withClass('active')
Selector('button').parent('.toolbar')不支持单行写法;可重构为 I.click('button', '.toolbar')
Selector(t => t.foo)(函数定位器)使用 page.locator / page.evaluate 编写 WebExtra 中的方法
自定义 t.fixtureCtx.selector = Selector(...)页面对象字段

step.opts(...) 来自 import step from 'codeceptjs/steps'。

7. 操作、断言、获取值

TestCafe 的 Test Controller(t)与 CodeceptJS 的 actor(I)非常接近——大多数操作只需重命名动词。主要改动是:去掉操作中的 await,将 t.expect(sel.X).Y(...) 链合并为单个 I.see* 调用,并将 t.eval / ClientFunction 改写为辅助方法。

TestCafeCodeceptJS 4
await t.click(sel)I.click(sel)
await t.typeText(sel, 'x')I.fillField(sel, 'x')(默认会清空,与 TestCafe 设置 replace: true 时相同)
await t.typeText(sel, 'x', { replace: false })I.appendField(sel, 'x')
await t.pressKey('enter')I.pressKey('Enter')
await t.hover(sel)I.moveCursorTo(sel)
await t.dragToElement(sel, target)I.dragAndDrop(sel, target)
await t.takeScreenshot('x.png')I.saveScreenshot('x.png')
await t.takeElementScreenshot(sel, 'x.png')I.saveElementScreenshot(sel, 'x.png')
await t.resizeWindow(W, H)I.resizeWindow(W, H)
await t.maximizeWindow()I.resizeWindow('maximize')
await t.setNativeDialogHandler(fn)针对每个对话框使用 I.acceptPopup() / I.cancelPopup()
await t.switchToIframe(sel)within({ frame: sel }, () => { ... })
await t.switchToMainWindow()(within 块结束时)
await t.openWindow(url)session('w2', () => I.amOnPage(url))
await t.eval(() => document.title)await I.executeScript(() => document.title)——或使用 WebExtra 中的方法
ClientFunction(() => window.location.href)()await I.grabCurrentUrl()(或使用第 4 阶段的 webExtra.grabLocationHref())
await t.wait(N)(N 毫秒)I.wait(N / 1000)——CodeceptJS 使用秒;避免在已提交的测试中使用
await t.getBrowserConsoleMessages()await I.grabBrowserLogs()
await t.expect(sel.innerText).eql('X')I.seeTextEquals('X', sel)——若要检查“包含”,则用 I.see('X', sel)
await t.expect(sel.innerText).contains('X')I.see('X', sel)
await t.expect(sel.value).eql('X')I.seeInField(sel, 'X')
await t.expect(sel.checked).ok()I.seeCheckboxIsChecked(sel)
await t.expect(sel.classNames).contains('active')I.seeElementHasClass(sel, 'active')
await t.expect(sel.exists).ok()I.seeElementInDOM(sel)
await t.expect(sel.exists).notOk()I.dontSeeElementInDOM(sel)
await t.expect(sel.visible).ok()I.seeElement(sel)
await t.expect(sel.visible).notOk()I.dontSeeElement(sel)
await t.expect(sel.count).eql(N)I.seeNumberOfElements(sel, N)
await t.expect(value).eql(expected)const v = await I.grabXxxFrom(...); I.expectEqual(v, expected)(ExpectHelper)
await sel.innerText(获取值)await I.grabTextFrom(sel)
await sel.getAttribute('data-id')await I.grabAttributeFrom(sel, 'data-id')
await sel.count(获取值)await I.grabNumberOfVisibleElements(sel)

只有获取值的操作才使用 await。普通操作会自动排队。

8. 会话与身份验证

Role(url, async t => { ... }) + t.useRole(role) → 使用 auth 插件。请参考 codeceptjs-auth 的设置指南。该插件会缓存登录后的 Cookie / 存储状态,并在每个测试中重放,这与 TestCafe 中 Role 的行为完全一致。如果第 4 阶段已将 Role 主体移植到 ApiExtras 中作为 I.loginViaApi(...),或移植到 WebExtra 中作为 I.login(...),那么 auth 插件的角色定义只需调用它。

对于多用户场景(TestCafe 通过多个角色和 t.useRole 切换角色来处理),请使用 codeceptjs/effects 中的 session(...)。

9. Fixtures、请求与任务
TestCafeCodeceptJS 4
import users from './fixtures/users.json'import users from './fixtures/users.json' with { type: 'json' }
t.request(...)(TestCafe 1.20+)通过 REST helper 使用 await I.sendPostRequest(...);在 phase 4 中将可复用流程封装到 ApiExtras helper
clientScripts(按页面注入 JS)helpers.Playwright.bootstrap(按上下文初始化脚本),或使用 page.addInitScript 的 WebExtra 方法
通过 fixture('X').meta(...) 添加测试数据Scenario(..., { tag: '@x' }) 加上常量模块

REST helper 身份验证:@codeceptjs/configure 中的 setSharedCookies() 会与 REST 共享浏览器会话,因此同一用户会在两侧登录;或者,为静态令牌设置 defaultHeaders,或在每个测试中使用 I.amBearerAuthenticated(secret(token))。参见 node_modules/codeceptjs/docs/api.md。

10. 网络模拟

RequestMock().onRequestTo(url).respond(body, status, headers) → I.mockRoute(url, route => route.fulfill({ status, headers, body }))(Playwright)。RequestHook 子类也会变成路由处理程序。使用 I.stopMockingRoute(url) 禁用。对于请求日志记录(RequestLogger),没有直接对应项——可基于 UI 结果进行断言(I.waitForText、I.see);如果确实需要测试的请求记录,则在 WebExtra 方法中添加 page.on('request' | 'response')。参见 node_modules/codeceptjs/docs/playwright.md § Mocking Network Requests。

11. 停用 TestCafe

只有在所有测试 spec 都已迁移且 CI 通过后:删除 .testcaferc.{json,js,ts,cjs},从 devDependencies 中移除 testcafe(以及任何 testcafe-browser-provider-*、testcafe-reporter-*、testcafe-react-selectors、testcafe-vue-selectors 附加组件),移除 TestCafe 的 CI 任务,并卸载任何独立的 TestCafe 二进制文件。

验证

  1. npx codeceptjs check -c <config> — 检查配置、helper 和 plugin 是否正常。
  2. npx codeceptjs list -c <config> — 每个已迁移的 helper 方法都应作为来自 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. 将工作交给 codeceptjs-run-analysis,检查 output/trace_*/ 中的产物(需要启用 aiTrace plugin)。
  6. 删除原始 TestCafe 目录前,运行 grep -rE "\\bfixture\\(|\\btest\\(|Selector\\(|ClientFunction\\(|\\bRole\\(|t\\.click\\(|t\\.typeText\\(" tests/,确保结果为空。

相关 skills

  • writing-codeceptjs-tests — 按 spec 重写的操作手册(由 MCP 驱动、步骤经过验证);也介绍如何重新创作 Studio 录制内容
  • debugging-codeceptjs-tests — 从第一次完整运行开始,用于处理每个失败的测试
  • codeceptjs-auth — 替代 Role + t.useRole
  • codeceptjs-fundamentals — 迁移后运行以确认连接配置;tryTo、within 等 effects 可替代 t.switchToIframe
  • 参考文档:node_modules/codeceptjs/docs/(basics、playwright、locators、custom-helpers、api、assertions、pageobjects、sessions、auth)

评论

支持 Markdown