@cypress/puppeteer 插件解析:在 Cypress 中调用 Puppeteer API 实现多标签页与高级浏览器自动化
【免费下载链接】cypressFast, easy and reliable testing for anything that runs in a browser.项目地址: https://gitcode.com/GitHub_Trending/cy/cypress
@cypress/puppeteer是 Cypress 官方仓库(npm/puppeteer子包)中提供的公开测试版 npm 插件,它通过 Chrome DevTools Protocol(CDP)把 Puppeteer 的浏览器 API 引入 Cypress 测试命令,让团队可以在既有 Cypress 测试旁使用 PDF 生成、多标签页切换、更底层的浏览器控制等 Puppeteer 能力。阅读完本文,你将掌握该插件的进程架构、setup/cy.puppeteer()/retry三件套 API、Node 端消息处理器与浏览器端命令的数据流,并能直接复刻仓库中"切换新标签页""创建新标签页"两套可运行示例与完整开发调试命令。
插件定位:为何需要在 Cypress 中使用 Puppeteer
Cypress 的 DOM 自动化能力强大,但在以下场景中会力不从心:
- 由应用代码触发(非测试代码
window.open)打开的新标签页或新窗口; - 通过 Cypress 命令难以触达的浏览器原生行为;
- 需要生成 PDF、管理下载、操作 DevTools 协议层能力的高级浏览器控制。
@cypress/puppeteer正是为此而生:它不要求你放弃 Cypress,而是把 Puppeteer 连接到Cypress 自己启动的浏览器实例上,让你在断言依旧由 Cypress 完成的前提下,用 Puppeteer API 弥补 Cypress 的覆盖盲区。
该包当前处于public beta(仓库内版本号由 semantic-release 管理,见 package.json,README 与 AGENTS.md 均明确标注测试版状态),意味着可能出现破坏性变更,反馈由 Cypress 官方维护并跟踪。
运行前提与兼容性
使用前需要满足以下条件(依据 README.md 的 Compatibility 一节与 package.json 的peerDependencies):
| 条件 | 要求 | 说明 |
|---|---|---|
| Cypress 版本 | >= 13.6.0 | 在 package.json 中声明为 peerDependency,同时setup内部依赖after:browser:launch事件注册 |
| 浏览器 | 仅支持 Chromium 系 | Chrome for Testing、Chromium、Electron 均可用;插件依赖 CDP 连接 Cypress 托管的浏览器 |
| Chrome 品牌限制 | 137+ 版本不支持有头模式 | 因 Chrome 移除了--load-extension标志,cypress open与cypress run --headed下不可用;headless 的cypress run不受影响 |
这一限制并非文档说辞,而是被写进了插件运行时逻辑:setup.ts 在after:browser:launch回调中检查majorVersion >= 137 && browser.name === 'chrome' && browser.isHeaded,命中即抛出错误并建议改用 Electron、Chrome for Testing 或 Chromium。
安装与项目骨架
# npm npm install --save-dev @cypress/puppeteer # yarn yarn add --dev @cypress/puppeteer使用 TypeScript 时,在tsconfig.json中加入类型声明:
{ "compilerOptions": { "types": ["cypress", "@cypress/puppeteer/support"] } }包结构上,package.json 声明main指向dist/plugin/index.js,发布内容仅包含dist与support两个目录,其中support/下预置了已编译的index.js与index.d.ts,供支持文件的import '@cypress/puppeteer/support'直接引用。
仓库中该包的源码布局如下(与 AGENTS.md 的 Architecture 一节一致):
npm/puppeteer/ ├── src/ │ ├── plugin/ # Node 端插件代码(在 setupNodeEvents 中注册) │ │ ├── index.ts # 插件主入口,导出 setup 与 retry │ │ ├── setup.ts # 初始化与 Cypress 托管浏览器的 Puppeteer 连接 │ │ ├── activateMainTab.ts # 聚焦/重新激活主浏览器标签 │ │ ├── retry.ts # Puppeteer 操作的重试工具 │ │ └── util.ts # 共享工具(pluginError) │ └── support/ │ └── index.ts # 浏览器端支持命令,注册 cy.puppeteer() ├── support/ # 发布用的支持文件目录(独立于 src/support 编译产物) ├── cypress/ │ ├── e2e/multi-tab.cy.ts # 多标签页端到端测试 │ └── fixtures/{page-1..4}.html # 示例页面 └── test/unit/{setup,retry,activateMainTab}.spec.ts # vitest 单元测试核心架构:一个浏览器端命令 + 一个 Node 端任务
理解该插件的关键是把握"代码跑在哪儿"。README 明确说明:cy.puppeteer()在浏览器(测试运行页)中执行,但绝大部分 Puppeteer 自动化代码跑在Node 进程(Cypress 配置)里,调用方式与cy.task()的消息传递模型一致。
完整调用链如下:
- 测试代码调用
cy.puppeteer('messageName', ...args),命令名与参数均为字符串/可序列化数据; - 浏览器端支持命令 src/support/index.ts 内部透传执行
cy.task('__cypressPuppeteer__', { name, args }, { log: false }); - Node 端 setup.ts 注册了同名 task:校验后通过
puppeteer.connect({ browserWSEndpoint: debuggerUrl })连上 Cypress 启动的浏览器,再调用onMessage中对应的消息处理器; - 处理器的返回值回到浏览器端,作为
cy.puppeteer()的 yield 值参与 Cypress 断言。
其中debuggerUrl来自after:browser:launch事件中的options.webSocketDebuggerUrl(见 setup.ts),这正是 CDP 连接的端点。因为 Cypress 与 Puppeteer 共享同一个浏览器进程,所以 Puppeteer 操作的新标签页与 Cypress 测试在同一个浏览器会话内。
值得注意的边界行为:
- 支持命令不是 Cypress 命令链:
cy.puppeteer()返回的是 Promise-like 值(实际由cy.task派生),可以直接.should(...)断言,但处理器代码运行于 Node,不能在其中调用任何 Cypress 命令或 DOM API; - 参数必须可 JSON 序列化:
cy.puppeteer(messageName, ...args)的 args 会反序列化后透传给处理器; - 错误打包:Node 端处理器抛出的错误会被
messageHandlerError序列化为{ __error__: { name, message, stack } },浏览器端检测到__error__后重新抛出,保证测试失败信息完整(见 setup.ts 与 src/support/index.ts); - undefined 归一化:
cy.task()在返回undefined时会报错,因此 Node 端在处理器无返回值时统一转为null(见 setup.ts)。
API 详解
setup(options) —— Cypress 配置端注册
在cypress.config.ts的setupNodeEvents(on)中调用,用于注册运行 Puppeteer 自动化的消息处理器:
import { setup } from '@cypress/puppeteer' export default defineConfig({ e2e: { setupNodeEvents (on) { setup({ on, onMessage: { async myMessageHander (browser) { // 利用 Puppeteer browser 实例操作浏览器 }, }, }) }, }, })选项:
| 选项 | 必填 | 说明 |
|---|---|---|
on | 是 | setupNodeEvents提供的on事件注册函数 |
onMessage | 是 | 键为字符串、值为函数的对象(详见下节) |
puppeteer | 否 | 从puppeteer-core导入的 Puppeteer 库实例,覆盖插件默认版本 |
setup对参数做了严格校验:options 必须是普通对象、on必须是函数、onMessage必须是普通对象,否则抛出错误(见 setup.ts)。当未传puppeteer时,默认使用插件依赖的puppeteer-core(package.json 声明依赖puppeteer-core ^21.2.1)。
onMessage —— 消息处理器
onMessage对象的键就是测试中cy.puppeteer(key)调用的名字;其值是在 Node.js 中执行的 Puppeteer 代码。处理器接收两个参数:
browser:连接到 Cypress 启动浏览器的 Puppeteer Browser 实例;...args:测试里传给cy.puppeteer()的反序列化参数。
// 测试侧 cy.puppeteer('testNewTab', 'value 1', 42, [true, false]) // 配置侧 setup({ on, onMessage: { testNewTab (browser, stringArg, numberArg, arrayOfBooleans) { // stringArg === 'value 1' // numberArg === 42 // arrayOfBooleans[0] === true / arrayOfBooleans[1] === false } } })retry(functionToRetry[, options]) —— 重试工具
由于新开标签页从触发到可交互存在时间差,插件提供了重试工具让首次可能失败的操作可靠完成(实现见 retry.ts):
retry(async () => { // 若抛错则按间隔重试,成功则返回其返回值 }, { timeout: 4000, // 总超时,默认 4000ms delayBetweenTries: 200, // 重试间隔,默认 200ms })从源码看,retry的实现是递归makeAttempt:每次尝试抛错就等待delayBetweenTries,累计时间达到timeout后抛出Failed retrying after ${timeout}ms: ${err.message}。
cy.puppeteer(messageName[, ...args]) —— 测试侧命令
messageName(必填):字符串,须与setup的onMessage某个键一致;...args(可选):透传给消息处理器的值,必须 JSON 可序列化。
支持文件侧只需一行导入即可获得类型与命令:
// cypress/support/e2e.ts import '@cypress/puppeteer/support'实战示例:多标签页测试
仓库在 cypress/e2e/multi-tab.cy.ts 和 cypress.config.ts 中给出了可直接运行的完整示例,两个用例恰好覆盖了插件的两大典型用法:接管 Cypress 操作打开的新标签页与用 Puppeteer 创建并驱动新标签页。以下代码即来自该仓库,可直接复制使用。
示例一:切换到 Cypress 触发打开的新标签页
该示例演示:切换到测试动作打开的新标签页 → 用retry获取 Page 实例 → 读取页面内容 → 交回 Cypress 断言。
cypress.config.ts
import { defineConfig } from 'cypress' import type { Browser as PuppeteerBrowser, Page } from 'puppeteer-core' import { setup, retry } from '@cypress/puppeteer' export default defineConfig({ e2e: { setupNodeEvents (on) { setup({ on, onMessage: { async switchToTabAndGetContent (browser: PuppeteerBrowser) { // 页面可能尚未打开加载完毕,因此用 retry 轮询查找目标页面 const page = await retry<Promise<Page>>(async () => { // 在 Puppeteer 中,标签页与窗口统一抽象为 Page 实例 const pages = await browser.pages() // 找到我们想交互的那个页面 const page = pages.find((page) => page.url().includes('page-2.html')) // 找不到就抛错,向 retry 表明需要重试 if (!page) throw new Error('Could not find page') // 找到则返回 page,retry 会原样返回它 return page }) // Cypress 会把焦点保持在它自己的标签页上,交互前通常需要把目标页带到前台 await page.bringToFront() const paragraph = (await page.waitForSelector('p'))! const paragraphText = await page.evaluate((el) => el.textContent, paragraph) // 结束前清理元素句柄引用 paragraph.dispose() await page.close() // 返回的文本就是 cy.puppeteer() 在 spec 中 yield 的值 return paragraphText }, }, }) }, }, })spec.cy.ts
it('switches to a new tab', () => { cy.visit('/cypress/fixtures/page-1.html') cy.get('input').type('Hello from Page 1') cy.get('button').click() // 触发打开新标签页 cy .puppeteer('switchToTabAndGetContent') .should('equal', 'You said: Hello from Page 1') })示例二:用 Puppeteer 创建新标签页并传参
该示例演示:向插件传入自定义版本的 Puppeteer → 从cy.puppeteer()传参给消息处理器 → 创建新标签页访问 URL → 取回内容断言。
cypress.config.ts
import { defineConfig } from 'cypress' import puppeteer, { Browser as PuppeteerBrowser, Page } from 'puppeteer-core' import { setup, retry } from '@cypress/puppeteer' export default defineConfig({ e2e: { setupNodeEvents (on) { setup({ on, // 传入你自己的 puppeteer 版本来替换插件默认版本 puppeteer, onMessage: { async createTabAndGetContent (browser: PuppeteerBrowser, text: string) { // 在 Cypress 启动的浏览器内创建新标签页 const page = await browser.newPage() // text 来自测试中 cy.puppeteer() 的调用参数 await page.goto(`http://localhost:8000/cypress/fixtures/page-4.html?text=${text}`) const paragraph = (await page.waitForSelector('p'))! const paragraphText = await page.evaluate((el) => el.textContent, paragraph) paragraph.dispose() await page.close() return paragraphText }, }, }) }, }, })spec.cy.ts
it('creates a new tab', () => { cy.visit('/cypress/fixtures/page-3.html') // 从页面动态取值,再透传给 puppeteer 消息处理器 cy.get('#message').invoke('text').then((message) => { cy .puppeteer('createTabAndGetContent', message) .should('equal', 'I approve this message: Cypress and Puppeteer make a great combo') }) })提示:Puppeteer 视角下标签页与窗口本质相同,都由 Page 类封装,因此上述两例只需稍加调整即可推广到多窗口场景。
底层机制:主标签页如何"归还"给 Cypress
多标签自动化有一个隐蔽问题:Puppeteer 操作后焦点停留在非 Cypress 标签页上,会影响后续 Cypress 命令。插件用 activateMainTab.ts 解决——每个消息处理器结束后,在特定条件下把主标签页重新激活:
- 仅面向有头的 Chromium 且非 Electron场景:Electron 没有标签概念,无头浏览器不涉及焦点、旧版 headless 也不运行扩展(对应注释见 setup.ts);
- 实现上先取
browser.pages()的第一个页面,执行page.evaluate注入sendActivationMessage:向windowpostMessage 发送cypress:extension:activate:main:tab,监听 Cypress Chrome 扩展回发的cypress:extension:main:tab:activated消息; - 整个过程有
ACTIVATION_TIMEOUT = 2000毫秒超时(见 activateMainTab.ts)。
如果激活失败,Node 端会返回知名错误:"Cannot communicate with the Cypress Chrome extension. Ensure the extension is enabled when using the Puppeteer plugin."。这个扩展在 open 模式下对焦点恢复至关重要,排查建议(README Troubleshooting 一节):
- Chrome 品牌 137+ 请改用 Chrome for Testing / Chromium;
- 在 Cypress 启动的 Chrome 中访问
chrome://extensions/确认扩展已启用; - 确保障碍安全策略放行该扩展(扩展 id
caljajdfkjjjdehjdoimjkkakekklcck)。
仓库内的测试与验证方式
插件在仓库内配有两层验证,可作为你自行改造或复刻时的参考:
单元测试(vitest)—— 覆盖三个核心模块:
- test/unit/setup.spec.ts:验证
setup的参数校验、task 注册与错误打包; - test/unit/retry.spec.ts:验证重试时机、成功返回与超时抛错;
- test/unit/activateMainTab.spec.ts:验证主标签激活逻辑。
端到端测试(Cypress 自举)—— cypress/e2e/multi-tab.cy.ts 使用插件自身跑通两个多标签用例,配套 cypress.config.ts 用 express 在localhost:8000托管cypress/fixtures/下的静态页面(page-1.html到page-4.html),模拟真实应用交互。
开发命令速查
AGENTS.md 的 Key Commands 一节与 package.json 的 scripts 保持了一致,常用命令如下:
yarn build # rimraf dist + tsc,产物输出到 dist/ yarn watch # rimraf dist + tsc 监听模式(增量重编译) yarn check-ts # TypeScript 类型检查,不输出(tsc --noEmit) yarn lint # ESLint 检查 yarn test -- src/plugin/setup.spec.ts # 运行指定 vitest 单测文件 yarn test -- "src/**/*.spec.ts" # 按 glob 运行匹配的单测 yarn cypress:run -- --spec "cypress/e2e/puppeteer.cy.ts" --browser chrome # 定向运行某个集成测试(需 Chrome)注意:cypress:run脚本显式传递了--browser chrome(见 package.json),因为插件的 CDP 连接依赖 Chromium 系浏览器;而test脚本底层是 vitest(vitest run)。仓库内的集成测试文件实际名为 multi-tab.cy.ts,运行时可用--spec "cypress/e2e/multi-tab.cy.ts"精确定位。
易错点小结
综合文档与源码,使用该插件时请留意:
- 处理器是 Node 代码:
onMessage函数体里不能使用 Cypress 命令、cy.*或 DOM API; - 有头 Chrome 137+ 不可用:headed 场景改选 Electron / Chrome for Testing / Chromium;
- 需要扩展配合:open/headed 模式下主标签回收依赖 Cypress Chrome 扩展,禁用会触发明确报错;
- 善用
retry:新标签页加载有竞态,先轮询browser.pages()匹配 URL 再交互是最稳的写法; - 关闭与清理:示例中
paragraph.dispose()、page.close()、Node 端browser.disconnect()共同保证会话与句柄被及时释放。
如需完整的 API 文档与更多细节,可继续查阅仓库内 npm/puppeteer/README.md 与 npm/puppeteer/CHANGELOG.md。
【免费下载链接】cypressFast, easy and reliable testing for anything that runs in a browser.项目地址: https://gitcode.com/GitHub_Trending/cy/cypress
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考