Testing an HTML canvas with Cypress

How to test an HTML canvas? Let's see an example of visual regression testing with Cypress.

Testing an HTML canvas with Cypress - Visual regression testing

What is a canvas?

An HTML canvas is an element which can be drawn on the page with JavaScript. Consider the following example:

A simple canvas

To draw this rectangle we use JavaScript on an HTML <canvas> element. HTML canvas are everywhere, from HTML games to charts. A popular chart library for JavaScript, Chart.js uses canvas to draw beautiful charts.

Canvas are great and all, but they're somewhat hard to test because of their nature. Let's see how to test a canvas in this quick guide.

Setting up the project

To start off create a new folder and move into it:

mkdir testing-canvas && cd $_

Initialize a new JavaScript project:

npm init -y

Inside the project folder create an HTML document in index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Testing a Canvas</title>
</head>
<body>
<div>
    <canvas width="400" height="400">
        Alternative text
    </canvas>
</div>
</body>
<script>
    const canvas = document.querySelector('canvas');
    const context = canvas.getContext('2d');
    context.fillStyle = 'yellow';
    context.fillRect(20, 20, 200, 200);
</script>
</html>

To test that everything is in place run from inside the project folder:

npx serve

Then visit http://localhost:5000/, you should see the canvas:

A simple canvas

Let's now discuss how to test such an element.

How do you even test an HTML canvas?

Testing tools fall into two broader categories:

  • unit testing frameworks.
  • functional testing (or end to end testing) frameworks.

Examples of unit testing frameworks are Mocha or Jest. Examples of functional testing frameworks are Cypress, or Testcafe.

A unit test tool won't be the right tool for testing a canvas. To render a canvas we need a real browser, and it's not always practical to mock out the DOM in unit testing.

On the other hand, functional testing tools are not strictly meant for visual testing. With a tool like Cypress we can click and select things around in the page, but testing the visual appearance (styles, charts) is not what these tools are good for.

Chart.js example chart A Chart.js chart made out of a canvas.

Sometimes functional testing tools include also some kind of visual regression testing.

Visual regression testing is a technique for spotting visual differences between one test and another.

This category of tools usually work by taking a picture of the baseline user interface that is considered the right implementation. Then the baseline is compared against each subsequent test.

Since canvas are purely visual elements, and are drawn when the user visits the page, it makes sense to render them with a functional testing tool which has support for visual regression testing.

Let's put things into pratice.

Installing Cypress and the snapshot plugin

One of these tool for visual regression testing is cypress-plugin-snapshots, easily pluggable into Cypress.

To install the plugin alongside with Cypress, run:

npm i cypress cypress-plugin-snapshots js-base64@2.5.2 --save-dev

(At the time of writing the exact version js-base64@2.5.2 is required to make the plugin work).

After installing the plugin configure cypress.json as follows:

{
  "ignoreTestFiles": [
    "**/__snapshots__/*",
    "**/__image_snapshots__/*"
  ],
  "env": {
    "cypress-plugin-snapshots": {
      "imageConfig": {
        "threshold": 0.01
      }
    }
  }
}

Here we say to Cypress to ignore snapshots files, and we configure cypress-plugin-snapshots with a low threshold.

The threshold is the amount of visual differences that triggers a failed test for the snapshot plugin. Failing to configure this threshold will make even incorrect tests to pass!

Once done with this part open up cypress/plugins/index.js and import the plugin:

const { initPlugin } = require("cypress-plugin-snapshots/plugin");

module.exports = (on, config) => {
  initPlugin(on, config);
  return config;
};

As a last step load commands for cypress-plugin-snapshots in cypress/support/commands.js:

import "cypress-plugin-snapshots/commands";

For convenience, you can configure a script for running Cypress in package.json:

  "scripts": {
    "e2e": "cypress open"
  },

If everything is in place run Cypress for the first time with:

node_modules/.bin/cypress open

From now on you'll run tests with:

npm run e2e

We're now ready to test the canvas.

Testing an HTML canvas with Cypress

A first, naive test

To start off create a new file cypress/integration/Canvas.spec.js:

describe("Some app", () => {
  it("should see a yellow canvas", () => {
    cy.visit("http://localhost:5000");
    cy.get("canvas");
  });
});

In this test we say to Cypress: visit the page on localhost and search for the canvas. To try the test launch the page in a terminal with:

npx serve

Then in another terminal run the test:

npm run e2e

You should see the test passing:

Cypress test canvas dummy test

Indeed, it passes. Most commands in Cypress, like .get() are also implicit assertions. It means that everything is good for Cypress, as long as it finds the given element in the page.

However, we're not actually testing anything here. Checking for the existence of the canvas does not tell you anything about its shape, neither about its color.

What we want is a visual regression test instead.

A visual regression test

For a proper visual test with cypress-plugin-snapshots we can take a snapshot of the page:

describe("Some app", () => {
  it("should see a yellow canvas", () => {
    cy.visit("http://localhost:5000");
    cy.get("canvas");

    // Take a snapshot
    cy.get("body").toMatchImageSnapshot();
  });
});

Here I chose to take the snapshot for the whole page:

cy.get("body").toMatchImageSnapshot();

If you're interested in a shot for a specific page's element you can of course tweak the selection.

Again, to ensure that visual differences are spotted correctly make sure to configure a low threshold in cypress.json:

{
  "ignoreTestFiles": [
    "**/__snapshots__/*",
    "**/__image_snapshots__/*"
  ],
  "env": {
    "cypress-plugin-snapshots": {
      "imageConfig": {
        "threshold": 0.01
      }
    }
  }
}

To try the new test launch the page in a terminal with:

npx serve

Then in another terminal run the test:

npm run e2e

You should see a first test passing:

Cypress first snapshot

How does this work?

When you take the snapshot for the first time, the plugin saves an actual picture in cypress/integration/__image_snapshots__. On subsequent runs the snapshot is compared against another snapshot of the current interface, and if the tool finds any difference the test fails.

In fact if we change the canvas color from yellow:

<!-- omitted -->
<script>
    const canvas = document.querySelector('canvas');
    const context = canvas.getContext('2d');
    context.fillStyle = 'yellow';
    context.fillRect(20, 20, 200, 200);
</script>
</html>

to red:

<!-- omitted -->
<script>
    const canvas = document.querySelector('canvas');
    const context = canvas.getContext('2d');
    context.fillStyle = 'red';
    context.fillRect(20, 20, 200, 200);
</script>
</html>

the snapshot does not match anymore, and the test fails:

Cypress failing snapshot

Note that visual regression testing with image snapshots is different from Jest snapshot testing.

While visual regression testing takes an actual picture of the user interface, Jest snapshot testing uses a serializable format to save snapshots.

What's your favourite tool for visual regression testing?

Thanks for reading!

Valentino Gagliardi

Hi! I'm Valentino! I'm a freelance consultant with a wealth of experience in the IT industry. I spent the last years as a frontend consultant, providing advice and help, coaching and training on JavaScript, testing, and software development. Let's get in touch!