sutee
sutee

Reputation: 12828

How to set up Visual Regression of react-chartjs-2 component

I am trying to set up visual regression testing for react-chartjs-2 components with React Testing library. However, all of the snapshots that are being generated are blank, but the component renders properly in the browser.

This is what I'm currently testing. I basically combined this blog post example with the pie chart example from react-chartjs-2.

import React from 'react';
import {generateImage, debug} from 'jsdom-screenshot';
import {render} from '@testing-library/react';
import {Pie} from "react-chartjs-2";

it('has no visual regressions', async () => {
    window.ResizeObserver =
        window.ResizeObserver ||
        jest.fn().mockImplementation(() => ({
            disconnect: jest.fn(),
            observe: jest.fn(),
            unobserve: jest.fn(),
        }));

    const data = {
        labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'],
        datasets: [
            {
                label: '# of Votes',
                data: [12, 19, 3, 5, 2, 3],
                backgroundColor: [
                    'rgba(255, 99, 132, 0.2)',
                    'rgba(54, 162, 235, 0.2)',
                    'rgba(255, 206, 86, 0.2)',
                    'rgba(75, 192, 192, 0.2)',
                    'rgba(153, 102, 255, 0.2)',
                    'rgba(255, 159, 64, 0.2)',
                ],
                borderColor: [
                    'rgba(255, 99, 132, 1)',
                    'rgba(54, 162, 235, 1)',
                    'rgba(255, 206, 86, 1)',
                    'rgba(75, 192, 192, 1)',
                    'rgba(153, 102, 255, 1)',
                    'rgba(255, 159, 64, 1)',
                ],
                borderWidth: 1,
            },
        ],
    };
    render(<div><Pie data={data}/></div>)
    expect(await generateImage()).toMatchImageSnapshot();
});

I am wondering if it's a timing issue. Running debug() before the expect shows a canvas with 0 width and height:

<canvas
  height="0"
  role="img"
  style="display: block; box-sizing: border-box; height: 0px; width: 0px;"
  width="0"
/>

Upvotes: 2

Views: 711

Answers (2)

gaitat
gaitat

Reputation: 12632

In order to test react-chartjs-2 with jest and jsdom you don't need to mock the component or mock the canvas rendering. You can do visual regression testing of the react components. I am using:

chart.js v4.4.0
react-chartjs-2 v5.2.0
jsdom v19.0.0
jsdom-screenshot v4.0.0
jest v27.4.3
jest-image-snapshot v5.2.0
@jest/globals v27.4.2
@testing-library/jest-dom v5.16.0
@testing-library/react v12.1.2

and in my jest.config.js I have:

testEnvironment: 'jsdom',
setupFilesAfterEnv: [
  '<rootDir>/jest.setup.afterEnv.js',
],

and in my jest.setup.afterEnv.js I have:

import { expect } from '@jest/globals';

import { configureToMatchImageSnapshot } from 'jest-image-snapshot';

const toMatchImageSnapshot = configureToMatchImageSnapshot({
  failureThresholdType: 'pixel',
  customSnapshotsDir: `${__dirname}/test/snapshots/`,
});

// extend Jest expect
expect.extend({ toMatchImageSnapshot });

With the setup above I can get an image out of the jest test of a generic <Canvas /> component.

const getCanvasSnapshot = (canvas: HTMLCanvasElement, failureThreshold = 0) => {
  const image = canvas.toDataURL();
  const data = image.replace(/^data:image\/\w+;base64,/, '');
  const snapshot = Buffer.from(data, 'base64');

  expect(snapshot).toMatchImageSnapshot({ failureThreshold });
};

