lundi 28 décembre 2015

How can I use Mocha to unit-test a React component that paints on a ?

I have a React component that uses a <canvas> for user interaction. I'm not using react-canvas or react-art or anything like that; instead, I just draw on the canvas's 2D context in componentDidMount and componentDidUpdate.

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. 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. 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:

// 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

// 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 refs aren't yet supported in the shallow renderer.

Is there a way to approach this test with my current unit testing frameworks (i.e., not adding Jest or anything like that) while reducing the amount that the test harness needs to know about?

(The full canvas component is available here.)

Aucun commentaire:

Enregistrer un commentaire