Created
April 25, 2018 14:16
-
-
Save thehig/3db814b6050427903664e8f6a4956f6a to your computer and use it in GitHub Desktop.
js: Decorated Enzyme
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | |
} | |
); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import initStoryshots from '@storybook/addon-storyshots'; | |
import { storyshotsConfig } from '../../../.storybook/testing'; | |
/* DOES NOT ACTUALLY PERFORM SNAPSHOT TESTING */ | |
initStoryshots(storyshotsConfig); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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