2

I'm using Jest/Testing-Library to write UI unit tests.

Components are not rendering on the DOM, and the culprit was the component 'RequireScope' which wraps all of the components individually.

In other words, every component returns this:

return ( <RequireScope>  // some MUI stuff</RequireScope>
)

This is preventing my components from being rendered in the DOM tree when tested.

This is because RequireScope makes sure to render its children only if authentication goes through.

How can I simulate a logged-in user given the following code?

RequireScope:

import React, { useEffect, useState } from 'react';
import useAuth from 'src/hooks/useAuth';

export interface RequireScopeProps {
  scopes: string[];
}

const RequireScope: React.FC<RequireScopeProps> = React.memo((props) => {
  const { children, scopes } = props;
  const { isInitialized, isAuthenticated, permissions } = useAuth();
  const [isPermitted, setIsPermitted] = useState(false);

  useEffect(() => {
    if (isAuthenticated && isInitialized) {
      (async () => {
        const hasPermissions = scopes
          .map((s) => {
            return permissions.includes(s);
          })
          .filter(Boolean);

        if (hasPermissions.length === scopes.length) {
          setIsPermitted(true);
        }
      })();
    }
  }, [isAuthenticated, isInitialized, scopes, permissions]);

  if (isPermitted) {
    return <>{children}</>;
  }

  return null;
});

export default RequireScope;

The ultimate goal is to have 'isPermitted' to be true. In order to do this 'isInitialized, isAuthenticated, permissions' has to be true. We bring these 3 values from useAuth().

useAuth:

    import { useContext } from 'react';
    import AuthContext from '../contexts/JWTContext';
    
    const useAuth = () => useContext(AuthContext);
    
    export default useAuth;

JWTContext:

const handlers: Record<string, (state: State, action: Action) => State> = {
  INITIALIZE: (state: State, action: InitializeAction): State => {
    const { isAuthenticated, permissions, user } = action.payload;

    return {
      ...state,
      isAuthenticated,
      isInitialized: true,
      permissions,
      user,
    };
  },
  LOGIN: (state: State): State => {
    return {
      ...state,
      isAuthenticated: true,
    };
  },
  LOGOUT: (state: State): State => ({
    ...state,
    isAuthenticated: false,
    permissions: [],
  }),
};

const reducer = (state: State, action: Action): State =>
  handlers[action.type] ? handlers[action.type](state, action) : state;

const AuthContext = createContext<AuthContextValue>({
  ...initialState,
  platform: 'JWT',
  login: () => Promise.resolve(),
  logout: () => Promise.resolve(),
});

export const AuthProvider: FC<AuthProviderProps> = (props) => {
  const { children } = props;
  const [state, dispatch] = useReducer(reducer, initialState);
  const router = useRouter();
  const reduxDispatch = useDispatch();

  useEffect(() => {
    const initialize = async (): Promise<void> => {
      try {
        if (router.isReady) {
          const { token, permissions, user, companyId } = router.query;

          const accessToken =
            (token as string) || window.localStorage.getItem('accessToken');
          const permsStorage = window.localStorage.getItem('perms');
          const perms = (permissions as string) || permsStorage;
          const userStorage = window.localStorage.getItem('user');
          const selectedCompanyId =
            (companyId as string) || window.localStorage.getItem('companyId');
          const authUser = (user as string) || userStorage;

          if (accessToken && perms) {
            setSession(accessToken, perms, authUser);

            try {
              // check if user is admin by this perm, probably want to add a flag later
              if (perms.includes('create:calcs')) {
                if (!selectedCompanyId) {
                  const response = await reduxDispatch(getAllCompanies());

                  const companyId = response.payload[0].id;
                  reduxDispatch(companyActions.selectCompany(companyId));
                  reduxDispatch(getCurrentCompany({ companyId }));
                } else {
                  reduxDispatch(
                    companyActions.selectCompany(selectedCompanyId),
                  );
                  await reduxDispatch(
                    getCurrentCompany({ companyId: selectedCompanyId }),
                  );
                }
              } else {
                reduxDispatch(companyActions.selectCompany(selectedCompanyId));
                await reduxDispatch(
                  getCurrentCompany({ companyId: selectedCompanyId }),
                );
              }
            } catch (e) {
              console.warn(e);
            } finally {
              dispatch({
                type: 'INITIALIZE',
                payload: {
                  isAuthenticated: true,
                  permissions: JSON.parse(perms),
                  user: JSON.parse(authUser),
                },
              });
            }

            if (token || permissions) {
              router.replace(router.pathname, undefined, { shallow: true });
            }
          } else {
            dispatch({
              type: 'INITIALIZE',
              payload: {
                isAuthenticated: false,
                permissions: [],
                user: undefined,
              },
            });
            setSession(undefined);

            if (router.pathname !== '/client-landing') {
              router.push('/login');
            }
          }
        }
      } catch (err) {
        console.error(err);
        dispatch({
          type: 'INITIALIZE',
          payload: {
            isAuthenticated: false,
            permissions: [],
            user: undefined,
          },
        });
        //router.push('/login');
      }
    };

    initialize();
  }, [router.isReady]);

  const login = useCallback(async (): Promise<void> => {
    const response = await axios.get('/auth/sign-in-with-intuit');
    window.location = response.data;
  }, []);

  const logout = useCallback(async (): Promise<void> => {
    const token = localStorage.getItem('accessToken');

    // only logout if already logged in
    if (token) {
      dispatch({ type: 'LOGOUT' });
    }

    setSession(null);

    router.push('/login');
  }, [dispatch, router]);

  return (
    <AuthContext.Provider
      value={{
        ...state,
        platform: 'JWT',
        login,
        logout,
      }}
    >
      {state.isInitialized && children}
    </AuthContext.Provider>
  );
};

