43

In my Jest unit test I am rendering a component with a ColorPicker. The ColorPicker component creates a canvas object and 2d context but returns 'undefined' which throws an error "Cannot set property 'fillStyle' of undefined"

if (typeof document == 'undefined') return null; // Dont Render On Server
var canvas = document.createElement('canvas'); 
canvas.width = canvas.height = size * 2;
var ctx = canvas.getContext('2d'); // returns 'undefined'
ctx.fillStyle = c1; // "Cannot set property 'fillStyle' of undefined"

I'm having troubles figuring out why I can't get a 2d context. Maybe there an issue with my test config?

"jest": {
  "scriptPreprocessor": "<rootDir>/node_modules/babel-jest",
  "unmockedModulePathPatterns": [
    "<rootDir>/node_modules/react",
    "<rootDir>/node_modules/react-dom",
    "<rootDir>/node_modules/react-addons-test-utils",
    "<rootDir>/node_modules/react-tools"
  ],
  "moduleFileExtensions": [
    "jsx",
    "js",
    "json",
    "es6"
  ],
  "testFileExtensions": [
    "jsx"
  ],
  "collectCoverage": true
}
mik01aj
  • 11,928
  • 15
  • 76
  • 119
a11hard
  • 1,904
  • 4
  • 19
  • 41

12 Answers12

48

For those looking for examples using create-react-app

Install

yarn add --dev jest-canvas-mock

Create a new ${rootDir}/src/setupTests.js with

import 'jest-canvas-mock';
Siva
  • 7,780
  • 6
  • 47
  • 54
  • 11
    in my case, i added `jest-canvas-mock` to my `jest.config.js` like so `setupFiles: [ "jest-canvas-mock" ]` – 1housand Oct 21 '19 at 20:37
  • 2
    create react app does not have jest.config.js file do we need to create it? and how it should look like when we add setupFiles: [ "jest-canvas-mock" ]? – Feruza Apr 16 '20 at 16:59
  • 2
    What imports this mysterious new `setupTests.js` file? **How** is it *magically* used? – vsync Jun 23 '20 at 11:44
26

It's because your test doesn't run in a real browser. Jest uses jsdom for mocking the necessary parts of the DOM to be able to run the tests in Node, thus avoiding style calculation and rendering that a browser would normally do. This is cool because this makes tests fast.

On the other hand, if you need browser APIs in your components, it's more difficult than in the browser. Luckily, jsdom has support for canvas. You just have to configure it:

jsdom includes support for using the canvas package to extend any <canvas> elements with the canvas API. To make this work, you need to include canvas as a dependency in your project, as a peer of jsdom. If jsdom can find the canvas package, it will use it, but if it's not present, then <canvas> elements will behave like <div>s.

Alternatively, you could replace Jest with some browser-based test runner like Karma. Jest is pretty buggy anyway.

Shukant Pal
  • 706
  • 6
  • 19
mik01aj
  • 11,928
  • 15
  • 76
  • 119
  • I'm switching over to karma. Installing the canvas package dependencies would be too much trouble to document for others. Thanks! – a11hard Oct 22 '15 at 13:28
  • 25
    Old question/answer, but Jest has improved a lot since then and to get canvas working with jsdom (on most systems) all you need to do is `npm install canvas-prebuilt` - https://www.npmjs.com/package/canvas-prebuilt – ChidG Mar 30 '17 at 02:50
  • 1
    @ChidG thank you for that info. the normal canvas package is impossible to build on windows 10, at least for me... – Chris Jun 13 '17 at 10:18
  • i have installed canvas also,even after that jest is failing to detect this canvas element,I have to use jest only as per project requirement...can you please help me out,even i tried with canvas-prebuilt...same problem.please help – Abhishek Oct 05 '17 at 10:17
  • Using create-react-app with Jest/Enzyme, I can confirm that adding canvas-prebuilt per @ChidG fixed canvas context errors. – davidchappy Dec 01 '17 at 14:27
  • 2
    In case anyone comes looking for a resolution to this, Jest comes with a very old version of `jsdom`. If you add `jsdom` to your dependencies and add a resolution to `"jest/**/jsdom": "13.0.0"`, this should fix it. – kb_ Nov 30 '18 at 23:31
  • @kb_ this returns `error Received malformed response from registry for undefined. The registry may be down.` – van_folmert Dec 21 '18 at 16:04
  • Within a project from an up to date version of CRA, I just had to run `npm i -D canvas` as suggested almost 7 years ago, and the tests with Jest and react-testing-library started working for the canvas (which in our case is included in the react-signature-canvas package) – Wu Wei Jun 17 '22 at 10:05
26

Jest / jsdom can handle canvas elements if the library node-canvas is installed.

Thus uninstall jest-canvas-mock (if installed) and install canvas:

