0

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
        }
    }
`
Mike K
  • 5,940
  • 7
  • 37
  • 87

0 Answers0