Reputation: 403
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
}}
Upvotes: 2
Views: 1117
Reputation: 20230
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.
Upvotes: 1