Cannot test AsyncTypeahead from react-bootstrap-typeahead with Enzyme

1.8k Views Asked by At

I am trying to test AsyncTypeahead from react-bootstrap-typeahead.

I have a very simple test component :

class AsyncTypeahead2 extends Component<Props, State> {

    constructor(props: Props) {
        super(props);
        this.state = {
            isLoading: false,
        };
    }
    render() {
        return ( <AsyncTypeahead
            isLoading={this.state.isLoading}
            onSearch={query => {
                this.setState({isLoading: true});
                fetch("http://www.myHTTPenpoint.com")
                    .then(resp => resp.json())
                    .then(json => this.setState({
                        isLoading: false,
                        options: json.items,
                    }));
            }}
            options={this.state.options}
            labelKey={option => `${option.stateName}`}
        /> )
    }
}

const url = "http://www.myHTTPenpoint.com"
fetchMock
    .reset()
    .get(
        url,
        {
            items: [
                {id:1, stateName:"Alaska"},
                {id:2, stateName:"Alabama"}
            ]
        },
    );

(Note that the URL is mocked to return two elements)

When I run this in my storybook it looks fine :

enter image description here

But if I want to test it (with Enzyme) it does not recognise the < li > items that pop up.

    let Compoment =
        <div>Basic AsyncTypeahead Example
            <AsyncTypeahead2/>
        </div>

    const wrapper = mount(Compoment);
    let json = wrapper.html();


    let sel = wrapper.find(".rbt-input-main").at(0)

    sel.simulate('click');
    sel.simulate('change', { target: { value: "al" } });

    expect(wrapper.find(".rbt-input-main").at(0).getElement().props.value).toBe("al")

    expect(wrapper.find(".dropdown-item").length).toBe(2) //but get just 1 element "Type to Search..."

Instead of finding two "dropdown-item" items there is just one item with the text "Type to Search...".

Is the AynchTypeahead not updating the DOM correctly with respect to Enzyme?

2

There are 2 best solutions below

0
Oliver Watkins On BEST ANSWER

The exact solution to my problem is in the following code (copy and paste into a JS file to see it work).

Things to note :

  • I needed to use the waitUntil function from the async-wait-until library. fetch-mock on its own does not provide the functionality to test async code.
  • I needed to add an ugly hack at global.document.createRange because of some tooltip issue with react-bootstrap-typeahead and jest.
  • use waitUntil to wait on changes on the internal state of the component
  • It is very important to call wrapper.update() to update the DOM afterwards.

..

import React, {Component} from 'react';

import waitUntil from 'async-wait-until';

import {mount} from "enzyme";
import fetchMock from "fetch-mock";
import {AsyncTypeahead} from "react-bootstrap-typeahead";


describe('Autocomplete Tests ', () => {

    test(' Asynch AutocompleteInput  ', async () => {

        class AsyncTypeaheadExample extends Component<Props, State> {

            constructor(props: Props) {
                super(props);
                this.state = {
                    isLoading: false,
                    finished: false
                };
            }

            render() {
                return (<AsyncTypeahead
                    isLoading={this.state.isLoading}
                    onSearch={query => {
                        this.setState({isLoading: true});
                        fetch("http://www.myHTTPenpoint.com")
                            .then(resp => resp.json())
                            .then(json => this.setState({
                                isLoading: false,
                                options: json.items,
                                finished: true
                            }));
                    }}
                    options={this.state.options}
                    labelKey={option => `${option.stateName}`}
                />)
            }
        }

        const url = "http://www.myHTTPenpoint.com"
        fetchMock
            .reset()
            .get(
                url,
                {
                    items: [
                        {id: 1, stateName: "Alaska"},
                        {id: 2, stateName: "Alabama"}
                    ]
                },
            );

        let Compoment =
            <AsyncTypeaheadExample/>


        // ugly hacky patch to fix some tooltip bug
        // https://github.com/mui-org/material-ui/issues/15726
        global.document.createRange = () => ({
            setStart: () => {
            },
            setEnd: () => {
            },
            commonAncestorContainer: {
                nodeName: 'BODY',
                ownerDocument: document,
            },
        });

        let wrapper = mount(Compoment);

        let sel = wrapper.find(".rbt-input-main").at(0)

        sel.simulate('click');
        sel.simulate('change', {target: {value: "al"}});
        expect(wrapper.find(".rbt-input-main").at(0).getElement().props.value).toBe("al")




        //now the async stuff is happening ...

        await waitUntil(() => {
            return wrapper.state().finished === true;
        }, 3000); //wait about 3 seconds

        wrapper.update() //need to update the DOM!

        expect(wrapper.find(".dropdown-item").length).toBe(2) //but get just 1 element "Type to Search..."
    })
});

UPDATE


I can also compare on wrapper items rather than doing a direct comparison on the state :

//now the async stuff is happening ...
await waitUntil(() => {
    wrapper.update() //need to update the DOM!

    return wrapper.find(".dropdown-item").length > 1
}, 3000); //wait about 3 seconds

This is probably better because it means i dont need to know about the component internals.

8
x00 On

<AsyncTypeahead> is asynchronous. On the other hand simulate() is synchronous. So at the time you get to expect() AsyncTypeahead not even started to populate the dropdown with <li> elements. You need to wait for it.

It's not specified, but it looks like you are using fetch-mock package. There is the flush function which

Returns a Promise that resolves once all fetches handled by fetch-mock have resolved

So this:

...

sel.simulate('click');
sel.simulate('change', { target: { value: "al" } });

await fetchMock.flush() // !!!

expect(wrapper.find(".rbt-input-main").at(0).getElement().props.value).toBe("al")
expect(wrapper.find(".dropdown-item").length).toBe(2)

should work.

...But probably it won't. Because

fetchMock.mock(...)
fetch(...)
await fetchMock.flush()

does work, but

fetchMock.mock(...)
setTimeout(() => fetch(...), 0)
await fetchMock.flush()

does not. await fetchMock.flush() returns right away if there was no call of fetch. And probably there won't be. Because <AsyncTypeahead> debounces.

(By the way, you can also try to mock fetch on a per-test basis. Just in case.)

So I see two options:

  1. Use something else instead of fetch-mock package. Where you can resolve your own Promises on mocked requests completion.
  2. https://tech.travelaudience.com/how-to-test-asynchronous-data-fetching-on-a-react-component-ff2ee7433d71
    import waitUntil from 'async-wait-until';
    ...
    test("test name", async () => {
        let Compoment = <AsyncTypeahead2/>
        ...
        await waitUntil(() => wrapper.state().isLoading === false);
        // or even
        // await waitUntil(() => wrapper.find(".dropdown-item").length === 2, timeout);
        expect(...)
    })
    
    This options if not pretty. But maybe it's your only option - there is not only the fetch-mock you should worry about. setState also asynchronous... and it looks like there is no pretty way to check when it's done updating the state and the DOM without changing the real code (which is quite undesirable).