Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to wait for setState in componentDidMount to resolve when testing with enzyme?

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?

like image 339
Andy Hansen Avatar asked Jun 08 '17 06:06

Andy Hansen


1 Answers

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):

  1. You add more calls either in sequence .then(...).then(... Another call).then(...)
  2. You replace server call with some sync operation(reading from Redux store or local storage)
  3. Refactor component to function version with hooks

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();
like image 155
skyboyer Avatar answered Oct 24 '22 06:10

skyboyer