Search code examples
enzymereact-bootstrap-typeaheadfetch-mock

Cannot test AsyncTypeahead from react-bootstrap-typeahead with Enzyme


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?


Solution

  • 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.