React-testing-library测试与浏览器行为不一致问题排查
React测试与浏览器行为不一致:防抖组件未触发父组件回调
我开发的FilterItems组件包含子输入组件SearchBar,浏览器中运行正常,但使用React-testing-library测试时出现问题:触发输入框change事件后,仅子组件中的console.log("setQuery triggered")被触发,父组件的handleChange函数未执行,导致测试无法获取带有data-testid="items"的元素。需要排查原因并让测试行为与浏览器保持一致。
相关代码
FilterItems.js
import { useState } from "react"; import SearchBar from "./Searchbar"; export default function FilterItems() { const [items, setItems] = useState([]); const handleChange = () => { console.log("Handle Change triggered"); setItems(["hello", "hello world", "hello there"]); }; return ( <div> <SearchBar defaultValue={""} handleChange={handleChange} /> {items.length ? ( <ul data-testid="items"> {items.map((el, index) => ( <li key={index}>{el}</li> ))} </ul> ) : null} </div> ); }
SearchBar.js
import React, { useEffect, useState, useRef } from "react"; const DebounceQuery = (value, delay) => { const [debouncedValue, setDebouncedValue] = useState(value); useEffect(() => { const handler = setTimeout(() => { setDebouncedValue(value); }, delay); return () => { clearTimeout(handler); }; }, [value, delay]); return debouncedValue; }; const SearchBar = ({ defaultValue = "", handleChange = () => {} }) => { const [query, setQuery] = useState(defaultValue); const debouncedQuery = DebounceQuery(query, 500); const ref = useRef(true); useEffect(() => { if (ref.current) { ref.current = false; return; } if (debouncedQuery !== "") handleChange(); }, [debouncedQuery]); useEffect(() => { setQuery(defaultValue); }, [defaultValue]); return ( <input placeholder="search" onChange={(e) => { console.log("setQuery triggered"); setQuery(e.target.value); }} value={query} /> ); }; export default SearchBar;
FilterItems.test.js
import React from "react"; import { render, fireEvent } from "@testing-library/react"; import FilterItems from "./FilterItems"; describe("Tests FilterItems", () => { it("Tests that items appear after search is filled", () => { const { getByPlaceholderText, getByTestId } = render(<FilterItems />); const searchInput = getByPlaceholderText("search"); fireEvent.change(searchInput, { target: { value: "hello" } }); const items = getByTestId("items"); expect(items).toBeInTheDocument(); }); });
问题原因
- 防抖延迟未等待:
SearchBar中使用了500ms的防抖逻辑,测试代码触发change事件后立刻尝试获取目标元素,此时防抖定时器尚未执行完毕,debouncedQuery未更新,因此不会触发父组件的handleChange。 - 测试同步执行特性:React-testing-library默认同步执行测试代码,不会自动等待异步操作(如定时器)完成,导致断言执行时DOM还未更新。
解决方案
方案1:使用waitFor等待异步DOM更新
React-testing-library提供的waitFor工具可以等待DOM异步更新完成,修改测试代码如下:
import React from "react"; import { render, fireEvent, waitFor } from "@testing-library/react"; // 引入waitFor import FilterItems from "./FilterItems"; describe("Tests FilterItems", () => { it("Tests that items appear after search is filled", async () => { // 标记为async函数 const { getByPlaceholderText, getByTestId } = render(<FilterItems />); const searchInput = getByPlaceholderText("search"); fireEvent.change(searchInput, { target: { value: "hello" } }); // 等待元素出现 await waitFor(() => { const items = getByTestId("items"); expect(items).toBeInTheDocument(); }); }); });
方案2:Mock防抖逻辑(加快测试速度)
如果不想等待真实的500ms,可以在测试中mock防抖函数,让它同步返回值,避免定时器等待:
import React from "react"; import { render, fireEvent, waitFor } from "@testing-library/react"; import FilterItems from "./FilterItems"; // Mock DebounceQuery函数,同步返回输入值 jest.mock("./Searchbar", () => { const OriginalSearchBar = jest.requireActual("./Searchbar").default; return { __esModule: true, default: (props) => { // 替换防抖逻辑为同步 const MockedSearchBar = OriginalSearchBar(props); return React.cloneElement(MockedSearchBar, { onChange: (e) => { e.target.value && props.handleChange(); MockedSearchBar.props.onChange(e); }, }); }, }; }); describe("Tests FilterItems", () => { it("Tests that items appear after search is filled", async () => { const { getByPlaceholderText, getByTestId } = render(<FilterItems />); const searchInput = getByPlaceholderText("search"); fireEvent.change(searchInput, { target: { value: "hello" } }); await waitFor(() => { expect(getByTestId("items")).toBeInTheDocument(); }); }); });
内容的提问来源于stack exchange,提问作者Donatello
相关产品推荐
相关产品推荐

