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
相关产品推荐
相关产品推荐

