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.
linkresize-observer-polyfill
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
BashThis error occurs since jsdom has not implemented the
SVGElement
and when running tests, theresize-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;
JsThis should allow your tests to work as expected and not crash.
linkwindow.matchMedia
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)'), })); }
JsSome more information can be found in #783.
linkSnapshot Testing
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 failThe 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 fromreact-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.linkHelper Utils
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; }); }
Preview.jsx Preview.test.jsx Search.jsx Search.test.jsximport 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