@cypress/puppeteer

中文文档
1.0.0发布于 2026/11/13MITTypeScriptCommonJS近 30 天下载 3.1万

Plugin for enhancing Cypress tests with Puppeteer

原始使用文档
文档目录31 节
  1. Table of Contents
  2. Installation
  3. TypeScript
  4. Upgrading to 1.0
  5. Does this affect me?
  6. What to do next
  7. Compatibility
  8. How it works
  9. Retries
  10. Timeouts
  11. Usage
  12. 1. Register message handlers in your Cypress config
  13. 2. Import the support file
  14. 3. Call cy.puppeteer() in your spec
  15. API
  16. setup(options)
  17. retry(functionToRetry[, options])
  18. cy.puppeteer(messageName[, ...args])
  19. Examples
  20. Switching to a new tab
  21. Creating a new tab
  22. Troubleshooting
  23. @cypress/puppeteer does not work in Google Chrome v137 and …
  24. Cannot communicate with the Cypress Chrome extension. Ensur…
  25. Lost the reference to the browser
  26. cy.task('__cypressPuppeteer__') timed out after waiting 600…
  27. Protocol error (Target.createTarget): Not supported
  28. Only browsers in the "Chromium" family are supported
  29. Could not find message handler with the name ...
  30. Contributing
  31. Changelog

@cypress/puppeteer

Use Puppeteer's browser API from your Cypress tests with a single command.

Cypress runs your tests in a single tab, so it can't switch to a tab or window that your app opens, or work with several pages at once. This plugin gives you a Puppeteer Browser connected to the browser Cypress launched. You do that work with Puppeteer, then pass the results back to your test for assertions.

For anything Cypress can do on its own, use Cypress commands instead. They retry automatically and show each step in the Command Log, while cy.puppeteer() doesn't retry and logs a single entry for the whole handler.

Table of Contents

Installation

With npm:

npm install --save-dev @cypress/puppeteer

With yarn:

yarn add --dev @cypress/puppeteer

TypeScript

To get types for cy.puppeteer(), add the support types to your tsconfig.json:

{
  "compilerOptions": {
    "types": ["cypress", "@cypress/puppeteer/support"]
  }
}

The setup and retry functions ship with their own type definitions, so no extra configuration is needed for your Cypress config file.

Upgrading to 1.0

Version 1.0 upgrades the bundled puppeteer-core from v21 to v24 and requires Cypress 14.0.0 or later. The plugin's own API hasn't changed: setup, retry, and cy.puppeteer() work the same way. Your message handlers, however, receive a Puppeteer Browser directly, so Puppeteer's breaking changes between v21 and v24 can affect the code inside them.

We made this upgrade so the plugin stays compatible with current Chrome releases and picks up Puppeteer's more reliable connection handling. Puppeteer v24 needs Node.js 18 or later, which is why Cypress 13, which still supports Node.js 16, is no longer supported.

Does this affect me?

It affects you if either of these is true:

  • You use Cypress 13. Upgrade to Cypress 14.0.0 or later first.
  • Your message handlers use a Puppeteer API that changed.

These are the Puppeteer changes most likely to show up in handlers:

Puppeteer v21Puppeteer v24
page.waitForTimeout(ms)Removed. Wait for a specific condition with page.waitForSelector() or page.waitForFunction(), or use new Promise((resolve) => setTimeout(resolve, ms)).
page.$x(expression) and page.waitForXPath(expression)Removed. Use the ::-p-xpath() selector, for example page.$$('::-p-xpath(//h2)') or page.waitForSelector('::-p-xpath(//h2)').
browser.createIncognitoBrowserContext()Renamed to browser.createBrowserContext().
puppeteer.devicesRemoved. Import KnownDevices from puppeteer-core instead.
addEventListener() and removeEventListener() on Puppeteer objectsRemoved. Use on() and off().
Screenshot and PDF methods are typed as returning a Node.js BufferThey're typed as returning a Uint8Array. In TypeScript, wrap the result in Buffer.from() if you need Buffer methods.
HTTPRequest.url() and HTTPResponse.url() leave out the URL fragmentThey include the fragment, such as #section.

