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

如何测试React Navigation组件的expanded状态变更与点击事件

React Navigation组件测试:验证expanded状态与路由跳转

需求说明

需要为以下React Navigation组件编写测试,验证两个核心交互逻辑:

  • 点击活跃导航项后,expanded状态更新为true
  • 当expanded为true时,点击非活跃导航项,状态变回false并跳转到对应路由

原组件代码

import React, {useState} from 'react';
import {App} from "../../../../interfaces/interfaces";
import {map, find, filter} from "lodash";
import NavigationItem from "./NavigationItem";
import {useLocation, useNavigate} from 'react-router-dom';

interface NavigationProps {
    applications: App[],
    companyName: string
}

const Navigation: React.FC<NavigationProps> = ({applications, companyName}: NavigationProps) => {
    const [expanded, setExpanded] = useState<boolean>(false)
    const location = useLocation();
    const navigate = useNavigate();

    const activeApplication = find(applications, application => application.url === location.pathname);
    const inactiveApplications = filter(applications, application => application.url !== location.pathname)

    return (
        <div data-testid="navigation-component" className="flex h-full">
            <div className="z-[2]">
                <NavigationItem application={activeApplication}
                                companyName={companyName}
                                handleClick={() => setExpanded(expanded => !expanded)}
                                selected={expanded}
                />
            </div>

            <div data-testid="inactive-items" className={`flex transform transition ${expanded ? 'translate-x-0' : '-translate-x-full'}`}>
                {map(inactiveApplications, application => {
                    return <NavigationItem key={application.id}
                                           application={application}
                                           companyName={companyName}
                                           handleClick={() => {
                                               setExpanded(false);
                                               navigate(application.url);
                                           }}
                    />
                })}
            </div>
        </div>
    );
};

export default Navigation;

已编写的测试代码(待完善)

import Header from "./Header";
import Navigation from "./components/Navigation";
import {MemoryRouter} from "react-router-dom";

import {describe, it} from "@jest/globals";
import {act, fireEvent, render, screen, waitFor} from "@testing-library/react";
import userEvent from "@testing-library/user-event";

describe("<Navigation />", () => {
it("checks navigation clicks events", () => {
    const applications = [
        {id: 1, name: "Home", url: "/", classes: "bg-gray-100 text-blue-800"},
        {id: 3, name: "Sales", url: "/sales", classes: "bg-red-500 text-white"},
        {id: 2, name: "Expert", url: "/expert", classes: "bg-green-400 text-white"}
    ];
    const companyName = 'My company';
    render(<MemoryRouter initialEntries={["/"]}>
        <Navigation applications={applications} companyName={companyName} />
    </MemoryRouter>);

    // Get the selected NavigationItem
    const activeNavigationItem = screen.getByTestId(applications[0].name);

    // Simulate click event
    fireEvent.click(activeNavigationItem);
    // Check if click updated the state, changed expanded to true AND maybe check if other inactive NavigationItems have translate-x-0 class


    // Check if expanded is true, if it's true, then check if clicking one of the other NavigationsItems sets it to false and navigate to proper route
});
});

解决方案:通过UI状态与路由间接验证内部状态

组件的expanded状态直接映射到UI表现(非活跃导航容器的显示/隐藏)和路由行为,因此无需直接访问组件内部状态,通过验证UI变化和路由跳转即可完成测试。

完整测试代码

import Navigation from "./components/Navigation";
import {MemoryRouter, useLocation} from "react-router-dom";

import {describe, it, expect} from "@jest/globals";
import {fireEvent, render, screen, waitFor} from "@testing-library/react";

// 自定义hook获取当前路由
const useCurrentLocation = () => useLocation();

describe("<Navigation />", () => {
  it("toggles expanded state when clicking active navigation item", () => {
    const applications = [
      {id: 1, name: "Home", url: "/", classes: "bg-gray-100 text-blue-800"},
      {id: 3, name: "Sales", url: "/sales", classes: "bg-red-500 text-white"},
      {id: 2, name: "Expert", url: "/expert", classes: "bg-green-400 text-white"}
    ];
    const companyName = 'My company';
    
    render(
      <MemoryRouter initialEntries={["/"]}>
        <Navigation applications={applications} companyName={companyName} />
      </MemoryRouter>
    );

    // 获取目标元素
    const activeItem = screen.getByTestId(applications[0].name);
    const inactiveContainer = screen.getByTestId("inactive-items");

    // 初始状态验证:容器隐藏
    expect(inactiveContainer).toHaveClass("-translate-x-full");
    expect(inactiveContainer).not.toHaveClass("translate-x-0");

    // 触发点击
    fireEvent.click(activeItem);

    // 验证展开状态:容器显示
    expect(inactiveContainer).toHaveClass("translate-x-0");
    expect(inactiveContainer).not.toHaveClass("-translate-x-full");
  });

  it("closes expanded menu and navigates when clicking inactive item", async () => {
    const applications = [
      {id: 1, name: "Home", url: "/", classes: "bg-gray-100 text-blue-800"},
      {id: 3, name: "Sales", url: "/sales", classes: "bg-red-500 text-white"},
      {id: 2, name: "Expert", url: "/expert", classes: "bg-green-400 text-white"}
    ];
    const companyName = 'My company';
    let currentLocation;

    // 渲染组件并捕获路由状态
    render(
      <MemoryRouter initialEntries={["/"]}>
        <Navigation applications={applications} companyName={companyName} />
        {/* 辅助组件跟踪路由变化 */}
        <CurrentLocationTracker setLocation={(loc) => currentLocation = loc} />
      </MemoryRouter>
    );

    // 先展开菜单
    const activeItem = screen.getByTestId(applications[0].name);
    fireEvent.click(activeItem);

    // 点击非活跃项
    const salesItem = screen.getByTestId(applications[1].name);
    fireEvent.click(salesItem);

    const inactiveContainer = screen.getByTestId("inactive-items");
    // 验证菜单收起
    expect(inactiveContainer).toHaveClass("-translate-x-full");
    expect(inactiveContainer).not.toHaveClass("translate-x-0");

    // 验证路由跳转
    await waitFor(() => {
      expect(currentLocation?.pathname).toBe("/sales");
    });
  });
});

// 辅助组件:捕获当前路由状态
const CurrentLocationTracker = ({ setLocation }) => {
  const location = useCurrentLocation();
  setLocation(location);
  return null;
};

核心逻辑说明

  1. 验证expanded状态:通过检查inactive-items元素的CSS类判断状态——translate-x-0对应展开(expanded=true),-translate-x-full对应收起(expanded=false)。
  2. 验证路由跳转:使用自定义辅助组件CurrentLocationTracker捕获路由变化,结合waitFor等待路由更新完成后断言路径正确性。
  3. 拆分测试用例:将两个交互逻辑拆分为独立测试,让测试目标更清晰,便于定位问题。

内容的提问来源于stack exchange,提问作者Boky

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.03 21:05:18