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

如何测试点击切换样式的组件?解决TypeError空值读取问题

问题

我正在测试一个菜单按钮组件,点击该按钮会显示另一个组件的导航链接覆盖层,但测试时持续出现错误:TypeError: Cannot read properties of null (reading 'style')。根据错误信息,我推测是style对象存在问题,但不清楚具体原因。请问是否需要进行Mock?如果需要,该如何操作?

相关代码:

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;
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} />
        </>
    );
};
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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.01 09:21:35