How to mock useRouter from next/navigation? #48937
-
SummaryHi everyone, good day! I am trying to test my login page to see if it renders correctly, but somehow I received an error. "Error: invariant expected app router to be mounted" "use client"
... // other imports
import { useRouter, useSearchParams } from "next/navigation";
export function LoginForm() {
const router = useRouter();
const searchParams = useSearchParams();
const onSubmit = (data) => {
// Insert logic here
}
return <form onSubmit={handleSubmit(onSubmit)}>...</form>
} login-form.test.jsx import { render, screen } from "@testing-library/react";
import { LoginForm } from "login-form";
import "@testing-library/jest-dom";
describe("Login Form", () => {
it("should have required fields and submit button", () => {
render(<LoginForm />);
const emailField = screen.getByLabelText("Email");
const passwordField = screen.getByLabelText("Password");
const submitButton = screen.getByRole("button");
expect(emailField).toBeInTheDocument();
expect(passwordField).toBeInTheDocument();
expect(submitButton).toBeInTheDocument();
});
});
I tried solutions from this discussion (#23034), but they are using useRouter from "next/router". Thank you in advance! P.S. I am using app directory Additional informationNo response ExampleNo response |
Beta Was this translation helpful? Give feedback.
Replies: 9 comments 20 replies
-
You can mock the entire module, heres how I did it with vitest. I just mocked out enough for my tests to pass so the implementations are empty but you could provide a better/more useful mock if you wished. vi.mock("next/navigation", () => {
const actual = vi.importActual("next/navigation");
return {
...actual,
useRouter: vi.fn(() => ({
push: vi.fn(),
})),
useSearchParams: vi.fn(() => ({
get: vi.fn(),
})),
usePathname: vi.fn(),
};
}); |
Beta Was this translation helpful? Give feedback.
-
I had the same issue and finally I solved with implementing the following provider mock: // app-router-context-provider-mock.tsx
import {
AppRouterContext,
AppRouterInstance,
} from 'next/dist/shared/lib/app-router-context';
import React from 'react';
export type AppRouterContextProviderMockProps = {
router: Partial<AppRouterInstance>;
children: React.ReactNode;
};
export const AppRouterContextProviderMock = ({
router,
children,
}: AppRouterContextProviderMockProps): React.ReactNode => {
const mockedRouter: AppRouterInstance = {
back: jest.fn(),
forward: jest.fn(),
push: jest.fn(),
replace: jest.fn(),
refresh: jest.fn(),
prefetch: jest.fn(),
...router,
};
return (
<AppRouterContext.Provider value={mockedRouter}>
{children}
</AppRouterContext.Provider>
);
}; Then use it at in test: import { render, screen } from "@testing-library/react";
import { LoginForm } from "login-form";
import { AppRouterContextProviderMock } from './app-router-context-provider-mock';
import "@testing-library/jest-dom";
describe("Login Form", () => {
it("should have required fields and submit button", () => {
const push = jest.fn();
render(<AppRouterContextProviderMock router={{ push }}><LoginForm /></AppRouterContextProviderMock>);
const emailField = screen.getByLabelText("Email");
const passwordField = screen.getByLabelText("Password");
const submitButton = screen.getByRole("button");
expect(emailField).toBeInTheDocument();
expect(passwordField).toBeInTheDocument();
expect(submitButton).toBeInTheDocument();
});
}); |
Beta Was this translation helpful? Give feedback.
-
Solution #53499 |
Beta Was this translation helpful? Give feedback.
-
I used this one https://www.npmjs.com/package/next-router-mock (suggested link) by NextJs docs https://nextjs.org/docs/messages/next-router-not-mounted |
Beta Was this translation helpful? Give feedback.
-
My approach import { render, screen } from '@testing-library/react';
import { redirect } from 'next/navigation';
import AuthProvider from '../AuthProvider';
import NeedAuthGuard from './NeedAuthGuard';
jest.mock('next/navigation', () => ({
redirect: jest.fn(),
}));
function Component() {
return <p>Component is present</p>;
}
describe('NeedAuthGuard', () => {
const redirectMock = redirect as unknown as jest.Mock;
it('should render children if jwt is present', () => {
render(
<AuthProvider _jwt="jwt">
<NeedAuthGuard>
<Component />
</NeedAuthGuard>
</AuthProvider>
);
expect(screen.getByText('Component is present')).toBeInTheDocument();
expect(redirectMock).not.toHaveBeenCalled();
});
it('should redirect to /login if jwt is not present', () => {
render(
<AuthProvider>
<NeedAuthGuard>
<Component />
</NeedAuthGuard>
</AuthProvider>
);
expect(redirectMock).toHaveBeenCalledWith('/login');
});
}); |
Beta Was this translation helpful? Give feedback.
-
My approach with Vitest: import { render } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import { LocaleSwitcher } from '../index';
const { useRouter, mockedRouterPush } = vi.hoisted(() => {
const mockedRouterPush = vi.fn();
return {
useRouter: () => ({ push: mockedRouterPush }),
mockedRouterPush,
};
});
vi.mock('next/navigation', async () => {
const actual = await vi.importActual('next/navigation');
return {
...actual,
useRouter,
};
});
describe('LocaleSwitcher', () => {
it('should programmatically redirect after selecting locale', () => {
render(<LocaleSwitcher />);
// ...
// ... emulates user interaction according to your component's behavior ...
// ...
expect(mockedRouterPush).toHaveBeenCalledWith(expectedUrl);
});
}); |
Beta Was this translation helpful? Give feedback.
-
Hello everyone, good day! Sorry for the delayed response, I was occupied with other projects. Anyway, I want to thank everyone who made an effort to answer my question. I appreciate it. I tested all the provided solutions, and @yoshiomiyamae 's answer successfully resolved the problem. Thank you, and have a nice day, everyone! |
Beta Was this translation helpful? Give feedback.
-
I've made a Cypress Component Test version of #48937 (comment)
|
Beta Was this translation helpful? Give feedback.
-
You can also go a step further and test that a specific router method has been called: Here is what I tried and what worked for me: const mockRouterReplace = jest.fn();
const mockRouterRefresh = jest.fn();
const mockRouterPush = jest.fn();
jest.mock('next/navigation', () => {
const originalModule = jest.requireActual('next/navigation');
return {
__esModule: true,
...originalModule,
useRouter: jest.fn().mockImplementation(() => {
return {
push: mockRouterPush,
replace: mockRouterReplace,
refresh: mockRouterRefresh
};
}),
useSearchParams: jest.fn().mockImplementation(() => {
return new URLSearchParams(window.location.search);
}),
usePathname: jest.fn().mockImplementation((pathArgs) => {
return pathArgs;
})
};
}); Then in your tests you can use it like so: afterEach(() => {
mockRouterRefresh.mockRestore();
mockRouterReplace.mockRestore();
mockRouterPush.mockRestore();
});
it('calls the router push method', async() => {
expect(mockRouterPush).toHaveBeenCalledTimes(1);
expect(mockRouterPush).toHaveBeenCalledWith('/'); // Route Push argument
});
it('calls the router replace method', async() => {
expect(mockRouterReplace).toHaveBeenCalledTimes(1);
expect(mockRouterReplace).toHaveBeenCalledWith('/home'); // Route Replace argument
});
it('calls the router push method', async() => {
expect(mockRouterReset).toHaveBeenCalledTimes(1);
}); |
Beta Was this translation helpful? Give feedback.
I had the same issue and finally I solved with implementing the following provider mock: