Testing UI and SEO using Jest and Puppeteer.

With the release of node.js making it possible to run JS on the server—on top of the move from handling all presentation logic on the server to handling it all in the browser testing—JS has never been so important.

Download/Clone a working example of these Jest tests from our github.

Gone are the days where frontend developers used to happily (and sometimes haphazardly) squirrel away writing javascript/jQuery to add simple enhancements to web pages without thinking about testing or efficiency.

We all remember these days...

$(‘.doCoolStuff’).click(function(){
  $(this).fadeOut(500, function()
    
  );
});

We all thought this was the bee’s knees, but oh, how wrong we were. With the release of node.js making it possible to run Javascript on the server - on top of the move from handling all presentation logic on the server to handling it all in the browser utilizing frameworks like React, Angular and Vue - Javascript has become the new go to language.

This means the Javascript we're writing has become much more complex, and the need for it to be testable has never been greater, which is why more and more JS testing frameworks have arisen. Some of the more popular ones to date are:

After doing some research and reading some good comparisons, we decided experiment using Jest by Facebook, complemented by Puppeteer, a headless chromium browser.

The Aim

To use jest and puppeteer to perform some simple UI tests. For this example, we'd write two simple SEO and UI interactivity tests that show the power of jest, and also shed a little light on what/how Jest can be used in a real world scenario. .

Setup

If you're using our example then just cd into where you cloned it and run yarn, otherwise just install the dependencies manually.

First install Jest.

// Install Jest using npm:
npm install --save-dev jest

// Or via yarn:
yarn add --dev jest

Then do the same for Puppeteer, which may take a while as you're essentially installing a standalone version of Chrome - but bear with it. Once that's done you're ready to start testing with Jest!

In Jest there are several ways to name a file so that it gets recognised as a test. You can either prefix the file with. test.js or .spec.js or you can put the tests into a __tests__ directory. For this example we're going to go with the .test.js prefix.

To start using Puppeteer we need to set it up. For this we will use Jest’s beforeAll and afterAll globals, which are both ‘does what it says on the tin’ functions, beforeAll running before any tests are run and afterAll running once all other tests have finished.

First things first, we need to import Puppeteer.

import puppeteer from 'puppeteer';

And then set up some globals of our own.

// the url to the page we are going to run our tests on
const URL = 'https://hatchd.com.au';

let page;
let browser;

// the width and height of the testing browser
const winWidth = 1920;
const winHeight = 1080;

To set up Puppeteer we run the following in the beforeAll function:

// hook to run before all testing
beforeAll(async () => {
  // open the testing browser using our options
  browser = await puppeteer.launch({
    headless: false,
    slowMo: 80,
    args: [`--window-size=${winWidth},${winHeight}`]
  });
  page = await browser.newPage();
  await page.setViewport({ width: winWidth, height: winHeight });
});

Notice we've set headless to false and slowMo to 80. This is just for demo purposes so you can see what’s going on in Puppeteer but for real world testing you would remove slowMo and set headless to true to vastly speed up the tests.

Then all that’s left to do is to tidy up and make sure we close our browser instance once all the tests have run.

// hook to run after all testing
afterAll(() => {
  // finished testing so close the browser
  browser.close();
});

We should now be ready to write our first test.

SEO test.

As you're probably aware in SEO Land it's extremely important make sure each page has a title, meta description and H1 tag, but on big projects it can also be really easy to forget these things, especially if there's a large team working on the website.

This is where Jest and Puppeteer can really help out. Using methods such as...

const description = await page.$eval('meta[name="description"]', el => el.content);

... to grab elements on the page and check if they have certain attributes or contain text etc., we can build up a set of tests that will alert us of any problems before we publish the site.

It's possible to group tests in Jest, so the first thing we'll do is create an SEO group that contains all our tests. The end result will look something like this:

  • SEO Test

To create the SEO group containing the tests is as simple as...

describe('SEO', () => {
  // put tests here
}

More on the describe global here.

We'll then write our first test. In Jest this is done using the test() global (more info here).

test('Title must be present', async () => {
  // test functionality here
});

There are three steps we need to test if the title is present. Firstly, we need to make sure that our Puppeteer instance is pointed to the correct URL.

We can instruct Puppeteer to visit any URL we choose; this could be a site running locally or a live site like Google.

await page.goto(‘https://google.com’);

We then need to grab the title of the page. With Puppeteer this is as simple as:

const title = await page.title();

Notice the use of the ES6 await function. This is used heavily when testing with Jest and ensures that the next step is not triggered until the previous one has finished. Read more here.

Finally, we need to check that the title returned is not empty and inform Jest about this result. We can do this using a regex. [/.*\S.*/]

expect(title).toMatch(/.*\S.*/);

expect() is a Jest function which allows you to use a set of ‘matchers’ to either pass or fail tests. There are a full set of matchers listed here, but for this example we only need .toMatch() to check if the title matches the regex.

The full test will look something like this:

test('Title must be present', async () => {
  await page.goto(`${URL}`);
  const title = await page.title();
  expect(title).toMatch(/.*\S.*/);
}, 90000);

Notice the 90000ms timeout we pass to the test function. This is mainly there for demo purposes, where we are actually opening up the Puppeteer browser and watching the results. This can be removed in a real work test where you would run puppeteer headlessly.

The next two tests are essentially the same, with a few tiny exceptions.

test('Meta description must be present', async () => {
  await page.goto(`${URL}`);
  const description = await page.$eval('meta[name="description"]', el => el.content);
  expect(description).toMatch(/.*\S.*/);
}, 90000);

test('H1 must be present', async () => {
  await page.goto(`${URL}`);
  const title = await page.$eval('h1', el => el.textContent);
  expect(title).toMatch(/.*\S.*/);
}, 90000);

Just place all these functions within our defined SEO testing block, and there you have it: a way to make sure a page has the minimum required elements on the page for Google to index it successfully.

Interaction with menu test

It’s also possible to test UI interaction with Jest and Puppeteer. We will now write a test to check that the sites menu opens/becomes visible when the hamburger is clicked.

We can interact with the Puppeteer browser much like good old jQuery...

await page.click('.js-toggle-nav');

It's pretty easy to then check if the menu is visible after clicking the nav toggle by checking the CSS values. In our case we are toggling the menu's visibility, so it’s safe to assume that if the visibility of the menu is ‘visible’ then the users will be able to see the menu and our test passed.

const navVisible = await page.$eval('.nav-primary', el => el.style.visibility);

So the whole test would look a little something like this…

// test the menu functionality
describe('Menu', () => {
  test('Menu must open when hamburger clicked', async () => {
    await page.goto(`${URL}`);await page.click('.js-toggle-nav');
    const navVisible = await page.$eval('.nav-primary', el => el.style.visibility);
    // timeout to allow for css animations
    setTimeout(() => {
      expect(navVisible).toBe('visible');
    }, 100);
  }, 90000);
});

We could even set up Puppeteer to take a screenshot before and after in case we needed to visually compare.

Summary

Jest is a really powerful testing tool and requires very little setup to get some basic tests working for your app or website. When coupled with Puppeteer the possibilities are endless; you could test if a hamburger menu changes into a fully-fledged menu at a certain breakpoint by dynamically changing Puppeteer's viewport width, or test all links to the correct pages or all forms can be successfully submitted.

It’s up to you how extensive you get with your testing, but the ability to catch errors in a site programmatically greatly improves site performance and user experience and takes the brunt of what would otherwise be a lot of tedious human-based testing.

Download/Clone a working example of the Jest tests from our github.

Previous
Previous

Switching to 'product-mode'.

Next
Next

State of the State: How the WA government is responding to digital disruption.