关于 migrate-testcafe-to-codeceptjs
这项迁移 Skill 指导将 TestCafe 测试套件转到 CodeceptJS 4,涵盖项目盘点、配置、可复用 Helper 和页面对象,并说明 TestCafe 特有模式如何对应到 CodeceptJS 的替代方案。
主要功能
- 01盘点 TestCafe 项目
- 02映射到 CodeceptJS 的配置
- 03设置 Playwright Helper
- 04转换自定义 Helper
- 05页面对象迁移指导
- 06替换 TestCafe 模式
适用场景
- 01规划渐进式 TestCafe 迁移
- 02转换 TestCafe 配置
- 03迁移可复用浏览器和 HTTP Helper
- 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.*),都有惰性、可链式调用的选择器,也都内置了基于角色的身份验证以及屏幕截图和视频支持。迁移工作大多是机械性的,但有三个基础差异会影响整个过程:
- CodeceptJS 的操作不需要
await。 录制器会自动将每个I.*调用加入队列。TestCafe 要求每个操作都使用await(await t.click(...));CodeceptJS 则禁止在操作上使用await,只将其用于抓取操作(await I.grabTextFrom(...))。这是转换测试规范时最大的机械性改动——删除I.click、I.fillField、I.see*、I.waitFor*前面,以及会返回 void 的页面对象方法调用前面的所有await。 - 使用 Helpers,而非捆绑式代理。 TestCafe 以 HTTP/HTTPS 代理的形式运行,向页面注入自动化代码;CodeceptJS 则将
I.*调度到已配置的 Helper。推荐使用 Playwright——体验最接近、速度最快,并且与 TestCafe 一样支持三种引擎(Chromium / Firefox / WebKit)。 - 内置一流抽象。 页面对象、多用户
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-testsMCP 脚手架与暂停模式,针对实时浏览器重新创建这些测试。 - 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']),或使用AppiumHelper 测试真实设备。 - 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(...)。转换为对应测试文件中的 CodeceptJSBefore/BeforeSuitehook。 - 自定义 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}:
| TestCafe | CodeceptJS 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 / assertionTimeout | helpers.Playwright.waitForTimeout |
pageLoadTimeout | helpers.Playwright.timeout |
concurrency: N | CLI:npx codeceptjs run-workers N |
screenshots.path / videoPath | 顶层 output: './output' |
screenshots.takeOnFails: true | screenshot 插件,设置 on: 'fail' |
设置了 videoPath | helpers.Playwright.video: true |
clientScripts: ['inject.js'] | 使用 page.addInitScript 的 WebExtra 方法,或使用 bootstrap() |
quarantineMode | 顶层 retry: N |
stopOnFirstFail: true | CLI:--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 驱动实时浏览器,并在提交前验证每个步骤。
| TestCafe | CodeceptJS 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'))。
- 语义字符串 — 按钮文本、标签、占位符、链接文本:
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"]' }。 - ARIA 角色 —
I.click({ role: 'button', name: 'Sign In' }, '#login-form')。现代应用的首选默认方案。 - 通过
customLocator插件使用$name— 当测试套件使用data-test/data-qa属性时。 locate()构建器 —I.click(locate('.row').withText('Acme').inside('table'))。适用于无法简化为定位器 + 上下文的 TestCafeSelector(...)链式调用。- 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 改写为辅助方法。
| TestCafe | CodeceptJS 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、请求与任务
| TestCafe | CodeceptJS 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 二进制文件。
验证
npx codeceptjs check -c <config>— 检查配置、helper 和 plugin 是否正常。npx codeceptjs list -c <config>— 每个已迁移的 helper 方法都应作为来自WebExtra或ApiExtras的I.*操作出现;每个页面对象的方法也都应出现。npx codeceptjs dry-run --steps -c <config>— 每个 Scenario 都能加载。- 完整运行:
npx codeceptjs run --steps -c <config>。首次运行时出现失败是正常的——通过debugging-codeceptjs-testsskill 逐一修复(不要使用retry,也不要盲目重写)。只有整个转换后的测试套件全部通过,迁移才算完成。 - 将工作交给
codeceptjs-run-analysis,检查output/trace_*/中的产物(需要启用aiTraceplugin)。 - 删除原始 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.useRolecodeceptjs-fundamentals— 迁移后运行以确认连接配置;tryTo、within等 effects 可替代t.switchToIframe- 参考文档:
node_modules/codeceptjs/docs/(basics、playwright、locators、custom-helpers、api、assertions、pageobjects、sessions、auth)