How to test React Router V6 Outlet using @testing-library/react
Asked Answered
F

2

8

I have a component that I am using in React Router v6 for managing private routes, that does some checks on an auth token, and will either render the Outlet component or will redirect to a login page.

I have -

import { Outlet } from 'react-router-dom';

export const CheckAuth = (props) => {
  const valid = ...;
  if (!valid) {
    window.location.replace(loginUrl);
    return null;
  }

  return <Outlet />;
};

and using it like -

<Route element={<CheckAuth token={authToken} />}>
   // ... private routes ...
</Route>

I can mock out window.location.replace with Jest

delete window.location;
window.location = { replace: jest.fn() };
...
render(<CheckAuth token={token} />)
expect(window.location.replace).toHaveBeenCalledWith(loginUrl);

but how can I test the Outlet component using Testing Library?

Foy answered 10/1, 2022 at 15:30 Comment(2)
Unrelated, why are you using window.location? You probably want to render a redirect, i.e. <Navigate to={loginUrl} replace /> instead of mutating the location.Thallus
It's an external URL which I believe react-router doesn't handle?Foy
F
9

If it helps anyone, I ended up just wrapping the components in the test with a react router components, and passed a dummy component as a child to Route and asserted that some fake text in that component was or was not rendered

Outside the test block -

const FakeComponent = () => <div>fake text</div>;

and for a failure scenario, where the outlet should not render -

    render(
      <MemoryRouter initialEntries={['/']}>
        <Routes>
          <Route element={<CheckAuth />}>
            <Route path="/" element={<FakeComponent />} />
          </Route>
        </Routes>
      </MemoryRouter>
    );

    await waitFor(() => expect(screen.queryByText('fake text')).not.toBeInTheDocument());
    await waitFor(() => expect(window.location.replace).toHaveBeenCalledWith(loginUrl));

and for a success scenario, assert that the text is present -

render(
      <MemoryRouter initialEntries={['/']}>
        <Routes>
          <Route element={<CheckAuth token={correctToken}/>}>
            <Route path="/" element={<FakeComponent />} />
          </Route>
        </Routes>
      </MemoryRouter>
    );

    expect(screen.getByText('fake text')).toBeInTheDocument();
Foy answered 4/8, 2022 at 12:55 Comment(0)
C
0

For anyone viewing this in 2024, I tried the accepted answer and while it does work, a colleague mentioned that it does not actually test the routing logic in App.jsx or App.tsx (which is true).

So, here's how you should actually go about it:

- You need to wait for the component to actually render before checking whether it did

For example, if you have a private route for /home that passes the auth token checks and renders the component below:

const HomePage = () => {
  //...some logic 
  
  return <div>Home page</div>

}

And your App.jsx looks like this:

import { Outlet } from 'react-router-dom';
// other imports

export const App = (props) => {
  //... app logic

 return (
   <Routes>
    <Route path='/' element={<Outlet />}>
       <Route element={<CheckAuth token={authToken} />}>
          // ... all private routes including '/home' ...
       </Route>
    </Route>
   </Routes>
 )
}

index.js:

import App from 'App'

const root = createRoot(document.getElementById('root'))
  root.render(
    <BrowserRouter>
      <App />
    </BrowserRouter>
  )

Then, your tests should look like this:

describe("App page", async () => {

   ...
   delete window.location;
   window.location = { replace: jest.fn() };
   ...


   test('should test for private route with invalid authToken', () => {

      // example private route with no valid auth: '/not-valid'
      render(
        <MemoryRouter initialEntries={['/not-valid']}>
          <App />
        </MemoryRouter>
      )

      expect(window.location.replace).toHaveBeenCalledWith(loginUrl)
   })

   test('should test for private route with valid authToken', () => {
      render(
        <MemoryRouter initialEntries={['/home']}>
          <App />
        </MemoryRouter>
      )

      expect(await screen.findByText('Home page')).toBeInTheDocument()
   })
})

The key point here is to ensure you wait for the rendering to have happened before checking for whether it has successfully rendered the component for /home

And you do this by checking with an await i.e expect(await screen.findBy........

Cellobiose answered 25/5 at 1:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.