Bonteq
Bonteq

Reputation: 881

Cypress component test with NextJS useRouter function

My Navbar component relies on the useRouter function provided by nextjs/router in order to style the active links.

I'm trying to test this behavior using Cypress, but I'm unsure of how I'm supposed to organize it. Cypress doesn't seem to like getRoutePathname() and undefined is returned while within my testing environment.

Here's the component I'm trying to test:

import Link from 'next/link'
import { useRouter } from 'next/router'

function getRoutePathname() {
  const router = useRouter()
  return router.pathname
}

const Navbar = props => {
  const pathname = getRoutePathname()

  return (
    <nav>
      <div className="mr-auto">
        <h1>Cody Bontecou</h1>
      </div>
      {props.links.map(link => (
        <Link key={link.to} href={link.to}>
          <a
            className={`border-transparent border-b-2 hover:border-blue-ninja 
            ${pathname === link.to ? 'border-blue-ninja' : ''}`}
          >
            {link.text}
          </a>
        </Link>
      ))}
    </nav>
  )
}

export default Navbar

I have the skeleton setup for the Cypress component test runner and have been able to get the component to load when I hardcode pathname, but once I rely on useRouter, the test runner is no longer happy.


import { mount } from '@cypress/react'

import Navbar from '../../component/Navbar'

const LINKS = [
  { text: 'Home', to: '/' },
  { text: 'About', to: '/about' },
]

describe('<Navbar />', () => {
  it('displays links', () => {
    mount(<Navbar links={LINKS} />)
  })
})

Upvotes: 6

Views: 2810

Answers (4)

Igor Pavlenko
Igor Pavlenko

Reputation: 717

i found this article https://www.cypress.io/blog/component-testing-next-js-with-cypress#customize-your-nextjs-testing-experience

works great but need to keep this disclaimer from cypress team in mind :)

Unfortunately, there’s no such thing as a free lunch—adding these extra items to every mount will affect performance and introduce global state elements outside the bounds of your component. It’s up to you to decide whether these trade-offs are worth it based on your use case.

Upvotes: 0

Jackson.J.Lamb
Jackson.J.Lamb

Reputation: 222

Since the original posting Cypress added some better documentation on component testing NextJs.

Specifically on the router Customize your Next.js Testing Experience this is the example (simplified)

If you add it to the custom nextMountWithStubbedRoutes() command, it can be used in any spec.

Cypress.Commands.add('nextMountWithStubbedRoutes', (component, options) => {
  const router = {
    route: '/',
    pathname: '/',
    query: {},
    asPath: '/',
    basePath: '',
    back: cy.stub().as('router:back'),
    forward: cy.stub().as('router:forward'),
    push: cy.stub().as('router:push'),
    reload: cy.stub().as('router:reload'),
    replace: cy.stub().as('router:replace'),
    isReady: true,
    ...(options?.router || {}),
  }

  return mount(
    <RouterContext.Provider value={router}>
      {component}
    </RouterContext.Provider>,
    options
  )
})

See caveat:

Unfortunately, there’s no such thing as a free lunch—adding these extra items to every mount will affect performance and introduce global state elements outside the bounds of your component. It’s up to you to decide whether these trade-offs are worth it based on your use case.

Given this warning, use specific nextMountWithStubbedRoutes() only with tests that need it.

Upvotes: 10

Ala Eddine Menai
Ala Eddine Menai

Reputation: 2880

To resolve your issue, you have to mock the NextJS Navigation router:

  1. Create your Next Router Mock component inside your mocks folder in cypress:

import { AppRouterContext, AppRouterInstance } from "next/dist/shared/lib/app-router-context"

const createNextRouter = (params: Partial<AppRouterInstance>) => ({
  back: cy.spy().as("back"),
  forward: cy.spy().as("forward"),
  prefetch: cy.stub().as("prefetch").resolves(),
  push: cy.spy().as("push"),
  replace: cy.spy().as("replace"),
  refresh: cy.spy().as("refresh"),
  ...params,
})

interface MockNextRouterProps extends Partial<AppRouterInstance> {
  children: React.ReactNode
}

export const NextRouterMock = ({ children, ...props }: MockNextRouterProps) => {
  const router = createNextRouter(props as AppRouterInstance)

  return <AppRouterContext.Provider value={router}>{children}</AppRouterContext.Provider>
}

  1. Wrap your component with this Mock Router:
describe('<Navbar />', () => {
  it('displays links', () => {

    mount(NextRouterMock>
            <Navbar links={LINKS} />
         </NextRouterMock>
    )
  })
})

Upvotes: -1

Will Squire
Will Squire

Reputation: 6595

Ideally, there'd be a provider for Next.js's useRouter to set the router object and wrap the component in the provider in mount. Without going through the code or Next.js supplying the documentation, here's a workaround to mock useRouter's pathname and push:

import * as NextRouter from 'next/router'

// ...inside your test:

const pathname = 'some-path'
const push = cy.stub()
cy.stub(NextRouter, 'useRouter').returns({ pathname, push })

I've added push because that's the most common use case, which you may also need.

Upvotes: 4

Related Questions