next-router-mock

repository·main·Indexed 19 days ago

https://github.com/scottrippey/next-router-mock

An in-memory implementation of the Next.js Router designed for testing environments like Jest and Storybook. It allows developers to simulate navigation, query parameters, and dynamic routes without interacting with the browser address bar. It provides a drop-in replacement for next/router and next/navigation (Beta), including a MemoryRouterProvider for scoped routing and support for both synchronous and asynchronous route changes.

Tokens
8.3K
Snippets
38
Records
39
Agent score
65%

What's inside next-router-mock

  1. Sync vs Async route changes

    main

    By default, next-router-mock handles route changes synchronously. If your code relies on Next.js's asynchronous routing behavior, use next-router-mock/async and ensure your tests use await waitFor to account for the delay.

    // Example of testing async behavior
    import { waitFor } from '@testing-library/react';
    
    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" },
        });
      });
    });
  2. Use MemoryRouterProvider in Storybook

    main

    To mock specific URLs or log router actions (like push or replace) in individual stories, wrap them with the <MemoryRouterProvider> component.

    Important: You must import MemoryRouterProvider from a path matching your Next.js version (e.g., next-13, next-12, next-11).

    Props:

    • url (string | object): Sets the current route's URL.
    • async (boolean): Enables async mode.
    • Events:
      • onPush(url, { shallow })
      • onReplace(url, { shallow })
      • onRouteChangeStart(url, { shallow })
      • onRouteChangeComplete(url, { shallow })
    // 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>
    );
  3. Configure next-router-mock for Storybook

    main

    To globally enable next-router-mock in Storybook, add a webpack alias for next/router in your .storybook/main.js file.

    module.exports = {
      webpackFinal: async (config, { configType }) => {
        config.resolve.alias = {
          ...config.resolve.alias,
          "next/router": "next-router-mock",
        };
        return config;
      },
    };
  4. Configure next-router-mock for Jest

    main

    To use next-router-mock as a drop-in replacement for next/router in unit tests, mock the module. You can do this per spec file or globally in your setupFilesAfterEnv configuration.

    jest.mock("next/router", () => require("next-router-mock"));
  5. Configure next/link compatibility

    main

    To test components using next/link, you must wrap the component in a <MemoryRouterProvider> during rendering.

    // React Testing Library
    render(<NextLink href="/example">Example Link</NextLink>, { wrapper: MemoryRouterProvider });
    
    // Enzyme
    const wrapper = shallow(<NextLink href="/example">Example Link</NextLink>, {
      wrapperComponent: MemoryRouterProvider,
    });
  6. Configure next/navigation (Beta) for Jest

    main

    For Next.js App Router (Beta) features, use next-router-mock/navigation as a drop-in replacement for next/navigation in your Jest tests.

    import mockRouter from "next-router-mock";
    import { render, screen, fireEvent } from "@testing-library/react";
    import { usePathname, useRouter } from "next/navigation";
    
    // Mock next/navigation with the mock implementation
    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" },
        });
      });
    });
  7. Use MemoryRouterProvider for Next.js routing mocks

    main

    The MemoryRouterProvider is the primary component used to mock the Next.js router in testing environments (like Jest) or UI environments (like Storybook). It provides a way to simulate navigation and access the router state without a real Next.js server or browser environment.

    This package automatically detects and exports the version of MemoryRouterProvider that matches your installed version of Next.js, supporting versions from Next.js 10 up to Next.js 13.5+.

    import { MemoryRouterProvider } from 'next-router-mock/MemoryRouterProvider';
    
    // Usage typically involves wrapping your component tree:
    function TestComponent() {
      return (
        <MemoryRouterProvider url="/some-route">
          <YourApp />
        </MemoryRouterProvider>
      );
    }
  8. Example: Using next-router-mock with Jest

    main

    In Jest tests, import mockRouter from next-router-mock to set the initial URL using .push() and to assert the state of asPath, pathname, and query after interactions.

    import { useRouter } from "next/router";
    import { render, screen, fireEvent } from "@testing-library/react";
    import mockRouter from "next-router-mock";
    
    // Mock next/router with the mock implementation
    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" },
        });
      });
    });
  9. Configure Dynamic Routes

    main

    By default, next-router-mock does not recognize dynamic routes (e.g., /[id].js). To support them, use createDynamicRouteParser to manually register your route patterns.

    import mockRouter from "next-router-mock";
    import { createDynamicRouteParser } from "next-router-mock/dynamic-routes";
    
    mockRouter.useParser(
      createDynamicRouteParser([
        "/[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" },
      });
    });
  10. Use MemoryRouter to mock Next.js routing

    main

    The MemoryRouter class is a mock implementation of NextRouter that stores the current route in memory instead of changing the browser URL. This is ideal for unit and integration testing components that rely on next/router.

    Key Features

    • Synchronous URL setting: Use setCurrentUrl to immediately jump to a specific route.
    • Async simulation: Set the async option to true in the constructor to simulate Next.js's asynchronous routing behavior (adds a small delay before updates).
    • Event Emitting: Emits standard RouterEvents (like routeChangeStart) and internal mock events.
    • Snapshotting: Use MemoryRouter.snapshot(original) to create a copy of the current router state.
    import { MemoryRouter } from 'next-router-mock';
    
    // Initialize with a starting URL
    const router = new MemoryRouter('/dashboard');
    
    // Navigate to a new route
    await router.push('/settings?user=123');
    
    console.log(router.pathname); // '/settings'
    console.log(router.query);     // { user: '123' }
  11. Reset MemoryRouter state

    main

    The reset() method clears all event handlers and returns the router to the default root path (/). This is useful between tests to ensure a clean state.

    const router = new MemoryRouter('/some-path');
    // ... run tests ...
    router.reset();
    // router is now at '/' and has no custom event listeners
    const router = new MemoryRouter('/some-path');
    router.reset();