Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
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 }
});
}
@joncursi

This comment has been minimized.

Copy link

commented May 7, 2016

This helper function worked for me:

/*
 * Wraps an internationalized React component within an <IntlProvider />
 * context with any BCP 47 language tag
 */

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

const enTranslationData = require('../../i18n/en.json');

function intl(component, locale) {
  return (
    <IntlProvider
      locale={locale}
      messages={enTranslationData}
    >
      {React.cloneElement(component)}
    </IntlProvider>
  );
}

export default intl;

Enzyme:

    const wrapper = mount(intl(<NotFoundPage />, 'en-US'));
@joncursi

This comment has been minimized.

Copy link

commented May 7, 2016

I still get the errors when trying to evaluate state of children though:

    const wrapper = mount(intl(<AppLayout />, 'en-US'));
    expect(
      wrapper.find('ChildComponent').state().drawerOpen
    ).toEqual(true);
     Error: ReactWrapper::state() can only be called on the root

Is this simply because we're tying to extend Enzyme into integration tests, when it is designed for unit tests?

@mirague

This comment has been minimized.

Copy link
Owner Author

commented May 7, 2016

@joncursi You can't return a mounted IntlProvider because then the root element will point to that component instead of the one you're trying to mount. You can only get the state of the mounted component (in your case AppLayout) because that instance is mapped to the ReactWrapper. The children will be instantiated, but their instances won't be mapped as far as I know.

I've experimented with this a lot. Hence you want to mount your testable component directly and "inject" or "wrap around" the intl context. The helper function will do this.

@joncursi

This comment has been minimized.

Copy link

commented May 7, 2016

You're right, thank you! Ended up going with your original script.

Cheers 🍻

@joncursi

This comment has been minimized.

Copy link

commented May 8, 2016

@mirague I'm running into one final snag: looking up the translations within en.json within the tests.

Sample <ActivityPage /> Component:

class ActivityPage extends Component {
  render() {
  ...
          <FormattedMessage
            id="lookmeup"
            defaultMessage="Unable to find"
          />
  ...
}
export default ActivityPage;

en.json:

{
  "lookmeup": "I was found!"
}

Enzyme:

      const wrapper = mountWithIntl(<ActivityPage />);
      expect(
        wrapper.find('span').text()
      ).toEqual('I was found!');

Error:

Error: Expected 'Unable to find' to equal 'I was found!'

It's unable to do the translation lookup, so it resorts to the default message. For your helpers to work, does this require the use of injectIntl in the actual component to be able to look up translations within en.json? I am currently wrapping my overall app component within an <IntlProvider> to achieve translation look-ups within the browser. Wondering if I'm missing something with injection. How is your app component configured?

App setup (React Router):

...
const enTranslationData = require('../i18n/en.json');
const userLocale = navigator.language;
...

    <IntlProvider
      locale={userLocale}
      messages={enTranslationData}
    >
      {renderRoutes()}
    </IntlProvider>, document.getElementById('render-target'));

...
@mirague

This comment has been minimized.

Copy link
Owner Author

commented May 9, 2016

@joncursi In our App we're providing the messages to <IntlProvider locale="en" messages={ messages } />. The messages we simply require and pass on, exactly like you're doing. For our tests we're doing something similar as you can see on enzyme-test-helper line 13. Make sure the require path is correct and it should be up and running.

Did you try that?

@SamAmiri

This comment has been minimized.

Copy link

commented May 9, 2016

Thanks for the great info, but it appears I am also having the TypeError: (0 , _intlEnzymeTestHelper.mountWithIntl) is not a function in the test.spec.jsx file. In WebStorm when I highlight mountWithIntl in import { mountWithIntl } from './intl-enzyme-test-helper.js'; it tells me that Cannot resolve symbol 'mountWithIntl'. How did you resolve this issue?

@joncursi

This comment has been minimized.

Copy link

commented May 10, 2016

@mirague thanks for your help! It was user error on my part that caused the translations to not look up properly 😅

@SamAmiri - what's happening is that you are importing the functions underneath a mountWithIntl object. Rather than doing:

export default {
  shallowWithIntl(node) {
    return shallow(nodeWithIntlProp(node), { context: { intl } });
  },

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

Try exporting each function individually by name:

export function shallowWithIntl(node) {
    return shallow(nodeWithIntlProp(node), { context: { intl } });
}

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

This should import properly now:

import { mountWithIntl } from './intl-enzyme-test-helper.js';
@mirague

This comment has been minimized.

Copy link
Owner Author

commented May 11, 2016

@joncursi The returning an object with functions approach worked fine for me with a babel transpiler, what environment do you have?

@tdurand

This comment has been minimized.

Copy link

commented May 13, 2016

Hello, thanks for the gist

This does not work for me if i have to use the injectIntl API in my component like:

this.props.intl.formatMessage(messages.MY_MESSAGE)}

With the component tested wrapped with injectIntl

But it does work for component that just use the react syntax:

<FormattedMessage {...messages.MY_MESSAGE} />

I'm looking into it, but if you have some inputs

@tdurand

This comment has been minimized.

Copy link

commented May 13, 2016

Ok, i've figured out why, i was testing for redux with a Provider Wrapper , so the nodeWithIntlProp needed to simulate the injectIntl wasn't setting the prop intl to my component, but to the wrapper

const container = mountWithIntl(
            <Provider store={store}>
                <ForgotPassword successful />
            </Provider>
        );

The working method is to export the nodeWithIntlProp function and do this instead:

const component = nodeWithIntlProp(<ForgotPassword error />);
const container = mountWithIntl(
    <Provider store={store}>
       {component}
    </Provider>
        );

Will try to figure out a better syntax

@Robinfr

This comment has been minimized.

Copy link

commented Jul 15, 2016

The shallowWithIntl is pretty pointless with this approach however, since it only shallowly mounts the injectIntl wrapper.. Is there any way to avoid all of this?

E.g. I can no longer search for components within my component using shallowWithIntl..

@maniax89

This comment has been minimized.

Copy link

commented Jul 27, 2016

I am having similar problems. See gist: https://gist.github.com/maniax89/62bddbfeefa2e368d37e7f3a9270bd6f

Basically this leads me to getting the
Invariant Violation: [React Intl] Could not find requiredintlobject. <IntlProvider> needs to exist in the component ancestry.

I believe it is because I have embedded a <Link> with an API call to react-intl's formatMessage within a <FormattedMessage>

Any ideas on how to fix the test framework so that it is injected at all levels? Do I have to use mount for this?

@JeroenNelen

This comment has been minimized.

Copy link

commented Aug 11, 2016

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

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link

commented Jan 12, 2017

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

@olivier-o

This comment has been minimized.

Copy link

commented Jan 20, 2017

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

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link

commented Apr 3, 2017

Try this package enzyme-react-intl

@svedova

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link

commented Sep 15, 2017

Got it working with @svedova suggestion.

@MarcoNicolodi

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link

commented Jan 30, 2018

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

@gatsbn

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link

commented Sep 5, 2018

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

@hurvajs77

This comment has been minimized.

Copy link

commented Mar 20, 2019

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.