I am trying to test a React component that runs some asynchronous code and calls setState in componentDidMount.
Here is my react component that I want to test:
/**
*
* AsyncComponent
*
*/
import React from 'react';
class AsyncComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
loaded: false,
component: null,
};
}
componentDidMount() {
this.props.component.then(component => {
this.setState({
loaded: true,
component: component.default ? component.default : component,
});
});
}
render() {
if (!this.state.loaded) {
return null;
}
const Component = this.state.component;
const { component, ...rest } = this.props;
return <Component {...rest} />;
}
}
export default AsyncComponent;
Here is the test case. I am using jest and enzyme.
import React from 'react';
import { mount } from 'enzyme';
import AsyncComponent from '../index';
const TestComponent = () => <div>Hello</div>;
describe('<AsyncComponent />', () => {
it('Should render loaded component.', () => {
const promise = Promise.resolve(TestComponent);
const rendered = mount(<AsyncComponent component={promise} />);
expect(rendered.state().loaded).toBe(true);
});
});
The test fails because state.loaded is still set to false. Is there a way I can make sure that AsyncComponent has fully loaded before calling expect?
I can get it to work if I wrap the expect assertion in a setTimeout, but that seems like a rather hacky way to do it. How should I go about doing this?
Approach with setTimeout
is totally fine. Since it's a macrotask, it will be guaranteed to be called only after microtasks queue becomes empty - in other words when all the Promises are resolved and .then
is processed
With this approach your test will legitimately pass after (we assume all server calls are properly mocked):
.then(...).then(... Another call).then(...)
The only thing I'd change - instead of checking state data(that would not with to function components and is fragile even to class components), I'd check .isEmptyRenderer()
that must be true
before timeout(so until promises are all settled) and false
inside of timeout
More on macrotask/microtask difference: https://javascript.info/event-loop
[UPD] as @Estus Flask noticed below, relying on setTimeout
in generic case might lead to callback hell(setTimeout
after first action, then nested setTimeout
to do next step etc). To avoid that we can use
await new Promise(resolve => { setImmediate(resolve); });
to flush microtasks queue. Or use tiny flush-promises
package that does the same under the hood but looks lighter:
await flushPromises();
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With