React Testing Library测试Portal组件元素不存在解决方案
React Portal组件测试失败解决方案
问题场景
现有一个通过React createPortal 实现的弹窗组件,会将内容挂载到id为#___portal的全局常驻div节点下,节点基础结构为:
<div id="___portal"></div>
组件实现代码:
import React, { useState, useEffect } from "react"; import { createPortal } from "react-dom"; const App = ({ children }) => { const [portalEl, setPortalEl] = useState(); useEffect(() => { // 实际业务环境中 #___portal 节点始终存在 setPortalEl(document.getElementById("___portal")); console.log(portalEl); }, [portalEl]); if (!portalEl) return null; return createPortal( <> <div id="modal" role="dialog"> {children} </div> </>, portalEl ); }; export default App;
编写测试用例验证组件是否能正常渲染传入的文本内容时,测试失败,打印的DOM结构中#___portal节点为空,内容未成功挂载。
测试用例代码:
import App from "./App"; import React from "react"; import { render } from "react-testing-library"; describe("<App />", () => { it("Should render text", async () => { let testId = "___portal"; let newDiv = document.createElement("div"); newDiv.setAttribute("id", testId); document.body.appendChild(newDiv); const text = "hello world"; const { getByText } = render( <App> <div>{text}</div> </App> ); expect(getByText(text)).toBeInTheDocument(); }); });
测试执行时输出的DOM结构:
<body> <div id="___portal" /> <div /> </body>
失败原因
- 组件中获取portal节点的逻辑放在
useEffect里,首次渲染时portalEl为undefined,组件直接返回null,等effect执行完赋值后才会触发二次渲染挂载portal内容,但测试用例没有等待异步状态更新完成就直接执行断言。 - 旧版
react-testing-library的render返回的查询方法默认只会搜索React挂载根节点下的内容,portal是挂载到根节点外的#___portal节点,不在默认查询范围内。 useEffect的依赖数组包含portalEl,会导致effect在portalEl更新后重复触发,存在冗余执行。
修复方案
1. 优化组件逻辑
去掉不必要的effect依赖,直接在state初始化时获取portal节点,避免首次渲染返回null导致的额外重渲染:
import React, { useState } from "react"; import { createPortal } from "react-dom"; const App = ({ children }) => { // 初始化时直接获取portal节点,不需要等待useEffect执行 const [portalEl] = useState(() => document.getElementById("___portal")); if (!portalEl) return null; return createPortal( <div id="modal" role="dialog"> {children} </div>, portalEl ); }; export default App;
2. 调整测试用例
- 每个用例执行前清理DOM,避免全局节点污染
- 优先使用
screen全局查询方法,它默认搜索整个document范围,不受React挂载容器限制,适配portal场景 - 如果保留原有effect逻辑,需要用
waitFor等待异步渲染完成后再断言
调整后的测试代码:
import App from "./App"; import React from "react"; import { render, screen, waitFor } from "@testing-library/react"; describe("<App />", () => { beforeEach(() => { // 测试前清空body,避免重复添加节点 document.body.innerHTML = ""; }); it("Should render text", async () => { // 提前创建portal挂载节点 const portalContainer = document.createElement("div"); portalContainer.id = "___portal"; document.body.appendChild(portalContainer); const testText = "hello world"; render( <App> <div>{testText}</div> </App> ); // 等待内容挂载完成后断言 await waitFor(() => { expect(screen.getByText(testText)).toBeInTheDocument(); }); }); });
提示:如果使用的是支持portal配置的Testing Library版本,也可以在render时直接指定portal容器,不需要手动创建节点。
内容的提问来源于stack exchange,提问作者yavgz
相关产品推荐
相关产品推荐

