Tutorial: User Interface Testing with Jest and Puppeteer

I started to consider testing with Jest and Puppeteer right after the library came out. Puppeteer has quite an interesting API.

Testing with Jest and Puppeteer

In the following post I’ll introduce you to a basic UI test for a contact form.

We will testing with Jest and Puppeteer. Even if it’s still under development and the API could be subject to changes, Puppeteer is here to stay.

I was writing some tests last day and at the very same time I’ve come across a post by Kent C. Dodds.

Making your UI tests resilient to change” explains how to use  data-*attribute to make UI testing less fragile.

The data-*attributes are basically custom data attributes you can define on almost every HTML element. This is useful especially for exchanging data with Javascript.

Kent came at the right time because in all honesty I was doing something like:

await page.waitForSelector("#contact-form");
await page.click("#name");
await page.type("#name", user.name);

(you know, I’m more on the back-end side of things)

While I’m still not sold about using data-* for testing I must admit it is a good approach nonetheless. It’s beneficial for larger applications but for now I’ll stick with the classical way of selecting elements.

Psst.. do you fancy Integration Testing? Check out Cypress >> Better Javascript End to End Testing with Cypress

UI testing with Jest and Puppeteer: testing a contact form

My goal is to test a contact form for a landing page that I’m building.

Consider this:

UI testing with Jest and Puppeteer: testing a contact form

It has the following elements:

  1. a name input
  2. an email input
  3. a phone input
  4. a textarea
  5. a privacy checkbox
  6. a submit button

What do I want to test?

Testing the above form means asserting that a user can submit a contact request.

UI testing with Jest and Puppeteer: setting up the project

Let’s take a look at the tooling.

Testing with Jest and Puppeteer

Jest: a testing framework by Facebook. Jest provides a platform for automated testing along with a basic assertion library (Expect).

Puppeteer: a Node.js library for controlling headless Chrome. It’s rather new but it is a good time to check it out and see how it could fit inside your workflow.

Faker: a Node.js library for generating random data. Names, phones, addresses. Yeah, it’s kind of like Faker for PHP.

If you have already a project in place install the libraries with:

npm i jest puppeteer faker --save-dev

Installing Puppeteer will take some time because it ships with its own version of Chromium.

Chromium is the open source browser behind Chrome. Chromium and Chrome shares almost the same functionalities minus some license details.

Once the installation is done configure Jest in package.json. The testcommand should point to the Jest executable:

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

Also, in Jest I would like to write:

import puppeteer from "puppeteer";

To do so we need Babel for Jest. Let’s pull the dependencies in with:

npm i babel-core babel-jest babel-preset-env --save-dev

and create a new file named .babelrcinside your project folder:

  "presets": ["env"]

With this in in place we can start to write a simple test.

UI testing with Jest and Puppeteer: writing the actual test

To start create a new directory inside your project folder: it could be testor spec. Then create a new file named form.spec.jsinside the same directory.

Now rather than throwing a lot of code at you I will break the test down starting from the import section. At the end we’ll see how the entire file looks like.

In order, import Faker and Puppeteer:

import faker from "faker";
import puppeteer from "puppeteer";

Configure the form url (you may want to test a development version on localhost rather than contacting the real website):

const APP = "https://www.change-this-to-your-website.com/contact-form.html";

Create a fake user with Faker:

