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
: whenfalse
, main component returns a<Card />
component; whentrue
, the component returns<Navigate to={...} replace />
to redirect user to account.isLoading
: whenfalse
, children of<Card />
is form content, whentrue
, 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!