Yomi Eluwande JavaScript developer. Wannabe designer and Chief Procrastinator at Selar.co and worklogs.co.

React end-to-end testing using Jest and Puppeteer

10 min read 2991

React End-to-End Testing Using Jest and Puppeteer

Editor’s note: This tutorial was last updated and validated for accuracy on 7 July 2021.

In this React end-to-end testing tutorial, we’ll show you how to write end-to-end tests for React apps using Jest and Puppeteer.

We’ll cover the following:

What is end-to-end testing in React?

End-to-end tests (E2E) simulate actual user actions and are designed to test how a real user would likely use the application. React E2E testing helps ensure that the code you wrote is functional and your app works as intended. It’s a way of catching bugs in your code before you go live with your React app.

There are three common approaches to testing React apps:

  1. Unit testing  to check that individual units of code (mostly functions) work as expected
  2. Integration testing  in which individual units/features of the app are combined and tested as a group
  3. End-to-end testing  to confirm that the entire range of features works from the user’s perspective

In this tutorial, we’ll use Jest and Puppeteer to perform end-to-end testing in React to verify that certain features work as expected.

What is Jest?

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.js apps, and can be used with NestJS and GraphQL as well.

Jest is designed with simplicity in mind. It offers a powerful and elegant API to build isolated tests, snapshot comparison, mocking, test coverage, and much more.

What is Puppeteer?

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, act as a crawler for SPA and generate prerendered content, automate your form submissions, test UI, access web pages and extra information using DOM API, and automate performance analysis.

We made a custom demo for .
No really. Click here to check it out.

In this tutorial, we’ll use Puppeteer to carry out tests from a user’s perspective.

React end-to-end testing examples

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 the codebase:

yarn add puppeteer

Start the React app with the command yarn start and the dev server should be up at localhost:3000 . You should see a welcome page similar to the one below:

React Example App Welcome Page

Testing that a page has a text

For our first end-to-end test, we’ll test to ensure that the correct text is displayed on the page. 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 is 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, Puppeteer waits (using the waitForSelector method) for an element with the selector .App-welcome-text .

The content of the element with the selector is then used to assert whether the page loads the correct text. Puppeteer allows you to target elements on a page using selectors (this 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 lines 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 only runs all tests (i.e., unit tests) except for end-to-end tests and the yarn test:e2e only runs end-to-end tests. The --testPathPattern means 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 built files in a folder called build. To set up a local server to host the built files, we install serve. serve is a static file server and directory listing server. The serve -s build command starts up the server and you should have the react app up and running at localhost:5000.

Now, run the yarn test:e2e command in another terminal window/tab. The test should start running and pass just like the screenshot below:

Testing a React App Page With Text Using Jest and Puppeteer

Testing page navigation

Because Puppeteer allows you to mimic user interactions on a page, we can test to verify that a page navigates to the correct next 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:

React Example App Page With Link

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());
});

Let’s go over the test above. 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. This can be done 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).

Run the command below to build the app and also start 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/tab and the test should pass successfully.

Testing Links in a React App With Jest and Puppeteer

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.

Testing form submissions

Puppeteer also supports simulating form submission in a web app. With methods such as .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 when wrong authentication details are entered.

Let’s start by cloning this branch, which contains the already-built login form and the logic to handle correct/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. It displays a You are now signed in. text if the details are correct.

React App Login Form

Any other combination of email address/password will trigger an error text that looks like the one below. Note how in the codebase, the error text has a class.

React App Login Form Error

Let’s get started writing the 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 this using the .click and .type methods. The submit button itself is then clicked and, because the form was filled with the correct login details, we expect to see a text with the CSS class form-success-message. We also expect it to contain the right text.

The second test is similar to the first test except that 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/tab and the test should pass successfully:

Testing a React Login Form With Jest and Puppeteer

Debugging options with Puppeteer

Puppeteer also provides some additional debugging options that can be useful for testing React apps end to end. Let’s go over some of these options and walk through how to add them to an existing test.

Headless mode

By default, Puppeteer loads the Chromium browser in headless mode. A headless browser is a browser with no visible UI shell but 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 a Chromium browser will be launched when the test is run.

Slow mode

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, of course, it’s being typed in almost instantly and 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();
  });

Emulate devices using page.emulate

The page.emulate() method enables you to emulate certain device metrics and its user agent. Properties such as the device’s width, height, and user agent can be modified:

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 can be obtained via the puppeteer.devices array. To emulate an iPhone 6 without explicitly specifying the width, height, or user agent like the one above, you would 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);
});

Using Jest Puppeteer

Jest Puppeteer provides all the required configuration to run unit tests using Puppeteer. This means Puppeteer tests can be written with less setup and in a simplified manner.

Let’s set it up quickly to see how it differs from the method we’ve been using for the previous tests. Go ahead to clone the GitHub branch or follow the instructions below.

First, install Jest Puppeteer with the command yarn add -D jest-puppeteer and then create a jest.config.js file and 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 of the major advantages 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 an already running local dev server. All you need to do is specify the command and the port.

Next, modify the yarn:test:e2e command in package.json 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",

Will all that setup done, 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, which means we don’t have to create an instance of Puppeteer or create a new page for every test suite.

Another change is how the form is filled. Jest Puppeteer allows you to assert that a form will be filled as long you have a selector for the form to be filled and its input fields have a name property.

Jest Puppeteer also simplifies asserting that a button will be clicked by using the content of the button itself. So in the test suites above, we use it to assert that the button with the text Submit will be clicked.

Finally, with Jest Puppeteer, asserting that a text is on a page becomes much simpler 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, context. There’s a high chance using these globals will trigger some ESLint errors in your editor, so add the code block below to the .eslintrc.js file to avoid those errors:

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:

Testing a React App With Jest and Puppeteer

Conclusion

In this tutorial, we demonstrated how to write tests for React apps using Jest as a testing suite and Puppeteer for simulations such as typing in inputs, clicking, etc.

Jest and Puppeteer are a combination that can’t go wrong when it comes to testing React apps. Puppeteer is still being actively developed, so make sure to check the API reference for more features.

The codebase for this tutorial can be found on GitHub.

Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — .

Yomi Eluwande JavaScript developer. Wannabe designer and Chief Procrastinator at Selar.co and worklogs.co.

3 Replies to “React end-to-end testing using Jest and Puppeteer”

  1. 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!

  2. 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

Leave a Reply