For the complete list, see the breaking changes for versions 22.0.0, 23.0.0, and 24.0.0 in the puppeteer-core changelog.

What to do next

  1. If you're on Cypress 13, upgrade to Cypress 14.0.0 or later.
  2. Search your message handlers for the APIs in the table above and update them.
  3. If you pass your own Puppeteer module with the puppeteer option, upgrade it to v24 as well.
  4. Run your specs that call cy.puppeteer().

If you can't make these changes yet, stay on the previous release with npm install --save-dev @cypress/puppeteer@0 until you're ready.

Compatibility

  • Cypress 14.0.0 or later.
  • Node.js 18 or later for the process that loads your Cypress config, which every Node.js version Cypress 14 supports already meets. The bundled puppeteer-core v24 needs it.
  • Chromium-family browsers only. Calling cy.puppeteer() in Firefox or WebKit fails with an error.

What works depends on the browser and on whether it runs headless (cypress run) or headed (cypress open or cypress run --headed):

BrowserHeadlessHeadedOpen new tabs with browser.newPage()
ElectronYesYesNo
Chrome for Testing, ChromiumYesYesYes
Google Chrome 136 and earlierYesYesYes
Google Chrome 137 and laterYesNoYes, headless only
Other Chromium-based browsers, such as EdgeYesOnly if the browser can load the Cypress extensionYes
Firefox, WebKitNoNoNo

Electron can't open new tabs or windows from Puppeteer. Electron doesn't support creating pages through the Chrome DevTools Protocol, so browser.newPage() fails with Protocol error (Target.createTarget): Not supported. Working with tabs and windows that your app opens, as in Switching to a new tab, works in Electron.

Google Chrome 137 and later can't run the plugin headed. In headed Chromium browsers, the plugin needs the Cypress extension (see How it works), and Chrome 137 removed the --load-extension flag that loads it. When the plugin detects this combination, it throws an error at browser launch. For headed runs, use Electron, Chrome for Testing, or Chromium instead. The Cypress Docker images repository has examples for Chrome for Testing and Chromium.

How it works

The plugin has two halves:

  • In your Cypress config, you register named message handlers with setup(). These handlers run in Node.js, not in the browser.
  • In your spec, you call cy.puppeteer('handlerName', ...args) to run one of them. Under the hood, this is a cy.task(), so it follows the same rules for arguments, return values, and timeouts.

Each time cy.puppeteer() runs, the plugin:

  1. Connects Puppeteer to the browser Cypress launched.
  2. Calls your message handler with that Puppeteer Browser instance and any arguments you passed.
  3. In headed Chromium browsers other than Electron, brings the main Cypress tab back to the front through the Cypress extension, so your test can keep running after you've worked with other tabs.
  4. Disconnects Puppeteer from the browser. The browser itself stays open.
  5. Yields your handler's return value to the Cypress command chain.

Because the connection is created fresh for every call and closed at the end, don't hold on to Browser, Page, or element references between cy.puppeteer() calls. Look them up again inside each handler.

Retries

cy.puppeteer() runs your handler once. If an assertion chained after it fails, such as .should('include', '/new-page'), Cypress doesn't run the handler again. When a handler depends on something that takes time to appear, like a tab that's still opening, wait for it inside the handler with retry or a Puppeteer method such as page.waitForSelector().

Timeouts

Each cy.puppeteer() call is limited by the Cypress taskTimeout setting, which defaults to 60000 milliseconds. cy.puppeteer() doesn't take its own timeout option, so to give a slow handler more time, raise taskTimeout in your Cypress config or for a single test:

it('waits for a slow tab', { taskTimeout: 120000 }, () => {
  cy.puppeteer('waitForSlowTab')
})

Usage

