Testing Forms with Jest and react-testing-library
I needed to test my login.tsx
form submit, but was running into this error.
Error:
expect(jest.fn()).toHaveBeenCalledTimes(1)
Expected mock function to have been called one time, but it was called zero times.
Problem: Submit Handler Isn't Being Called
For some reason the mock, const submitLogin = jest.fn();
was not being called in the form.
Interestingly, putting the submitLogin
handler directly on the button onClick
, would actually call submitLogin
.
LoginFormComponent.test.tsx
import React from "react";
import "jest-dom/extend-expect";
import {
cleanup,
fireEvent,
render,
RenderResult
} from "react-testing-library";
//Generate fake user data
import { generate } from "../../../../test-utils/test-utils";
import LoginFormComponent from "../LoginFormComponent";
afterEach(cleanup);
describe("LoginFormComponent", () => {
describe("Submitting form", () => {
//Arrange--------------
// Set up variables accessible in tests
let wrapper: RenderResult;
let fakeUser: { email: string; password: string };
let emailNode: HTMLInputElement;
let passwordNode: HTMLInputElement;
let loginButtonNode: HTMLInputElement;
let submitLogin: () => void;
beforeEach(() => {
// Here's my submitHandler mock that isn't getting called
submitLogin = jest.fn();
const props = {
submitLogin
};
wrapper = render(<LoginFormComponent {...props} />);
fakeUser = generate.loginForm();
emailNode = wrapper.getByPlaceholderText(
"E-mail address"
) as HTMLInputElement;
passwordNode = wrapper.getByPlaceholderText(
"Password"
) as HTMLInputElement;
loginButtonNode = wrapper.getByText("Login") as HTMLInputElement;
//Act--------------
// Change the input values
fireEvent.change(emailNode, { target: { value: fakeUser.email } });
fireEvent.change(passwordNode, { target: { value: fakeUser.password } });
// This should submit the form?
fireEvent.click(loginButtonNode); });
test("Shows loading spinner", () => {
// This test passes // so fireEvent.click(loginButtonNode) does work
//Assert--------------
expect(loginButtonNode).toHaveAttribute(
"class",
"ui facebook large fluid loading button" // Ugly, I know. I want to assert 'loading'
);
});
test("Submits Login with email and password", () => {
//Assert--------------
expect(submitLogin).toHaveBeenCalledTimes(1); // <- Error. Doesn't get called expect(submitLogin).toHaveBeenCalledWith(fakeUser);
});
});
});
LoginFormComponent.tsx
import React from "react";
import { Formik, FormikProps, Field, FieldProps } from "formik";
import styled from "react-emotion";
import { Button, Form, Grid, Header, Image, Message } from "semantic-ui-react";
import UsernameInput from "../components/UsernameInput";
import PasswordInput from "../components/PasswordInput";
const LoginFormStyles = styled("div")({
height: "50vh"
});
export interface FormValues {
email: string;
password: string;
}
interface ILoginFormProps {
loginError?: string;
submitLogin: (credentials: FormValues) => void;
}
export const LoginForm: React.SFC<ILoginFormProps> = ({
loginError,
submitLogin
}) => {
return (
<LoginFormStyles className="login-form">
{loginError && (
<Message negative>
<Message.Header>Your login was unsuccessful</Message.Header>
<p>Your username or password is invalid.</p>
</Message>
)}
<Grid
verticalAlign="middle"
textAlign="center"
style={{ height: "100%" }}
>
<Grid.Column style={{ maxWidth: 450 }}>
<Image inline={false} size="big" src="https://img.png" />
<Header as="h2" color="blue" textAlign="center">
Log-in to Lender Dealer Mapping Tool
</Header>
// Formik form starts here ========================================
<Formik
initialValues={{ email: "", password: "" }}
onSubmit={(formValues: FormValues) => {
submitLogin(formValues); // <--- This is where the submitLogin mock should be called }}
render={(formikProps: FormikProps<FormValues>) => {
return (
<Form onSubmit={formikProps.handleSubmit}>
<Field
name="email"
render={(fieldProps: FieldProps<FormValues>) => {
return <UsernameInput {...fieldProps.field} />;
}}
/>
<Field
name="password"
render={(fieldProps: FieldProps<FormValues>) => {
return <PasswordInput {...fieldProps.field} />;
}}
/>
<Button
type="submit"
color="facebook"
fluid
size="large"
loading={formikProps.isSubmitting && !loginError}
>
Login
</Button>
</Form>
);
}}
/>
</Grid.Column>
</Grid>
</LoginFormStyles>
);
};
export default LoginForm;
Solution
Jest was completing the test without waiting for the Formik component to call its own onSubmit.
react-testing-library has a wait API
test("Submits Login with email and password", async () => {
//Assert--------------
await wait(() => {
expect(submitLogin).toHaveBeenCalledTimes(1);
expect(submitLogin).toHaveBeenCalledWith(fakeUser);
});
});