Skip to content

Instantly share code, notes, and snippets.

@thehig
Created April 25, 2018 14:16
Show Gist options
  • Save thehig/3db814b6050427903664e8f6a4956f6a to your computer and use it in GitHub Desktop.
Save thehig/3db814b6050427903664e8f6a4956f6a to your computer and use it in GitHub Desktop.
js: Decorated Enzyme
// Derivative work of https://github.com/yahoo/react-intl/wiki/Testing-with-React-Intl#helper-function-1
// Related to https://gist.github.com/mirague/c05f4da0d781a9b339b501f1d5d33c37/
import React from 'react';
import PropTypes from 'prop-types';
import {
configure,
mount as enzMount,
shallow as enzShallow,
render as enzRender
} from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
configure({ adapter: new Adapter() });
// === I18N ===
// Configure an i18n provider that will use the default language from the sample config
import { IntlProvider, intlShape } from 'react-intl';
import { defaultLanguage } from '../src/utils/i18n';
import sampleConfig from '../src/utils/sampleConfig';
const { messages } = sampleConfig;
const locale = defaultLanguage(messages);
const intlProvider = new IntlProvider(
{ locale, messages: messages[locale] },
{}
);
const intlContext = intlProvider.getChildContext().intl;
// === ROUTER ===
// Note: Even though this adds the router props, sometimes things will still fail
// with error: "You should not use <Link> outside a <Router>​​"
// To circumvent this replace
// .addDecorator(StoryRouter())
// With
// .addDecorator(getStory => <MemoryRouter>{getStory()}</MemoryRouter>)
import createRouterContext from 'react-router-test-context';
const routerContext = createRouterContext().router;
// === REDUX ===
import configureMockStore from 'redux-mock-store';
import { middlewares } from '../src/redux/store/configureStore';
const mockStoreCreator = configureMockStore(middlewares);
/**
* Create enzyme functions that will inject props into the provided component
*
* @param {Object} injectProps Props to be injected into the component
* @param {Object} injectPropTypes Prop Types to be injected into the component
*
* @returns { mount, shallow, render } with the injected props automatically injected around the component
*/
const createDecoratedEnzyme = (injectProps = {}, injectPropTypes = {}) => {
function nodeWithAddedProps(node) {
return React.cloneElement(node, injectProps);
}
/**
* Enzyme shallow render node with injected props
*/
function shallow(node, { context } = {}) {
return enzShallow(nodeWithAddedProps(node), {
context: { ...injectProps, ...context }
});
}
/**
* Enzyme mount node with injected props
*/
function mount(node, { context, childContextTypes } = {}) {
return enzMount(nodeWithAddedProps(node), {
context: { ...injectProps, ...context },
childContextTypes: {
...injectPropTypes,
...childContextTypes
}
});
}
/**
* Enzyme render node with injected props
*/
function render(node, { context, childContextTypes } = {}) {
return enzRender(nodeWithAddedProps(node), {
context: { ...injectProps, ...context },
childContextTypes: {
...injectPropTypes,
...childContextTypes
}
});
}
return { shallow, mount, render };
};
/**
* Create a set of enzyme mount, shallow and render that can inject intl, store and router as requested
*
* @param {intl} Boolean should intl be injected
* @param {store} Boolean should store be injected
* @param {router} Boolean should router be injected
*/
export default function decoratedEnzyme(
{ intl = false, store = false, router = false } = {},
{
injectIntl = intlContext,
injectIntlTypes = intlShape,
injectStore = mockStoreCreator({}),
injectStoreTypes = PropTypes.object,
injectRouter = routerContext,
injectRouterTypes = PropTypes.object
} = {}
) {
// === INJECT ===
let injectProps = {};
let injectPropTypes = {};
if (intl === true) {
injectProps.intl = injectIntl;
injectPropTypes.intl = injectIntlTypes;
}
if (store === true) {
injectProps.store = injectStore;
injectPropTypes.store = injectStoreTypes;
}
if (router === true) {
injectProps.router = injectRouter;
injectPropTypes.router = injectRouterTypes;
}
return createDecoratedEnzyme(injectProps, injectPropTypes);
}
import React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import {
storeDecorator,
paperDecorator
} from '../../../.storybook/decorators';
import { storySpec } from '../../../.storybook/storyTesting';
import { MyComponent } from './';
const withForm = label =>
storiesOf(label, module) //eslint-disable-line
.addDecorator(storeDecorator())
.addDecorator(paperDecorator());
const stories = storySpec(withForm('MyComponent'));
const minProps = {
handleSavingLink: action('handleSavingLink'),
handleNewSavingLink: action('handleNewSavingLink')
};
stories(
'Min Props',
() => <MyComponent {...minProps} savings={[]} />,
story => {
it('should render MyComponent', () =>
expect(story.find('MyComponent').length).toBe(1));
}
);
import initStoryshots from '@storybook/addon-storyshots';
import { storyshotsConfig } from '../../../.storybook/testing';
/* DOES NOT ACTUALLY PERFORM SNAPSHOT TESTING */
initStoryshots(storyshotsConfig);
// Files related to testing storybooks
// Currently using storyshots and addon-specifications for running Jest tests
export const enzymeRendersWithoutError = ({ story, context }) => {
// Spy on console.warn and console.err
const consoleWarn = jest.spyOn(console, 'warn');
const consoleError = jest.spyOn(console, 'error');
try {
// Use Enzyme to `render` the story, and the story to `render` the context
// We can't use 'shallow' because the storybook decorators would be all that gets rendered
// https://gist.github.com/thevangelist/e2002bc6b9834def92d46e4d92f15874
// https://github.com/storybooks/storybook/issues/995
const wrapper = Enzyme.render(story.render(context));
// Expect the render to complete successfully
expect(wrapper.length).toBe(1);
// Expect console.warn and console.error not to be called
expect(consoleWarn).not.toHaveBeenCalled();
expect(consoleError).not.toHaveBeenCalled();
// Enabling snapshot testing will cause out of memory errors, depending on how many tests you aim to run
// https://github.com/facebook/jest/issues/2179
// https://github.com/facebook/jest/issues/5239
// expect(wrapper).toMatchSnapshot();
} finally {
// Cleanup
consoleWarn.mockReset();
consoleError.mockReset();
consoleWarn.mockRestore();
consoleError.mockRestore();
}
};
export const storyshotsConfig = {
// integrityOptions is not supported in current version but next release
// tests should run approximately 20 seconds faster
integrityOptions: false,
test: enzymeRendersWithoutError
};
import { specs, describe, it } from 'storybook-addon-specifications';
import expect from 'jest-matchers';
import decoratedEnzyme from './decoratedEnzyme';
const { mount } = decoratedEnzyme({ intl: true, router: true, store: true });
/**
* Uses a provided storybook to create stories with specs (optional)
* @param {*} storybook
*/
const storySpec = (storybook, enzyme = mount) => (name, story, spec) => {
storybook.add(name, () => {
const instance = story();
if (spec) {
specs(() =>
describe(name, () => {
spec(enzyme(instance));
})
);
}
return instance;
});
};
export { storySpec, describe, it, expect };
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment