
Discover the potent combination of Cypress and the Puppeteer plugin, revolutionizing the way developers/testers approach multi-tab test scenarios. This dynamic duo extends your testing capabilities and closely mimics real-world user behavior, offering unparalleled insights into your web applications’ performance under diverse conditions.
Prerequisites
To leverage the power of the @cypress/puppeteer plugin (Note that this is still in development branch and not is release branch), ensure you’re running Cypress version 13.6.0 or higher. This setup is optimized for Chromium-based browsers, such as Chrome, Chromium, and Electron, providing a seamless testing experience.
Kickstarting Your Journey
- Begin by cloning the repository.
- Navigate to the repo’s root directory and execute the following command to install necessary dependencies:
npm install
3. Start the react app
npm start
4. To run cypress tests in open mode
npx cypress open
Harnessing Cypress for Multi-Tab Testing
The @cypress/puppeteer plugin seamlessly integrates Puppeteer with Cypress, introducing a new command cy.puppeteer(). This command executes Puppeteer scripts directly within the browser context, as defined in the cypress.config.js{ts} file. To incorporate this plugin into your existing repository, install it as a development dependency:
npm install --save-dev @cypress/puppeteer
For TypeScript projects, add the following types to your tsconfig.json:
{
"compilerOptions": {
"types": ["cypress", "@cypress/puppeteer/support"]
}
}
Further, include the plugin in your support/e2e.js{ts} file:
import '@cypress/puppeteer/support'
Now, you’re all set to write Puppeteer scripts within the cypress.config.js{ts} file, enabling tests that simulate user interactions across multiple tabs.
Illustrative Test Scenario
Imagine a web application with a landing page that links to a user registration form in a new tab. Our test will validate the following user journey:
1. Access the landing page.
2. Click on the user registration link, opening the form in a new tab.

3. Fill out and submit the registration form.

4. Confirm the success message upon form submission.

