如何测试点击切换样式的组件?解决TypeError空值读取问题
问题
我正在测试一个菜单按钮组件,点击该按钮会显示另一个组件的导航链接覆盖层,但测试时持续出现错误:TypeError: Cannot read properties of null (reading 'style')。根据错误信息,我推测是style对象存在问题,但不清楚具体原因。请问是否需要进行Mock?如果需要,该如何操作?
相关代码:
Nav.js 文件
import "../../styles/Nav.sass"; import { useState } from "react"; const Nav = () => { const [navIsExpanded, setNavIsExpanded] = useState(false); return ( <button className="nav-button" onClick={() => { const navOverlay = document.getElementById("layer"); if (navIsExpanded) { setNavIsExpanded(false); navOverlay.style.left = "-80vw"; } else { setNavIsExpanded(true); navOverlay.style.left = "0vw"; } }} > {navIsExpanded ? "CLOSE" : "MENU"} </button> ); }; export default Nav;
Nav.test.js 文件
import React from "react"; import { render, screen } from "@testing-library/react"; import "@testing-library/jest-dom"; // optional import userEvent from "@testing-library/user-event"; import Nav from "../Components/Nav/Nav"; describe("Menu Button Component", () => { it("changes it's textContent onclick", () => { render(<Nav />); const navButton = screen.getAllByRole("button"); userEvent.click(navButton[0]); expect(setStateMock.mock.calls).toHaveLength(1); }); });
解答
错误根源很明确:测试时你只渲染了<Nav />组件,ID为layer的导航覆盖层根本不在测试环境的DOM里,所以document.getElementById("layer")返回null,自然无法读取style属性。
需要Mock,不过更推荐调整组件逻辑(符合React最佳实践),下面两种方案任选:
方案1:调整React组件逻辑(优先推荐)
直接操作DOM是React的反模式,应该用状态控制覆盖层的显示,而非手动获取DOM元素修改样式。可以把navIsExpanded状态提升,让Nav组件传递状态给父组件,再由父组件控制覆盖层的显示:
修改Nav.js
import "../../styles/Nav.sass"; import { useState } from "react"; const Nav = ({ onToggle }) => { const [navIsExpanded, setNavIsExpanded] = useState(false); const handleClick = () => { const newState = !navIsExpanded; setNavIsExpanded(newState); onToggle?.(newState); // 将状态传递给父组件 }; return ( <button className="nav-button" onClick={handleClick} > {navIsExpanded ? "CLOSE" : "MENU"} </button> ); }; export default Nav;
父组件示例
import Nav from "./Nav"; import NavOverlay from "./NavOverlay"; import { useState } from "react"; const App = () => { const [isOverlayOpen, setIsOverlayOpen] = useState(false); return ( <> <Nav onToggle={setIsOverlayOpen} /> <NavOverlay isOpen={isOverlayOpen} /> </> ); };
NavOverlay.js 用状态控制样式
import "./NavOverlay.sass"; const NavOverlay = ({ isOpen }) => { return ( <div id="layer" className={`nav-overlay ${isOpen ? 'open' : ''}`} > {/* 导航链接内容 */} </div> ); }; export default NavOverlay;
对应的Sass样式
.nav-overlay left: -80vw transition: left 0.3s ease &.open left: 0vw
调整后的测试用例(无需Mock)
import React from "react"; import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import Nav from "../Components/Nav/Nav"; describe("Menu Button Component", () => { it("changes text content on click and triggers toggle callback", async () => { const mockToggle = jest.fn(); render(<Nav onToggle={mockToggle} />); const navButton = screen.getByRole("button"); expect(navButton).toHaveTextContent("MENU"); await userEvent.click(navButton); expect(navButton).toHaveTextContent("CLOSE"); expect(mockToggle).toHaveBeenCalledWith(true); await userEvent.click(navButton); expect(navButton).toHaveTextContent("MENU"); expect(mockToggle).toHaveBeenCalledWith(false); }); });
方案2:Mock DOM元素(临时 workaround)
如果暂时不想修改组件逻辑,可以在测试前Mockdocument.getElementById,让它返回一个带style属性的模拟对象:
修改Nav.test.js
import React from "react"; import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import Nav from "../Components/Nav/Nav"; describe("Menu Button Component", () => { let mockNavOverlay; beforeEach(() => { // 创建模拟的覆盖层元素 mockNavOverlay = { style: {} }; // 替换document.getElementById方法 jest.spyOn(document, "getElementById").mockImplementation((id) => { if (id === "layer") return mockNavOverlay; return null; }); }); afterEach(() => { // 恢复原方法,避免影响其他测试 jest.restoreAllMocks(); }); it("changes its textContent on click and updates overlay style", async () => { render(<Nav />); const navButton = screen.getByRole("button"); expect(navButton).toHaveTextContent("MENU"); // 第一次点击打开菜单 await userEvent.click(navButton); expect(navButton).toHaveTextContent("CLOSE"); expect(mockNavOverlay.style.left).toBe("0vw"); // 第二次点击关闭菜单 await userEvent.click(navButton); expect(navButton).toHaveTextContent("MENU"); expect(mockNavOverlay.style.left).toBe("-80vw"); }); });
注意:方案2只是临时解决办法,长期来看还是推荐方案1。因为React的核心是状态驱动UI,直接操作DOM会导致组件耦合度高、测试困难,还容易引发其他潜在Bug。
内容的提问来源于stack exchange,提问作者Somto
相关产品推荐
相关产品推荐

