npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@appaka/next-router-mock

v0.0.3

Published

Mock implementation of the Next.js Router

Downloads

45

Readme

@appaka/next-router-mock

Fork from:

  1. https://github.com/scottrippey/next-router-mock
  2. https://github.com/ergofriend/next-router-mock/tree/add-history-support

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.

Tested with NextJS v13, v12, v11, and v10.

Install via NPM: npm install --save-dev @appaka/next-router-mock

Table of Contents generated with DocToc

Usage with Jest

Jest Configuration

For unit tests, the @appaka/next-router-mock module can be used as a drop-in replacement for next/router:

jest.mock('next/router', () => require('@appaka/next-router-mock'));

You can do this once per spec file, or you can do this globally using setupFilesAfterEnv.

Jest Example

In your tests, use the router from @appaka/next-router-mock to set the current URL and to make assertions.

import { useRouter } from 'next/router';
import { render, screen, fireEvent } from '@testing-library/react';
import mockRouter from '@appaka/next-router-mock';

jest.mock('next/router', () => jest.requireActual('@appaka/next-router-mock'))

const ExampleComponent = ({ href = '' }) => {
  const router = useRouter();
  return (
    <button onClick={() => router.push(href)}>
      The current route is: "{router.asPath}"
    </button>
  );
}

describe('@appaka/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')).toHaveText(
      '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 @appaka/next-router-mock by adding the following webpack alias to your Storybook configuration.

In .storybook/main.js add:

module.exports = {
  webpackFinal: async (config, { configType }) => {
    config.resolve.alias = {
      ...config.resolve.alias,
      "next/router": "@appaka/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.

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.

// ActiveLink.story.jsx
import { action } from '@storybook/addon-actions';
import { MemoryRouterProvider } 
  from '@appaka/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 '@appaka/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 })

Mock next/navigation

import mockRouter from '@appaka/next-router-mock'

jest.mock('next/navigation', () => ({
  ...jest.requireActual('next/navigation'),
	useRouter: mockRouter,
	useParams: () => {
		const { query } = mockRouter
		return query
	},
	useSearchParams: () => {
		const { query } = mockRouter
		const search = new URLSearchParams()

		Object.entries(query).forEach(([key, value]) => {
			search.append(key, `${value}`)
		})

		return search
	},
}))

Compatibility with next/link

To use @appaka/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 }

import { render } from '@testing-library/react';
import NextLink from 'next/link';

import mockRouter from '@appaka/next-router-mock';
import { MemoryRouterProvider } from '@appaka/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 }

import { shallow } from 'enzyme';
import NextLink from 'next/link';

import mockRouter from '@appaka/next-router-mock';
import { MemoryRouterProvider } from '@appaka/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).

// example.story.jsx
import NextLink from 'next/link';
import { action } from '@storybook/addon-actions';

import { MemoryRouterProvider } from '@appaka/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 for details).
Global example:

// .storybook/preview.js
import { MemoryRouterProvider } from '@appaka/next-router-mock/MemoryRouterProvider';

export const decorators = [
  (Story) => <MemoryRouterProvider><Story /></MemoryRouterProvider>
]; 

Dynamic Routes

By default, @appaka/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:

import mockRouter from "@appaka/next-router-mock";
import { createDynamicRouteParser } from "@appaka/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, @appaka/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 @appaka/next-router-mock/async. Tests will need to account for the async behavior too; for example:

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.back()
  • 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.beforePopState(cb)
  • router.reload()
  • router.events not implemented:
    • routeChangeError
    • beforeHistoryChange