1. Register message handlers in your Cypress config

// cypress.config.ts
import { defineConfig } from 'cypress'
import { setup, retry } from '@cypress/puppeteer'

export default defineConfig({
  e2e: {
    setupNodeEvents (on) {
      setup({
        on,
        onMessage: {
          async getNewTabTitle (browser, urlFragment) {
            // cy.puppeteer() doesn't retry, so wait here until the tab has opened
            const page = await retry(async () => {
              const pages = await browser.pages()
              const page = pages.find((page) => page.url().includes(urlFragment))

              if (!page) throw new Error(`No tab with a URL containing ${urlFragment} yet`)

              return page
            })

            // Whatever you return is yielded by cy.puppeteer() in your spec
            return page.title()
          },
        },
      })
    },
  },
})

2. Import the support file

// cypress/support/e2e.ts
import '@cypress/puppeteer/support'

3. Call cy.puppeteer() in your spec

// cypress/e2e/spec.cy.ts
it('opens a new tab', () => {
  cy.visit('/')
  cy.get('button').click() // opens a new tab

  cy.puppeteer('getNewTabTitle', '/new-page').should('equal', 'New page')
})

API

setup(options)

Call this inside setupNodeEvents to register your message handlers.

import { setup } from '@cypress/puppeteer'

setup({ on, onMessage, puppeteer })

Options

OptionRequiredDescription
onYesThe on event registration function that setupNodeEvents receives.
onMessageYesAn object whose keys are message names and whose values are handler functions. See Message handlers.
puppeteerNoA Puppeteer module (from puppeteer or puppeteer-core) to use instead of the puppeteer-core bundled with this plugin. Install a version that satisfies this plugin's puppeteer-core dependency, so your package manager shares one copy between your project and the plugin. In TypeScript, a module from a different copy fails to type-check, even within the same major version.

setup throws right away if options, on, or onMessage is missing, or if onMessage isn't a plain object.

Message handlers

Each key in onMessage is a name you can pass to cy.puppeteer(). Its function runs in Node.js, so Cypress commands and DOM APIs aren't available inside it — use the Puppeteer API instead.

A handler receives:

  • browser: A Puppeteer Browser instance connected to the Cypress-launched browser.
  • ...args: Any arguments passed to cy.puppeteer() after the message name.

Handlers can be synchronous or async. What they return (or resolve to) is yielded by cy.puppeteer():

  • The return value must be serializable, just like a cy.task() result.
  • Returning undefined yields null, since cy.task() doesn't allow undefined.
  • If a handler throws or rejects, cy.puppeteer() fails the test with the error message.

retry(functionToRetry[, options])

A helper for your message handlers that keeps calling a function until it succeeds. It's useful for things that take a moment to show up, like a tab that's still opening.

import { retry } from '@cypress/puppeteer'

const page = await retry(async () => {
  const pages = await browser.pages()
  const page = pages.find((page) => page.url().includes('page-2.html'))

  if (!page) throw new Error('Could not find page')

  return page
})

functionToRetry

Required. A function (synchronous or async) to call. If it throws or rejects, retry waits and calls it again. As soon as it succeeds, retry resolves with its return value.

options

Optional.

OptionDefaultDescription
timeout4000How long to keep retrying, in milliseconds. Once it's reached, retry rejects with Failed retrying after <timeout>ms: <last error message>.
delayBetweenTries200How long to wait between attempts, in milliseconds.

The timeout is counted in delays between attempts, so time spent inside functionToRetry itself isn't included. A slow function can make retry run longer than timeout.

cy.puppeteer(messageName[, ...args])

Runs the message handler registered under messageName and yields its return value.

cy.puppeteer('testNewTab', 'value 1', 42, [true, false])

messageName

Required. A string matching one of the keys in the onMessage option you passed to setup. If there's no match, the command fails and lists the registered names.

...args

Optional. Values to pass to the message handler. They're sent from the browser to Node.js, so they must be serializable.

