21

EDIT: Solved! Scroll down for the answer


In our Component tests we need them to have access to the react-intl context. The problem is that we are mounting single components (with Enzyme's mount()) without their <IntlProvider /> parent wrapper. This is solved by wrapping the provider around but then the root points to the IntlProvider instance and not to CustomComponent.

The Testing with React-Intl: Enzyme docs are still empty.

<CustomComponent />

class CustomComponent extends Component {
  state = {
    foo: 'bar'
  }

  render() {
    return (
      <div>
        <FormattedMessage id="world.hello" defaultMessage="Hello World!" />
      </div>
    );
  }
}

Standard Test Case (Desired) (Enzyme + Mocha + Chai)

// This is how we mount components normally with Enzyme
const wrapper = mount(
  <CustomComponent
    params={params}
  />
);

expect( wrapper.state('foo') ).to.equal('bar');

However, since our component uses FormattedMessage as part of the react-intl library, we get this error when running the above code:

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


Wrapping it with IntlProvider

const wrapper = mount(
  <IntlProvider locale="en">
    <CustomComponent
      params={params}
    />
  </IntlProvider>
);

This provides CustomComponent with the intl context it asks for. However, when trying to do test assertions such as these:

expect( wrapper.state('foo') ).to.equal('bar');

raises the following exception:

AssertionError: expected undefined to equal ''

This ofcourse because it tries to read the state of IntlProvider and not our CustomComponent.


Attempts to access CustomComponent

I have tried the below to no avail:

const wrapper = mount(
  <IntlProvider locale="en">
    <CustomComponent
      params={params}
    />
  </IntlProvider>
);


// Below cases have all individually been tried to call `.state('foo')` on:
// expect( component.state('foo') ).to.equal('bar');

const component = wrapper.childAt(0); 
> Error: ReactWrapper::state() can only be called on the root

const component = wrapper.children();
> Error: ReactWrapper::state() can only be called on the root

const component = wrapper.children();
component.root = component;
> TypeError: Cannot read property 'getInstance' of null

The question is: How can we mount CustomComponent with the intl context while still being able to perform "root" operations on our CustomComponent?

Mirage
  • 957
  • 1
  • 6
  • 24

1 Answers1

26

I have created a helper functions to patch the existing Enzyme mount() and shallow() function. We are now using these helper methods in all our tests where we use React Intl components.

You can find the gist here: https://gist.github.com/mirague/c05f4da0d781a9b339b501f1d5d33c37


For the sake of keeping data accessible, here's the code in a nutshell:

helpers/intl-test.js

/**
 * 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';

const messages = require('../locales/en'); // en.json
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 default {
  shallowWithIntl(node) {
    return shallow(nodeWithIntlProp(node), { context: { intl } });
  },

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

CustomComponent

class CustomComponent extends Component {
  state = {
    foo: 'bar'
  }

  render() {
    return (
      <div>
        <FormattedMessage id="world.hello" defaultMessage="Hello World!" />
      </div>
    );
  }
}

CustomComponentTest.js

import { mountWithIntl } from 'helpers/intl-test';

const wrapper = mountWithIntl(
  <CustomComponent />
);

expect(wrapper.state('foo')).to.equal('bar'); // OK
expect(wrapper.text()).to.equal('Hello World!'); // OK
Mirage
  • 957
  • 1
  • 6
  • 24
  • 2
    Using your helper above, I am getting `TypeError: (0 , _intl.mountWithIntl) is not a function` when trying to import mountWithIntl – Jon Cursi May 06 '16 at 05:40
  • Interesting, are you sure the reference to the `intl-test.js` helper file is correct? It seems to me the `import` is failing. Perhaps your `require()` set-up does not look for `.js` files by default? You could try to add the `.js` extension to your import file. – Mirage May 06 '16 at 12:08
  • I tried adding that and same error. What is your babel setup? – Jon Cursi May 07 '16 at 02:46
  • 1
    Got it! https://gist.github.com/mirague/c05f4da0d781a9b339b501f1d5d33c37#gistcomment-1771277 – Jon Cursi May 07 '16 at 03:17
  • Opted to go with your original helpers so that I could evaluate state of child components - that is an awesome feature. However, I'm now stuck on one final snag: looking up translations once the component is mounted https://gist.github.com/mirague/c05f4da0d781a9b339b501f1d5d33c37#gistcomment-1771847 – Jon Cursi May 08 '16 at 05:20
  • Thank you for this answer, it has helped me greatly. However, I now find myself needing to test with a locale other than 'en'. Changing the locale in the instantiation of IntlProvider doesn't quite do the job. For example, instantiate with a locale of 'fr' and try using formatNumber - a period is still used as a decimal separator rather than a comma. How can I ensure that IntlProvider also loads the required locale? – Wayne Birch May 24 '17 at 08:44
  • if reference is right, all functions are coming in one export ypu can separate exports of helper file and then it worked for me ! – Shatayu Darbhe Feb 11 '19 at 11:55