Enzyme: With shallow, redux store update does not trigger componentDidUpdate

Created on 7 Feb 2019  路  14Comments  路  Source: enzymejs/enzyme

Hi,

I'm having trouble with a specific test scenario in my React + redux application. I want to test that a change to the redux store is correctly reflected in my component. For that, in my test I create a shallow component, connected to redux, and then I update the store using an action, then verify that the mounted component has the correct data. Which it doesn't.

Here's a test case to reproduce this:

import * as React from 'react';
import { connect } from 'react-redux';
import { createStore } from 'redux';

import { shallow } from 'enzyme';
import sinon from 'sinon';


const actions = {
    // This would fetch a resource on the network.
    getList: () => {},

    receiveList: list => {
        return {
            type: 'RECEIVE_LIST',
            list,
        };
    },

    // This would fetch a list of resources on the network.
    getObjects: () => {},
};


const reducer = (state = { list: [] }, action) => {
    switch (action.type) {
        case 'RECEIVE_LIST':
            return { ...state, list: action.list };
        default:
            return state;
    }
};


class MyAppBase extends React.Component {
    componentDidMount() {
        this.props.dispatch(actions.getList());
    }

    componentDidUpdate(nextProps) {
        if (nextProps.list !== this.props.list) {
            this.props.dispatch(actions.getObjects(nextProps.list));
        }
    }

    render() {
        return null;
    }
}


const MyApp = connect(state => { return { list: state.list }; })(MyAppBase);



describe('<MyApp>', () => {
    it('fetches objects once list is received', () => {
        // Mock actions that call the network.
        sinon.stub(actions, 'getList');
        actions.getList.returns({type: 'whatever'});
        sinon.stub(actions, 'getObjects');
        actions.getObjects.returns({type: 'whatever'});

        // Create a redux store.
        const store = createStore(reducer);

        // Create the component.
        const wrapper = shallow(<MyApp store={store} />).dive();

        // Verify that `componentDidMount` was called but not `componentDidUpdate`.
        expect(actions.getList.callCount).toEqual(1);
        expect(actions.getObjects.callCount).toEqual(0);

        // Dispatch an action that will change the redux store.
        store.dispatch(actions.receiveList([ 'aa', 'bb' ]));

        wrapper.update();

        // Expect the `componentDidUpdate` method to be called.
        expect(actions.getObjects.callCount).toEqual(1);

        // This doesn't work, for some reason `componentDidUpdate` is never called.
    });
});

Current behavior

My component's componentDidUpdate method is never called, and thus I cannot verify that my component has the correct data.

Expected behavior

componentDidUpdate is called with the state from the redux store.

Your environment

API

  • [x] shallow
  • [ ] mount
  • [ ] render

Version

| library | version
| ------------------- | -------
| enzyme | 3.7.0
| react | 16.6.0
| react-dom | 16.6.0
| react-test-renderer | n/a
| adapter (below) | 1.6.0

Adapter

  • [x] enzyme-adapter-react-16
  • [ ] enzyme-adapter-react-16.3
  • [ ] enzyme-adapter-react-16.2
  • [ ] enzyme-adapter-react-16.1
  • [ ] enzyme-adapter-react-15
  • [ ] enzyme-adapter-react-15.4
  • [ ] enzyme-adapter-react-14
  • [ ] enzyme-adapter-react-13
  • [ ] enzyme-adapter-react-helper
  • [ ] others ( )

Thanks!

All 14 comments

What version of react-redux are you using? v6 uses new Context, which enzyme does not yet support.

I'm using "react-redux": "5.1.0".

I think the issue is that .dive() doesn't currently bring context along correctly; this should be fixed in the next release.

Hi @ljharb, I've tried with the latest release (enzyme=3.9.0, enzyme-adapter-react-16=1.11.2) to no success. Was this expected to be fixed with that release? I've used the exact same test case I put in my first comment.

Thanks!

@adngdb yes, i'd expect it to be. if there's any way to make a minimal test case without redux, that would make it much easier to fix for sure :-)

Just to confirm that this happens without any use of Redux either - though I am using getDerivedStateFromProps - put a quick console.log in the constructor, getDerivedStateFromProps, and componentDidUpdate, and the last one doesn't get called.

class MyAppBase extends React.Component {
    static getDerivedStateFromProps() {
        console.log("getDerivedStateFromProps called");
        return { random: Math.random() }; // Force it to actually change state every time
    }

    constructor(props) {
        super(props);
        console.log("constructor called");
    }

    componentDidUpdate() {
        console.log("componentDidUpdate called");
    }

    render() {
        return null;
    }
}

It is still in a Redux app, but the tests I'm running are against the Component that is not connected to Redux as in the original example.

+1 to last comment.
I can also reproduce this with only getDerivedStateFromProps, no redux needed.

I think this is also already fixed in master, as I can't reproduce it from master, only in 3.9.0.

Given that ^ I'm going to close this; the next release of enzyme should fix it.

It's great that this fix is in master, but when will it be released? Given your package monolith, I have to jump through some serious hoops to make this work right now.

I think the getDerivedStateFromProps maybe be fixed as per https://github.com/airbnb/enzyme/issues/2009#issuecomment-484969410 but I'm not sure that the original issue is fixed on master.

The last expect of this example still fails for me with enzyme @ da5320719ba5416e33b37ef4c011a9bf3eeea87c and the enzyme-adapter-react-16 @ 1.13.0.

Note that this is using react-redux @ v5, so perhaps it's only a problem with legacy context. I have not had a chance to test further yet.

import * as React from 'react';
import { connect } from 'react-redux';
import { createStore } from 'redux';
import { shallow } from 'enzyme';

const reducer = (state = { count: 0 }, action) => {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    default:
      return state;
  }
};

class MyComponent extends React.Component {
  render() {
    return <div>{this.props.count}</div>;
  }
}

const MyConnectedComponent = connect((s) => s)(MyComponent);

describe('shallow mounted, connected component', () => {
  it('updates the wrapper returned from dive when the store changes', () => {
    const store = createStore(reducer);
    const wrapper = shallow(<MyConnectedComponent store={store} />).dive();

    expect(wrapper.text()).toEqual('0');

    store.dispatch({ type: 'increment' });

    expect(store.getState().count).toEqual(1);
    expect(wrapper.text()).toEqual('1');
  });
});

@tilgovi if latest master isn't fixing your issue, could you send a PR with a test case, ideally not using react-redux? That should help me quickly find a solution.

v3.10.0 has now been released.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

nelsonchen90 picture nelsonchen90  路  3Comments

abe903 picture abe903  路  3Comments

ahuth picture ahuth  路  3Comments

aweary picture aweary  路  3Comments

benadamstyles picture benadamstyles  路  3Comments