Page navigation

Custom wrapper

Okay, let's start by making sure that router-dependent components can render in isolation. For that, I am going to create a custom wrapper function:
import { MemoryRouter } from 'react-router'

const wrapper: React.JSXElementConstructor<{
	children: React.ReactNode
}> = ({ children }) => {
	return <MemoryRouter>{children}</MemoryRouter>
}
The wrapper here is a regular React component that wraps any provided children in a <MemoryRouter /> from react-router.
🦉 While our app uses the <BrowserRouter /> component, our tests will rely on the <MemoryRouter /> instead. It provides the same behavior as the browser router but keeps the routing state in-memory instead of the page URL.
Now, I will provide this component as the wrapper property on individual render() calls in my tests:
render(<DiscountCodeForm />, {
	wrapper,
})
This will result in the following component tree being rendered:
<MemoryRouter>
	<DiscountCodeForm />
</MemoryRouter>
Now that my tested component has a parent router, it can access all the router-related information, like the useNavigate() hook without issues.
Don't be hesitant to create wrappers to suit the needs of individual tests. Custom wrappers are not something you usually reuse.
All that remains now is to finish the new test case for the "Back to cart" link.
As usual, I will render our component and provide it with the custom wrapper:
test('displays the "Back to cart" link', async () => {
	render(<DiscountCodeForm />, { wrapper })
})
Next, let's find the link element on the page by its role and accessible name:
test('displays the "Back to cart" link', async () => {
	render(<DiscountCodeForm />, { wrapper })

	const backToCartLink = page.getByRole('link', { name: 'Back to cart' })
})
Next, I will add a quick assertion that this link is visible:
test('displays the "Back to cart" link', async () => {
	render(<DiscountCodeForm />, { wrapper })

	const backToCartLink = page.getByRole('link', { name: 'Back to cart' })
	await expect.element(backToCartLink).toBeVisible()
})
🦉 The .toBeVisible() matcher provides a bunch of implicit assertions to make sure that the element is visible and also accessible to the user.
This is the moment where the user would click on the link and land on the cart page. But our test is not going to do that. Instead, all I will do is make sure that the link got rendered with the correct attributes:
test('displays the "Back to cart" link', async () => {
	render(<DiscountCodeForm />, { wrapper })

	const backToCartLink = page.getByRole('link', { name: 'Back to cart' })
	await expect.element(backToCartLink).toHaveAttribute('href', '/cart')
})
Let me explain.
Any navigation on the web consists of the source and the destination. In the case of our <DiscountCodeForm />, the source is the "Back to cart" link while the destination is the cart page. In other words, there are two pages involved (thus the navigation).
But we aren't testing pages here. We are testing components. It is crucial we keep in mind what's within our component's control and what is not.
Let's focus on what our component can actually control:
  • Render the link;
  • Make sure it leads to the right page.
What gets rendered on that page or whether the page exists at all isn't the responsibility of <DiscountCodeForm />. It cannot be responsible for the other end of this navigation, and so we should not include it in its tests.
Think about it: should a <DiscountCodeForm /> test fail because of issues on the cart page? Of course not. That's what the cart page's own tests are for.
This once again brings me to 📜 The Golden Rule of Assertions, proving how invaluable it is when making decisions around your tests.

Testing navigation

But where should you test the actual navigation then?
Test the actual navigation in end-to-end tests, and test it implicitly.
Getting from one page to another is a part of the user's journey. Naturally, both sides of the navigation would have to exist for that journey to complete. This is where you act as the user, click on links or buttons, and assert which page they land on.
Testing navigation on the integration level is unreliable. There may be other components in the router tree that affect things. For example, some other route may render a position:fixed element with incorrect styles that would obstruct the link you are trying to click, making it inaccessible. You won't catch that when rendering components in isolation, but you will in an end-to-end test.

Please set the playground first

Loading "Page navigation"
Loading "Page navigation"

No tests here 😢 Sorry.