Test Implementation
A typical Cypress test for this scenario might look as follows:
it("Multitab example using cypress-puppeteer", () => {
cy.visit("/");
cy.get("#registrationLink").click();
cy.puppeteer("switchToNewTabAndRegisterUser").should(
"equal",
"User Registration Completed"
);
});
This script performs a series of actions: visiting the homepage, interacting with the registration link, and utilizing the cy.puppeteer() command to execute a Puppeteer script for form submission and validation.
Code break down is as follows:
Cypress command to visit the url
cy.visit("/")
Cypress command to get the register link, validate the link’s text and then click on the link
cy.get("#registrationLink").should("have.text", "Register Here").click();
Below command is coming from Cypress Puppeteer plugin
cy.puppeteer("switchToNewTabAndRegisterUser").should(
"equal",
"User Registration Completed"
);
This is similar to cy.task() where we pass the task name and with cy.puppeteer() we pass the message handler name which we are going to write it in cypress.config.js file and the message handler will return the success message once the user registration is completed.
Now let’s write the message handler in cypress.config.js file and which will look like below
import { defineConfig } from "cypress";
import { Browser as PuppeteerBrowser, Page } from "puppeteer-core";
import { setup, retry } from "@cypress/puppeteer";
module.exports = defineConfig({
e2e: {
baseUrl: "http://localhost:3000/",
setupNodeEvents(on, config) {
// implement node event listeners here
setup({
on,
onMessage: {
async switchToNewTabAndRegisterUser(browser: PuppeteerBrowser) {
const page = await retry<Promise<Page>>(async () => {
const pages = await browser.pages();
const page = await pages.find(page =>
page.url().includes("register")
);
if (!page) {
throw new Error("Could not find page");
}
return page;
});
await page.bringToFront();
await page.waitForSelector("#firstname");
await page.type("#firstname", "test");
await page.type("#lastname", "test");
await page.type("#email", "test@test.com");
const submitButton = await page.$("#submit");
await submitButton.click();
const successMsg = await page.$eval(
"#successMsg",
el => el.textContent
);
submitButton.dispose();
await page.close();
return successMsg;
}
}
});
}
}
});
Code Walk through
We are importing below classes and functions
import { defineConfig } from "cypress";
import { Browser as PuppeteerBrowser, Page } from "puppeteer-core";
import { setup, retry } from "@cypress/puppeteer";
Note that PuppeteerBrowser and Page are classes from puppeteer-core and setup, retry functions are from @cypress/puppeteer
We are going to use them in writing our Message Handler like below in the e2e config and within setupNodeEvents
e2e: {
baseUrl: "http://localhost:3000/",
setupNodeEvents(on, config) {
// implement node event listeners here
setup({
on,
onMessage: {
async switchToNewTabAndRegisterUser(browser: PuppeteerBrowser) {
const page = await retry<Promise<Page>>(async () => {
const pages = await browser.pages();
const page = await pages.find(page =>
page.url().includes("register")
);
if (!page) {
throw new Error("Could not find page");
}
return page;
});
await page.bringToFront();
await page.waitForSelector("#firstname");
await page.type("#firstname", "test");
await page.type("#lastname", "test");
await page.type("#email", "test@test.com");
const submitButton = await page.$("#submit");
await submitButton.click();
const successMsg = await page.$eval(
"#successMsg",
el => el.textContent
);
submitButton.dispose();
await page.close();
return successMsg;
}
}
});
}
}
Now we focus on the setup function
setup({
on,
onMessage: {
async switchToNewTabAndRegisterUser(browser: PuppeteerBrowser) {
const page = await retry<Promise<Page>>(async () => {
const pages = await browser.pages();
const page = await pages.find(page =>
page.url().includes("register")
);
if (!page) {
throw new Error("Could not find page");
}
return page;
});
await page.bringToFront();
await page.waitForSelector("#firstname");
await page.type("#firstname", "test");
await page.type("#lastname", "test");
await page.type("#email", "test@test.com");
const submitButton = await page.$("#submit");
await submitButton.click();
const successMsg = await page.$eval(
"#successMsg",
el => el.textContent
);
submitButton.dispose();
await page.close();
return successMsg;
}
}
});
}
We create on new Message Handler within onMessage, which says as soon as the Message is called, execute the code which is contained within Message Handler – In our case the Message Handler is switchToNewTabAndRegisterUser
async switchToNewTabAndRegisterUser(browser: PuppeteerBrowser) {
const page = await retry<Promise<Page>>(async () => {
const pages = await browser.pages();
const page = await pages.find(page =>
page.url().includes("register")
);
if (!page) {
throw new Error("Could not find page");
}
return page;
});
await page.bringToFront();
await page.waitForSelector("#firstname");
await page.type("#firstname", "test");
await page.type("#lastname", "test");
await page.type("#email", "test@test.com");
const submitButton = await page.$("#submit");
await submitButton.click();
const successMsg = await page.$eval(
"#successMsg",
el => el.textContent
);
submitButton.dispose();
await page.close();
return successMsg;
}
}
Observe that switchToNewTabAndRegisterUser takes the parameter browser and this parameter input will be injected by our Cypress when this message handler is called in the code.
Now coming to Puppeeteer code, below will be the way our code is created
- Create page reference
const page = await retry<Promise<Page>>(async () => {
const pages = await browser.pages();
const page = await pages.find(page =>
page.url().includes("register")
);
if (!page) {
throw new Error("Could not find page");
}
return page;
});
Observe that we are using retry function(There could be delay in the new page load and hence this retry function) and within that we are getting all the pages from the current browser and then finding the required page which is open and returning it back. We also throw error if we dont find the expected page.
2. Once we get the required page reference, we fill the form and return the success message
await page.bringToFront();
await page.waitForSelector("#firstname");
await page.type("#firstname", "test");
await page.type("#lastname", "test");
await page.type("#email", "test@test.com");
const submitButton = await page.$("#submit");
await submitButton.click();
const successMsg = await page.$eval(
"#successMsg",
el => el.textContent
);
submitButton.dispose();
await page.close();
return successMsg;
The message will then be asserted from chai’s assertion command
Conclusion
Blending Cypress with Puppeteer for multi-tab testing not only elevates your testing game but also ensures your applications can withstand the complex interactions modern users expect. This guide serves as a starting point for developers and testers eager to explore the depth of testing possible with these tools. Embrace experimentation within your testing practices to discover the full potential of your web applications.

Leave a comment