0

I have tried a few ways to do this correctly, but lack the testing experience to catch what I'm missing. I have a LoginForm.tsx component that inside holds a few event handlers and a couple bits of local state using React.useState(). The component returns a ternary statement conditionally rendering two components, and within one of them, that component renders different content based on another boolean condition.

  • authSuccess: when false, main component returns a <Card /> component; when true, the component returns <Navigate to={...} replace /> to redirect user to account.

  • isLoading: when false, children of <Card /> is form content, when true, children is a <Spinner /> component.

The problem is, I can't seem to find how to change those useState values in my tests and mock the behavior of this component. I would like to test that errors are rendering correctly as well. I am not using Enzyme since it seems it is dead for anything after React 17, so I have been trying to find a way to do this using just React Testing Library out of the box with Create React App Typescript.

The component code looks like this:

import * as React from 'react'
// ...

export default function LoginForm() {
    const [isLoading, setIsLoading] = React.useState<boolean>(false);
    const [errors, setErrors] = React.useState<{ [key: string]: string } | any>({});
    const [authSuccess, setAuthSuccess] = React.useState<boolean>(false);

    const initialState: LoginFormInitialState = {
        email: '',
        password: '',
    };


    // Form Hook
    const { values, onChange, onSubmit } = useForm({ callback: handleLogin, initialState });

    // ==> HANDLERS
    function successCallback(data) {
        // ...
        return setAuthSuccess(true); // <---- Changes state to return redirect
    }

    function errorHandler(e: any) {
        // ...
        setErrors(errors);  // <---- // sets errors object to render errors
        return setIsLoading(false); // <---- Changes contents of <Card />
    }

    function handleLogin() {
        setIsLoading(true);   // <---- Changes content of card
        setErrors({}); // <---- Clears errors
               
        // Passes handlers as callbacks into api
        return userAccountAPI.login({ data: values, successCallback, errorHandler });
    }

    return authSuccess ? (
        // If authentication was successful, redirect user to account page
        <Navigate to={ACCOUNT_OVERVIEW} replace />
    ) : (
        // No auth success yet, keep user on login page
        <Card
            title={<h1 data-testid="form-header">Login To Account</h1>}
            data-testid={'login-card'}
            bodyStyle={{
                display: isLoading ? 'flex' : 'block',
                justifyContent: 'center',
                padding: isLoading ? '100px 0 ' : '',
            }}>
            {isLoading ? (
                <Spin size='large' data-testid={'login-spinner'}></Spin>
            ) : (
                <Form name='login_form' data-testid={'login-form'} initialValues={{ remember: true }}>
                    <Form.Item name='email' rules={[{ required: true, message: 'Please input your email!' }]}>
                        <Input
                            name='email'
                            onChange={onChange}
                            prefix={<UserOutlined className='site-form-item-icon' />}
                            placeholder='Email'
                        />
                    </Form.Item>
                    <Form.Item name='password' rules={[{ required: true, message: 'Please input your Password!' }]}>
                        <Input
                            name='password'
                            onChange={onChange}
                            prefix={<LockOutlined className='site-form-item-icon' />}
                            type='password'
                            placeholder='Password'
                        />
                    </Form.Item>

                    <Form.Item>
                        <Button
                            data-testid='login-button'
                            onClick={onSubmit}
                            type='primary'
                            name='login'
                            htmlType='submit'
                            className='login-form-button'
                            block>
                            Log in
                        </Button>
                    </Form.Item>
                </Form>
            )}

            {/* Render out any form errors from the login attempt */}
            {Object.entries(errors).length > 0 && (
                <Alert
                    type='error'
                    message={
                        <ul style={{ margin: '0' }}>
                            {Object.keys(errors).map((er, i) => {
                                return <li key={i}>{errors[er]}</li>;
                            })}
                        </ul>
                    }
                />
            )}
        </Card>
    );
}

I would like to be able to make an assertion about the card, but not if authSuccess=true, in which case I'd want to assert that we do not have the card and that the redirect has been rendered. I would want to test that the Spinner is a child of the card if isLoading = true, but also that I have the form as a child if it is false.

I have tried some of the approaches I've seen in other issues, but many of them have a button in the UI that directly changes the value and the solution is typically "grab that button and click it" and there you go. But the only button here is the login, and that doesn't directly change the local state values I need to mock.

I have also tried something like this which seems to have worked for some people but not for me here..

import * as React from 'react'

describe('<LoginForm />', () => {
    const setup = () => {
        const mockStore = configureStore();
        render(
            <Provider store={mockStore()}>
                <LoginForm />
            </Provider>
        );
    };


    it('should have a spinner as child of card', () => {
        setup();
        jest.spyOn(React, 'useState')
            .mockImplementationOnce(() => ['isLoading', () => true])
            .mockImplementationOnce(() => ['errors', () => {}])
            .mockImplementationOnce(() => ['authSuccess', () => false]);

        
        const card = screen.getAllByTestId('login-card');
        const spinner = screen.getAllByTestId('login-spinner');
        expect(card).toContainElement(spinner);
    });
});

It seems like Enzyme provided solutions for accessing and changing state, but as mentioned, I am not using Enzyme since I am using React 18.

How can I test this the way I intend to, or am I making a fundamental mistake with how I am approaching testing this? I am somewhat new to writing tests beyond that basics.

Thanks!

1 Answers1

1

From the test I see that you are using react testing library. In this case you should "interact" with your component inside the test and check if the component reacts properly.

The test for spinner should be like that:

  • render the component
  • find the email input field and "type" there an email - use one of getBy* methods and then type with e.g. fireEvent.change(input, {target: {value: 'test@example.com'}})
  • find the password input field and "type" there a password - same as above
  • find the submit button and "click" it - use one of getBy* methods to find it and then use fireEvent to click it
  • this (I assume) should trigger your onSubmit which will call the handleLogin callback which will update the state and that will render the spinner.
  • check if spinner is in the document.

Most probably you would need some mocking for your userAccountAPI so it calls a mock function and not some real API. In here you can also mock that API to return whatever response you want and check if component displays correct content.

Marek Rozmus
  • 773
  • 7
  • 13
  • You were correct about the adjustment to my approach. Rather than trying to directly manipulate the state, I used Mock Service Worker to mock the server response when it is clicked and asserted in the order that things should change in the UI. Thanks for the help! – Bryan Jastrzembski Dec 04 '22 at 22:24