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

@render-with/react-router

v5.0.0

Published

Render decorators for components under test that require a React Router or Routes.

Downloads

158

Readme

Render decorators 🪆 for React Router

GitHub Workflow Status Code Coverage npm (scoped) NPM PRs welcome All Contributors

Use one of these decorators if your component under test requires a React Router:

  • withRouter()
  • withLocation(..)
  • withRouting(..)

Example:

import { render, screen, withLocation } from './test-utils'

it('shows login page', () => {
  render(<App />, withLocation('/login'))
  expect(screen.getByRole('heading', { name: /login/i })).toBeInTheDocument()
})

Note: Refer to the core library to learn more about how decorators can simplify writing tests for React components with React Testing Library.

Table of Contents

Installation

This library is distributed via npm, which is bundled with node and should be installed as one of your project's devDependencies.

First, install the core library with a render function that supports decorators:

npm install --save-dev @render-with/decorators

Next, install the React Router decorators provided by this library:

npm install --save-dev @render-with/react-router

or

for installation via yarn:

yarn add --dev @render-with/decorators
yarn add --dev @render-with/react-router

This library has the following peerDependencies:

npm peer dependency version

and supports the following node versions:

node-current (scoped)

Setup

In your test-utils file, re-export the render function that supports decorators and the React Router decorators:

// test-utils.js
// ...
export * from '@testing-library/react'    // makes all React Testing Library's exports available
export * from '@render-with/decorators'   // overrides React Testing Library's render function
export * from '@render-with/react-router' // makes decorators like withLocation(..) available

And finally, use the React Router decorators in your tests:

import { render, screen, withLocation } from './test-utils'

it('shows login page', () => {
  render(<App />, withLocation('/login'))
  expect(screen.getByRole('heading', { name: /login/i })).toBeInTheDocument()
})

Guiding Principle

React Router 6 made it very hard to inspect the current location in tests. It takes some time to get used to, but it turns out to be a good change as it supports Testing-Library's guiding principle:

The more your tests resemble the way your software is used, the more confidence they can give you.

An actual end-user does not look at the URL while using a web app. They look at what the browser presents to them.

A bad workaround has been spreading in the wild that involves mocking React Router hooks like useNavigate.

We do not recommend mocking hooks. It ties the tests to implementation details and, besides other downsides, makes refactoring harder.

Instead, we recommend using the decorators in this library.

Test Scenarios

Just need a Router?

If your test does not care about the current location, history, or navigation, you can use withRouter(). The decorator will create and use a MemoryRouter for you and initialize the current location with /:

import { useNavigate } from 'react-router'

export const Page = () => {
  const navigate = useNavigate()
  return (
    <>
      <h1>Page</h1>
      <button onClick={() => navigate(-1)}>Go back</button>
    </>
  )
}
import { render, screen, withRouter } from './test-utils'

it('uses given name as heading', () => {
  render(<Page />, withRouter())
  expect(screen.getByRole('heading', { name: /page/i })).toBeInTheDocument()
})

Need to provide a specific path or verify the handling of query params?

If your component cares about a certain path or your test cares about the handling of certain query params, you can use the withLocation(..) decorator:

import { Routes, Route } from 'react-router'

export const App = () => (
  <Routes>
    <Route path='/' element={<h1>Homepage</h1>} />
    <Route path='/product' element={<h1>Products</h1>} />
  </Routes>
)
import { render, screen, withLocation } from './test-utils'

it('shows product page', () => {
  render(<App />, withLocation('/product'))
  expect(screen.getByRole('heading', { name: /products/i })).toBeInTheDocument()
})

The withLocation decorator also supports path params in case your project uses routes with variable parts:

import { Routes, Route } from 'react-router'

export const App = () => (
  <Routes>
    <Route path='/' element={<h1>Homepage</h1>} />
    <Route path='/users/:userId' element={<h1>User</h1>} />
  </Routes>
)
import { render, screen, withLocation } from './test-utils'

it('shows user page', () => {
  render(<App />, withLocation('/users/:userId', { userId: '42' }))
  expect(screen.getByRole('heading', { name: /user/i })).toBeInTheDocument()
})

Need to verify navigational changes?

If your test cares about navigation and location, you can use the versatile withRouting(..) decorator.

Current Page

The component under test is not just rendered within MemoryRouter but also within a small page wrapper component that identifies the current page by name and the current location by rendering a simplified breadcrumb:

import { render, screen, withRouting } from './test-utils'

it('shows current page and location', async () => {
  render(<div>Test</div>, withRouting())
  expect(screen.getByRole('main', { name: /current page/i })).toBeInTheDocument()
  expect(screen.getByRole('link')).toHaveAttribute('href', '/current')
})

Other Page

The decorator provides a catch-all route that renders an "Other Page" in case the target route does not exist.

import { render, screen, withRouting } from './test-utils'

it('shows other page and location', async () => {
  render(<Link to='/users'>Users</Link>, withRouting())
  await userEvent.click(screen.getByRole('link', { name: /users/i }))
  expect(screen.getByRole('main', { name: /other page/i })).toBeInTheDocument()
  expect(screen.getByRole('link')).toHaveAttribute('href', '/users')
})

