错误:不变的预期应用程序路由器要安装为什么在使用反应测试库时发生这种情况

How*_*Cui 9 next.js react-testing-library ts-jest

当我使用react-testing-library时,它说错误:不变的预期应用程序路由器要安装,在开发环境中运行时没有这样的问题。

测试逻辑在这里

import { render, screen } from "@testing-library/react"
import userEvent from "@testing-library/user-event"
import NavBar from "@/components/NavBar";

describe("<NavBar>", () => {
    it ("the login pop out displayed after click the login/sign up button", async () => {
        render(<NavBar />);

        const loginButton = screen.getByRole("link", {
            name: "LOGIN/SIGN UP"
        });
        const loginCard = screen.getByTestId("loginCard");

        await userEvent.click(loginButton);
        expect(loginButton).toBeCalled();
        expect(loginCard).toBeVisible();
    })
});
Run Code Online (Sandbox Code Playgroud)

组件在这里:导航栏:

"use client"

import React, { Dispatch, SetStateAction } from "react";
import { useRouter } from "next/navigation";

interface NavBarProps {
  setVisibleLogin?: Dispatch<SetStateAction<boolean>>;
}

const NavBar = ({ setVisibleLogin }: NavBarProps) => {
  const router = useRouter();

  const handleLoginBtn = () => {
    setVisibleLogin && setVisibleLogin(true);
  };

  const handleHome = () => {
    router.push("/");
  };

  return (
    <div className="bg-slate-50 flex justify-center">
      <div className="fix w-912px top-0 navbar w-require">
        <div className="navbar-start">
          <a
            className="btn btn-ghost normal-case text-xl text-sky-500"
            onClick={handleHome}
          >
            Alimama MealGPT
          </a>
        </div>
        <div className="navbar-center hidden lg:flex"></div>
        {
          // for distinguish between the page for login and others
          setVisibleLogin && (
          <div className="navbar-end">
            <a className="btn" onClick={handleLoginBtn}>
              Login/Sign Up
            </a>
          </div>
          )
        }
      </div>
    </div>
  );
};

export default NavBar;
Run Code Online (Sandbox Code Playgroud)

登录卡:

"use client"

import React, { Dispatch, SetStateAction } from "react";
import InputBox from "./InputBox";
import Image from "next/image";
import { useRouter } from "next/navigation";

import GoogleLoginCard from "@/assets/btn_google_signin_dark_normal_web@2x.png"

interface LoginCardProps {
  visibleLogin: boolean;
  setVisibleLogin: Dispatch<SetStateAction<boolean>>;
}

const LoginCard = ({visibleLogin, setVisibleLogin}: LoginCardProps) => {
  const router = useRouter();

  const handleCancel = () => {
    setVisibleLogin(false);
  }

  const handleSignUp = () => {
    router.push("/SignUp");
  }

  return (
    <div 
    className="fixed left-1/2 top-1/2 -translate-x-2/4 -translate-y-2/4 z-50" 
    style={{ visibility: visibleLogin ? "visible" : "hidden" }}>
      <div className="card w-96 bg-neutral text-neutral-content bg-slate-200">
        <div className="flex flex-row-reverse">
          <div className="flex flex-row-reverse" style={
            {
              position: "relative",
              top: "0.5rem",
              right: "0.5rem"
            }
          }>
            <button className="btn btn-outline w-2 h-2" onClick={handleCancel}>X</button>
          </div>
          <a className="relative right-10 top-3 btn btn-ghost normal-case text-xl text-sky-500">Alimama MealGPT</a>
        </div>
        <div className="card-body items-center text-center">
          <h2 className="card-title">Login</h2>
          <InputBox
            title="Email/ User ID"
            textHolder="Please Enter Your Email or ID Here"
            otherLeftOption={false}
            otherRightOption={false}
          />
          <InputBox
            title="Password"
            textHolder="Please Enter Your Password Here"
            otherLeftOption={true}
            otherRightOption={true}
            optionLeftText="Forget Password?"
            optionRightText="Any Other Helps?"
          />
          <div className="card-actions justify-end">
            <button className="btn btn-primary w-27">Log In</button>
            <button className="btn btn-primary w-25" onClick={handleSignUp}>Sign Up</button>
            <button className="btn btn-outline" onClick={handleCancel}>Cancel</button>
          </div>
          <div>
            <Image src={GoogleLoginCard}
              height={200}
              width={200}
              alt="google login button"
            />
          </div>
        </div>
      </div>
    </div>
  );
};