npm uninstall jest-canvas-mock
npm i --save-dev canvas
sstauross
  • 2,602
  • 2
  • 30
  • 50
6

For my use case I did simple monkey patching like this

beforeEach(() => {
    const createElement = document.createElement.bind(document);
    document.createElement = (tagName) => {
        if (tagName === 'canvas') {
            return {
                getContext: () => ({}),
                measureText: () => ({})
            };
        }
        return createElement(tagName);
    };
});

No need to install canvas-prebuilt or sinon.

Andzej Maciusovic
  • 4,306
  • 1
  • 29
  • 40
3

I had the exact same issue. I'm deploying to gitlab ci to run my tests, and since npm canvas requires an installation of Cairo, using that wasn't a viable option.

All I really wanted to do was mock the implementation through Jest so that it doesn't actually try to create a real context. Here's how I solved it:

added to package.json

"jest": {
  "setupFiles": ["./tests/setup.js"],
}

tests/setup.js

import sinon from 'sinon';

const createElement = global.document.createElement;
const FAKECanvasElement = {
  getContext: jest.fn(() => {
    return {
      fillStyle: null,
      fillRect: jest.fn(),
      drawImage: jest.fn(),
      getImageData: jest.fn(),
    };
  }),
};

/**
 * Using Sinon to stub the createElement function call with the original method
 * unless we match the 'canvas' argument.  If that's the case, return the Fake 
 * Canvas object.
 */
sinon.stub(global.document, 'createElement')
  .callsFake(createElement)
  .withArgs('canvas')
  .returns(FAKECanvasElement);
2

jest-canvas-mock will work nice.

  1. Install with npm i --save-dev jest-canvas-mock

  2. In your jest jest.config.js add "setupFiles": ["jest-canvas-mock"] attribute.
    (If you already have a setupFiles attribute you can also append jest-canvas-mock to the array eg "setupFiles": ["something-xyz.js", "jest-canvas-mock"]).

All Done.

agilgur5
  • 667
  • 12
  • 30
zernab hussain
  • 339
  • 3
  • 6
1

To test canvas output in jest you need to do the following:

Make sure you are using at least jsdom 13. You can do this by including the jsom package for jest, for 14 it is:

jest-environment-jsdom-fourteen

And configuring jest to use this

jest --env=jest-environment-jsdom-fourteen

or in the package.json

"jest": {
   ...
   "testEnvironment": "jest-environment-jsdom-fourteen",

Include the canvas npm package. (as of 2.x this includes built version so canvas-prebuilt is deprecated).

Tom
  • 12,591
  • 13
  • 72
  • 112
1

If you are using create-react-app, install jest-canvas-mock using npm i --save-dev jest-canvas-mock and at the top of your test files put import 'jest-canvas-mock'

0

I managed to create an image snapshot test from a canvas in jest with react-testing-library and jest-image-snapshot. That's sort of buzzwordy but it worked nicely.

If you are able to properly setup your jest testing with node-canvas (not jest-canvas-mock or similar) then you can literally call toDataURL on the canvas element.


  import {render, waitForElement} from 'react-testing-library'
  import React from 'react'
  import { toMatchImageSnapshot } from 'jest-image-snapshot'

  expect.extend({ toMatchImageSnapshot })

  test('open a canvas', async () => {
    const { getByTestId } = render(
      <YourCanvasContainer />,
    )
    const canvas = await waitForElement(() =>
      getByTestId('your_canvas'),
    )
    const img = canvas.toDataURL()
    const data = img.replace(/^data:image\/\w+;base64,/, '')
    const buf = Buffer.from(data, 'base64')
    expect(buf).toMatchImageSnapshot({
      failureThreshold: 0.001,
      failureThresholdType: 'percent',
    })
  })

I found that I needed to use a low threshold rather than directly comparing the image/png data URL because there were two pixels that were just randomly different when running on travis-CI

Also consider manually upgrading jest environment jsdom to jest-environment-jsdom-thirteen or jest-environment-jsdom-fourteen (other answers on this page suggest similar) and refer to https://github.com/jsdom/jsdom/issues/1782

Colin D
  • 2,822
  • 1
  • 31
  • 38
0

I am using react, in my case.

npm uninstall jest-canvas-mock

npm i --save-dev canvas

These two commands are helpful.

ouflak
  • 2,458
  • 10
  • 44
  • 49
0

Try installing the canvas package, because jest converts the canvas to div so after installing this package your test cases will work as same as canvas need to work.

npm install canvas

Hope this works for you.

Coder
  • 82
  • 7
-1
npm install -D canvas-prebuilt@1

This provides support for canvas for jest.This even works if someone is getting error due to Lottie.js.

Serenity
  • 35,289
  • 20
  • 120
  • 115