3

I'm trying to verify with a test that a stateful component's state is appropriately changed in componentDidMount, but hit a wall due to react-router.

I'm using Enzyme, so I use mount in order to evaluate lifecycle methods such as componentDidMount. Typically, this works just fine...

it("changes state after mount", () => {
  const newValue = "new value";

  const testPropertyRetriever = () => newValue;

  const wrapper = mount(
      <StatefulPage
        myProperty="initial value"
        propertyRetriever={testPropertyRetriever}
      />
  );

  // componentDidMount should have executed and changed the state's myProperty value
  //     from "initial value" to "new value"
  expect(wrapper.instance().state.myProperty).toEqual(newValue);
});

...but the component in question is problematic because mount renders a couple children deep, and in this case one of those descendants uses react-router's <Link>. So, running the above test results in errors: TypeError: Cannot read property 'history' of undefined and Failed context type: The context `router` is marked as required in `Link`, but its value is `undefined`.

The react-router docs recommend surrounding a test render of a component that requires context (for example, uses react-router's <Link>) with either <MemoryRouter> or <StaticRouter>, but that won't work because that makes the component under test a child instead of the root of the ReactWrapper, which makes it impossible (as far as I know) to retrieve the state of the component under test. (Given the example above...

// ...
const wrapper = mount(
  <MemoryRouter>
    <StatefulPage
      myProperty="initial value"
      propertyRetriever={testPropertyRetriever}
    />
  </MemoryRouter>
);

expect(wrapper.childAt(0).instance().state.myProperty).toEqual(newValue);

...the test fails with error ReactWrapper::instance() can only be called on the root).

I soon learned that enzyme's mount takes an options argument that allows for context to be passed into the render, which is what react-router needs. So I tried removing the router containment and providing context (based on this answer)...

//...
const wrapper = mount(
  <StatefulPage
    myProperty="initial value"
    propertyRetriever={testPropertyRetriever}
  />,
  { router: { isActive: true } }
);

expect(wrapper.instance().state.myProperty).toEqual(newValue);

...but that results in the same errors about context type that I began with. Either I'm not passing context in correctly, I don't know how to get the context carried down to the descendant that needs it, or there isn't a way (with these tools) to do so.

From here, I've been looking all over for details of how I might stub the context or mock one of the components, but haven't managed to put together the puzzle pieces effectively enough to successfully write and run this test.

How can I validate a component's state as changed by componentDidMount when it has descendants that depend on a context that satisfies react-router modules?

Matt
  • 33
  • 6

1 Answers1

6

The router definition provided to the mount function was incomplete.

const MountOptions = {
    context: {
        router: {
            history: {
                createHref: (a, b) => {
                },
                push: () => {
                },
                replace: () => {
                }
            }
        }
    }, childContextTypes: {
        router: PropTypes.object
    }
};
const wrapper = mount(
    <StatefulPage
        myProperty="initial value"
        propertyRetriever={testPropertyRetriever}
    />,
    MountOptions
);
Jeff Siver
  • 7,434
  • 30
  • 32
  • That was it! I had seen some things about childContextTypes specifying the PropType in my research, but it never quite clicked. This project is using Flow instead of PropTypes so I suppose I was avoiding it. – Matt Jun 22 '17 at 22:56