3

I'm trying to test a React component which has an async componentDidMount.

The promise itself doesn't need to be mocked, it's not necessarily for accessing outer content, mostly just a wrapper for props.

However, in order to test it I need to use wrapper.update() 4 times which seems really weird to me.

The solutions in:

all didn't work for me.

Here's what my test looks like (which currently works, but this solution isn't elegant at all, and not too scalable):

import * as React from 'react'
import { shallow, mount } from 'enzyme'
import LargeSelector from './LargeSelector'

describe('<LargeSelector />', async () => {
    const componentDidMountSpy = jest.spyOn(LargeSelector.prototype, 'componentDidMount')

    describe('search', async () => {
        it('should save initial response in cache', async () => {
            const wrapper = await shallow(<LargeSelector query={async (search) => ['search:' + search]} />)

            // WHY DO I NEED 4 UPDATES???
            await wrapper.update()
            await wrapper.update()
            await wrapper.update()
            await wrapper.update()

            expect(LargeSelector.prototype.componentDidMount).toHaveBeenCalledTimes(1) // works fine
            // these 2 only pass the expectation if I call wrapper.update() no less than 4 times    
            expect(wrapper.state()).toHaveProperty('options', ['search:'])
            expect(wrapper.state()).toHaveProperty('initialOptions', ['search:'])
        })
    })
})

Here are the implementations of componentDidMount and filterResults (call in the former):

public async componentDidMount() {
    if (this.props.searchOnInit) {
        const results = await this.filterResults('', [])
        if (this.props.cacheInitialResponse) {
            this.setState({ initialOptions: results })
        }
    }
}

private async filterResults(search: string, filters: IFilter[]) {
    const results = await this.props.query(search, filters)
    this.setState({ options: results })
    return results
}
skyboyer
  • 22,209
  • 7
  • 57
  • 64
casraf
  • 21,085
  • 9
  • 56
  • 91

3 Answers3

5

I was facing the exact same problem. The problem is that the test won't wait for the promises to be fulfilled. My solution was to use the the done callback, provided by Jest, to signal that the test ended.

Like this:

it('wait async code before assert something', (doneCallback) => {
    const wrapper = shallow(<Component />);

    setImmediate(() => {
        expect(wrapper.find('.async').length).toBe(1);
        doneCallback();
    });
});
3

enzyme-async-helpers helped me a lot with this sort of problem.
You could easily get it working by adding a loading state and then doing something like:

import * as React from 'react';
import { shallow, mount } from 'enzyme';
import LargeSelector from './LargeSelector';
import { waitForState } from 'enzyme-async-helpers';

describe('<LargeSelector />', async () => {

  describe('search', async () => {
    it('should save initial response in cache', async () => {
      const wrapper = await shallow(<LargeSelector query={async (search) => ['search:' + search]} />);

      await waitForState(wrapper, state => state.loading === false);

      expect(LargeSelector.prototype.componentDidMount).toHaveBeenCalledTimes(1); 
      expect(wrapper.state()).toHaveProperty('options', ['search:']);
      expect(wrapper.state()).toHaveProperty('initialOptions', ['search:']);
    });
  });
});

And:
this.setState({ initialOptions: results })
Would have to be updated to:
this.setState({ initialOptions: results, loading: false })

Community
  • 1
  • 1
cameck
  • 2,058
  • 20
  • 32
2

The weird behavior is probably because you used async for the componentDidMount implementation.

Currently, all React rendering process is synchronous, thus everything related to its rendering flow needs to be treated as synchronous. And it happens that currently too, the React team is developing a breaking change feature to allow async rendering.

But! Even after this feature is available, keep in mind that componentDidMount lifecycle hook will still be synchronous, and all the others hooks too, so it's important to be aware that React won't wait any Promise to resolve inside the hooks.

You can start a Promise inside the componentDidMount if it fits your use case, and let the resolved result change the state. But the lifecycle hook will finish before it resolves, this will affect your test case because the test will need to wait for that result of the resolved Promise to be processed before asserting it, you can use jest.runAllTicks() to guarantee this behavior.

rodgobbi
  • 1,442
  • 14
  • 10
  • This problem persists if there is any async call inside componentDidMount. Is there any way to specifically hook to componentDidMount and await that? (js.runAllTicks() didn't help) – casraf Jun 06 '18 at 08:01
  • Let's see, in the test, you can remove the `async`, and specially the `await` for `shallow` and `update`, you don't need that because everything is synchronous. To add to that, the test doesn't need to spy on the `componentDidMount`, because it is tested implicitly, if the state has changed because of the code inside it, that means it was called correctly, this is just a minor improvement. Try to log the steps that the async function accomplishes, so we can discuss. – rodgobbi Jun 06 '18 at 13:49
  • If `runAllTicks()` doesn't work, try this solution that often use: https://stackoverflow.com/questions/50454116/redux-observable-promise-is-not-getting-resolved-in-unit-test/50538026#5053802 – rodgobbi Jun 06 '18 at 13:50
  • While the run All Ticks didnt work for me. The explanation really helped me to understand the code flow, thank you. – Dale C Jan 27 '21 at 22:34