Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
220 lines (181 sloc) 6.05 KB
date title template thumbnail slug categories tags
2018-10-15
Testing Formik 'onSubmit' with Jest
post
../thumbnails/jest.png
testing-formik-form-onsubmit
Testing
Popular
formik
react-testing-library
jest

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);
  });
});
You can’t perform that action at this time.