Skip to content

Instantly share code, notes, and snippets.

@mirague
Last active November 9, 2021 09:32
Show Gist options
  • Save mirague/c05f4da0d781a9b339b501f1d5d33c37 to your computer and use it in GitHub Desktop.
Save mirague/c05f4da0d781a9b339b501f1d5d33c37 to your computer and use it in GitHub Desktop.
Testing React-Intl components with Enzyme's mount() and shallow() methods. This is a helper function which wraps the `intl` context around your component tests in an easy and efficient way.

When using Enzyme and React-Intl you are likely run into the issues when testing components that use some of React-Intl's components or injected formatting functions. These helper functions (mountWithIntl and shallowWithIntl) aim to address some of the below errors:

Uncaught Invariant Violation: [React Intl] Could not find required ``intl`` object. <IntlProvider> needs to exist in the component ancestry.

TypeError: Cannot read property 'getInstance' of null

Error: ReactWrapper::state() can only be called on the root

Checkout this question at StackOverflow for a complete overview of the issues I ran into:

Injecting react-intl object into mounted Enzyme components for testing

import { mountWithIntl } from 'helpers/intl-enzyme-test-helper.js';
const wrapper = mountWithIntl(
<CustomComponent />
);
expect(wrapper.state('foo')).to.equal('bar'); // OK
expect(wrapper.text()).to.equal('Hello World!'); // OK
import React from 'react';
import { FormattedMessage } from 'react-intl';
class CustomComponent extends React.Component {
state = {
foo: 'bar'
}
render() {
return (
<div>
<FormattedMessage id="world.hello" defaultMessage="Hello World!" />
</div>
);
}
}
/**
* Components using the react-intl module require access to the intl context.
* This is not available when mounting single components in Enzyme.
* These helper functions aim to address that and wrap a valid,
* English-locale intl context around them.
*/
import React from 'react';
import { IntlProvider, intlShape } from 'react-intl';
import { mount, shallow } from 'enzyme';
// You can pass your messages to the IntlProvider. Optional: remove if unneeded.
const messages = require('../locales/en'); // en.json
// Create the IntlProvider to retrieve context for wrapping around.
const intlProvider = new IntlProvider({ locale: 'en', messages }, {});
const { intl } = intlProvider.getChildContext();
/**
* When using React-Intl `injectIntl` on components, props.intl is required.
*/
function nodeWithIntlProp(node) {
return React.cloneElement(node, { intl });
}
/**
* Export these methods.
*/
export function shallowWithIntl(node) {
return shallow(nodeWithIntlProp(node), { context: { intl } });
}
export function mountWithIntl(node) {
return mount(nodeWithIntlProp(node), {
context: { intl },
childContextTypes: { intl: intlShape }
});
}
@JeroenNelen
Copy link

I'm having a similar issue as @maniax89, when trying to use the shallowWithIntl on a
<Provider store={store}> <MyComponent {...this.props} /> </Provider>

results in a Invariant Violation: [React Intl] Could not find requiredintlobject. <IntlProvider> needs to exist in the component ancestry.

Or does it simply makes no sense to try and use shallow on a setup with a provider wrapped around a component?

Kind regards,

@silasb
Copy link

silasb commented Aug 31, 2016

@Robinfr you can if you get the WrappedComponent from the injectIntl component.

const Component = MyInjectIntlComponent.WrappedComponent;
let _wrapper = shallowWithIntl(<Component />, messages);

@damonbauer
Copy link

damonbauer commented Oct 26, 2016

As a follow up to what @silasb said, here's an example of using enzyme to find a component inside of a shallow mounted component:

it('renders a ChildComponent', () => {
  let WrappedParentContainer = ParentContainer.WrappedComponent;
  let wrapper = shallowWithIntl(<WrappedParentContainer {...props} />);
  let child = wrapper.find(ChildComponent);
  expect(child.length).toEqual(1);
});

@DVLP
Copy link

DVLP commented Jan 12, 2017

This is great! Why don't you wrap in NPM module for ease of use?

@olivier-o
Copy link

Here is a modified version if you need to pass some extract context

export function mountWithIntl(node, nodeContext = {}) {
  let fullContext = {
    context: {intl, ...nodeContext.params},
    childContextTypes: { intl: intlShape, ...nodeContext.types }
  }
  return mount(nodeWithIntlProp(node), fullContext)
}

nodeContext must be defined that way: an object with 2 properties, params and types where property names must match one for one. See example below.

  let nodeContext = { params:{requestedFunction: function(){}}, types:{requestedFunction: React.PropTypes.func} }

@vilvadot
Copy link

vilvadot commented Jan 23, 2017

Great gist!

When trying it Im getting this error though:

PhantomJS 2.1.1 (Mac OS X 0.0.0) ERROR
  Invariant Violation: [React Intl] The `Intl` APIs must be available in the runtime, and do not appear to be built-in. An `Intl` polyfill should be loaded.
  See: http://formatjs.io/guides/runtime-environments/
  at webpack:///~/react-intl/~/invariant/browser.js:47:0 <- tests/test-bundler.js:165012

Any idea why?