export default LoginCard;
Run Code Online (Sandbox Code Playgroud)

我看到其他一些类似的问题有这样的解决方案,例如在layout.tsx上添加html head和body标签,我已经将head和body标签放在layout.tsx中。

有谁知道如何解决这个问题?

我期待解决问题的解决方案。

ran*_*gfu 17

对我来说,它可以在 Jest 测试中模拟 useRouter,如下所示:

imports ...

// Mock useRouter:
jest.mock("next/navigation", () => ({
  useRouter() {
    return {
      prefetch: () => null
    };
  }
}));

describe("Test something", () => { ...
Run Code Online (Sandbox Code Playgroud)

这个想法来自这里(我刚刚将“next/router”更改为“next/navigation”)。


sts*_*nau 6

在真正的 Next.js 应用程序中,您的组件树被包裹在底层的一大堆不同的提供程序中。

 // from the Next.js source code...
 <AppRouterContext.Provider value={adaptedForAppRouter}>
    <SearchParamsContext.Provider value={adaptForSearchParams(router)}>
      <PathnameContextProviderAdapter
        router={router}
        isAutoExport={self.__NEXT_DATA__.autoExport ?? false}
      >
        <PathParamsContext.Provider value={adaptForPathParams(router)}>
          <RouterContext.Provider value={makePublicRouterInstance(router)}>
            <HeadManagerContext.Provider value={headManager}>
              <ImageConfigContext.Provider
                value={
                  process.env
                    .__NEXT_IMAGE_OPTS as any as ImageConfigComplete
                }
              >
                {children}
              </ImageConfigContext.Provider>
            </HeadManagerContext.Provider>
          </RouterContext.Provider>
        </PathParamsContext.Provider>
      </PathnameContextProviderAdapter>
    </SearchParamsContext.Provider>
  </AppRouterContext.Provider>
Run Code Online (Sandbox Code Playgroud)

当您调用useRouter()组件时,它会尝试从 中获取路由器实例AppRouterContext,并在无法读取时抛出错误。

// from the Next.js source code...
export function useRouter(): import('../../shared/lib/app-router-context.shared-runtime').AppRouterInstance {
  clientHookInServerComponentError('useRouter')
  const router = useContext(AppRouterContext)
  if (router === null) {
    throw new Error('invariant expected app router to be mounted')
  }

  return router
}
Run Code Online (Sandbox Code Playgroud)

在单元测试中,组件是单独呈现的。这意味着当您在测试中调用时,render(<MyComponent />)它对上面树中存在于真实 Next.js 或普通 React 应用程序中的提供程序一无所知。

useRouter因此,当在测试下的组件中进行调用并且使用真实的useRouterfromnext/navigation钩子并且相应的上下文提供程序不可用/未传递正确的值时,它将尝试读取上下文,失败并抛出错误。

一般来说,如 React 测试库文档中所述,使用自定义render自动将测试下的组件包装在应用程序中存在的所有提供程序中是一个好方法。

另一种选择是模拟(或部分模拟)从上下文中读取的模块,如下所示......

// example is for Vitest, but it's very similar for Jest
vi.mock('next/navigation', async () => {
  const actual = await vi.importActual('next/navigation');
  return {
    ...actual,
    useRouter: vi.fn(() => ({
      push: vi.fn(),
      replace: vi.fn(),
    })),
    useSearchParams: vi.fn(() => ({
      // get: vi.fn(),
    })),
    usePathname: vi.fn(),
  };
});
Run Code Online (Sandbox Code Playgroud)

在上面的示例中,模块中的真实useRouter钩子next/navigation被替换为不从上下文读取的模拟函数(因此,将不再抛出不变的预期应用程序路由器安装错误),并且只返回一个假值router错误),并且仅返回一个带有几个方法 ( push, replace) 也被设置为模拟函数。

如果它适合您的需求,您还可以模拟其他导出/方法(例如useSearchParamsusePathname)并更改它们的实现。

此类设置可以在测试设置文件中仅完成一次,也可以在每个测试的基础上完成。