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 }
});
}
@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