const lead = {
  name: faker.name.firstName(),
  email: faker.internet.email(),
  phone: faker.phone.phoneNumber(),
  message: faker.random.words()

Define some variables for Puppeteer:

let page;
let browser;
const width = 1920;
const height = 1080;

Define how Puppeteer should behave :

beforeAll(async () => {
  browser = await puppeteer.launch({
    headless: false,
    slowMo: 80,
    args: [`--window-size=${width},${height}`]
  page = await browser.newPage();
  await page.setViewport({ width, height });

afterAll(() => {

Both beforeAll and afterAll are Jest methods. In brief, before running any test we must spawn a browser with Puppeteer. Then, a new page could be opened with browser.newPage().

When the test suite finishes running the browser must be closed with browser.close().

You’re not limited to beforeAll and afterAll, check out the Jest documentation to see what’s available. Anyway, it’s better to have one browser instance for the entire test suite rather than opening and closing a browser for every test.

A few things to note about the above code:

you can see I’m launching Chrome in its own window with headless: false. It’s because I needed to record a video to show you how the test works.

In real life you don’t want to see the actual browser. Simply remove all the options from within the launch() method.

Same for setViewport(), you can remove it. Or even better, you could set up two different environments: one for visual debugging and another for headless testing. Learn how.

Now we’re ready to define the actual test:

describe("Contact form", () => {
  test("lead can submit a contact request", async () => {
    await page.goto(APP);
    await page.waitForSelector("[data-test=contact-form]");
    await page.click("input[name=name]");
    await page.type("input[name=name]", lead.name);
    await page.click("input[name=email]");
    await page.type("input[name=email]", lead.email);
    await page.click("input[name=tel]");
    await page.type("input[name=tel]", lead.phone);
    await page.click("textarea[name=message]");
    await page.type("textarea[name=message]", lead.message);
    await page.click("input[type=checkbox]");
    await page.click("button[type=submit]");
    await page.waitForSelector(".modal");
  }, 16000);

Notice how it’s possible to use async/await within Jest. Assuming that you’re using one of the latest versions of Node.js.

Let’s break down the above test. This is what Chrome headless does when testing with Jest and Puppeteer:

  1. go to the page defined inside APP
  2. wait for the contact form to appear
  3. click and fill in every input
  4. check the checkbox
  5. submit the form
  6. wait for the modal to appear

Another note: notice the timeout (16000) passed to Jasmine as a second argument in test(). This is useful when you want to see Chrome interacting with the page.

When not in headless mode, if you do not configure a timeout you’ll get the following error:

Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL

Anyway, you could remove the timeout when Chrome runs in headless mode.

Now by running the test with:

npm test

I can see the magic happen:

NOTE TO SELF: The video has been recorded with recordmydesktop on Fedora, with the following options:

recordmydesktop --width 1024 --height 768 -x 450 -y 130 --no-sound

But wait!! There is more!

Testing with Jest and Puppeteer: testing the frontend and some more

Now that I’m happy with my contact form I could move on to testing some other elements inside the page.

Every web page should have a meaningful title right?

Let’s test that <title></title>is correct:

describe("Testing the frontend", () => {
  test("assert that <title> is correct", async () => {
    const title = await page.title();
      "Gestione Server Dedicati | Full Managed | Assistenza Sistemistica"
  // Insert more tests starting from here!

What about a navigation bar? There should be one!

Testing that a navigation bar exists with Jest and Puppetter:

  test("assert that a div named navbar exists", async () => {
    const navbar = await page.$eval(".navbar", el => (el ? true : false));

or testing that a given elements contains the expected text:

  test("assert that main title contains the correct text", async () => {
    const mainTitleText = await page.$eval("[data-test=main-title]", el => el.textContent);
    expect(mainTitleText).toEqual("GESTIONE SERVER, Full Managed");

How do you feel about SEO? Check this out. Testing SEO concerns with Jest and Puppeteer, for example whether a canonical link exists or not:

describe("SEO", () => {
  test("canonical must be present", async () => {
    await page.goto(`${APP}`);
    const canonical = await page.$eval("link[rel=canonical]", el => el.href);

and so on.

At the end of the day I’ll be mostly happy because of those green checkmarks:

Testing with Jest and Puppeteer: in testing we trust

Puppeteer gives you endless possibilities. A lot of folks are building new testing frameworks right now, with Puppeteer. The API could be improved, sure, but knowing the basics is a must.

And it plays nicely with Jest.

Testing with Jest and Puppeteer: where to go from here

You might not feel comfortable with Puppeteer itself or with the Puppeteer’s API. I feel you.

It’s rather new but it is a good time to check it out and see how it could fit inside your workflow.

Puppeteer is still in development and there will be improvements. In the meantime you can take a look at Cypress for example.

How do you test your applications? A lot of other folks are doing E2E testing with Puppeteer. And you?

Bonus: Testing with Jest and Puppeteer, visual debugging

With Puppeteer you can choose whether to launch Chromium in headless mode or not.

We’ve seen it before:

beforeAll(async () => {
  browser = await puppeteer.launch({
      // Debug mode !
      headless: false,
      slowMo: 80,
      args: [`--window-size=1920,1080`]
  page = await browser.newPage();

Also, you must provide Jasmine a timeout when launching the browser in visual mode. Otherwise the test will stop abruptly. The timeout is specified as a second argument for test():

describe("Contact form", () => {
    "lead can submit a contact request",
    async () => {
    ///// some assertions
    16000 // <<< Jasmine timeout

In an automated testing environment you don’t want to see the browser. It will take forever to run all the tests. So how to switch easily between visual debugging and headless mode?

Code yourself an helper function. Put it in some file named testingInit.js:

export const isDebugging = () => {
  let debugging_mode = {
    puppeteer: {
      headless: false,
      slowMo: 80,
      args: [`--window-size=1920,1080`]
    jasmine: 16000
  return process.env.NODE_ENV === "debug" ? debugging_mode : false;

Then you can reference the function inside your test:

import { isDebugging } from "./testingInit.js";

beforeAll(async () => {
  browser = await puppeteer.launch(isDebugging().puppeteer)); // <<< Visual mode
  page = await browser.newPage();


describe("Contact form", () => {
    "lead can submit a contact request",
    async () => {
    ///// some assertions
    }, isDebugging().jasmine // <<< Jasmine timeout

Next time you’ll be able to use headless testing:

npm test

or the debugging mode:

NODE_ENV=debug npm test

Thanks for reading!

Photo credit: https://unsplash.com/@shotbyjames

Valentino Gagliardi

Valentino Gagliardi

Web Developer & IT Consultant, with over 10 year of experience, I'm here to help you developing your next idea.
Valentino Gagliardi

17 Replies to “Tutorial: User Interface Testing with Jest and Puppeteer”

  1. In the words of Ravazzi, “Tutto Molto Interessante!!”

    Seriously, your tip about configuring the timeout made my evening a lot less frustrating.


  2. Is there a reason why you’re using preset-es2015 instead of preset-env configured for your target browsers?

    preset-env saves newer browsers from using the transpiled version where native support is available.

    1. Even though I’m not targeting a front-end environment here, yes you’re right. babel-preset-env is the way to go. Be aware though if you want to use async/await in your code: you’ll need babel-preset-es2015 either way.

  3. Thank you for introducing Jest.

    It would be remiss not to mention Webdriver.IO in place of puppeteer which with the standardised Selenium API also works on the browsers a real-world app/site will need to target.

    In addition to utilising HTML data- (which could be considered superfluous and unsemantic if merely for testing purposes) one can compose tests with Page/View objects for cleaner less brittle tests.

    Using Async await fits well with sequential testing; however with one caveat – since applications can carry out tasks in asynchronous patterns, testers should also ensure matrix coverage for alternative sequences of events which aynchronous (e.g Promise.all) behaviours may invoke.

    For icing on the cake take a look at BDD styles and Product-Owner-Friendly abstractions such as Cucumber.io.

    Thanks for sharing.

  4. Any particular reason why to use Puppeteer over Cypress?
    I currently use Cypress and find it very slick – just to get rid of all those async/await:s is a big win (as cypress handles that for you) and the biggest(?) win is that you can use Chrome DevTools to debug your tests as they execute.

    Drawbacks to cypress – Only Chrome for now. But so is headless Chrome. 🙂

  5. Nice little tutorial. Would have helped newbies like myself to see the project structure as a whole… Everything needed to get this going

  6. Thank you so much for this tutorial. But I have aquestion as a somehwat junior QA guy. While this seems super great and fun for web scraping and smoke tests, are teams actually going to use this in e2e test suites? What about crossbrowser testing, and then this has its own api that breaks from the Webdriver standard. I feel like this can’t ever grow into more than just a toy.

    1. Jon, your opinion is interesting.

      Puppeteer was born for controlling Chrome headless within Node.js. It’s not aimed specifically at E2E. Yet testing with Jest and Puppeteer makes a lot of sense.

      Think about a small app.

      Of course you won’t write a solid testing suite from scratch with Puppeteer. For more complex cases I would look elsewhere. Maybe Cypress?

  7. You defined the APP variable, but never show where or how it is used. As a result, when attempting to follow this tutorial, I run into the timeout as Chromium opens but does not know to navigate to any URL.

Leave a Reply to Valentino Gagliardi Cancel reply

Your email address will not be published. Required fields are marked *