next-router-mock
Version:
Mock implementation of the Next.js Router
381 lines (292 loc) • 11.8 kB
Markdown
# `next-router-mock`
An implementation of the Next.js Router that keeps the state of the "URL" in memory (does not read or write to the
address bar). Useful in **tests** and **Storybook**.
Inspired by [`react-router > MemoryRouter`](https://github.com/remix-run/react-router/blob/main/docs/router-components/memory-router.md).
Tested with NextJS v13, v12, v11, and v10.
Install via NPM: `npm install --save-dev next-router-mock`
For usage with `next/navigation` jump to [Usage with next/navigation Beta](#usage-with-nextnavigation-beta)
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
- [Usage with Jest](#usage-with-jest)
- [Jest Configuration](#jest-configuration)
- [Jest Example](#jest-example)
- [Usage with Storybook](#usage-with-storybook)
- [Storybook Configuration](#storybook-configuration)
- [Storybook Example](#storybook-example)
- [Compatibility with `next/link`](#compatibility-with-nextlink)
- [Example: `next/link` with React Testing Library](#example-nextlink-with-react-testing-library)
- [Example: `next/link` with Enzyme](#example-nextlink-with-enzyme)
- [Example: `next/link` with Storybook](#example-nextlink-with-storybook)
- [Dynamic Routes](#dynamic-routes)
- [Sync vs Async](#sync-vs-async)
- [Supported Features](#supported-features)
- [Not yet supported](#not-yet-supported)
- [Usage with next/navigation Beta](#usage-with-nextnavigation-beta)
- [Usage with Jest](#usage-with-jest-1)
- [Jest Configuration](#jest-configuration-1)
- [Jest Example](#jest-example-1)
- [Supported](#supported)
- [Not supported yet](#not-supported-yet)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
# Usage with Jest
### Jest Configuration
For unit tests, the `next-router-mock` module can be used as a drop-in replacement for `next/router`:
```js
jest.mock("next/router", () => require("next-router-mock"));
```
You can do this once per spec file, or you can [do this globally using `setupFilesAfterEnv`](https://jestjs.io/docs/configuration/#setupfilesafterenv-array).
### Jest Example
In your tests, use the router from `next-router-mock` to set the current URL and to make assertions.
```jsx
import { useRouter } from "next/router";
import { render, screen, fireEvent } from "@testing-library/react";
import mockRouter from "next-router-mock";
jest.mock("next/router", () => jest.requireActual("next-router-mock"));
const ExampleComponent = ({ href = "" }) => {
const router = useRouter();
return <button onClick={() => router.push(href)}>The current route is: "{router.asPath}"</button>;
};
describe("next-router-mock", () => {
it("mocks the useRouter hook", () => {
// Set the initial url:
mockRouter.push("/initial-path");
// Render the component:
render(<ExampleComponent href="/foo?bar=baz" />);
expect(screen.getByRole("button")).toHaveTextContent('The current route is: "/initial-path"');
// Click the button:
fireEvent.click(screen.getByRole("button"));
// Ensure the router was updated:
expect(mockRouter).toMatchObject({
asPath: "/foo?bar=baz",
pathname: "/foo",
query: { bar: "baz" },
});
});
});
```
# Usage with Storybook
### Storybook Configuration
Globally enable `next-router-mock` by adding the following webpack alias to your Storybook configuration.
In `.storybook/main.js` add:
```js
module.exports = {
webpackFinal: async (config, { configType }) => {
config.resolve.alias = {
...config.resolve.alias,
"next/router": "next-router-mock",
};
return config;
},
};
```
This ensures that all your components that use `useRouter` will work in Storybook. If you also need to test `next/link`, please see the section [Example: **`next/link` with Storybook**](#example-nextlink-with-storybook).
### Storybook Example
In your individual stories, you might want to mock the current URL (eg. for testing an "ActiveLink" component), or you might want to log `push/replace` actions. You can do this by wrapping your stories with the `<MemoryRouterProvider>` component.
```jsx
// ActiveLink.story.jsx
import { action } from "@storybook/addon-actions";
import { MemoryRouterProvider } from "next-router-mock/MemoryRouterProvider/next-13";
import { ActiveLink } from "./active-link";
export const ExampleStory = () => (
<MemoryRouterProvider url="/active" onPush={action("router.push")}>
<ActiveLink href="/example">Not Active</ActiveLink>
<ActiveLink href="/active">Active</ActiveLink>
</MemoryRouterProvider>
);
```
> Be sure to import from **a matching Next.js version**:
>
> ```
> import { MemoryRouterProvider }
> from 'next-router-mock/MemoryRouterProvider/next-13.5';
> ```
>
> Choose from `next-13.5`, `next-13`, `next-12`, or `next-11`.
The `MemoryRouterProvider` has the following optional properties:
- `url` (`string` or `object`) sets the current route's URL
- `async` enables async mode, if necessary (see "Sync vs Async" for details)
- Events:
- `onPush(url, { shallow })`
- `onReplace(url, { shallow })`
- `onRouteChangeStart(url, { shallow })`
- `onRouteChangeComplete(url, { shallow })`
# Compatibility with `next/link`
To use `next-router-mock` with `next/link`, you must use a `<MemoryRouterProvider>` to wrap the test component.
### Example: `next/link` with React Testing Library
When rendering, simply supply the option `{ wrapper: MemoryRouterProvider }`
```jsx
import { render } from "@testing-library/react";
import NextLink from "next/link";
import mockRouter from "next-router-mock";
import { MemoryRouterProvider } from "next-router-mock/MemoryRouterProvider";
it("NextLink can be rendered", () => {
render(<NextLink href="/example">Example Link</NextLink>, { wrapper: MemoryRouterProvider });
fireEvent.click(screen.getByText("Example Link"));
expect(mockRouter.asPath).toEqual("/example");
});
```
### Example: `next/link` with Enzyme
When rendering, simply supply the option `{ wrapperComponent: MemoryRouterProvider }`
```jsx
import { shallow } from "enzyme";
import NextLink from "next/link";
import mockRouter from "next-router-mock";
import { MemoryRouterProvider } from "next-router-mock/MemoryRouterProvider";
it("NextLink can be rendered", () => {
const wrapper = shallow(<NextLink href="/example">Example Link</NextLink>, {
wrapperComponent: MemoryRouterProvider,
});
wrapper.find("a").simulate("click");
expect(mockRouter.asPath).to.equal("/example");
});
```
### Example: `next/link` with Storybook
In Storybook, you must wrap your component with the `<MemoryRouterProvider>` component (with optional `url` set).
```jsx
// example.story.jsx
import NextLink from "next/link";
import { action } from "@storybook/addon-actions";
import { MemoryRouterProvider } from "next-router-mock/MemoryRouterProvider/next-13.5";
export const ExampleStory = () => (
<MemoryRouterProvider url="/initial">
<NextLink href="/example">Example Link</NextLink>
</MemoryRouterProvider>
);
```
This can be done inline (as above).
It can also be implemented as a `decorator`, which can be per-Story, per-Component, or Global (see [Storybook Decorators Documentation](https://storybook.js.org/docs/react/writing-stories/decorators) for details).
Global example:
```
// .storybook/preview.js
import { MemoryRouterProvider } from 'next-router-mock/MemoryRouterProvider';
export const decorators = [
(Story) => <MemoryRouterProvider><Story /></MemoryRouterProvider>
];
```
# Dynamic Routes
By default, `next-router-mock` does not know about your dynamic routes (eg. files like `/pages/[id].js`).
To test code that uses dynamic routes, you must add the routes manually, like so:
```typescript
import mockRouter from "next-router-mock";
import { createDynamicRouteParser } from "next-router-mock/dynamic-routes";
mockRouter.useParser(
createDynamicRouteParser([
// These paths should match those found in the `/pages` folder:
"/[id]",
"/static/path",
"/[dynamic]/path",
"/[...catchAll]/path",
])
);
// Example test:
it("should parse dynamic routes", () => {
mockRouter.push("/FOO");
expect(mockRouter).toMatchObject({
pathname: "/[id]",
query: { id: "FOO" },
});
});
```
# Sync vs Async
By default, `next-router-mock` handles route changes synchronously. This is convenient for testing, and works for most
use-cases.
However, Next normally handles route changes asynchronously, and in certain cases you might actually rely on that
behavior. If that's the case, you can use `next-router-mock/async`. Tests will need to account for the async behavior
too; for example:
```jsx
it("next/link can be tested too", async () => {
render(
<NextLink href="/example?foo=bar">
<a>Example Link</a>
</NextLink>
);
fireEvent.click(screen.getByText("Example Link"));
await waitFor(() => {
expect(singletonRouter).toMatchObject({
asPath: "/example?foo=bar",
pathname: "/example",
query: { foo: "bar" },
});
});
});
```
# Supported Features
- `useRouter()`
- `withRouter(Component)`
- `router.push(url, as?, options?)`
- `router.replace(url, as?, options?)`
- `router.route`
- `router.pathname`
- `router.asPath`
- `router.query`
- Works with `next/link` (see Jest notes)
- `router.events` supports:
- `routeChangeStart(url, { shallow })`
- `routeChangeComplete(url, { shallow })`
- `hashChangeStart(url, { shallow })`
- `hashChangeComplete(url, { shallow })`
## Not yet supported
PRs welcome!
These fields just have default values; these methods do nothing.
- `router.isReady`
- `router.basePath`
- `router.isFallback`
- `router.isLocaleDomain`
- `router.locale`
- `router.locales`
- `router.defaultLocale`
- `router.domainLocales`
- `router.prefetch()`
- `router.back()`
- `router.beforePopState(cb)`
- `router.reload()`
- `router.events` not implemented:
- `routeChangeError`
- `beforeHistoryChange`
# Usage with next/navigation Beta
## Usage with Jest
### Jest Configuration
For unit tests, the `next-router-mock/navigation` module can be used as a drop-in replacement for `next/navigation`:
```js
jest.mock("next/navigation", () => require("next-router-mock/navigation"));
```
You can do this once per spec file, or you can [do this globally using `setupFilesAfterEnv`](https://jestjs.io/docs/configuration/#setupfilesafterenv-array).
### Jest Example
In your tests, use the router from `next-router-mock` to set the current URL and to make assertions.
```jsx
import mockRouter from "next-router-mock";
import { render, screen, fireEvent } from "@testing-library/react";
import { usePathname, useRouter } from "next/navigation";
jest.mock("next/navigation", () => jest.requireActual("next-router-mock/navigation"));
const ExampleComponent = ({ href = "" }) => {
const router = useRouter();
const pathname = usePathname();
return <button onClick={() => router.push(href)}>The current route is: {pathname}</button>;
};
describe("next-router-mock", () => {
it("mocks the useRouter hook", () => {
// Set the initial url:
mockRouter.push("/initial-path");
// Render the component:
render(<ExampleComponent href="/foo?bar=baz" />);
expect(screen.getByRole("button")).toHaveTextContent("The current route is: /initial-path");
// Click the button:
fireEvent.click(screen.getByRole("button"));
// Ensure the router was updated:
expect(mockRouter).toMatchObject({
asPath: "/foo?bar=baz",
pathname: "/foo",
query: { bar: "baz" },
});
});
});
```
### Supported
- useRouter
- usePathname
- useParams
- useSearchParams
### Not supported yet
- Storybook
- useSelectedLayoutSegment, useSelectedLayoutSegments
- non-hook utils in next/navigation