Editor’s note: This tutorial was last updated and validated for accuracy on 20 September 2022.
Testing is a crucial factor in ensuring that your application works as expected. In React applications, there are three common approaches to testing:
End-to-end tests simulate actual user actions and are designed to test how a real user would likely use the application. In React, E2E testing helps to ensure that the code you wrote is functional and your app works as intended, allowing you to catch bugs in your code before your app is live.
While there are many testing frameworks available for React, in this tutorial, we’ll perform end-to-end testing in React using Jest and Puppeteer, two popular testing tools. As an example of E2E testing, we’ll verify that our webpage displays text and handles page navigation and form submissions as intended. Let’s get started!
Jest is a testing tool created by Facebook for testing React apps. It’s also used to test Babel, JavaScript, Node.js, Angular, and Vue apps, and it can be used with NestJS and GraphQL as well.
Designed with simplicity in mind, Jest offers a powerful and elegant API for building isolated tests, snapshot comparison, mocking, test coverage, and much more.
Puppeteer is a Node.js library that provides a high-level API to control headless Chrome or Chromium over the DevTools Protocol.
With Puppeteer, you can scrape websites, generate screenshots and PDFs of pages, automate your form submissions, test your UI, access webpages and additional information using the DOM API, automate performance analysis, act as a crawler for SPA, and generate pre-rendered content.
In this tutorial, we’ll use Puppeteer to carry out tests from a user’s perspective.
To show how E2E testing works in React, we’ll write tests for a functional React app and see what happens when the tests pass and fail. To get started, we’ll use Create React App to create a new React app:
npx create-react-app react-puppeteer
Once the project directory has been created and installed, navigate to the newly created directory and run the command below in your terminal to install Puppeteer in your codebase:
yarn add puppeteer
Start the React app with the yarn start
command, and the dev server should be available at localhost:3000
. You should see a welcome page similar to the one below:
For our first end-to-end test, we’ll ensure that the correct text is displayed on the page. In our example, we want to verify that the text Edit src/App.js and save to reload.
is present on the homepage.
Create a new file called e2e.test.js
and edit it with the code below:
import puppeteer from "puppeteer"; describe("App.js", () => { let browser; let page; beforeAll(async () => { browser = await puppeteer.launch(); page = await browser.newPage(); }); it("contains the welcome text", async () => { await page.goto("http://localhost:5000"); await page.waitForSelector(".App-welcome-text"); const text = await page.$eval(".App-welcome-text", (e) => e.textContent); expect(text).toContain("Edit src/App.js and save to reload."); }); afterAll(() => browser.close()); });
In the beforeAll
function, which is called before any of the tests in a test file are run, a new instance of Puppeteer is created and assigned to the browser
variable. In turn, a new page instance is created and assigned to the page
variable.
A good way to test that a page has a text or a certain element is to test whether that element’s selector is present on the page. In the test block, we tell Puppeteer to first navigate to localhost:5000
, which is a server that we’ll set up shortly, using the goto
method. Then, using the waitForSelector
method, Puppeteer waits for an element with the .App-welcome-text
selector.
We then use the content of the element with the selector to assert whether the page loads the correct text. Puppeteer allows you to target elements on a page using selectors, which could be a CSS class
or an id
, and then test for different cases.
To run the end-to-end test, we’ll use react-scripts
, which has Jest installed internally. Modify the package.json
file and add the code below to the scripts
object:
"test": "react-scripts test --testPathIgnorePatterns=.*e2e.test.js", "test:e2e": "react-scripts test --testPathPattern=.*e2e.test.js$",
This will ensure that yarn test
runs all tests except for end-to-end tests, i.e., unit tests, and the yarn test:e2e
only runs end-to-end tests. The --testPathPattern
means that Jest will only run files that have the pattern e2e.test.js
.
Before we run the test, we need to build the React app for production and set it up on a simple local server. Run the following commands:
yarn build yarn global add serve serve -s build
The yarn build
command builds the app for production and stores the files built in a folder called build
. To set up a local server to host the built files, we’ll install serve
, a static file server and directory listing server. The serve -s build
command starts up the server, and you should then have the React app up and running at localhost:5000
.
Now, run the yarn test:e2e
command in another terminal window or tab. The test should start running and pass, just like the screenshot below:
Because Puppeteer allows you to mimic user interactions on a page, we can test to verify that a page navigates to the next correct page.
This GitHub branch contains a modified version of the Create React App app we created earlier. I’ve gone ahead and installed React Router and also added a new page at the /about
route that simply displays the text This is the about page
.
Clone the branch and start the React app with yarn start
:
We’ll write a test to verify that clicking the about
page link on the homepage loads the about
page. Create a new file called e2e.test.js
and edit it with the code below:
import puppeteer from "puppeteer"; describe("App.js", () => { let browser; let page; beforeAll(async () => { browser = await puppeteer.launch(); page = await browser.newPage(); }); it("navigates to the about page", async () => { await page.goto("http://localhost:5000"); await page.waitForSelector(".App-welcome-text"); await page.click("#about-page-link"); await page.waitForSelector(".App-welcome-text"); const text = await page.$eval(".App-welcome-text", (e) => e.textContent); expect(text).toContain("This is the about page."); }); afterAll(() => browser.close()); });
Like the previous test we wrote, the beforeAll
function is used to create new instances of Puppeteer and a new page. Then, we navigate to localhost:5000
and wait for the page to load.
In the App.js
file, the about
page link has an ID of about-page-link
, which is what we used as a selector to simulate clicking on a link. We can do so by using Puppeteer’s .click()
method. The method triggers a navigation event in the browser, i.e., going backward or forward in the browser’s history.
The command below builds the app and starts a simple static server so that the React app is available at localhost:5000
:
yarn build && serve -s build
Now, run the yarn test:e2e
command in another terminal window or tab, and the test should pass successfully:
A common use case for testing page navigation is when you need to ensure that a user is redirected to the dashboard page after logging in from a login page.
Puppeteer also supports simulating form submission in a web app. With methods like .type
and .click
, you can simulate a user entering their details and clicking a submit button.
In this example, we’ll test a login form that has two input fields, Email Address
and Password
, and a Submit
button. We’ll test to verify what happens when the email or password details are correct and what happens when the wrong authentication details are entered.
Let’s start by cloning this branch, which contains the pre-built login form and the logic to handle correct and incorrect authentication details.
Start the dev server with yarn start
, and you should see a page similar to the one below. The form here has been hardcoded to only accept [email protected]
for the email address and password
for the password. If the details are correct, it displays text reading You are now signed in.
:
Any other combination of email address and password will trigger an error text that looks like the one in the screenshot below. Note how in the codebase, the error text has a class:
To write our tests, create a new file called e2e.test.js
and edit it with the code below:
import puppeteer from "puppeteer"; describe("App.js", () => { let browser; let page; beforeAll(async () => { browser = await puppeteer.launch(); page = await browser.newPage(); }); it("shows a success message after submitting a form", async () => { await page.goto("http://localhost:5000"); await page.waitForSelector(".form-header"); await page.click(".form-input__email"); await page.type(".form-input__email", "[email protected]"); await page.click(".form-input__password"); await page.type(".form-input__password", "password"); await page.click(".form-submit-button"); await page.waitForSelector(".form-success-message"); const text = await page.$eval( ".form-success-message", (e) => e.textContent ); expect(text).toContain("You are now signed in."); }); it("shows an error message if authentication fails", async () => { await page.goto("http://localhost:5000"); await page.waitForSelector(".form-header"); await page.click(".form-input__email"); await page.type(".form-input__email", "[email protected]"); await page.click(".form-input__password"); await page.type(".form-input__password", "password123"); await page.click(".form-submit-button"); await page.waitForSelector(".form-error-text"); const text = await page.$eval(".form-error-text", (e) => e.textContent); expect(text).toContain("Please enter a correct username/password."); }); afterAll(() => browser.close()); });
In the first test, we tested for a success message after logging in successfully using Puppeteer to fill the form. The input fields are targeted and filled with the right details using their CSS classes.
We do so using the .click
and .type
methods. When the user clicks the submit button, we expect to see a text with the CSS class form-success-message
because the form was filled with the correct login details. We also expect it to contain the right text.
The second test is similar to the first test, however, it is testing for an error message when authentication fails. The test is given incorrect login details and we expect it to find an error message with a CSS class of form-error-text
.
Again, build the app and start a static server with the command below so that the React app is available at localhost:5000
:
yarn build && serve -s build
Then, run the yarn test:e2e
command in another terminal window or tab, and the test should pass successfully:
Puppeteer provides some additional debugging options that we can use for React e2e tests. Let’s go over some of these options and walk through how to add them to an existing test.
By default, Puppeteer loads the Chromium browser in headless mode. A headless browser is a browser with no visible UI shell that still offers the full features of a normal browser. It is a great tool for automated testing and server environments.
To see what the browser is displaying while the test is running, launch a full version of the browser using headless: false
:
beforeAll(async () => { browser = await puppeteer.launch({ headless: false }); page = await browser.newPage(); });
Now, when the test is run, a Chromium browser will be launched.
With headless mode enabled, sometimes it can be hard to keep up with what’s happening on the actual browser. For example, form submissions happen very fast because they’re being filled in almost instantly, not by human beings.
To see what’s actually happening in a test, Puppeteer has the slowMo
option, which slows down Puppeteer operations by the specified amount of milliseconds:
beforeAll(async () => { browser = await puppeteer.launch({ headless: false, slowMo: 250, // slow down by 250ms }); page = await browser.newPage(); });
page.emulate
The page.emulate()
method enables you to emulate certain device metrics and its user agent. You can modify properties like the device’s width, height, and user agent:
beforeAll(async () => { browser = await puppeteer.launch({ headless: false }); page = await browser.newPage(); page.emulate({ viewport: { width: 500, height: 900, }, userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36', deviceScaleFactor: 1, isMobile: false. hasTouch: false, isLandscape: false }); });
To make emulation easier, Puppeteer also provides a list of device descriptors that you can obtain via the puppeteer.devices
array. To emulate an iPhone 6 without explicitly specifying the width, height, or user agent like the one above, you could write the test like this:
const iPhone = puppeteer.devices['iPhone 6']; beforeAll(async () => { browser = await puppeteer.launch({ headless: false, slowMo: 250, // slow down by 250ms }); page = await browser.newPage(); await page.emulate(iPhone); });
Jest Puppeteer provides all the required configuration to run unit tests using Puppeteer. Therefore, you can write Puppeteer tests in a more simplified manner with less setup.
Let’s set up Jest Puppeteer quickly to see how it differs from the method we’ve used for the previous tests. Go ahead and clone the GitHub branch or follow the instructions below.
Install Jest Puppeteer with the yarn add -D jest-puppeteer
command, create a jest.config.js
file, and finally edit it with the code below:
module.exports = { preset: "jest-puppeteer", testRegex: "./*\\e2e\\.test\\.js$", };
In the code block above, the preset for Jest is configured to be jest-puppeteer
, and there’s also a regex for which filename patterns to look out for. Only test files that match the regex will be run.
Next, create another file, jest-puppeteer.config.js
, and edit it with the code below:
module.exports = { server: { command: "yarn start", port: 3000, launchTimeout: 10000, debug: true, }, };
One major advantage of using Jest Puppeteer is not having to first build the site for production and then serve in a static server before running the test. Instead, you can run the test command alongside a local dev server that is already running. All you need to do is specify the command and the port.
Next, modify the yarn:test:e2e
command in the package.json
file so that it’s controlled by Jest and picks up its configuration from the jest.config.js
file:
"test:e2e": "jest -c jest.config.js",
With this setup complete, let’s modify the existing form submission tests. Open the e2e.test.js
file and edit it with the code below:
describe("App.js", () => { it("shows a success message after submitting a form", async () => { await page.goto("http://localhost:3000"); await page.waitForSelector(".form-header"); await expect(page).toFillForm('.form', { emailAddress: '[email protected]', password: 'password', }) await expect(page).toClick('button', { text: 'Submit' }) await expect(page).toMatch('You are now signed in.') }); it("shows an error message if authentication fails", async () => { await page.goto("http://localhost:3000"); await page.waitForSelector(".form-header"); await expect(page).toFillForm('.form', { emailAddress: '[email protected]', password: 'password123', }) await expect(page).toClick('button', { text: 'Submit' }) await expect(page).toMatch('Please enter a correct username/password.') }); });
As mentioned above, Jest Puppeteer makes writing tests easier, meaning we don’t have to create an instance of Puppeteer or create a new page for every test suite.
Jest Puppeteer also changes how the form is filled. As long as you have a selector for the form to be filled, and its input fields have a name
property, Jest Puppeteer allows you to assert that a form will be filled.
Jest Puppeteer simplifies asserting that a button will be clicked by using the content of the button itself. In the test suites above, we use it to assert that the button with the text Submit
will be clicked.
Finally, Jest Puppeteer makes it much simpler to assert that a text is on a page without any need for selectors.
Since the test above expects the input fields email
and password
to have a name
property, let’s add their respective names. Edit form
in App.js
with the code below:
<input type="email" required placeholder="Email Address" className="form-input form-input__email" onChange={(e) => { setEmail(e.target.value); }} name="emailAddress" /> <input type="password" required placeholder="Password" className="form-input form-input__password" onChange={(e) => { setPassword(e.target.value); }} name="password" />
One thing to note before running the test is that Jest Puppeteer exposes three new globals, browser
, page
, and context
. There’s a high chance that using these globals will trigger some ESLint errors in your editor, but you can avoid them by adding the code block below to the .eslintrc.js
file:
module.exports = { env: { jest: true, }, globals: { page: true, browser: true, context: true, jestPuppeteer: true, }, };
Now, run the test with the yarn test:e2e
command, and you should get an output similar to the one below with all tests passed:
In this tutorial, we demonstrated how to write e2e tests for React apps using Jest as a testing suite and Puppeteer for simulations like typing in inputs, clicking, etc.
When it comes to testing React apps, you can’t go wrong by combining Jest and Puppeteer. Puppeteer is still being actively developed at the time of writing, so make sure to check the API reference for more features.
The full codebase for this tutorial can be found on GitHub. I hope you enjoyed this article, and be sure to leave a comment if you have any questions. Happy coding!
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowDesign React Native UIs that look great on any device by using adaptive layouts, responsive scaling, and platform-specific tools.
Angular’s two-way data binding has evolved with signals, offering improved performance, simpler syntax, and better type inference.
Fix sticky positioning issues in CSS, from missing offsets to overflow conflicts in flex, grid, and container height constraints.
From basic syntax and advanced techniques to practical applications and error handling, here’s how to use node-cron.
3 Replies to "React end-to-end testing with Jest and Puppeteer"
The CRA seems to have updated the App.js, the css selector is now .App-header and the content is not Welcome to React. It would be helpful if you can update the App.test.js. Thanks!
How does this compare to using enzyme or react-testing-library for high-level testing?
I use those other libraries and I end up having to manually specifying redux store state, and managing react-router as well. Which seems tedious and anti-“test emulate the user”-approach.
What behavior should be tested by E2E testing with puppeteer and what should be tested with enzyme/RTL?
I think we want to minimize puppeteertests because they take longer? right?
Why not just have 3 sets of tests?
– Puppeteer for E2E testing (anything more than a single component)
– Enyzyme or RTL for single-component rendering
– unit tests for things like Util functions
Are you supposed to run e2e tests on the production build?