简体   繁体   English

我如何使用 Mocha 对绘制在屏幕上的 React 组件进行单元测试<canvas> ?

[英]How can I use Mocha to unit-test a React component that paints on a <canvas>?

I have a React component that uses a <canvas> for user interaction.我有一个使用<canvas>进行用户交互的 React 组件。 I'm not using react-canvas or react-art or anything like that;我没有使用react-canvasreact-art或类似的东西; instead, I just draw on the canvas's 2D context in componentDidMount and componentDidUpdate .相反,我只是在componentDidMountcomponentDidUpdate绘制画布的 2D 上下文。

I extracted as much of the logic as possible to two separate modules: one comprises entirely pure functions and provides the core operations independent of React, and the other provides event handlers and lifecycle mixins to be attached to the React component.我将尽可能多的逻辑提取到两个单独的模块中:一个包含完全纯函数并提供独立于 React 的核心操作,另一个提供要附加到 React 组件的事件处理程序和生命周期 mixin。 I can test the first one easily, and the second one with a bit of mocking.我可以轻松地测试第一个,并通过一些嘲讽来测试第二个。

However, I'd also like to very minimally test the main canvas component just to make sure that it can render with no errors given some reasonable set of props.但是,我还想对主画布组件进行最低限度的测试,以确保它可以在给定一些合理的道具集的情况下无错误地呈现。 This is proving rather difficult because componentDidMount calls this.refs.canvas.getContext('2d') , which doesn't appear to be defined in the node environment.这证明相当困难,因为componentDidMount调用this.refs.canvas.getContext('2d') ,它似乎没有在节点环境中定义。 So I came up with the following solution, which I don't like very much;所以我想出了以下解决方案,我不太喜欢它; it involves both patching React.createElement and creating a fake context object:它涉及修补React.createElement和创建一个假的上下文对象:

// file: test/components/MyCanvasTest.jsx
import {describe, it} from 'mocha';
import {expect} from 'chai';

import React, {Component} from 'react';

import {declareMochaMock} from '../TestUtils';
import {
    renderIntoDocument,
    scryRenderedDOMComponentsWithTag,
} from 'react-addons-test-utils';

import MyCanvas from '../../src/components/MyCanvas';

describe('MyCanvas', () => {

    const noop = () => {};

    // These things are used in my component
    // but I don't want them to actually do anything,
    // so I mock them as no-ops.
    const propsToMockAsNoop = [
        'addEventListener',
        'removeEventListener',
        'setInterval',
        'clearInterval',
    ];
    propsToMockAsNoop.forEach(prop => declareMochaMock(window, prop, noop));

    // This thing is used in my component
    // and I need it to return a dummy value.
    declareMochaMock(window, 'getComputedStyle', () => ({ width: "720px" }));

    // This class replaces <canvas> elements.
    const canvasMockClassName = 'mocked-canvas-component';
    class CanvasMock extends Component {
        render() {
            return <div className={canvasMockClassName} />;
        }
        constructor() {
            super();
            this.width = 720;
            this.height = 480;
        }
        getContext() {
            // Here I have to include all the methods
            // that my component calls on the canvas context.
            return {
                arc: noop,
                beginPath: noop,
                canvas: this,
                clearRect: noop,
                fill: noop,
                fillStyle: '',
                fillText: noop,
                lineTo: noop,
                measureText: () => 100,
                moveTo: noop,
                stroke: noop,
                strokeStyle: '',
                textAlign: 'left',
                textBaseline: 'baseline',
            };
        }
    }

    const originalCreateElement = React.createElement;
    declareMochaMock(React, 'createElement', (...args) => {
        const newArgs = args[0] === 'canvas' ?
            [CanvasMock, ...args.slice(1)] :
            args;
        return originalCreateElement.apply(React, newArgs);
    });

    it("should render a <canvas>", () => {
        const element = <MyCanvas />;
        const component = renderIntoDocument(element);
        expect(scryRenderedDOMComponentsWithTag
            (component, canvasMockClassName)).to.have.length(1);
    });

});

The declareMochaMock function is defined as declareMochaMock函数定义为

