Reputation: 881
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
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
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
Reputation: 2880
To resolve your issue, you have to mock the NextJS Navigation router:
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>
}
describe('<Navbar />', () => {
it('displays links', () => {
mount(NextRouterMock>
<Navbar links={LINKS} />
</NextRouterMock>
)
})
})
Upvotes: -1
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