I'm trying to test login functionality with Apollo Client.
The first thing a user sees (if there is no token in localStorage) is the Login component, which has email and password fields as well as a login button. Upon a successful login, a user should receive the object,
{
data: {
status: 200,
token: 'some-token', // or null, if incorrect credentials
},
}
If there's a token, it's stored in localStorage and the user is redirected to the Dashboard component (doLogin function shown below).
My test fails, because it says that localStorage was called 0 times. I'm fairly new to writing tests so I'm not sure what I'm doing wrong here.
Here's what I have:
describe('Testing Login functionality', () => {
afterEach(cleanup);
it('Successfully logs a user in', async () => {
// taken from here ==> https://www.apollographql.com/docs/react/development-testing/testing/#defining-mocked-responses
const successfulLogin = {
request: {
query: graphQlQueries.authQueries.LOGIN,
variables: {
input: {
login: 'test-username',
password: 'test-password',
},
},
},
result: {
data: {
login: { status: 200, token: '1234' },
},
},
};
const loginWrapper = render(
// defined below
<MountWrapper
component={
<MockedProvider mocks={[successfulLogin]} addTypename={false}>
<Entry />
</MockedProvider>
}
withRouter={false}
/>,
);
// find the button and fields
const submitButton = loginWrapper.getByTestId(dict.testIds.loginButton);
const emailFieldInputNode = loginWrapper.getByTestId(dict.testIds.emailField).querySelector('input');
const passwordFieldInputNode = loginWrapper.getByTestId(dict.testIds.passwordField).querySelector('input');
// enter values into fields
userEvent.type(passwordFieldInputNode!, dict.mockInputValues.passwordFieldInputNode);
userEvent.type(emailFieldInputNode!, dict.mockInputValues.emailFieldInputNode);
// initiate login
userEvent.click(submitButton);
// apparently, I have to wait for a response ==> https://www.apollographql.com/docs/react/development-testing/testing/#network-errors
await new Promise((resolve) => setTimeout(resolve, 2000)); // wait for response
// taken from here ==> https://stackoverflow.com/a/61739703/2891356
const spy = jest.spyOn(Storage.prototype, 'setItem');
/*
test throws an error at this point:
Expected: "token"
Number of calls: 0
*/
expect(spy).toBeCalledWith('token');
// the Dashboard component has the below text on screen:
const lastLoginTextExists = loginWrapper.getByText('Last login:');
// but the test fails here as well, because the text can't be found
expect(lastLoginTextExists).not.toBeNull();
});
});
My MountWrapper component just takes a component prop and renders it in all the providers my app needs in order to run (redux, mui, etc):
import * as React from 'react';
import { Provider } from 'react-redux';
import { BrowserRouter as Router } from 'react-router-dom';
import { StylesProvider, ThemeProvider as MuiThemeProvider } from '@material-ui/core';
import { ThemeProvider as EmotionThemeProvider } from '@emotion/react';
import store from 'src/redux/store';
import { useCustomTheme } from 'src/hooks';
interface ContainerProps {
component: React.ReactNode;
withRouter: boolean;
}
const Container: React.FC<ContainerProps> = React.memo(({ component, withRouter }) => {
const theme = useCustomTheme({ themeType: null });
const content = (
<StylesProvider injectFirst>
<MuiThemeProvider theme={theme}>
<EmotionThemeProvider theme={theme}>{component}</EmotionThemeProvider>
</MuiThemeProvider>
</StylesProvider>
);
if (!withRouter) {
return content;
}
return <Router>{content}</Router>;
});
interface MountWrapperProps {
component: React.ReactNode;
withRouter?: boolean;
}
const MountWrapper: React.FC<MountWrapperProps> = React.memo(({ component, withRouter = true }) => {
return (
<Provider store={store}>
<Container component={component} withRouter={withRouter} />
</Provider>
);
});
export default MountWrapper;
My doLogin function in my actual app looks like this:
export type TLoginProps = {
payload: {
login: string;
password: string;
};
};
export const login =
({ payload: { login, password } }: TLoginProps): ThunkAction =>
async (dispatch): Promise<void> => {
try {
dispatch(incrementLoadingCount());
const { status, token } = (await graphQlRequest({
query: graphQlQueries.authQueries.LOGIN, // show below
variables: {
input: {
login,
password,
},
},
returnKey: 'login',
})) as TLoginResponse;
if (token) {
localStorage.setItem('token', token);
document.location.replace('/');
return;
}
const errorMessage = status === 401 ? 'Invalid credentials' : Dict.CORE.AUTH_ERROR;
throw Error(errorMessage);
} catch (err) {
dispatch(errorHandler({ payload: { err } }));
} finally {
dispatch(decrementLoadingCount());
}
};
The graphQlQueries.authQueries.LOGIN query looks like this:
const LOGIN = gql`
query($input: AuthCredentials!) {
login(input: $input) {
status
token
}
}
`