describe('<Canvas />', () => {
  test('render into canvas', () => {
    // @see https://medium.com/@pdx.lucasm/canvas-with-react-js-32e133c05258
    const Canvas = (props: any) => {
      const { draw, ...rest } = props;
      const canvasRef = React.useRef(null);

      React.useEffect(() => {
        const canvas = canvasRef.current as unknown as HTMLCanvasElement;
        const context = canvas.getContext('2d');
        draw(context);
      }, [draw]);

      return <canvas ref={canvasRef} {...rest} />;
    };

    const App = () => {
      const draw = (ctx: CanvasRenderingContext2D) => {
        ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
        ctx.fillStyle = '#0000ff';
        ctx.beginPath();
        ctx.arc(50, 50, 5, 0, 2 * Math.PI);
        ctx.fill();
      };

      return <Canvas draw={draw} width={100} height={100} />;
    };

    const { container } = render(<App />);

    const canvas = container.querySelector('canvas') as HTMLCanvasElement;
    getCanvasSnapshot(canvas);
  });
});

Extending this to react-chartjs-2 is then simple. Couple issues that I had. In browser mode the Chart can get its size from the parent container but when jest is running, the Chart could not get a size. So I had to set

options.responsive = false; // only when jest is running

The setting of options.maintainAspectRatio was invariant to my application.

Then I had to set a width and height directly on the Chart component

  return (
    <Chart
      {...(isJestRunning() ? { width: '198' } : {})}
      {...(isJestRunning() ? { height: '202' } : {})}
      type="scatter"
      data={data as ChartData}
      options={options as ChartOptions}
      plugins={plugins}
    />
  );

Then the jest/jsdom test would produce an image. Note that <Plot /> is just a simple wrapper around <Chart />.

describe('<Plot />', () => {
  let Wrapper: any;

  beforeEach(() => {
    window.ResizeObserver =
      window.ResizeObserver ||
      jest.fn().mockImplementation(() => ({
        disconnect: jest.fn(),
        observe: jest.fn(),
        unobserve: jest.fn(),
      }));

    // wrap the code with hooks; otherwise we get
    // Invalid hook call. Hooks can only be called inside the body of a function component. This could happen for one of the following reasons:
    Wrapper = () => (
      <Plot
        xScale={{ type: 'linear', min: 0, max: 1 }}
        yScale={{ type: 'linear', min: 0, max: 1 }}
        ...
        ...
      />
    );
  });

  afterEach(cleanup);

  test('render component', () => {
    const { container } = render(<Wrapper />);

    const canvas = container.querySelector('canvas') as HTMLCanvasElement;

    // make sure the canvas has the correct size
    expect(canvas.width).toEqual(198);
    expect(canvas.height).toEqual(202);

    // get an image of the canvas
    getCanvasSnapshot(canvas);
  });
});

Hope this helps.

Upvotes: 0

superhawk610
superhawk610

Reputation: 2653

The charting library you're using, react-chartjs-2, wraps chart.js, which relies on the canvas element to render charts to the screen. It's important to note that canvas contents are not part of the DOM, but are instead rendered virtually on top of a single DOM canvas element.

Keeping that in mind, take a look at how jsdom-screenshot renders images (from the explanation of method):

We use jsdom to obtain the state of the HTML which we want to take a screenshot of. Consumers can use jsdom to easily get components into the state they want to take a screenshot of. jsdom-screenshot then uses the markup ("the HTML") at that moment (of that state). jsdom-screenshot launches a local webserver and serves the obtained markup as index.html. It further serves assets provided through serve so that local assets are loaded. Then jsdom-screenshot uses puppeteer to take a screenshot take screenshots of that page using headless Google Chrome.

When you call generateImage(), the HTML you posted (just an empty canvas element) is copied into a file, index.html, opened in Puppeteer, then a screenshot is taken. Because canvas elements don't include their contents in their markup, when the HTML is copied into a file the chart is lost.

In short, jsdom-screenshot does not support drawing on canvas elements.

I would recommend checking out jest-puppeteer, which runs your entire test suite inside of Puppeteer. Instead of using a virtualized DOM implementation jsdom, it will use the actual Chromium DOM implementation, which has the added benefit of functioning closer to runtime usage. You can use page.screenshot() to take a screenshot of the chart exactly as it appears in-browser, with full DOM support (canvas included). Check out the jest-puppeteer README to get started.

Upvotes: 3

Related Questions