EDIT: It was a problem with PhantomJS which doesn't support Intl natively. I had to use karma-intl-shim and now I don't get that message but this.

Could not find "store" in either the context or props of "Connect(InjectIntl(ReduxForm))". Either wrap the root component in a <Provider>, or explicitly pass "store" as a prop to "Connect(InjectIntl(ReduxForm))

So I should fake the store too?

@joetidee
Copy link

joetidee commented Apr 3, 2017

Try this package enzyme-react-intl

@svedova
Copy link

svedova commented Jul 25, 2017

I blogged about a different approach. Basically, if you are using jest, you can mock the import statements and return a simple object. You won't have to think about importing helper functions anymore. You can read the details here.

@batjko
Copy link

batjko commented Jul 27, 2017

Similar to @svedova's approach, we just mock up the intl prop on the component, and then shallow-render the unwrapped component itself.
E.g.:

import { MyComp } from './MyComp' // as opposed to the default export

describe...

it('blabla', () => {
  const intl = {
    formatMessage: () => 'translatedText',
  }
  const wrapper = shallow(<MyComp intl={intl} />)
  expect(...
})

Obviously, this assumes you export MyComp as a named export (in addition to a default export of injectIntl(MyComp)), which I think is absolutely fine.

@alopes
Copy link

alopes commented Sep 15, 2017

Got it working with @svedova suggestion.

@MarcoNicolodi
Copy link

MarcoNicolodi commented Dec 13, 2017

Im having problems with these aproaches when testing redux connected components + intl injected components.

The test is rising this error:

Invariant Violation: Could not find "store" in either the context or props of "Connect(List)". Either wrap the root component in a , or explicitly pass "store" as a prop to "Connect(List)"

code:
const wrapper = shallowWithIntl(<List />, store);
wrapper.dive()

I want to Dive to traverse to my List component to make some assertions.

@mrcosta
Copy link

mrcosta commented Dec 29, 2017

Changing the locale to german, 'de', in the instantiation of IntlProvider doesn't "load" the translation.

I'm passing a set of german messages with translations, but the IntlProvider just work properly if we pass the locale as 'en'.

Do you know how to handle this? (I'm trying different approachs here, but none of them worked)

@gurusewak
Copy link

gurusewak commented Jan 18, 2018

@mrcosta I changed the code to the following to pass the locale while mounting my component. This makes it a bit easier for me to test the localized content.

import React from 'react';
import { IntlProvider, intlShape } from 'react-intl';
import { mount, shallow } from 'enzyme';

function nodeWithIntlProp(node, { intl }) {
  return React.cloneElement(node, { intl });
}

export function shallowWithIntl(node, setLocale) {
  const messages = require(`../../locales/${setLocale}-messages`); // locale.json
  const intlProvider = new IntlProvider({ locale: setLocale, messages }, {});
  const { intl } = intlProvider.getChildContext();
  return shallow(nodeWithIntlProp(node, { intl }), {
    context: { intl }
  });
};

export function mountWithIntl(node, setLocale) {
  const messages = require(`../../locales/${setLocale}-messages`); // locale.json
  const intlProvider = new IntlProvider({ locale: setLocale, messages }, {});
  const { intl } = intlProvider.getChildContext();

  return mount(nodeWithIntlProp(node, { intl }), {
    context: { intl },
    childContextTypes: { intl: intlShape }
  });
};

@danielnass
Copy link

@gurusewak thanks for this solution! Works like a charm!

@gatsbn
Copy link

gatsbn commented Feb 13, 2018

Try to fix working tests after i18n implementation:

 it('Component renders', () => {
    const wrapper = shallowWithIntl(<Component />)
    expect(wrapper.length).toBe(1)
    expect(wrapper.hasClass('Component')).toBeTruthy()
  })

and have error

TypeError: Cannot read property 'intl' of undefined
 at Object.<anonymous> (helpers\intl-enzyme-test-helper.js

if test without helper
const wrapper = shallow(<AddToRosterResponse />)
get error

expect(received).toBeTruthy()
    Expected value to be truthy, instead received
      false

Can you pls tell what is wrong?

@rickarubio
Copy link

rickarubio commented Feb 27, 2018

Here's what I'm using in case it is helpful to anyone else:

/**
 * Components using the react-intl module require access to the intl context.
 * This is not available when mounting single components in Enzyme.
 * These helper functions aim to address that and wrap a valid,
 * English-locale intl context around them.
 */

import React from 'react';
import { IntlProvider, intlShape } from 'react-intl';
import { mount, shallow } from 'enzyme';
import localizationHelper, { englishLocalizationFilePath } from '../../app/assets/js/apps/foo/localizationHelper';

// Create the IntlProvider to retrieve context for wrapping around.
const intlProvider = new IntlProvider(
  { locale: 'en', messages: localizationHelper }, {}
);
const { intl } = intlProvider.getChildContext();

/**
 * When using React-Intl `injectIntl` on components, props.intl is required.
 */
function nodeWithIntlProp(node) {
    return React.cloneElement(node, { intl });
}

export function shallowWithIntl(node, { context, ...additionalOptions } = {}) {
    if (node.type.name === 'InjectIntl') {
      const unwrappedType = node.type.WrappedComponent;
      node = React.createElement(unwrappedType, node.props);
    }
    return shallow(
        nodeWithIntlProp(node),
        {
            context: Object.assign({}, context, {intl}),
            ...additionalOptions,
        }
    );
}

export function mountWithIntl(node, { context, childContextTypes, ...additionalOptions } = {}) {
    if (node.type.name === 'InjectIntl') {
      const unwrappedType = node.type.WrappedComponent;
      node = React.createElement(unwrappedType, node.props);
    }
    return mount(
        nodeWithIntlProp(node),
        {
            context: Object.assign({}, context, {intl}),
            childContextTypes: Object.assign({}, { intl: intlShape }, childContextTypes),
            ...additionalOptions,
        }
    );
}

Now I can use mountWithIntl and shallowWithIntl without having to have double exports in my files (one for the wrapped version, one for the unwrapped).

I can simply do:

wrapper = shallowWithIntl(
        <MyComponent
          onSubmit={submitSpy}
          localeData={localeData}
        />
      );

Basically, when a node gets passed in to mountWithIntl, I get the wrapped component and create a new node using that unwrapped component. Then the helper injects intl into props.

My specs can all pretty much stay the same now. The only changes in my specs are:

1 - Change references to mount and shallow to mountWithIntl and shallowWithIntl for components wrapped by injectIntl.
2 - When spying on functions on the prototype, use:

      spy = sinon.spy(Foo.WrappedComponent.prototype, 'handleSubmit');

instead of

      spy = sinon.spy(Foo.prototype, 'handleSubmit');

Hope this helps someone!

@xiaodanpostmates
Copy link

rickarubio's method works! Especially when you use @injectIntl on your component.

@hurvajs77
Copy link

I try use solution from @gurusewak, but I still have error with intl.

 FAIL  src/modules/Public/Login/__tests__/WelcomeMessage.test.js
  LoginPage WelcomeMessage
     Should have a welcome text (9ms)

   LoginPage WelcomeMessage  Should have a welcome text

    TypeError: Cannot read property 'intl' of undefined

      11 |   const messages = require(`../i18n/locales/${setLocale}.json`); // locale.json
      12 |   const intlProvider = new IntlProvider({ locale: setLocale, messages }, {});
    > 13 |   const { intl } = intlProvider.getChildContext();
         |           ^
      14 |   return shallow(nodeWithIntlProp(node, { intl }), {
      15 |     context: { intl },
      16 |   });

and test:

import React from 'react';
import { shallowWithIntl } from '../../../../helpers/intl-enzyme-test-helper';
import WelcomeMessage from '../Components/WelcomeMessage';

describe('LoginPage WelcomeMessage', () => {
  it('Should have a welcome text', () => {
    const { wrapper } = shallowWithIntl(<WelcomeMessage />, 'en-GB');
    expect(wrapper.find('h1').exists()).toBe(true);
  });
});

Thanks for advice

@AlexKund
Copy link

AlexKund commented Dec 6, 2019

Thats how I achieve the things:

import React from 'react';
import StandardFilterIntl, {StandardFilter} from 'bundles/components/Filter/StandardFilter';
import {mountWithIntl} from 'enzyme-react-intl';

const FilterComponent = mountWithIntl(<StandardFilterIntl {...standardFilterProps} />);
FilterComponent.find(StandardFilter).state()

@linzhaoqiang
Copy link

@mirague I use your method but cannot render the component properly. Can you help me with this problem?

import React from 'react';
import {injectIntl, IntlProvider} from 'react-intl'

import { mount, shallow } from 'enzyme';
import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
const defaultLocale = 'en'
const locale = defaultLocale

function mountWithIntl(node) {
  return mount(node, {
    wrappingComponent: IntlProvider,
    wrappingComponentProps: {
      locale,
      defaultLocale,
      messages: {},
    },
  })
}

function shallowWithIntl(node) {
  return shallow(node, {
    wrappingComponent: IntlProvider,
    wrappingComponentProps: {
      locale,
      defaultLocale,
      messages: {},
    },
  })
}

Enzyme.configure({
  adapter: new Adapter()
});

class Func extends React.Component {
  render() {
    return <div>{int.formatMessage({id:'xxx'})}</div>
  }
}

const Component = injectIntl(Func);

describe('Name of the group', () => {
  it('should ', () => {
    const component = shallowWithIntl(<Component/>);
    expect(component.find('div').length).toBe(1);
    console.log(component);
  });
});

`

 FAIL  src/App.test.js
  Name of the group
    × should  (15ms)

   Name of the group  should 

    expect(received).toBe(expected) // Object.is equality

    Expected: 1
    Received: 0

      45 |   it('should ', () => {
      46 |     const component = shallowWithIntl(<Component/>);
    > 47 |     expect(component.find('div').length).toBe(1);
         |                                          ^
      48 |     console.log(component);
      49 |   });
      50 | });

      at Object.<anonymous> (src/App.test.js:47:42)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        4.275s

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment