Testing

linkTesting with react-md

Right now you should be able to test most your application using your favorite test runner. The problems you will run into are when components use the ResizeObserver or you use a snapshotting feature like jest's snapshot testing.

Some of the components use the ResizeObserver component to handle positioning calculations. Since it uses the resize-observer-polyfill behind the scenes, you might run into an error:

ReferenceError: SVGElement is not defined
  at ~/code/your-repo/node_modules/resize-observer-polyfill/dist/ResizeObserver.js:651:57
Bash

This error occurs since jsdom has not implemented the SVGElement and when running tests, the resize-observer-polyfill does an unsafe check for determining if the element is an SVG. You can do a "hack" before your test to get your tests working:

global.SVGElement = Element;
Js

This should allow your tests to work as expected and not crash.

When running tests using the Drawer/NavigationDrawer component, you might run into an error:

TypeError: window.matchMedia is not a function

This can be fixed by mocking the window.matchMedia function in your test setup scripts. A simple mock would be:

if (typeof window.matchMedia !== 'function') {
  window.matchMedia = jest.fn(query => ({
    matches: query.includes('(min-width: 1025px)'),
  }));
}
Js

Some more information can be found in #783.

Some of my components use findDOMNode behind the scenes to be able to do calculations for positioning and other things, but this doesn't work with react-test-renderer.

There are a couple of ways to work around this:

  • mock the react-md components that fail

    The problem with mocking out the react-md components is that it becomes difficult if you want some reasonable markup after mocking. It will basically be the entire component from react-md, but without some of the lifecycle methods and ref callbacks.

  • use a different renderer for the snapshots

    A downside with this is that the snapshots have a little bit less information than the react-test-renderer snapshots.

  • snapshot something other than the html

Out of these three, I prefer using a different renderer for the snapshots and this is how this documentation site is tested. The enzyme renderer works quite well as long as you also install the enzyme-to-json package to create the snapshots as well.

I normally create a test helper file for re-use across tests. Here is the one I use for this website:

/* eslint-env jest */
/* eslint-disable react/jsx-filename-extension */
import React from 'react';
import { StaticRouter } from 'react-router';
import { Provider } from 'react-redux';
import renderer from 'react-test-renderer';
import { createStore } from 'redux';
import { render, shallow, mount } from 'enzyme';
import rootReducer from 'state';

/**
 * I'm lazy.
 *
 * Could be lazier and do `expectSnapshot` though..
 */
export function createSnapshot(children) {
  return renderer.create(children).toJSON();
}

/**
 * A simple test wrapper to create a snapshot with react-test-renderer and a component that
 * uses something from react-router.
 */
export function createRouterSnapshot(children, location = '/', context = {}) {
  return createSnapshot(
    <StaticRouter location={location} context={context}>
      {children}
    </StaticRouter>
  );
}

/**
 * A simple test wrapper to create a snapshot with react-test-renderer and a component that
 * requires the redux provider.
 *
 * > It is better to test "pure" components instead of these, so this probably shouldn't
 * > get used much.
 */
export function createReduxSnapshot(children, state) {
  const store = createStore(rootReducer, state);
  return createSnapshot(
    <Provider store={store}>
      {children}
    </Provider>
  );
}

/**
 * A simple test wrapper to create a snapshot with react-test-renderer and a component that
 * requires both the redux Provider and something from react-router.
 *
 * > It is better to test "pure" components instead of these, so this probably shouldn't
 * > get used much.
 */
export function createReduxRouterSnapshot(children, state, location = '/', context = {}) {
  const store = createStore(rootReducer, state);
  return createSnapshot(
    <Provider store={store}>
      <StaticRouter location={location} context={context}>
        {children}
      </StaticRouter>
    </Provider>
  );
}

/**
 * This is a simple test wrapper to create a snapshot with enzyme's render instead of react-test-renderer
 * and when the component or child component relies on react-router. This should be used when a component
 * or child component uses findDOMNode or the CSSTransitionGroup, otherwise the createRouterSnapshot should
 * be used.
 */
export function renderRouterSnapshot(children, location = '/', context = {}) {
  return render(
    <StaticRouter location={location} context={context}>
      {children}
    </StaticRouter>
  );
}

/**
 * This is a simple test wrapper to create a snapshot with enzyme's render instead of react-test-renderer
 * and when the component or child component requires the redux Provider. This should be used when a component
 * or child component uses findDOMNode or the CSSTransitionGroup, otherwise the createReduxSnapshot should
 * be used.
 */
export function renderReduxSnapshot(children, state) {
  const store = createStore(rootReducer, state);
  return render(
    <Provider store={store}>
      {children}
    </Provider>
  );
}

/**
 * This is a simple test wrapper to create a snapshot with enzyme's render instead of react-test-renderer
 * and when the component or child component requires both the redux Provider and something from react-router.
 * This should be used when a component * or child component uses findDOMNode or the CSSTransitionGroup,
 * otherwise the createReduxRouterSnapshot should be used.
 */
export function renderReduxRouterSnapshot(children, state, location = '/', context = {}) {
  const store = createStore(rootReducer, state);
  return render(
    <Provider store={store}>
      <StaticRouter location={location} context={context}>
        {children}
      </StaticRouter>
    </Provider>
  );
}

/**
 * A simple wrapper to use enzyme's shallow rendering when the component or a child
 * requires something from react-router.
 */
export function shallowWithRouter(children, location = '/', context = {}) {
  return shallow(
    <StaticRouter location={location} context={context}>
      {children}
    </StaticRouter>
  );
}


/**
 * A simple wrapper to use enzyme's mount rendering when the component or a child
 * requires something from react-router.
 */
export function mountWithRouter(children, location = '/', context = {}) {
  return mount(
    <StaticRouter location={location} context={context}>
      {children}
    </StaticRouter>
  );
}

/**
 * A simple wrapper to use enzyme's shallow rendering when the component or a child
 * requires something from react-redux.
 *
 * > It is better to test "pure" components instead of these, so this probably shouldn't
 * > get used much.
 */
export function shallowWithProvider(children, state) {
  const store = createStore(rootReducer, state);
  return shallow(<Provider store={store}>{children}</Provider>);
}

/**
 * A simple wrapper to use enzyme's mount rendering when the component or a child
 * requires something from react-redux.
 *
 * > It is better to test "pure" components instead of these, so this probably shouldn't
 * > get used much.
 */
export function mountWithProvider(children, state) {
  const store = createStore(rootReducer, state);
  return mount(<Provider store={store}>{children}</Provider>);
}

/**
 * A utility function that will override the console's behavior and throw errors
 * if console.error occurs. This is useful when you want to consider React warnings
 * as failures.
 */
export function captureConsole(match = null) {
  const console = global.console;

  beforeAll(() => {
    global.console = {
      ...console,
      error: (error) => {
        if (match === null || error.match(match)) {
          throw new Error(error);
        }
      },
    };
  });

  afterAll(() => {
    global.console = console;
  });
}
Jsx

Here are a couple of tests that use the utility functions.

import React, { PureComponent } from 'react';
import { Button, Grid, Cell, Toolbar, Drawer, bem } from 'react-md';

const NAV_ITEMS = [
  { primaryText: 'Woop woop' },
  { primaryText: 'That\'s the sound' },
  { primaryText: 'Of the Police' },
];

const idPrefix = 'theme-builder-preview';

export default class Preview extends PureComponent {
  state = { visible: false };

  toggleDrawer = () => {
    this.setState({ visible: !this.state.visible });
  };

  handleVisibilityChange = (visible) => {
    this.setState({ visible });
  };

  render() {
    return (
      <section className="md-background--card theme-preview" ref={(container) => { this._container = container; }}>
        <Toolbar
          id={`${idPrefix}-toolbar`}
          nav={<Button id={`${idPrefix}-drawer-toggle`} icon onClick={this.toggleDrawer}>menu</Button>}
          title="Theme Preview"
          colored
          fixed
        />
        <Grid className="md-toolbar-relative">
          <Cell component="h2" size={12} className="md-display-1">Look at this</Cell>
          <Button id={`${idPrefix}-btn`} primary raised className={bem('theme-preview', 'btn')}>
            Button
          </Button>
        </Grid>
        <Drawer
          id={`${idPrefix}-drawer`}
          renderNode={this._container}
          header={<Toolbar title="Theme Preview" id={`${idPrefix}-drawer-toolbar`} />}
          navClassName="md-toolbar-relative"
          navItems={NAV_ITEMS}
          visible={this.state.visible}
          onVisibilityChange={this.handleVisibilityChange}
          overlay
          type={Drawer.DrawerTypes.TEMPORARY}
        />
        <Button id={`${idPrefix}-floating-btn`} floating secondary fixed>email</Button>
      </section>
    );
  }
}
Jsx