For example, the call above arrives in the handler like this:

setup({
  on,
  onMessage: {
    testNewTab (browser, stringArg, numberArg, arrayOfBooleans) {
      // stringArg === 'value 1'
      // numberArg === 42
      // arrayOfBooleans[0] === true, arrayOfBooleans[1] === false
    },
  },
})

Examples

Both examples live in this package's Cypress tests and cypress.config.ts, where you can run them yourself. That config also starts a small static server on http://localhost:8000 and sets it as the baseUrl, which is why the URLs below point there.

The examples use tabs, but the same approach works for windows. Puppeteer treats both as instances of the Page class.

Switching to a new tab

This example shows how to:

  • Find a tab that an action in your Cypress test opened
  • Wait for that tab with retry
  • Read content from the tab with Puppeteer
  • Pass that content back to Cypress for assertions

cypress/e2e/multi-tab.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() // Triggers a new tab to open

  cy
  .puppeteer('switchToTabAndGetContent')
  .should('equal', 'You said: Hello from Page 1')
})

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) {
            // The new tab may not have opened and loaded yet, so keep looking until it has
            const page = await retry<Promise<Page>>(async () => {
              // The browser will eventually have 2 tabs open: the Cypress tab and the new tab.
              // In Puppeteer, tabs and windows are called pages.
              const pages = await browser.pages()
              const page = pages.find((page) => page.url().includes('page-2.html'))

              // Throwing tells `retry` to try again
              if (!page) throw new Error('Could not find page')

              // Returning resolves `retry` with the page
              return page
            })

            // Cypress keeps focus on its own tab, so bring the new page to the front before interacting with it
            await page.bringToFront()

            const paragraph = (await page.waitForSelector('p'))!
            const paragraphText = await page.evaluate((el) => el.textContent, paragraph)

            // Clean up references before finishing
            paragraph.dispose()

            await page.close()

            // This value is yielded by cy.puppeteer() in the spec
            return paragraphText
          },
        },
      })
    },
  },
})

Creating a new tab

This example doesn't work in Electron, because Electron can't open new tabs from Puppeteer. Run it in Chrome for Testing, Chromium, or Edge. See Compatibility for details.

This example shows how to:

  • Pass arguments from cy.puppeteer() to a message handler
  • Open a new tab and visit a page with Puppeteer
  • Pass content from that tab back to Cypress for assertions

cypress/e2e/multi-tab.cy.ts

it('creates a new tab', () => {
  cy.visit('/cypress/fixtures/page-3.html')
  // Read a value from the page and pass it through to the message handler
  cy.get('#message').invoke('text').then((message) => {
    cy
    .puppeteer('createTabAndGetContent', message)
    .should('equal', 'I approve this message: Cypress and Puppeteer make a great combo')
  })
})

cypress.config.ts

import { defineConfig } from 'cypress'
import type { Browser as PuppeteerBrowser } from 'puppeteer-core'
import { setup } from '@cypress/puppeteer'

export default defineConfig({
  e2e: {
    setupNodeEvents (on) {
      setup({
        on,
        onMessage: {
          async createTabAndGetContent (browser: PuppeteerBrowser, text: string) {
            // Opens a new tab in the Cypress-launched browser
            const page = await browser.newPage()

            // `text` comes from the cy.puppeteer() call in the spec
            await page.goto(`http://localhost:8000/cypress/fixtures/page-4.html?text=${encodeURIComponent(text)}`)

            const paragraph = (await page.waitForSelector('p'))!
            const paragraphText = await page.evaluate((el) => el.textContent, paragraph)

            // Clean up references before finishing
            paragraph.dispose()

            await page.close()

            // This value is yielded by cy.puppeteer() in the spec
            return paragraphText
          },
        },
      })
    },
  },
})

Troubleshooting

@cypress/puppeteer does not work in Google Chrome v137 and higher in cypress open mode (or headed run mode)

