无法使用酶从 react-bootstrap-typeahead 测试 AsyncTypeahead

Cannot test AsyncTypeahead from react-bootstrap-typeahead with Enzyme

我正在尝试从 react-bootstrap-typeahead.

测试 AsyncTypeahead

我有一个非常简单的测试组件:

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"}
            ]
        },
    );

(注意URL被mock成了return两个元素)

当我 运行 在我的故事书中看到它时,它看起来不错 :

但如果我想测试它(使用酶),它无法识别弹出的 < li > 项目。

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

没有找到两个 "dropdown-item" 项,而是只有一个包含文本 "Type to Search...".

的项

AynchTypeahead 是否没有根据酶正确更新 DOM?

<AsyncTypeahead> 异步的 。另一方面 simulate() 同步的 。因此,当您到达 expect() AsyncTypeahead 时,甚至还没有开始使用 <li> 元素填充下拉列表。您需要等待。

未指定,但看起来您正在使用 fetch-mock 包。 flush 函数

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

所以这个:

...

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)

应该可以。

...但可能不会。因为

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

确实有效,但是

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

没有。 await fetchMock.flush() returns 如果没有 fetch 的调用,马上。而且可能不会有。因为 <AsyncTypeahead> 去抖动。

(顺便说一句,您也可以尝试在每次测试的基础上模拟 fetch。以防万一。)

所以我看到两个选项:

  1. 使用其他东西代替 fetch-mock 包。在模拟请求完成时,您可以在哪里解决自己的 Promises
  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(...)
    })
    
    这个选项如果不漂亮。但也许这是您唯一的选择 - 您应该担心的不只是 fetch-mocksetState 也是异步的...而且看起来没有很好的方法来检查何时完成更新状态和 DOM 而不更改真实代码(这是非常不可取的)。

我的问题的确切解决方案在以下代码中(复制并粘贴到 JS 文件中以查看它是否有效)。

注意事项:

  • 我需要使用 async-wait-until 库中的 waitUntil 函数。 fetch-mock 本身不提供测试异步代码的功能。
  • 由于 react-bootstrap-typeahead 和 jest 的一些工具提示问题,我需要在 global.document.createRange 添加一个丑陋的 hack。
  • 使用 waitUntil 等待组件内部状态的变化
  • 之后调用wrapper.update()更新DOM非常重要。

..

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..."
    })
});

更新


我也可以比较包装项目而不是直接比较状态:

//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

这可能更好,因为这意味着我不需要了解组件内部。