React's setState method allows to pass a callback called after the state has been changed.
I think it would be useful to be able to pass a callback the same way for asynchronous testing purpose.
Example :
it("should render childComponent when state says so", (done) => {
const wrapper = shallow(<ParentComponent />);
wrapper.setState({renderChild : true}, () => {
expect(wrapper.find(childComponent)).to.have.length(1);
done();
});
});
Does the test not work if you do these calls subsequentially?
const wrapper = shallow(<ParentComponent />);
wrapper.setState({renderChild : true});
expect(wrapper.find(childComponent)).to.have.length(1);
@blainekasten
Due to the asynchronous nature of setState, we have to wrap the expect like this:
const wrapper = shallow(<ParentComponent />);
wrapper.setState({renderChild : true});
setTimeout(() => {
expect(wrapper.find(childComponent)).to.have.length(1);
}, 50);
I could be wrong. But i'm pretty sure (especially with shallow) that setState is forced to be sychroneous.
For reference: https://github.com/airbnb/enzyme/blob/master/src/ShallowWrapper.js#L254-L256
Could you entertain me and remove the setTimeout call and show me the error you get?
We still rely on setState on the renderer returned from TestUtils.createRenderer, which as far as I know is still asynchronous.
At least, I didn't see anything in ReactTestUtils that indicates its not using the standard ReactUpdatesQueue which is async.
on that note, I have no problem passing the callback through if it works. Would you mind putting up a PR for us to see it in action?
I'm not sure why setState need a callback in testing with Enzyme.
I may be wrong.
But setState behaves as asynchronous only in batchedUpdates.
React is using batchedUpdates on mounting, unmounting, event handling.
(With the exception when you use ReactDOM.unstable_batchedUpdates as batchedUpdates)
In most cases, I think setState provided by Enzyme is using in the outside of batchedUpdates like this.
const wrapper = shallow(<ParentComponent />);
wrapper.setState({renderChild : true});
expect(wrapper.find(childComponent)).to.have.length(1);
So it behaves synchronously.
I think it's a rare case to need a callback on setState.
Am I missing something?
@koba04 when you call wrapper.setState enzyme will in turn call setState on the instance returned from React's shallow renderer. Which as far as I can tell, inherits from ReactCompositeComponent
https://github.com/facebook/react/blob/master/src/test/ReactTestUtils.js#L421-L434
Nothing that I can see indicates they are injecting an update strategy other than the default, but of course the React source is really dense so I might be missing something.
@blainekasten sure, as soon as I'll have some time
Might be worth seeing if someone from React core could comment on. I'll try and reach out to one of them.
I think it's generally a good idea to assume setState won't always act synchronously. Assuming it in tests can lead to assuming it works that way in actual code. I don't actually know enough about the thinking behind shallow renderer to say if it'll never be possible to hit situations where batching will be used, even today.
Thanks @zpao, if there's nothing explicit in the shallow renderer that forces synchronous state updates, then we should definitely allow a callback to be passed through.
I've come across this issue where I'm testing handlers with setState(state => nextState) calls in my components.
I wonder if it's possible to have wrapper.resolveState() or something similar which forces sync processing of the react state queue so we can assert on the next line without timeouts.
馃憤 for forcing sync setState() when run in tests.
It would save tons of code lines:
setTimeout(() => {
expect(foo).toBe("bar");
}, 50);
And than it still fails sometimes. Because async can be longer than 50ms.
@aronwoost
return Promise.resolve().then(() => {
expect(foo).toBe("bar");
});
@ljharb I don't get it.
@aronwoost then you're returning a promise, which mocha will wait on, you don't have to couple to a timeout value, and any exceptions will correctly fail your tests (which they won't with setTimeout)
@ljharb the promise resolves on next tick. Is it guaranteed that new state is populated and componentDidUpdate()/render() happened by then?
You'd handle that by using React callbacks, for example, like a callback to setState.
Yes, if setState() would allow a callback or return a Promise we could use it (thats what this ticket is about, right).
Until that however, we are stuck with setInterval.
Still, my statement from above (sync setState() would be useful in the case) stand.
hey I was facing a similar problems when doing some unit testing recently. I just opened up a PR that lets you pass a callback to the setState and setProps for mount and setState for shallow.
I got an unexpected behavior, the callback seems that is not working properly in some cases.
Component
import React, { Component } from 'react';
import isEmpty from 'lodash/isEmpty';
import { validation } from 'custom-validation'; // This is not async stuff
export default class MyComponent extends Component {
onSubmit() {
// In real component I perform a simple validation that is not async.
if(!isEmpty( validation(this.state) )) {
this.setState({ error: 'Some nasty error' });
}
}
render() {
const { foo, error } = this.state;
console.log('error', error);
return (
<form onSubmit={this.onSubmit}>
{foo}
</form>
);
}
state = {
foo: 'bar',
error: null
};
}
This returned null in console.log
const wrapper = mount(<Component />);
wrapper.setState({foo: 'baz'}, () => {
// My Test goes here.
});
This returned Some nasty error in console.log, which is the expected behavior.
const wrapper = mount(<Component />);
wrapper.setState({foo: 'baz'});
setTimeout(() => {
// My Test goes here
}, 0);
_Edit:_
Hmm after seeing the code maybe my component is broken :)
I came across this too. While testing event based actions I wanted to test them in the top level component. The function updates the state, and I when I went to check the state the state wasn't updated yet. I'd love a stateResolve function, but until then the new callback in setState seems to work well:
dashboard.instance().updateTablePage(5);
dashboard.setState({}, () => {
expect(dashboard.state().currentPage).toEqual(5);
done();
});
Thanks @alecrobins!
Edit: just kidding, this isn't a real solution. still need a stateResolve function, might look into it.
++ force resolving the setState cycle!
this is critical for testing components that depend on setState's callback chain, i don't see any way of accomplishing this without using setTimeout/setInterval, which is far from reliable
ex:
onAction(value) {
this.setState({ value }, () => {
this.someActionCallback();
});
}
how do I test for things that someActionCallback does?
any follow back on @mdreyer said.
@adeelibr update enzyme to v3.4.3 and try it out.
Most helpful comment
hey I was facing a similar problems when doing some unit testing recently. I just opened up a PR that lets you pass a callback to the
setStateandsetPropsformountandsetStateforshallow.