Chrome 137 removed support for loading extensions from the command line, and the plugin needs the Cypress extension in headed mode. Switch to Electron, Chrome for Testing, or Chromium for headed runs, or run Chrome headlessly with cypress run. See Compatibility for details.

Cannot communicate with the Cypress Chrome extension. Ensure the extension is enabled when using the Puppeteer plugin.

After each handler runs in a headed Chromium browser, the plugin asks the Cypress extension to bring the main Cypress tab back to the front. This error means the extension didn't respond within 2 seconds. A few things to check:

  • If you're using Google Chrome 137 or later, switch to Chrome for Testing or Chromium. See download instructions.
  • Make sure the Cypress extension is enabled in the browser Cypress launched by visiting chrome://extensions/.
  • Make sure your company's security policy allows the Cypress extension. Its ID is caljajdfkjjjdehjdoimjkkakekklcck.

Lost the reference to the browser

This usually happens when your Cypress config reloads but the browser doesn't relaunch. Close the browser and open it again from Cypress.

cy.task('__cypressPuppeteer__') timed out after waiting 60000ms

A message handler took longer than the Cypress taskTimeout. Check whether the handler is waiting for something that never happens, such as a tab with a URL that doesn't match. If the handler is just slow, raise taskTimeout. See Timeouts.

Protocol error (Target.createTarget): Not supported

Your message handler called browser.newPage() while running in Electron, which can't open new tabs or windows from Puppeteer. Run the spec in Chrome for Testing, Chromium, or Edge instead. Tabs and windows that your app opens itself still work in Electron.

Only browsers in the "Chromium" family are supported

You're running in Firefox or WebKit. Switch to a Chromium-family browser such as Electron, Chrome for Testing, Chromium, or Edge.

Could not find message handler with the name ...

The name passed to cy.puppeteer() doesn't match any key in onMessage. The error lists the names that are registered, so it's usually a quick typo fix.

Contributing

This package lives in the Cypress monorepo. Run yarn from the repository root first, then run these commands from npm/puppeteer.

Build the TypeScript files into dist/:

yarn build

Rebuild whenever a file changes:

yarn watch

Type-check without emitting files (yarn build doesn't fail on type errors, so run this before opening a PR):

yarn check-ts

Lint:

yarn lint

Run the unit tests once:

yarn test

Run the unit tests in watch mode:

yarn test-watch

Open the Cypress tests:

yarn cypress:open

Run the Cypress tests once in Chrome:

yarn cypress:run

Changelog

版本与发布记录

@cypress/puppeteer 共发布 14 个版本,首次发布于 2023/11/29;过去 12 个月发布了 5 个版本。

版本发布日期距今
1.0.0latest2026/11/13今天
0.0.0-dev.876162026/11/0310 天前
0.0.0-dev.876132026/11/0310 天前
0.1.102026/10/011 个月前
0.1.92026/09/251 个月前
0.1.82025/10/041.1 年前

关于 @cypress/puppeteer 的常见问题

@cypress/puppeteer 是什么?

Plugin for enhancing Cypress tests with Puppeteer

如何安装 @cypress/puppeteer?

在项目目录运行 npm install @cypress/puppeteer,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:cypress(>=14.0.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@cypress/puppeteer 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@cypress/puppeteer 支持 ESM 还是 CommonJS?

以 CommonJS 形式发布,可以直接 require();在 ES Module 项目中也可以用 import 引入(Node.js 会把 module.exports 作为默认导出)。

@cypress/puppeteer 可以商用吗?

@cypress/puppeteer 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@cypress/puppeteer 的下载量有多少?

根据 npm 官方下载统计,@cypress/puppeteer 在 2026/10/11 – 2026/11/09 这 30 天内被下载 30,608 次,日均约 1,020 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@cypress/puppeteer 还在维护吗?

最新版本 1.0.0 发布于 2026/11/13(今天);至今共发布 14 个版本,其中过去 12 个月发布了 5 个;首次发布于 2023/11/29。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。