hustcc/jest-canvas-mock

Doesn't work with latest Create React App

ztolley opened this issue ยท 17 comments

Steps to reproduce:

  1. npx create-react-app myapp --template typescript

  2. cd myapp

  3. npm install jest-canvas-mock

4: Edit App.test.tsx and add the following test

import "jest-canvas-mock";

test("Canvas support works with context", () => {
  const canvas: HTMLCanvasElement = document.createElement("canvas");
  const context = canvas.getContext("2d");

  expect(context).not.toBeUndefined();
});
  1. Run npm run test

The test should be able to create a canvas context but it comes back undefined

Same here. It worked with "react-scripts": "^3.4.4" but not with "react-scripts": "4.0.0". Probably due to the different Jest version used by the CRA 4? In your tests, according to your package.json, you use "jest": "^25.3.0". CRA 4 uses Jest 26. It would be very much appreciated if you could upgrade to Jest 26 / CRA 4. Thank you!

Update: As far as I can tell, Jest / jsdom can handle canvas elements if the library node canvas is installed as a peer dependency.
https://github.com/jsdom/jsdom#canvas-support
https://www.npmjs.com/package/canvas
Thus, making this library obsolete, at least for testing purposes?

My restriction is I cannot do this, hence using this package

@albert-schilling Do you have any resource how to make it working?
What I did:

  1. Added peerDependencies to my package.json
    "peerDependencies": { "canvas": "^2.6.1" }

  2. npm install
    but no success...

Thanks!

@mregula
I think it suffices if you install canvas as a regular package. npm i --save-dev canvas
At least, this is all we did.
I guess JSDOM tries to import canvas from your node_modules folder and uses the library if it can import it.

Thanks @albert-schilling !

What I did: npm install --save-dev canvas. If your component draws on canvas and uses img.onload to put the data into canvas, you need to mock also Image object in your jest test file:

import { Image } from "canvas";
beforeAll(() => {
  (global as any).Image = Image;
});

I'm running in to the same problem here. I follow the steps outlines above by @ztolley and I also update the file setupTests.ts as suggested here adding the line import 'jest-canvas-mock' but the result doesn't change, the test still fails.

Afterwards I follow the suggestion of @albert-schilling installing node canvas and the test passed !, jsdom indeed was able to instantiate a canvas object.

However it is my understanding that node canvas is less portable then jest-canvas-mock because it relies on Cairo v1.10.0 libraries, and so using jest-canvas-mock would make the code more easily portable.

Hey! Following up on that, the node canvas library indeed fixes the issue but I am not sure that would work when the test cannot control the creation of the canvas directly. For example if the canvas creation is happening inside a React component. How would you go about that?

For example:

test('test canvas', () => {
  render(<canvas data-testid="canvas" />);
  let canvas = screen.getByTestId("canvas")
  expect(canvas.getContext("2d")).not.toBeUndefined()

});

Actually, the solution is to remove all dependencies to jest-canvas-dom and install canvas instead, that's it. Node takes care of the rest. The test above works just fine after that change.

Actually, the solution is to remove all dependencies to jest-canvas-dom and install canvas instead, that's it. Node takes care of the rest. The test above works just fine after that change.

The lib canvas is quite limited, it doesn't support Video element as an argument and many other things. Any suggestion for use this in lasted CRA?

I'm having this same problem. While using canvas works, it is not a good solution if you need to actually assert on your canvas, as you don't get any spy/mock functionality if you use the canvas library.

jest-canvas-mock does not work with CRA 4 (react-scripts@4.0.3) - I tried this with freshly started project and it fails to work. On the other hand, with Parcel it works like a charm (tested with https://github.com/kolorobot/parcel-starters/tree/master/parcel-react-typescript-starter)

Using canvas is not an option for me neither.

  • Are there any plans to fix this?
  • Is there any solution that could work with CRA?

Let me look into this, but I believe that this package doesn't work with the latest react-create-app is because of the fact that the jest configuration file has resetMocks to true.

I was testing against a file that had

beforeEach(() => {
  jest.resetAllMocks();
})

which is functionally equivalent to having resetMocks set to true. So try flipping the resetMocks flag in the jest configuration file to false and the library should work.

  • I created new CRA (5.0.0) app
  • I added the following configuration to package.json:
 "jest": {
    "resetMocks": false
  }

Now the tests do not fail anymore.

Source code of my dummy component:

// @ts-nocheck
import * as React from 'react';
import { createRef, Ref, useState } from 'react';

type CanvasProps = {
    onUpdate?: (dataURL: string) => void,
}

const Canvas: React.FC<CanvasProps> = ({ onUpdate }) => {
    const canvasRef: Ref<HTMLCanvasElement> = createRef();

    const draw = () => {
        // https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Drawing_shapes
        const canvas = canvasRef.current;

        const ctx = canvas.getContext('2d');

        ctx.beginPath();
        ctx.arc(75, 75, 50, 0, Math.PI * 2, true);
        ctx.moveTo(110, 75);
        ctx.arc(75, 75, 35, 0, Math.PI, false);
        ctx.moveTo(65, 65);
        ctx.arc(60, 65, 5, 0, Math.PI * 2, true);
        ctx.moveTo(95, 65);
        ctx.arc(90, 65, 5, 0, Math.PI * 2, true);
        ctx.stroke();
        onUpdate && onUpdate(canvas.toDataURL());
    };

    const clear = () => {
        const canvas = canvasRef.current;
        const ctx = canvas.getContext('2d');
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        onUpdate && onUpdate(canvas.toDataURL());
    };

    return (
        <>
            <div>
                <button onClick={draw}>Draw</button>
                <button onClick={clear}>Clear</button>
            </div>
            <div>
                <canvas data-testid='canvas' ref={canvasRef} width={150} height={150}
                        style={{ border: '1px solid #000' }} />
            </div>
        </>
    );
};

export default Canvas;

And the test:

// @ts-nocheck
import * as React from 'react';
import 'jest-canvas-mock';
import Canvas from './Canvas';
import { getByTestId, getByText, render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

describe('Canvas', () => {

    it('renders empty canvas', async () => {
        const { container } = render(<Canvas />);
        expect(container).toMatchSnapshot();
    });

    it('renders canvas element', async () => {
        const { container } = render(<Canvas />);

        userEvent.click(getByText(container, 'Draw'));
        expect(container).toMatchSnapshot();
    });

    it('validates canvas state after draw was clicked', () => {
        const { container } = render(<Canvas />);

        userEvent.click(getByText(container, 'Draw'));

        const canvas: HTMLCanvasElement = getByTestId(container, 'canvas');
        const ctx = canvas.getContext('2d');

        const events = ctx.__getEvents();

        expect(events).toMatchSnapshot();
    });

    it('gets updated with `dataURL` on draw', () => {
        const callback = jest.fn();
        const { container } = render(<Canvas onUpdate={callback} />);

        userEvent.click(getByText(container, 'Draw'));

        expect(callback).toBeCalledWith('data:image/png;base64,00');
    });
});

So, @ggayowsky you were right as it goes to the workaround.

wowry commented

Same here, but import "jest-canvas-mock"; and "resetMocks": false solved my error. Thank you very much :)

nicks commented

would the maintainers accept a PR to expose mockWindow https://github.com/hustcc/jest-canvas-mock/blob/master/src/index.js#L6, so that my test can setup the mock itself?

I'm working in a codebase where lots of existing tests use resetAllMocks, which breaks this library.

nicks commented

I was able to workaround this by yarn patch-ing #98 into my project.