Skip to content

Instantly share code, notes, and snippets.

@mirague
Last active November 9, 2021 09:32
Show Gist options
  • Star 47 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • 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 }
});
}
@joncursi
Copy link

joncursi commented May 7, 2016

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

Cheers 🍻

@joncursi
Copy link

joncursi 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
Copy link
Author

mirague 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
Copy link

SamAmiri 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
Copy link

joncursi 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
Copy link
Author

mirague 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
Copy link

tdurand 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
Copy link

tdurand 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
Copy link

Robinfr 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
Copy link

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
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