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
}}