I have a single atom that stores an object with authToken
and username
strings. If authToken
is present, I want to present protected content. If not, I want to redirect the user to the login page.
Tests:
When executed in order 2, 1 they are successful. When executed in order 1,2 they fail.
I wonder if there is something that I am missing or is this a known issue. Any help will be appreciated.
atom
import { atom } from 'recoil';
type AuthDetail = {
authToken?: string;
userName?: string;
};
const authStateAtom = atom<AuthDetail>({
key: 'auth-state',
default: {},
});
export { authStateAtom };
export type { AuthDetail };
PrivateRoute
import React, { FC } from 'react';
import { authStateAtom } from './atom';
import { Redirect, Route, RouteProps } from 'react-router-dom';
import { useRecoilValue } from 'recoil';
const PrivateRoute: FC<RouteProps> = ({ children, ...rest }) => {
const authState = useRecoilValue(authStateAtom);
return (
<Route
{...rest}
render={({ location }) =>
authState.authToken ? (
children
) : (
<Redirect
to={{
pathname: '/login',
state: { from: location },
}}
/>
)}
/>
);
}
export { PrivateRoute };
Tests
import { act, render, RenderResult } from '@testing-library/react';
import React from 'react';
import { HashRouter, Route, Switch } from 'react-router-dom';
import { MutableSnapshot, RecoilRoot } from 'recoil';
import { authStateAtom } from './atom';
import { PrivateRoute } from './PrivateRoute';
describe('PrivateRoute:', () => {
const protectedComponent = 'protected-component';
const loginComponent = 'login-component';
it('redirects to login if not authenticated', async () => {
let result: RenderResult;
await act(async () => {
result = render(
<RecoilRoot>
<Switch>
<Route exact={true} path="/login">
<div data-testid={loginComponent}>LOGIN COMPONENT</div>
</Route>
<PrivateRoute path="/">
<div data-testid={protectedComponent}>PROTECTED COMPONENT</div>
</PrivateRoute>
</Switch>
</RecoilRoot>,
{ wrapper: HashRouter },
)
});
// @ts-ignore
expect(result.getByTestId(loginComponent)).toBeInTheDocument();
// @ts-ignore
expect(result.queryByTestId(protectedComponent)).not.toBeInTheDocument();
});
it('renders the component if authenticated', async () => {
const initialRecoilState = (snap: MutableSnapshot) => {
snap.set(authStateAtom, { authToken: '1234' });
};
let result: RenderResult;
await act(async () => {
result = render(
<RecoilRoot initializeState={initialRecoilState}>
<Switch>
<Route exact={true} path="/login">
<div data-testid={loginComponent}>LOGIN COMPONENT</div>
</Route>
<PrivateRoute path="/">
<div data-testid={protectedComponent}>PROTECTED COMPONENT</div>
</PrivateRoute>
</Switch>
</RecoilRoot>,
{ wrapper: HashRouter },
);
});
// @ts-ignore
expect(await result.findByTestId(protectedComponent)).toBeInTheDocument();
// @ts-ignore
expect(result.queryByTestId(loginComponent)).not.toBeInTheDocument();
});
});
https://codesandbox.io/s/vigilant-shamir-27s35?file=/src/PrivateRoute.test.tsx
When you are testing with a <Router>
, window.history
in your environment can be polluted. If you only run one of your tests, the other one will pass. But when you run them together, your first test will set the URL to "/login"
. You begin will that URL in your second test.
According to react-router's testing guide, you should wrap your components with <MemoryRouter>
to protect the global environment.
Here's codesandbox link with both tests passing: https://codesandbox.io/s/frosty-rhodes-xxuwk?file=/src/PrivateRoute.test.tsx