Search code examples
reactjsjestjsreact-routerreact-testing-libraryrecoiljs

Two recoil react tests conflicting based off of rendered value


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:

  1. Does not populate the "authToken" and we expect login content in the rendered result.
  2. Populates "authToken" and I expect protected content in the rendered result.

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


Solution

  • 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