如何测试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; };
核心逻辑说明
- 验证expanded状态:通过检查
inactive-items元素的CSS类判断状态——translate-x-0对应展开(expanded=true),-translate-x-full对应收起(expanded=false)。 - 验证路由跳转:使用自定义辅助组件
CurrentLocationTracker捕获路由变化,结合waitFor等待路由更新完成后断言路径正确性。 - 拆分测试用例:将两个交互逻辑拆分为独立测试,让测试目标更清晰,便于定位问题。
内容的提问来源于stack exchange,提问作者Boky
相关产品推荐
相关产品推荐

