You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React Testing Library测试AsyncTypeahead时onChange未触发问题

AsyncTypeahead测试时onChange、selected不触发问题修复

问题场景

测试react-bootstrap-typeahead提供的AsyncTypeahead组件时,onChange处理函数与selected属性完全未触发。实际UI操作中输入内容、选择下拉选项时逻辑可正常运行,仅测试用例执行时无响应,相关代码如下:

Search.jsx

const getSelectedObject = value => {
        if (value) {
            return {name: value};
        }
        return value;
    };
const Search = props => {
    const client = useApolloClient();
    const [options, setOptions] = useState([]);
    const [isLoading, setIsLoading] = useState(false);

    const onSearch = async searchInput => {
        props.clear();
        if (!searchInput) return;
        setIsLoading(true);
        try {
            const variables = {
                fields: props.fields,
                value: searchInput,
            };
            const res = await client.query({
                query: props.query,
                variables,
            });
            const newOptions = res ? res.data: [];
            setOptions(newOptions);
        } catch (err) {
            console.error(err);
        } finally {
            setIsLoading(false);
        }
    };

    const searchValue = debounce(onSearch, 2000);
    return (
        <th style={props.style || {minWidth: "100px"}} id={props.id}>
            <label style={{visibility: "hidden"}} htmlFor={`${props.id}_input`}>
                {props.id}
            </label>
            <AsyncTypeahead
                inputProps={{name: props.name, id: `${props.id}_input`}}
                labelKey="name"
                id="fields"
                placeholder={props.placeholder || "Search"}
                onChange={(value) => console.log("onchange trigger", value)}
                selected={(val) => getSelectedObject(val)}
                isLoading={isLoading}
                onSearch={searchValue}
                options={options}
            />
        </th>
    );
};

Search.propTypes = {
    id: PropTypes.string.isRequired,
    name: PropTypes.string.isRequired,
    onChange: PropTypes.func.isRequired,
    selected: PropTypes.object,
    placeholder: PropTypes.string,
    accountRole: PropTypes.string.isRequired,
    fieldNames: PropTypes.array.isRequired,
};

Search.defaultProps = {
    selected: undefined,
    placeholder: "Search",
};

export default Search;

Search.test.jsx

test.only("selecting an option from search result SHOULD call ONCHANGE handler", async () => {
    render(<ConversationSearch {...props} />);

    // When
    userEvent.type(screen.getByRole("combobox"), "to");

    // Then
    expect(screen.getByRole("combobox").getAttribute("value")).toBe("to");
    await waitFor(() =>
        expect(screen.getByRole("listbox")).toBeInTheDocument(),
    );
    expect(
        screen.getByRole("link", {name: "Type to search..."}),
    ).toBeDefined();
    await waitFor(() =>
        expect(
            screen.getByRole("link", {name: "Searching..."}),
        ).toBeInTheDocument(),
    );
    await waitFor(() =>
        expect(
            screen.getByRole("option", {name: "option1"}),
        ).toBeInTheDocument(),
    );
    expect(
        screen.getByRole("option", {name: "option2"}),
    ).toBeInTheDocument();

    expect(screen.getByLabelText("option1")).toBeVisible();

    fireEvent.change(screen.getByRole("combobox"), {target: {value: "option1"}});

    // expect(screen.getByRole("combobox").getAttribute("value")).toBe("option1");
});

问题根因

一共三个核心问题:

  • selected属性用法错误:该属性是受控模式下传入当前选中值的prop,仅接收符合选项格式的对象/数组,不接收函数。当前代码把处理函数传给selected,组件无法解析正确的选中状态,关联逻辑全部失效。
  • 测试事件触发逻辑错误:AsyncTypeahead的onChange仅在用户明确选中下拉选项时触发,当前测试用fireEvent.change修改输入框值,只会触发搜索逻辑,不会触发选中回调。
  • 防抖逻辑未处理:搜索方法加了2000ms防抖,测试环境默认不会等待这么久,搜索逻辑根本没执行,下拉选项不会按预期渲染,后续选中操作自然无效。

修复方案

1. 修正组件逻辑

在组件内维护选中值的状态,将状态值传给selected prop,在onChange回调中更新选中状态,核心修改代码如下:

const Search = props => {
    const client = useApolloClient();
    const [options, setOptions] = useState([]);
    const [isLoading, setIsLoading] = useState(false);
    // 新增:维护本地选中状态
    const [selectedVal, setSelectedVal] = useState(props.selected);

    // ...保留原有onSearch、searchValue逻辑不变

    return (
        <th style={props.style || {minWidth: "100px"}} id={props.id}>
            <label style={{visibility: "hidden"}} htmlFor={`${props.id}_input`}>
                {props.id}
            </label>
            <AsyncTypeahead
                inputProps={{name: props.name, id: `${props.id}_input`}}
                labelKey="name"
                id="fields"
                placeholder={props.placeholder || "Search"}
                onChange={(value) => {
                    console.log("onchange trigger", value);
                    // 更新本地选中状态
                    setSelectedVal(getSelectedObject(value));
                    // 透传选中事件给父组件
                    props.onChange?.(value);
                }}
                {/* 传入选中值,不再传函数 */}
                selected={selectedVal}
                isLoading={isLoading}
                onSearch={searchValue}
                options={options}
            />
        </th>
    );
};

2. 修正测试用例逻辑

  • 启用Jest假计时器跳过防抖等待,不需要真实等待2000ms
  • 下拉选项渲染完成后,模拟用户点击选项的操作,而非直接修改输入框值
    核心修改代码如下:
test.only("selecting an option from search result SHOULD call ONCHANGE handler", async () => {
    // 启用假计时器,配置userEvent跳过延迟
    jest.useFakeTimers();
    const user = userEvent.setup({delay: null});
    render(<ConversationSearch {...props} />);

    // 输入搜索关键词
    await user.type(screen.getByRole("combobox"), "to");
    // 快进时间跳过2000ms防抖
    jest.advanceTimersByTime(2000);

    // 等待搜索完成、选项渲染
    await waitFor(() =>
        expect(screen.getByRole("option", {name: "option1"})).toBeInTheDocument()
    );
    expect(screen.getByRole("option", {name: "option2"})).toBeInTheDocument();

    // 模拟点击选中option1
    await user.click(screen.getByRole("option", {name: "option1"}));

    // 断言选中结果
    expect(screen.getByRole("combobox").value).toBe("option1");
    // 可在此处添加onChange被正确调用的断言
});

内容的提问来源于stack exchange,提问作者Ashwini

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 18:39:27