Yomi Eluwande JavaScript Developer. Wannabe Designer and Chief Procrastinator at Selar.co and Worklogs.co

React end-to-end testing using Jest and Puppeteer

11 min read 3132

React End-to-End Testing Using Jest and Puppeteer

Editor’s note: This tutorial was last updated on 30 March 2021.

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

We’ll cover the following:

What is end-to-end testing in React?

End-to-end testing is an important part of modern web app development. React E2E testing helps you ensure that the code you wrote is functional and 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 end-to-end testing:

  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 check 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 example

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 quickly scaffold a 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:

npm i --save-dev jest jest-cli puppeteer faker

This command installs Jest, jest-cli (a runner for Jest), Puppeteer, and faker,  a tool that helps to generate massive amounts of fake data in the browser. We’ll use it to generate data for Puppeteer.

In your package.json file, add the following line of code in the scripts object:

"test": "jest"

With all the necessary packages installed, you can run the React app with the command npm start and just leave it running in the background.

Writing end-to-end test in React

Example No. 1: Testing for text

To start off with the tests, we’ll first write a test to check if there’s a particular text on a page and also a test to see if a contact form submits successfully. Let’s start off with checking if there’s a particular text on a page.

The App.test.js file is where we’ll be writing the tests. Jest is automatically configured to run tests on files have the word test in them. Open up the App.test.js and edit with the following code.

const faker = require('faker');
const puppeteer = require('puppeteer');

const person = {
  name: faker.name.firstName() + ' ' + faker.name.lastName(),
  email: faker.internet.email(),
  phone: faker.phone.phoneNumber(),
  message: faker.random.words()
};

