Skip to content

Instantly share code, notes, and snippets.

@thehig
Created January 23, 2019 18:07
Show Gist options
  • Select an option

  • Save thehig/27b090e154ca80218e1709bc0564825b to your computer and use it in GitHub Desktop.

Select an option

Save thehig/27b090e154ca80218e1709bc0564825b to your computer and use it in GitHub Desktop.

Revisions

  1. thehig created this gist Jan 23, 2019.
    197 changes: 197 additions & 0 deletions decorated_enzyme.jsx
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,197 @@
    // 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 merge from 'lodash.merge';

    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 'shared/i18n/utils';

    // Use the message bundler to assemble all the `defaultMessages` bundles
    import messageBundler from './messageBundler';
    const messages = messageBundler();
    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 'core/redux/store';
    export 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);
    }

    /**
    * Create a shallow renderer for a component
    *
    * @param {function} Component - The component to render with enzyme shallow
    * @param {Object} decorators - Defines which decorators will be injected into Component eg: `{ intl: true, store: true, router: true }`
    * @param {function} defaultStore - Returns object of values that will be added to every redux store
    * @param {function} defaultProps - Returns object of values that will be spread to every Component
    * @param {function} postProcess - Function run on wrapper before returning
    *
    * @returns {function} - A shallow renderer that will render Component as defined above
    *
    * @param {Object} store - Values to be injected to redux store
    * @param {Object} props - Values to be spread onto component
    *
    * @example Creating a shallow renderer with default store and default props
    *
    * const shallow = shallowRenderer({
    * Component: AuditLog,
    * decorators: { intl: true, store: true },
    * defaultStore: () => ({ error: undefined }),
    * defaultProps: () => ({ ready: false }),
    * postProcess: wrapper => wrapper.dive().dive()
    * });
    *
    * @example Using the renderer
    *
    * const wrapper = shallow({
    * store: { loading: false, data: undefined },
    * props: { ready: true }
    * });
    */
    export const shallowRenderer = ({
    Component,
    decorators = { store: true },
    defaultStore = () => ({}),
    defaultProps = () => ({}),
    postProcess = f => f
    } = {}) => ({ store = {}, props = {} } = {}) => {
    // Use decoratedEnzyme to create { mount, shallow, render } with appropriate contexts
    // Create mock store that has middlewares injected, and is populated with the default and test data
    const { shallow } = decoratedEnzyme(decorators, {
    injectStore: mockStoreCreator({
    ...defaultStore(),
    ...store
    })
    });

    const instanceProps = merge({}, defaultProps(), props);
    // Render the Component with the default and test props
    const wrapper = shallow(<Component {...instanceProps} />); //-? $.text()
    // Apply any enzyme post processing like `.dive()`
    return postProcess(wrapper);
    };