|
1 | 1 | # Page navigation
|
2 | 2 |
|
3 |
| -... |
| 3 | +## Custom wrapper |
4 | 4 |
|
5 |
| -## Why not test the navigation? |
| 5 | +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: |
6 | 6 |
|
7 |
| -- Not reliable on the integration level anywhere. There may be other elements rendered in the router tree that can affect how the pages render or even whether some elements on the page are interactive (imagine a random component rendering an `opacity:0` overlay over the "Back to cart" button. It's broken now!). |
8 |
| -- Clear responsibilities. Clear boundaries. The Golden Rule of Assertion. |
| 7 | +```tsx filename=discount-code-form.browser.test.tsx add=1,3-7 |
| 8 | +import { MemoryRouter } from 'react-router' |
9 | 9 |
|
10 |
| -## When to test the navigation |
| 10 | +const wrapper: React.JSXElementConstructor<{ |
| 11 | + children: React.ReactNode |
| 12 | +}> = ({ children }) => { |
| 13 | + return <MemoryRouter>{children}</MemoryRouter> |
| 14 | +} |
| 15 | +``` |
11 | 16 |
|
12 |
| -- E2E tests as a part of some user flows. Navigation would act as the implied assertion there, which is fantastic. |
| 17 | +The `wrapper` here is a regular React component that wraps any provided children in a `<MemoryRouter />` from `react-router`. |
| 18 | + |
| 19 | +> 🦉 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. |
| 20 | +
|
| 21 | +Now, I will provide this component as the `wrapper` property on individual `render()` calls in my tests: |
| 22 | + |
| 23 | +```tsx filename=discount-code-form.browser.test.tsx add=2 |
| 24 | +render(<DiscountCodeForm />, { |
| 25 | + wrapper, |
| 26 | +}) |
| 27 | +``` |
| 28 | + |
| 29 | +This will result in the following component tree being rendered: |
| 30 | + |
| 31 | +```tsx nonumber nocopy |
| 32 | +<MemoryRouter> |
| 33 | + <DiscountCodeForm /> |
| 34 | +</MemoryRouter> |
| 35 | +``` |
| 36 | + |
| 37 | +Now that my tested component has a parent router, it can access all the router-related information, like the `useNavigate()` hook without issues. |
| 38 | + |
| 39 | +<callout-success>Don't be hesitant to create wrappers to suit the needs of individual tests. Custom wrappers is not something you usually reuse.</callout-success> |
| 40 | + |
| 41 | +## Testing links |
| 42 | + |
| 43 | +All that remains now is to finish the new test case for the "Back to cart" link. |
| 44 | + |
| 45 | +As usual, I will render our component and provide it with the custom `wrapper`: |
| 46 | + |
| 47 | +```tsx filename=discount-code-form.browser.test.tsx add=2 |
| 48 | +test('displays the "Back to cart" link', async () => { |
| 49 | + render(<DiscountCodeForm />, { wrapper }) |
| 50 | +}) |
| 51 | +``` |
| 52 | + |
| 53 | +Next, let's find the link element on the page by its role and accessible name: |
| 54 | + |
| 55 | +```tsx filename=discount-code-form.browser.test.tsx add=4 |
| 56 | +test('displays the "Back to cart" link', async () => { |
| 57 | + render(<DiscountCodeForm />, { wrapper }) |
| 58 | + |
| 59 | + const backToCartLink = page.getByRole('link', { name: 'Back to cart' }) |
| 60 | +}) |
| 61 | +``` |
| 62 | + |
| 63 | +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: |
| 64 | + |
| 65 | +```tsx filename=discount-code-form.browser.test.tsx add=5 |
| 66 | +test('displays the "Back to cart" link', async () => { |
| 67 | + render(<DiscountCodeForm />, { wrapper }) |
| 68 | + |
| 69 | + const backToCartLink = page.getByRole('link', { name: 'Back to cart' }) |
| 70 | + await expect.element(backToCartLink).toHaveAttribute('href', '/cart') |
| 71 | +}) |
| 72 | +``` |
| 73 | + |
| 74 | +Let me explain. |
| 75 | + |
| 76 | +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). |
| 77 | + |
| 78 | +**But we aren't testing pages here**. We are testing components. It is crucial we keep in mind what's within our component's power and what is not. |
| 79 | + |
| 80 | +So, what is in our component's power? |
| 81 | + |
| 82 | +- Render the link; |
| 83 | +- Make sure it leads to the right page. |
| 84 | + |
| 85 | +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 it cannot include it in its tests. |
| 86 | + |
| 87 | +Ask yourself this: Do I want the `<DiscountCodeForm />` tests to fail if something is off on the cart page? You really don't. You would want the cart page tests to fail in that case, wouldn't you? |
| 88 | + |
| 89 | +> This once again brings me to 📜 [The Golden Rule of Assertions](https://www.epicweb.dev/the-golden-rule-of-assertions), proving how invaluable it is when making decisions around your tests. |
| 90 | +
|
| 91 | +## Testing navigation |
| 92 | + |
| 93 | +But where should you test the actual navigation then? |
| 94 | + |
| 95 | +<callout-success>Test the actual navigation in end-to-end tests, and test is _implicitly_.</callout-success> |
| 96 | + |
| 97 | +Getting from one page to another is a part of the user's journey. Naturally, both counterparts 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 what page they land on. |
| 98 | + |
| 99 | +**Testing navigation on the integration level is unreliable**. There may be other components in the router tree that affect the navigation. 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 rendering components in isolation. You will catch that in an end-to-end test. |
0 commit comments