// file: test/TestUtils.js
export function declareMochaMock(target, propertyName, newValue) {
    let oldExisted;
    let oldValue;
    before(`set up mock for '${propertyName}'`, () => {
        oldValue = target[propertyName];
        oldExisted = Object.prototype.hasOwnProperty.call(
            target, propertyName);
        target[propertyName] = newValue;
    });
    after(`tear down mock for '${propertyName}'`, () => {
        if (oldExisted) {
            target[propertyName] = oldValue;
        } else {
            delete target[propertyName];
        }
    });
}

I can't use a shallow renderer because my component accesses the canvas via a ref , and ref s aren't yet supported in the shallow renderer.我无法使用浅渲染器,因为我的组件通过ref访问画布,而浅渲染器尚不支持ref

Is there a way to approach this test with my current unit testing frameworks (ie, not adding Jest or anything like that) while reducing the amount that the test harness needs to know about?有没有办法用我当前的单元测试框架来处理这个测试(即,不添加 Jest 或类似的东西),同时减少测试工具需要知道的数量?

(The full canvas component is available here .) (完整的画布组件可在此处获得。)

You should be able to use:您应该能够使用:

https://www.npmjs.com/package/canvas#installation along with JSDOM. https://www.npmjs.com/package/canvas#installation以及 JSDOM。 (make sure you follow the installation procudure for your OS) (确保您遵循操作系统的安装程序)

Example test:示例测试:

import React from 'react';
import { mount } from 'enzyme';
import { expect } from 'chai';
import { jsdom } from 'jsdom';

import Chart from '../../src/index';

const createDOM = () => jsdom('<!doctype html><html><body><div></div></body></html>');

describe('<Chart />', () => {
  let DOM;

  const data = {
    labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'],
    datasets: [
      {
        label: 'My First dataset',
        backgroundColor: 'rgba(255,99,132,0.2)',
        borderColor: 'rgba(255,99,132,1)',
        borderWidth: 1,
        hoverBackgroundColor: 'rgba(255,99,132,0.4)',
        hoverBorderColor: 'rgba(255,99,132,1)',
        data: [65, 59, 80, 81, 56, 55, 40]
      }
    ]
  };

  const mountComponent = props => mount(
    <Chart data={data} {...props} />,
    { attachTo: DOM.body.firstChild }
  );

  beforeEach(() => {
    DOM = createDOM();
  });

  it('renders', () => {
    const wrapper = mountComponent();
    console.log(wrapper.html());

    expect(true).to.be.true;
  });
});

It's testing Chart.js which mounts to canvas elements.它正在测试挂载到画布元素的 Chart.js。 I'm using mocha and enzyme but shouldn't be different for w/e test suite you are using.我正在使用 mocha 和酶,但对于您正在使用的 w/e 测试套件,应该不会有所不同。

I ran into this issue testing Lottie animations using AVA.我在使用 AVA 测试 Lottie 动画时遇到了这个问题。 With help from gor181 's solution, here is how I got it to work, using the latest version of JSDOM 16.2.1 in 2020.gor181的解决方案的帮助下,这是我在 2020 年使用最新版本的JSDOM 16.2.1使其工作的方法。

First Install canvas and JSDOM npm i canvas jsdom --save-dev首先安装 canvas 和 JSDOM npm i canvas jsdom --save-dev

Here is an example test using JSDOM:这是一个使用 JSDOM 的示例测试:

import { mount } from 'enzyme';
import { JSDOM } from 'jsdom';

const DOM = new JSDOM('<!doctype html><html><body><div></div></body></html>');
const mountComponent = props =>
    mount(<AnimationComponent options={defaultOptions} />, {
        attachTo: DOM.body
    });

test('Animation', t => {
    const wrapper = mountComponent();
    console.log(wrapper.html()); 
    t.is(wrapper.find('div').length, 2);
});

You can console.log DOM to see which elements you want to attach in your mountComponent您可以在 console.log DOM 中查看您要在mountComponent附加哪些元素

console.log('DOM:', DOM);

Hope this helps!希望这可以帮助!

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM