floating-ui 如何为浮动元素组件编写 React 测试并避免 act 警告?

发布时间:2026/9/12 3:07:59
floating-ui 如何为浮动元素组件编写 React 测试并避免 act 警告? floating-ui 如何为浮动元素组件编写 React 测试并避免 act 警告【免费下载链接】floating-uiA JavaScript library to position floating elements and create interactions for them.项目地址: https://gitcode.com/GitHub_Trending/fl/floating-ui用floating-ui/react写的 tooltip、popover 组件在渲染后会通过异步微任务更新位置状态。直接在测试里render()之后立刻断言要么拿不到最终位置要么触发 React 的act警告——组件在测试框架之外改变了状态。floating-ui 的 React 文档给出了官方做法在浮动元素渲染后立即 flush 微任务即调用一次空act(async () {})。本文按仓库中packages/react自己的单测配置给出从环境搭建到跑通测试的完整路径。准备条件组件依赖安装来自 React 文档npm install floating-ui/react仓库中packages/react的单测使用 Vitest jsdom配套依赖可在 packages/react/package.json 中对照仓库实际使用的版本npm install -D vitest jsdom testing-library/react testing-library/jest-dom testing-library/user-event resize-observer-polyfill测试运行脚本就是包内定义的test命令见 packages/react/package.json# 在 packages/react 目录下 npx vitest run测试环境配置仓库的单测入口配置在 packages/react/vite.config.mts关键部分是test: { environment: jsdom, root: ./test/unit, setupFiles: [./setupTests.ts], },即 jsdom 环境、单测文件放在test/unit目录、每个测试文件加载setupTests.ts作为前置脚本。setupTests.ts是这套单测里容易踩坑的一环完整文件见 packages/react/test/unit/setupTests.ts各段的作用以文件内注释为准import * as matchers from testing-library/jest-dom/matchers; import {expect, vi} from vitest; import ResizeObserverPolyfill from resize-observer-polyfill; // 扩展 jest-dom 匹配器使 toBeInTheDocument() 可用 expect.extend(matchers); // testing-library 相关 issue 的兼容处理把 vi 暴露为 global jest (globalThis as any).jest vi; // 因为下面把 requestAnimationFrame 同步 mock // 浏览器中防止 ResizeObserver 报错的保护逻辑不再生效这里改为直接抛出 globalThis.addEventListener(error, (error) { throw error; }); // 把 requestAnimationFrame mock 成同步执行让测试更容易 vi.spyOn(window, requestAnimationFrame).mockImplementation( (callback: FrameRequestCallback): number { callback(0); return 0; }, ); // jsdom 缺少 inert 属性和 ResizeObserver补上 Object.defineProperty(HTMLElement.prototype, inert, { configurable: true, enumerable: false, writable: true, value: true, }); globalThis.ResizeObserver ResizeObserverPolyfill;文件末尾还补充了一个PointerEvent类继承MouseEvent字段按 W3C 指针事件规范设默认值因为useHover、useClick等 Hook 会派发指针事件jsdom 中没有这个全局类。如果你的组件只测useFocus/useDismiss这类交互可以只保留前几段只要测到鼠标/触摸交互就保留PointerEvent部分。避免 act 警告渲染后立即 flush 微任务React 文档的 Testing 一节给出的最小写法import {act} from testing-library/react; test(something, async () { render(Tooltip open /); await act(async () {}); // Flush microtasks. // Position state is ready by this line. });注释说明 flush 之后位置状态已就绪再往下断言位置相关属性即可且不会触发act警告。因为几乎每个测试都要写这一步文档建议封装成函数const waitForPosition () act(async () {}); test(something, async () { render(Tooltip open /); await waitForPosition(); expect(screen.queryByRole(tooltip)).toBeInTheDocument(); });这里screen.queryByRole(tooltip)和toBeInTheDocument()来自文档示例前者按role查询浮动元素后者是setupTests.ts中扩展进来的 jest-dom 匹配器。render、screen来自testing-library/react。一个完整的交互测试示例仓库自己的 useHover.test.tsx 展示了带交互 Hook 的组件怎么测。被测组件对应文档 Interactions 一节的用法function App(props: UseHoverProps) { const [open, setOpen] useState(false); const {refs, context} useFloating({ open, onOpenChange: setOpen, }); const {getReferenceProps, getFloatingProps} useInteractions([ useHover(context, props), ]); return ( button {...getReferenceProps({ref: refs.setReference})} / {open ( div roletooltip {...getFloatingProps({ref: refs.setFloating})} / )} / ); }hover 打开/关闭的测试test(opens on mouseenter, () { render(App /); fireEvent.mouseEnter(screen.getByRole(button)); expect(screen.queryByRole(tooltip)).toBeInTheDocument(); cleanup(); }); test(closes on mouseleave, () { render(App /); fireEvent.mouseEnter(screen.getByRole(button)); fireEvent.mouseLeave(screen.getByRole(button)); expect(screen.queryByRole(tooltip)).not.toBeInTheDocument(); cleanup(); });要点打开状态由useHover的onOpenChange驱动浮动元素是条件渲染的{open ...}所以断言元素是否存在即可每个用例结尾调用cleanup()卸载 DOM避免用例之间互相污染。可选分支带 delay 的交互用假定时器被测 Hook 支持delay参数见 useHover.test.tsx 中的delaydescribe 块。延迟逻辑依赖定时器仓库的做法是vi.useFakeTimers()并把时间推进包在act里执行vi.useFakeTimers(); test(symmetric number, async () { render(App delay{1000} /); fireEvent.mouseEnter(screen.getByRole(button)); await act(async () { vi.advanceTimersByTime(999); }); expect(screen.queryByRole(tooltip)).not.toBeInTheDocument(); await act(async () { vi.advanceTimersByTime(1); }); expect(screen.queryByRole(tooltip)).toBeInTheDocument(); cleanup(); });这段直接取自仓库测试delay的具体行为以 useHover 文档为准本文不展开。运行与验证在packages/react目录运行单测脚本npx vitest run判断标准以 React 文档的 Testing 一节和仓库测试的实际断言为准所有用例通过控制台没有act警告——waitForPosition()或act包裹定时器推进覆盖了渲染和交互之后的微任务位置相关断言在await waitForPosition()之后执行才有效文档明确说明“Position state is ready by this line”flush 之前的位置状态不可依赖延迟类用例中advanceTimersByTime(999)后断言未打开、再推进 1ms 后断言已打开说明延迟边界按 Hook 配置生效。限制与说明微任务 flush 只解决“位置状态在断言前就绪”和act警告问题组件本身的交互行为open/close、延迟仍要靠fireEvent/userEvent驱动后断言。上面的环境配置同步 rAF mock、ResizeObserver/PointerEvent polyfill是 jsdom 下模拟浏览器 API 的必要铺垫缺了PointerEvent或ResizeObserver时对应交互的测试无法运行。packages/react另有一套 browser 模式下的可视化测试vite.config.mts中TEST_ENVbrowser时启用基于 Playwright 的 chromium 实例与本文的 jsdom 单测是两条独立路径本文不涉及。可继续深入的材料useFloating 测试相关文档、setupTests.ts、useHover.test.tsx、useClick.test.tsx。【免费下载链接】floating-uiA JavaScript library to position floating elements and create interactions for them.项目地址: https://gitcode.com/GitHub_Trending/fl/floating-ui创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考