为何Testing Library的user-event需两次点击才等效fireEvent单次点击?
问题
我用React Hook Form做了一个带必填文本输入框和提交按钮的表单,用户空提交时,会通过RHF的formState: { errors }显示原本隐藏的错误提示div。
用React Testing Library的fireEvent写的测试能正常验证这个功能:
render(<MyPage />); const submitButton = screen.getByRole("button", { name: "Submit form" }); await act(async () => { fireEvent.click(submitButton); }); const errorMessage = screen.queryByText("This value is required"); expect(errorMessage).toBeVisible();
但换成更贴近真实用户行为的@testing-library/user-event测试时却失效:
const user = userEvent.setup(); render(<MyPage />); const submitButton = screen.getByRole("button", { name: "Submit form" }); await user.click(submitButton); const errorMessage = screen.queryByText("This value is required"); expect(errorMessage).toBeVisible();
这段代码查不到错误元素,用act包裹未await的user.click也没用。
只有用act包裹并await user.click时测试能过,但会出现“当前测试环境未配置支持act”的警告:
await act(async () => { await user.click(submitButton); });
有意思的是,不用包裹act,连续调用两次user-event事件(比如两次点击或一次点击加一次tab)就能无警告通过测试:
await user.click(submitButton); await user.click(submitButton);
或者
await user.click(submitButton); await user.tab();
我已经导入了@testing-library/jest-dom,猜测这和act警告有关。想知道为什么用user-event时需要两次事件才能达到fireEvent单次点击的效果?手动在浏览器测试时,点一次按钮错误信息就正常显示了。
原因与解决方案
核心差异
user-event和fireEvent的底层逻辑完全不同:
fireEvent是同步触发单一DOM事件,跳过了真实用户交互的完整流程,直接触发目标事件,所以能立刻触发RHF的验证逻辑并同步更新DOM。user-event是模拟真实用户的完整交互链,比如点击按钮会依次触发mousedown、focus(若按钮未聚焦)、mouseup、click等一系列异步事件,RHF的表单验证逻辑可能绑定在这些流程的后续步骤中,单次点击后验证流程还未完成,React状态未更新到DOM,导致查询不到错误元素。
正确测试写法
不需要用act包裹,也不用多次触发事件,只需使用异步查询方法或waitFor等待React完成状态更新和DOM渲染:
方法1:使用findByText(推荐)
findByText是异步查询,会自动等待元素出现:
const user = userEvent.setup(); render(<MyPage />); const submitButton = screen.getByRole("button", { name: "Submit form" }); await user.click(submitButton); const errorMessage = await screen.findByText("This value is required"); expect(errorMessage).toBeVisible();
方法2:使用waitFor包裹断言
waitFor会重复执行回调,直到断言通过或超时:
const user = userEvent.setup(); render(<MyPage />); const submitButton = screen.getByRole("button", { name: "Submit form" }); await user.click(submitButton); await waitFor(() => { const errorMessage = screen.getByText("This value is required"); expect(errorMessage).toBeVisible(); });
为什么多次事件能生效?
第二次调用user-event事件时,相当于给React留出了足够时间完成第一次点击后的验证流程和状态更新,同时第二次事件触发的交互会再次刷新React的渲染周期,此时错误状态已经同步到DOM,所以能查询到错误元素。本质上是利用第二次事件的异步等待时间,间接让React完成了第一次操作的后续更新。
内容的提问来源于stack exchange,提问作者MJV

