Enzyme: Asynchronous setState not reflecting changes in shallow rendering

Created on 10 Jun 2016  路  12Comments  路  Source: enzymejs/enzyme

I'm having issues testing asynchronous events with the shallow renderer. Here's an example:

const Actions = {
  someAsync() {
    return Promise.resolve();
  }
};

const Test = React.createClass({
  getInitialState() {
    return {counter: 0};
  },

  render() {
    console.log('rendering!', this.state.counter);
    return (
      <div>
        <div id='counter'>{this.state.counter}</div>
        <button onClick={this._handleClick}>
          Increment
        </button>
      </div>
    );
  },

  _handleClick() {
    Actions.someAsync()
      .then(() => this.setState({counter: this.state.counter + 1}));
  }
});

describe('<Test />', function() {
  it('increments when clicked', function() {
    const wrapper = shallow(<Test />);

    const promise = Promise.resolve();
    sinon.stub(Actions, 'someAsync', () => promise);

    wrapper.find('button').simulate('click');

    return promise
      .then(() => {
        console.log('testing!');
        expect(wrapper.find('#counter').text()).to.equal('1');
      });
  });
});

The console output is:

LOG: 'rendering!', 0
LOG: 'rendering!', 1
LOG: 'testing!'

So, I'm pretty sure the test is running after the re-render. When I switch to using mount, the tests pass.

Any ideas?

question

Most helpful comment

@rylanc

ShallowRendering requires updating a shallowRender tree manually.
simulate method updates the shallowRender tree after calling event handler.

https://github.com/airbnb/enzyme/blob/b4007e6cf180af6dcbd8c2c9be11fdaaa07a4b92/src/ShallowWrapper.js#L484

In this case, you need to call update manually because you are calling setState asynchronously.

expect(wrapper.update().find('#counter').text()).to.equal('1');

All 12 comments

@rylanc

ShallowRendering requires updating a shallowRender tree manually.
simulate method updates the shallowRender tree after calling event handler.

https://github.com/airbnb/enzyme/blob/b4007e6cf180af6dcbd8c2c9be11fdaaa07a4b92/src/ShallowWrapper.js#L484

In this case, you need to call update manually because you are calling setState asynchronously.

expect(wrapper.update().find('#counter').text()).to.equal('1');

This appears to be answered - happy to reopen if not.

@koba04's link has changed since this was posted, here's a permalink for others using this issue as a reference.

Thanks!! I'll just update the original link in-place.

wow guys... I just want to share my experience. No matter what I did, I was not able to get the component to update.

Turns out that if you want to test a catch like:

Actions.someAsync()
    .then(() => this.setState({counter: this.state.counter + 1}))
    .catch(() => this.setState({counter: -1}))

In your test you have to...

return promise
    .then(() => {})  <- if you omit this, the next catch() will be fired before the one in your component
    .catch(() => {
        expect(wrapper.update().find('#counter').text()).to.equal('-1');
    });

I know this is more of a Promise thing, you can verify it in your browser as such:

var p = Promise.reject('ads');

p.then(function() { console.log('then1'); }).catch(function() { console.log('catch1'); });
p.catch(function() { console.log('catch2'); });
p.then(function() { console.log('then3'); }).catch(function() { console.log('catch3'); });

// prints:
// catch2
// catch1
// catch3

@mikegleasonjr I ran into this exact same issue an hour ago and your very timely post saved me a lot of time trying to figure out what was going on! <3

@will3216 Thank you very much for the feedback, I lost like 2 hours trying to figure this thing out. I'm glad to know it helped you! Next time someone help me like that I'll tell them!

@ljharb regarding @mikegleasonjr 's last comments: I'm confused, this accepted behaviour? The workaround posted is kinda... well, a workaround.

Yes, this is the nature of asynchrony in JS, combined with the fact that React doesn鈥檛 offer any way to know when async actions have completed.

myComponent.setState({}, () => {
 // state is guaranteed up-to-date
});

Just don't test via simulate('click') or whatever since React is responsible for passing click events, you just care that your handlers behave correctly

myComponent.instance().onClick({ /* whatever mock or stub you need */ })


function tick(component) {
  return new Promise(resolve => component.setState({}, resolve));
}

toggleSwitch.instance().onChange({ target: { checked: true }});
tick(toggleSwitch).then(() => {
  expect(toggleSwitch.state().checked).toBe(true);
});

@rylanc

ShallowRendering requires updating a shallowRender tree manually.
simulate method updates the shallowRender tree after calling event handler.

https://github.com/airbnb/enzyme/blob/b4007e6cf180af6dcbd8c2c9be11fdaaa07a4b92/src/ShallowWrapper.js#L484

In this case, you need to call update manually because you are calling setState asynchronously.

expect(wrapper.update().find('#counter').text()).to.equal('1');

No, this doesn't work for me. Still the same issue.

@rylanc

ShallowRendering requires updating a shallowRender tree manually.
simulate method updates the shallowRender tree after calling event handler.

https://github.com/airbnb/enzyme/blob/b4007e6cf180af6dcbd8c2c9be11fdaaa07a4b92/src/ShallowWrapper.js#L484

In this case, you need to call update manually because you are calling setState asynchronously.

expect(wrapper.update().find('#counter').text()).to.equal('1');

No, this doesn't work for me. Still the same issue.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

potapovDim picture potapovDim  路  3Comments

blainekasten picture blainekasten  路  3Comments

ivanbtrujillo picture ivanbtrujillo  路  3Comments

aweary picture aweary  路  3Comments

blainekasten picture blainekasten  路  3Comments