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

为何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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.29 19:42:37