End-to-end testing with ReactJS and Puppeteer

Puppeteer is a Node library that provides a high-level API to control Chrome or Chromium over the DevTools Protocol. It can be used to automate tasks in the browser, such as generating screenshots and PDFs or tracking user interactions.

It can also be used for web scraping, testing, and crawling. But in this article, we will use it for end-to-end testing.

Today we will learn how to do end-to-end testing with Jest and Puppeteer in a ReactJS application.

What is end-to-end testing?

End-to-end testing is software testing, where the software is tested from start to end. It is also called black-box testing because it tests the software without knowing the application's internal structure.

Let's get started!

Create the boilerplate project

First, we need to create a new ReactJS project. We will use the create-react-app tool to do that.

npx create-react-app e2e-react --language typescript

Setup with Create react app

To setup Puppeteer with Create React App, we need to install the following dependencies:

npm install --save-dev jest-puppeteer puppeteer

And add the following script to the package.json file:

"scripts": {
  "test": "react-scripts test --env=jsdom --watchAll=false"
}

And that's it! We are ready to start writing our tests.

Other dependencies

We will need to install the following dependencies:

  • Jest - Our test runner
  • Puppeteer - Our browser automation tool
  • Jest-Puppeteer - A Jest plugin to run Puppeteer tests

Run the following command to install them:

cd e2e-react
npm install --save jest puppeteer jest-puppeteer

Configure Jest

Then first, we need to add the jest configuration file. Create a file called jest.config.js in the root of the project and add the following content:

module.exports = {
  preset: "jest-puppeteer",
  testMatch: ["**/tests/**/*.test.js"],
  verbose: true,
};

Here we are telling Jest to use the jest-puppeteer preset and to look for tests in the tests folder.

Then we need to add the following script to the package.json file:

"scripts": {
  "test": "jest"
}

Configure Puppeteer

Then, we need to create a new file called jest-puppeteer.config.js in the project's root. This file will contain the configuration for Puppeteer.

module.exports = {
  launch: {
    headless: process.env.HEADLESS !== "false",
    slowMo: process.env.SLOWMO ? parseInt(process.env.SLOWMO, 10) : 0,
    devtools: process.env.DEVTOOLS === "true",
    product: "chrome",
    args: [
      "--no-sandbox",
      "--disable-setuid-sandbox",
      "--disable-dev-shm-usage",
      "--disable-accelerated-2d-canvas",
      "--disable-gpu",
      "--window-size=1920,1080",
    ],
  },
};

Here we are telling Puppeteer to run in headless mode, not to use the sandbox, to disable the GPU, and to set the window size to 1920x1080.

We are also telling the puppeteer to use the Chrome browser. If you want to use the Firefox browser, you can change the product property to firefox.

We can also set the slowMo option to slow down the execution of the tests. This is useful for seeing what is happening in the browser.

We can set this option by selecting the SLOWMO environment variable. For example, to set the slowMo to 100ms, we can run the following command:

SLOWMO=100 npm test

Create the test

We will create a simple test that will open the browser, navigate to the Google homepage, and search for the word "puppeteer".


So this was a generic example of how to use Puppeteer with Jest. You can use this as a starting point to write your own tests.

Now let's focus on a real-world example.

Real-world example

We will create a form that will allow a user to log in. The form will have two fields: username and password. Let's create the form component.

// src/components/LoginForm.tsx
import React, { useState } from "react";

export const Login = () => {
  const [username, setUsername] = useState("");
  const [password, setPassword] = useState("");
  const [isLoggedIn, setIsLoggedIn] = useState(false);

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log("Username: " + username + " Password: " + password);
    setIsLoggedIn((prev) => !prev);
  };

  return (
    <div>
      <h1 id="page-title">{isLoggedIn ? "Logged In" : "Login"}</h1>
      <form onSubmit={handleSubmit}>
        <div>
          <label htmlFor="username">Username</label>
          <input
            type="text"
            id="username"
            onChange={(e) => setPassword(e.target.value)}
          />
        </div>
        <div>
          <label htmlFor="password">Password</label>
          <input
            type="password"
            id="password"
            onChange={(e) => setUsername(e.target.value)}
          />
        </div>
        <button type="submit" id="btn-submit">
          Login
        </button>
      </form>
    </div>
  );
};

The text "Logged In" will be displayed when the user clicks the submit button. This is because we use the useState hook to set the isLoggedIn state to true.

Now let's create the test. Create a new test file called login.test.js in the tests folder.

First, import the launch function from the jest-puppeteer package.

const launch = require("puppeteer").launch;

Then set up the beforeAll and afterAll hooks.

beforeAll(async () => {
  browser = await launch({
    headless: false,
    slowMo: 100,
    devtools: true,
  });
  page = await browser.newPage();
});

afterAll(async () => {
  await browser.close();
});

Here we are telling Puppeteer to run in headless mode and to set the slowMo to 100ms.

Then we need to create a test to check if the login form is displayed.

it("should display the login form", async () => {
  await page.goto("http://localhost:3000");
  await page.waitForSelector("#page-title");
  const pageTitle = await page.$eval("#page-title", (e) => e.innerHTML);
  expect(pageTitle).toMatch("Login");
});

This test will navigate to the application's root, wait for the page title to be displayed, and then check if the page title is "Login".

Now let's create a test to check if the user can log in.

it("should allow the user to log in", async () => {
  await page.goto("http://localhost:3000");
  await page.waitForSelector("#username");
  await page.type("#username", "test");
  await page.type("#password", "test");
  await page.click("#btn-submit");
  await page.waitForSelector("#page-title");
  const pageTitle = await page.$eval("#page-title", (e) => e.innerHTML);
  expect(pageTitle).toMatch("Logged In");
});

To find a particular item on the screen:

await page.waitForSelector("#username");

Here username is the id field of the input element.

Also, we can emulate user actions like typing and clicking.

await page.type("#username", "test");
await page.type("#password", "test");
await page.click("#btn-submit");

This test will navigate to the root of the application, wait for the username field to be displayed, type the username and password, click the submit button, wait for the page title to be displayed, and then check if the page title is "Logged In".

The final code for the test is as follows:

// __tests__/login.test.js

const launch = require("puppeteer").launch;

describe("Login", () => {
  let browser;
  let page;

  beforeAll(async () => {
    browser = await launch({
      headless: false,
      slowMo: 100,
      devtools: true,
    });
    page = await browser.newPage();
  });

  afterAll(async () => {
    await browser.close();
  });

  it("should display the login form", async () => {
    await page.goto("http://localhost:3000");
    await page.waitForSelector("#page-title");
    const pageTitle = await page.$eval("#page-title", (e) => e.innerHTML);
    expect(pageTitle).toMatch("Login");
  });

  it("should allow the user to log in", async () => {
    await page.goto("http://localhost:3000");
    await page.waitForSelector("#username");
    await page.type("#username", "test");
    await page.type("#password", "test");
    await page.click("#btn-submit");
    await page.waitForSelector("#page-title");
    const pageTitle = await page.$eval("#page-title", (e) => e.innerHTML);
    expect(pageTitle).toMatch("Logged In");
  });
});

Conclusion

In this article, we learned how to use Puppeteer with Jest. We also created a real-world example that will allow us to test a login form.

We learned how to navigate to a page, find an element, and emulate user actions like typing and clicking.

We also learned how to use the beforeAll and afterAll hooks to setup and teardown the browser.

Repository:

react-e2e-testing-puppeteer

Resources