describe('H1 Text', () => {
  test('h1 loads correctly', async () => {
    let browser = await puppeteer.launch({
      headless: false
    });
    let page = await browser.newPage();

In the first part of the code block above, faker and puppeteer are both imported and we generate a bunch of data from faker which will be used later.

The describe function acts as a container that’s used to create a block that groups related tests into one test suite. This can be helpful if you want your tests to be organized into groups. The test function is declared by Jest with the name of the test suite.

Inside the test function, a browser is launched with Puppeteer with the option of headless mode set to false. This means that we can see the browser while testing. browser.newPage() allows you to create a new page.

The .emulate() function gives you the ability to emulate certain device metrics and User-agent. In the code block above, we set it to have a viewport of 500x2400.

With the page open and it’s viewport defined, we then tell it to navigate to the app we’ll be testing with the .goto() function. The .waitForSelector() function tells Puppeteer to hold on until the particular selector has been loaded on the DOM. Once it’s loaded, the innerText of that selector is stored in a variable called html.

The next line is where the real testing happens.

expect(html).toBe('Welcome to React')

In the line of code above, the Jest expect function is set to check if the content of the variable html is the same as Welcome to React. As previously mentioned, we are testing if a particular text on the app is what it should be, a very straightforward test. At the end of the test, the browser is closed with the .close() function.

Now let’s now run the test. In your terminal, run the command below:

npm run test

React End-to-End Testing Example With Jest and Puppeteer

The test would pass. Therefore, your command output should be the same as above. You can actually change the content of the .App-title selector to see what would happen to failed tests.

React End-to-End Testing Example With Jest and Puppeteer

Here, the output actually indicates both that the test failed and why it failed. In this case, the received value is not the same as the expected value.

Example No. 2: Submitting a contact form

For the next test, we’ll simulate and test submitting a contact form on the React app.

In the React app code, open up the App.js file and edit with the code below.

import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';

class App extends Component {

  constructor(props) {
    super(props);
    this.state = {
      fullname: '',
      email: '',
      message: '',
      terms: false,
      test: ''

    }

    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  handleChange(event) {
    const target = event.target;
    const value = target.type === "checkbox" ? target.checked : target.value;
    const name = target.name;

    this.setState({
      [name]: value
    });
  }

  handleSubmit(event) {
    event.preventDefault();
    console.log(this.state)
  }

  render() {
    return (
      &ltdiv className="App"&gt
        &ltheader className="App-header"&gt
          &ltimg src={logo} className="App-logo" alt="logo" /&gt
          &lth1 className="App-title"&gtWelcome to a React app&lt/h1&gt
        &lt/header&gt
        &ltp className="App-intro"&gt
          To get started, edit &ltcode&gtsrc/App.js&lt/code&gt and save to reload.
        &lt/p&gt

        &ltdiv className="container contact-form m-t-20"&gt

          &ltp&gtContact Form&lt/p&gt
          &ltform onSubmit={this.handleSubmit}&gt
            &ltdiv className="field"&gt
              &ltdiv className="control"&gt
                &ltlabel className="label"&gtFull Name&lt/label&gt
                &ltinput name="fullname" type="text" placeholder="Full Name" className="input" value={this.state.fullname} onChange={this.handleChange}/&gt
              &lt/div&gt
            &lt/div&gt

            &ltdiv className="field"&gt
              &ltdiv className="control"&gt
                &ltlabel className="label"&gtEmail Address&lt/label&gt
                &ltinput name="email" type="email" placeholder="Email Address" className="input" value={this.state.email} onChange={this.handleChange}/&gt
              &lt/div&gt
            &lt/div&gt

            &ltdiv className="field"&gt
              &ltdiv className="control"&gt
                &ltlabel className="label"&gtMessage&lt/label&gt
                &lttextarea className="textarea" placeholder="Message here" name="message" value={this.state.message} onChange={this.handleChange}&gt&lt/textarea&gt
              &lt/div&gt
            &lt/div&gt

            &ltdiv className="field"&gt
              &ltdiv className="control"&gt
                &ltlabel className="checkbox"&gt
                  &ltinput
                    name="terms"
                    type="checkbox"
                    checked={this.state.terms}
                    onChange={this.handleChange}
                  /&gt
                  I agree to the{" "}
                  &lta href="https://google.com"&gtterms and conditions&lt/a&gt
                &lt/label&gt
              &lt/div&gt
            &lt/div&gt

            &ltdiv className="field"&gt
              &ltdiv className="control"&gt
                &ltlabel className="label"&gt
                  Do you test your React code?
                &lt/label&gt
                &ltlabel className="radio"&gt
                  &ltinput
                    type="radio"
                    name="test"
                    onChange={this.handleChange}
                    value="Yes"
                    checked={this.state.test === "Yes"}
                  /&gt
                  Yes
                &lt/label&gt
                &ltlabel className="radio"&gt
                  &ltinput
                    type="radio"
                    name="test"
                    onChange={this.handleChange}
                    value="No"
                    checked={this.state.test === "No"}
                  /&gt
                  No
                &lt/label&gt
              &lt/div&gt
            &lt/div&gt

            &ltdiv className="field"&gt
              &ltdiv className="control"&gt
                &ltbutton type="submit" className="button is-link"&gtSubmit&lt/button&gt
              &lt/div&gt
            &lt/div&gt
          &lt/form&gt
        &lt/div&gt
      &lt/div&gt
    );
  }
}

export default App;

The App.js has been updated to have a contact form and the submit button simply logs the form data to the console.

Next, add the code block below to your App.test.js file.

describe('Contact Form', () => {
  test('Can submit contact form', async () => {
    let browser = await puppeteer.launch({
      headless: false,
      devtools: true,
      slowMo: 250
    });
    let page = await browser.newPage();

    page.emulate({
      viewport: {
        width: 500,
        height: 900
      },
      userAgent: ''
    });

    await page.goto('http://localhost:3002/');
    await page.waitForSelector('.contact-form');
    await page.click("input[name=fullname]");
    await page.type("input[name=fullname]", person.name);
    await page.click("input[name=email]");
    await page.type("input[name=email]", person.email);
    await page.click("textarea[name=message]");
    await page.type("textarea[name=message]", person.message);
    await page.click("input[type=checkbox]");

    await page.click("input[name=question]");

    await page.click("button[type=submit]");

    browser.close();
  }, 9000000);
});

This test suite is similar to the one above, the puppeteer.launch() function launches a new browser along with some config. In this case, there are two additional options, devtools which displays the Chrome devtools and slowMo which slows down Puppeteer processes by the specified amount of milliseconds. This allows us to see what is going on.

Puppeteer has the .click and .type actions that actually simulate the whole process of clicking on input fields and typing in the values. The details for the form fields are gotten from faker which set in the person object earlier.

This test suite fills the contact form and tests if the user can actually submit this form successfully.

You can run the npm run test command in your terminal and you should a Chrome browser open and watch the actual testing process.

Example No. 3: Testing the login functionality

For the next set of tests, we’ll write tests to assert that:

  • Users can log in
  • Users can logout
  • Users are redirected to the login page for unauthorized view
  • Nonexistent views/route returns a 404 page

These are all end-to-end tests that are carried out from the user’s perspective. We are checking if a user can actually use the app for the most basic things.

To carry out these tests, we’ll need a React app. We’ll use Robin Wieruch’s React Firebase Authentication boilerplate code on GitHub. It ships with a built-in authentication system, so all you need to do is create a Firebase project and add the Firebase keys.

I modified the code a bit and added some selectors and IDs that makes the app suitable for testing. Go ahead and clone the GitHub repo to your local system and run the following commands:

npm i
npm start

Don’t forget to create a Firebase account and add your credentials in the src/firebase/firebase.js file.

Screenshot of Creating a Firebase Account

Let’s go ahead with writing tests for the React app. Once again, we’ll need to install jest faker, and puppeteer, also a App.test.js file is needed.

npm i --save-dev jest jest-cli puppeteer faker

Once the installation is done, create a file named App.test.js. Let’s begin to edit with the code block below:

const faker = require('faker');
const puppeteer = require('puppeteer');
const person = {
   email: faker.internet.email(),
   password: faker.random.word(),
};
const appUrlBase = 'http://localhost:3002'
const routes = {
public: {
      register: `${appUrlBase}/register`,
      login: `${appUrlBase}/login`,
      noMatch: `${appUrlBase}/ineedaview`,
   },
private: {
      home: `${appUrlBase}/home`,
      account: `${appUrlBase}/account`,
   },
};

Just like the tests written above, faker and puppeteer are imported. A person object is created and it stores a random email and password that will be used for testing. The appUrlBase constant is the link to the React app — if you haven’t started the React app, run npm start in your terminal and change appUrlBase to the link.

The routes object contains the various URLs to the views we’ll be testing. The public object contains links to routes in the React app that can viewed by anyone (not logged in), while the private object contains links to routes that can only be viewed if you’re logged in.

Note that noMatch is what will be used to test for nonexistent views/route returns a 404 page, that’s why it aptly leads to /ineedaview.

Alright, let’s write the first test now.

Testing whether users can log in

//create global variables to be used in the beforeAll function
let browser
let page

beforeAll(async () =&gt {
  // launch browser 
  browser = await puppeteer.launch(
    {
      headless: false, // headless mode set to false so browser opens up with visual feedback
      slowMo: 250, // how slow actions should be
    }
  )
  // creates a new page in the opened browser   
  page = await browser.newPage()
})

describe('Login', () =&gt {
  test('users can login', async () =&gt {
    await page.goto(routes.public.login);
    await page.waitForSelector('.signin-form');

    await page.click('input[name=email]')
    await page.type('input[name=email]', '[email protected]')
    await page.click('input[name=password]')
    await page.type('input[name=password]', 'password')
    await page.click('button[type=submit]')
    await page.waitForSelector('[data-testid="homepage"]')
  }, 1600000);
});

// This function occurs after the result of each tests, it closes the browser
afterAll(() =&gt {
  browser.close()
})

The code block above is different from the first set of tests we wrote above. First, the beforeAll function is used to launch a new browser with its options and create a new page in that browser as opposed to creating a new browser in every single test suite like we did in the tests earlier.

So how do we test here? In the test suite, the browser is directed to the login page which is routes.public.login and just like the contact form test, puppeteer is used to fill the form and submit it. After submitting the form, puppeteer then waits for a selector data-testid='homepage' which is a data-id that is present on the home page  —  the page the React app redirects to after a successful login.

I already created an account with the user details in the code block. Therefore, this test should pass:

React End-to-End Testing Example With Jest and Puppeteer

The afterAll function happens after the end of tests and it closes the browser.

Testing whether users can logout

Screenshot of a React End-to-End Test With Jest and Puppeteer
🤮 This is not a CSS tutorial

This is the view that’s shown after a successful login. Now we want to test what happens when a user clicks on the Sign Out button. The expected outcome is that the localStorage is cleared and logged out and the user is redirected back to the Sign In page.

In the same App.test.js file, add the code below just before the afterAll function:

describe('Logout', () =&gt {
  test('users can logout', async () =&gt {
    await page.waitForSelector('.nav-link');

    await page.click('[data-testid="signoutBtn"]')
    await page.waitForSelector('.signin-form')
  }, 9000000);
});

This test is fairly straightforward. puppeteer waits for the .nav-link selector and it clicks on the button with a data attribute of data-testid=”signoutBtn” and that is actually testing if the button can be clicked. After the page.click() function, puppeteer waits for the selector .signin-form which can be found on the Sign In page.

Congratulations! Another test passed:

React End-to-End Testing Example With Jest and Puppeteer

Testing that users are redirected to the login page for unauthorized view

We don’t want users having access to views and routes that they are not authorized to view. So let’s test if the code does that.

Add the code block below to the existing code, just before the afterAll function

describe('Unathorized view', () => {
  test('users that are not logged in are redirected to sign in page', async () => {
    await page.goto(routes.private.home);
    await page.waitForSelector('.signin-form')
  }, 9000000);
});

In the code block above, we test by going to a private route in the React and then wait for the signin-form selector.

This means after a user navigates to a private route, they are automatically redirected to the login form.

React End-to-End Testing Example With Jest and Puppeteer

Nonexistent views/route returns a 404 page

It’s important for all apps to have a 404 page so as to explain to a user that that particular route doesn’t exist. It was implemented in this React app too, let’s test if it works as expected.

Add the code block below to the existing code, just before the afterAll function.

describe('404 Page', () => {
  test('users are redirected to a 404 page for nonexistent views', async () => {
    await page.goto(routes.public.noMatch);
    await page.waitForSelector('.no-match')
  }, 9000000);
});

The routes.public.noMatch link we created earlier points to a route that doesn’t exist. Therefore when puppeteer goes to that link, it’s expecting that it automatically redirects to the 404 page. The .no-match selector is located on the 404 page.

React End-to-End Testing Example With Jest and Puppeteer

Conclusion

In this tutorial, we have seen firsthand how to write tests for React apps using Jest as a testing suite and puppeteer for simulations like typing in inputs, clicking, etc.

Jest and Puppeteer are a combination that can surely never go wrong when it comes to testing to 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 seen on GitHub here and here.

Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are difficult 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