AuthProvider.propTypes = {
  children: PropTypes.node.isRequired,
};

export default AuthContext;

To achieve what is described above, we just have to make sure the 'finally' statement runs if I am correct. Thus the conditional statements:

if (router.isReady)

and

if (accessToken && perms)

has to be met.

How can I make the router to exist when I render this AuthProvider component in Jest?

Or are there any other alternatives to simulate a logged in user?

My test looks like this:

// test BenchmarksPage
test('renders benchmark', () => {
render(
    <HelmetProvider>
      <Provider store={mockStore(initState)}>
        <AuthProvider>
          <BenchmarksPage />
        </AuthProvider>
      </Provider>
    </HelmetProvider>,
  );

  localStorage.setItem('accessToken', 'sampletokenIsInR5cCI6');
  localStorage.setItem(
    'perms',
    JSON.stringify([
      'create:calcs',
    // and so on
}}

1 Answer 1

1

As your component has side effects in it (i.e. gtm.push, redux-thunk) you may need to wait for the component state to be stable before testing it (as I don't know what is going on in the CalculationTable component). Hence try changing your test to:

// Make the test asynchronous by adding `async`
test('renders header and export dropdown', async () => {
  const initState = {};
  const middlewares = [thunk];
  const mockStore = configureStore(middlewares);

  const { findByRole, getByText, getByTestId } = render(
    <Provider store={mockStore(initState)}>
      <CalculationsPage />
    </Provider>,
  );

   // findByRole will wait for the element to be present.
   // Note the `await` keyword
  const header = await findByRole('heading', { name: /calculations/i });
  
  await waitFor(() => expect(getByTestId('analysis-categories-header')).toBeVisible());
}

"findBy methods are a combination of getBy queries and waitFor." - see here for more info.

Sign up to request clarification or add additional context in comments.

5 Comments

unfortunately, it is still not working.. any other suggetions?
@Jake.K Hmmm, I do expect that its the async events thats causing the issue. I've updated the answer where waitFor is used to wait for analysis-categories-header is visible. Give that a go, you'll need to import it.
I think your right. What work is being done in the thunks? Network requests? Like you say, you could mock redux-thunk. Alternatively you could mock the network requests using a library such as mswjs.io which can simplify testing components with network calls.
I'd seriously consider using MSW to mock the POST to /calculation/run-all as I expect it'll be way less work than mocking redux. The example on the front page of mswjs.io is for a POST. Sorry I couldnt solve your issue but I hope Ive helped. Think you are on the right track now.
Hello, I figured out the cause. It was a component called 'RequireScope' as written in the question. I would really appreciate if you would take a look at the question please. Thank you.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.