Previous Page

The decorator provides a "Previous Page" and sets up the history accordingly:

import { render, screen, withRouting } from './test-utils'

it('shows previous page and location', async () => {
  render(<Link to={-1}>Back</Link>, withRouting())
  await userEvent.click(screen.getByRole('link', { name: /back/i }))
  expect(screen.getByRole('main', { name: /previous page/i })).toBeInTheDocument()
  expect(screen.getByRole('link')).toHaveAttribute('href', '/previous')
})

Next Page

The decorator provides a "Next Page" and sets up the history accordingly:

import { render, screen, withRouting } from './test-utils'

it('shows next page and location', async () => {
  render(<Link to={+1}>Next</Link>, withRouting())
  await userEvent.click(screen.getByRole('link', { name: /next/i }))
  expect(screen.getByRole('main', { name: /next page/i })).toBeInTheDocument()
  expect(screen.getByRole('link')).toHaveAttribute('href', '/next')
})

Routes

You can configure the decorator with one or more routes. This can be useful if user interaction with the component under test results in navigational changes away from the current location to a different route:

import { render, screen, withRouting } from './test-utils'

it('shows next page and location', async () => {
  render(<Link to='/users'>Customers</Link>, withRouting({ routes: { users: 'Customers' } }))
  await userEvent.click(screen.getByRole('link', { name: /customers/i }))
  expect(screen.getByRole('main', { name: /customers/i })).toBeInTheDocument()
  expect(screen.getByRole('link')).toHaveAttribute('href', '/users')
})

Subroutes

You can configure the decorator with one or more subroutes. This can be useful if user interaction with the component under test results in navigational changes away from the current location to a subroute:

import { render, screen, withRouting } from './test-utils'

it('shows next page and location', async () => {
  render(<Link to='/current/details'>More</Link>, withRouting({ subroutes: { details: 'More' } }))
  await userEvent.click(screen.getByRole('link', { name: /more/i }))
  expect(screen.getByRole('main', { name: /more/i })).toBeInTheDocument()
  expect(screen.getByRole('link')).toHaveAttribute('href', '/current/details')
})

Current path

You can also configure the current path and current page name:

import { render, screen, withRouting } from './test-utils'

it('shows current page and location', async () => {
  render(<div>Test</div>, withRouting({ path: '/users', name: 'Users' }))
  expect(screen.getByRole('main', { name: /users/i })).toBeInTheDocument()
  expect(screen.getByRole('link')).toHaveAttribute('href', '/users')
})

Path params

The decorator supports path params in the current path in case your project uses routes with variable parts:

import { render, screen, withRouting } from './test-utils'

it('shows current page and location', async () => {
  render(<div>Test</div>, withRouting({ path: '/users/:userId', params: { userId: '42' } }))
  expect(screen.getByRole('main', { name: /current/i })).toBeInTheDocument()
  expect(screen.getByRole('link')).toHaveAttribute('href', '/users/42')
})

Routes and subroutes can also have path params that React Router will resolve:

import { render, screen, withRouting } from './test-utils'

it('shows current page and location', async () => {
  render(<Link to='/users/42'>John</Link>, withRouting({ routes: { ['/users/:userId']: 'User' } }))
  await userEvent.click(screen.getByRole('link', { name: /john/i }))
  expect(screen.getByRole('main', { name: /user/i })).toBeInTheDocument()
  expect(screen.getByRole('link')).toHaveAttribute('href', '/users/42')
})

API

Note: This API reference uses simplified types. You can find the full type specification here.

function withRouter(): Decorator

Wraps component under test in a MemoryRouter and initializes location with /.

function withLocation(path?: string, params?: Params): Decorator

Wraps component under test in a MemoryRouter and initializes location with the given path. Resolves path parameters with the given values.

function withRouting(options?: {
  name?: string,
  path?: string,
  params?: Params,
  routes?: Routes,
  subroutes?: Routes
}): Decorator

Wraps component under test in a MemoryRouter and a simples Routes infrastructure with routes history for the Current page, a Previous page, and a Next page. Supports a custom path and name for the current page and additional routes and subroutes.

type Params = { [param: string]: string }

Maps path parameter names to parameter values.

type Routes = { [route: string]: string }

Maps routes to page names.

Issues

Looking to contribute? PRs are welcome. Checkout this project's Issues on GitHub for existing issues.

🐛 Bugs

Please file an issue for bugs, missing documentation, or unexpected behavior.

See Bugs

💡 Feature Requests

Please file an issue to suggest new features. Vote on feature requests by adding a 👍. This helps maintainers prioritize what to work on.

See Feature Requests

📚 More Libraries

Please file an issue on the core project to suggest additional libraries that would benefit from decorators. Vote on library support adding a 👍. This helps maintainers prioritize what to work on.

See Library Requests

❓ Questions

For questions related to using the library, file an issue on GitHub.

See Questions

Changelog

Every release is documented on the GitHub Releases page.

Contributors

Thanks goes to these people:

This project follows the all-contributors specification. Contributions of any kind welcome!

LICENSE

MIT