next.js 错误:Invariant预期应用程序路由器将被挂载,为什么在使用React测试库时会发生这种情况

slsn1g29  于 12个月前  发布在  React
关注(0)|答案(2)|浏览(299)

当我使用react-testing-library时,它说error:invariant expected app router to be mounted,在开发环境中运行时没有这样的问题。
测试逻辑在这里

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

字符串
组件在这里:导航栏:

"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;


登录卡:

"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/[email protected]"

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;


我看到其他一些类似的问题有这样的解决方案,如添加HTML头和身体标记上的布局。tsx,我把头和身体标记在布局。tsx已经。
有人知道如何解决这个问题吗?
我期待解决问题的办法。

kd3sttzy

kd3sttzy1#

对我来说,在Jest测试中模拟useRouter是这样的:

imports ...

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

describe("Test something", () => { ...

字符串
这个想法是从这里取来的(我只是把“next/router”改成了“next/navigation”)。

mzsu5hc0

mzsu5hc02#

在一个真实的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>

字符串
当您在组件中调用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
}

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

因此,当在测试中的组件中调用useRouter,并使用来自next/navigation的真实的useRouter钩子,并且相应的上下文提供程序不可用/未传递正确的值时,它将尝试读取上下文,失败并抛出错误。
通常,如React Testing Library文档所述,使用自定义render自动将测试中的组件 Package 在应用中存在的所有提供程序中是一种很好的方法。
另一种选择是模拟(或部分模拟)从上下文读取的模块,如下所示。

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


在上面的示例中,来自next/navigation模块的真实的useRouter钩子被替换为不从上下文读取的mock函数(因此,invariant expected app router to be mounted错误将不再抛出),并返回一个带有几个方法的假router对象。(pushreplace),它们也被设置为mock函数。
如果它适合您的需求,您还可以模拟其他导出/方法(如useSearchParamsusePathname)并更改它们的实现。
这样的设置可以在测试设置文件中只完成一次,也可以在每个测试的基础上完成。

相关问题