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

React end-to-end testing with Jest and Puppeteer

10 min read 3026

React Jest Pupeteer E2e Testing

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:

  1. Unit testing : Checks that individual units of code, mainly functions, work as expected
  2. Integration testing: Individual units or features of the app are combined and tested as a group
  3. End-to-end testing (E2E) : Confirms that the entire range of features works from the user’s perspective

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!

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

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

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

React App Yarn Start Loading Screen

Testing a page with text

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:

React Jest Puppeteer E2E Testing Text

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

React App Homepage Create React App

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:

Testing Links React App Jest 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 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.:

React App Login Form

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:

React App Login Form Error

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:

Testing React Login Form Jest Pupeteer

Debugging options with Puppeteer

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.

Headless mode

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.

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 they’re being filled in almost instantly, not by human beings.


More great articles from LogRocket:


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

Using Jest Puppeteer

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:

Testing React App Jest Pupeteer

Conclusion

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!

Cut through the noise of traditional React error reporting with LogRocket

LogRocket is a React analytics solution that shields you from the hundreds of false-positive errors alerts to just a few truly important items. LogRocket tells you the most impactful bugs and UX issues actually impacting users in your React applications.

LogRocket automatically aggregates client side errors, React error boundaries, Redux state, slow component load times, JS exceptions, frontend performance metrics, and user interactions. Then LogRocket uses machine learning to notify you of the most impactful problems affecting the most users and provides the context you need to fix it.

Focus on the React bugs that matter — .

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

3 Replies to “React end-to-end testing with 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