how to test button that call submit form using jest and react testing library
Asked Answered
T

4

8

so I am trying to test that the onSubmit function is getting triggered if the button is clicked - the way im doing this is through testing the internals of the onSubmit function is getting calles (axios post method)

the test

describe('RecipeSearch', () => {
    test('submit button should return post function to recipes/search/', () => {
        let mock = new MockAdapter(axios);
        userEvent.selectOptions(screen.getByRole('combobox'), 'Sweet');
        userEvent.click(screen.getByText('Search'));

        const config = {
            headers: {
                'Content-Type': 'application/json',
            },
        };
        const searchRecipes = mock.onPost(
            `${process.env.REACT_APP_API_URL}/recipes/search/`,
            { flavor_type: 'Sweet' },
            { config }
        );
        expect(searchRecipes).toHaveBeenCalled();
    });
});

the Error

    expect(received).toHaveBeenCalled()

    Matcher error: received value must be a mock or spy function

    Received has type:  object
    Received has value: {"abortRequest": [Function abortRequest], "abortRequestOnce": [Function abortRequestOnce], "networkError": [Function networkError], "networkErrorOnce": [Function networkErrorOnce], "passThrough": [Function passThrough], "reply": [Function reply], "replyOnce": [Function replyOnce], "timeout": [Function timeout], "timeoutOnce": [Function timeoutOnce]}

the function

const recipeSearch = (props) => {
    const [formData, setFormData] = useState({
        flavor_type: 'Sour',
    });

    const { flavor_type } = formData;

    const [loading, setLoading] = useState(false);

    const onChange = (e) => setFormData({ ...formData, [e.target.name]: e.target.value });

    const onSubmit = (e) => {
        e.preventDefault();

        const config = {
            headers: {
                'Content-Type': 'application/json',
            },
        };

        setLoading(true);
        axios
            .post(
                `${process.env.REACT_APP_API_URL}/recipes/search/`,
                {
                    flavor_type,
                },
                config
            )
            .then((res) => {
                setLoading(false);
                props.setRecipes(res.data);
                window.scrollTo(0, 0);
            })
            .catch((err) => {
                setLoading(false);
                window.scrollTo(0, 0);
            });
    };

    return (
        <form  onSubmit={(e) => onSubmit(e)}>
            <div>
                <div>
                    <div>
                        <label htmlFor='flavor_type'>Choose Flavor</label>
                        <select
                            name='flavor_type'
                            onChange={(e) => onChange(e)}
                            value={flavor_type}
                        >
                            <option value='Sour'>Sour</option>
                            <option>Sweet</option>
                            <option>Salty</option>
                        </select>
                    </div>

                    <div>
                            <button type='submit'>Search</button> 
                    </div>
                </div>
            </div>
        </form>
    );
};

i have added the whole test and the component code so helping would be easier. thanks in advance

(added the onChange + onSubmit functions)

Twana answered 8/2, 2021 at 22:1 Comment(3)
Sometimes it takes a few ticks for Formik to get through validations and everything-- have you tried wrapping that expectation in await waitFor()?Larry
Tried it, still doesn't work.Twana
On your component's code, where does the onSubmit function come from?Intensity
I
4

Creating an onSubmit mock and passing it as a prop won't work since the onSubmit callback is internal to the component and not a prop - you don't have access to it from the test.

Rather than testing if the onSubmit has been called, you should test the result of triggering the submit event. Which in this case could mean verifying that the axios request is made.

See How do I test axios in Jest? for examples on how to mock axios in your test.

Intensity answered 20/2, 2021 at 17:39 Comment(7)
Thanks you helped a lot now I understand why the test keep failing ! But isn't there's a way that I can mock internal functions to use in tests? or should I just manually import the functions to the test?Twana
There isn't a way to mock internal functions. You could import it from an external file and mock the imported function during the test if you really wanted.Intensity
That being said, I wouldn't recommend changing your code just for testing purposes. As I said, you should test the result of triggering the onSubmit action instead.Intensity
Thanks man you helped a ton. I will give it a shotTwana
so I tried to test the result of the onSubmit function but still the test fails.Twana
You're not mocking axios properly in that test. Check How do I test axios in Jest? for possible solutions to that.Intensity
so I tried to mock the axios but still with no success, I have updated the test and the error in question. i would really appreciate if you could guide me how you would write the test.Twana
F
2

I personally don't like the idea of changing the Form component code by passing a mock or spy function just for testing purpose.

For my form, I instead come up with this idea -

  • basically I created handleOnSubmitMock function, and then assigned it to screen.getByRole("form", { name: "signup-form" }).onsubmit GlobalEventHandler. (Not passing the mock function to the Form)
  • then I checked if expect(handleOnSubmitMock).toHaveBeenCalled() or expect(handleOnSubmitMock).not.toHaveBeenCalled() passes.
  • Note that form data validation needs to be done using HTML required attribute, regex pattern and onChange handler to prevent form submission with invalid data.
    import React from "react";
    import { render, screen, fireEvent } from "@testing-library/react";
    import SignupForm from "../components/SignupForm";

    describe("SignupForm Component", () => {
      // Helper function to render the component
      const renderComponent = () => {
        return render(<SignupForm />);
      };
      const handleOnSubmitMock = jest.fn();

      it("does not submit an empty form", () => {
        renderComponent();
        screen.getByRole("form", { name: "signup-form" }).onsubmit =
          handleOnSubmitMock;

        // Submit the empty form
        fireEvent.click(screen.getByRole("button", { name: "Sign Up" }));

        // Expectations for form submission
        expect(handleOnSubmitMock).not.toHaveBeenCalled();
      });

      it("does not submit the form with an invalid username", () => {
        // complete the test case similarly
      });

      it("does not submit the form with an invalid email", () => {
        renderComponent();
        screen.getByRole("form", { name: "signup-form" }).onsubmit =
          handleOnSubmitMock;

        fireEvent.change(screen.getByPlaceholderText("Username"), {
          target: { value: "validUsername" },
        });
        fireEvent.change(screen.getByPlaceholderText("Email"), {
          target: { value: "[email protected]" },
        });
        fireEvent.change(screen.getByPlaceholderText("Password"), {
          target: { value: "ValidPassword1!" },
        });
        fireEvent.change(screen.getByPlaceholderText("Confirm Password"), {
          target: { value: "ValidPassword1!" },
        });

        // Submit the form
        fireEvent.click(screen.getByRole("button", { name: "Sign Up" }));

        // Expectations for form submission
        expect(handleOnSubmitMock).not.toHaveBeenCalled();
      });

      it("does not submit the form with an invalid password", () => {
        // complete the test case similarly
      });

      it("does not submit the form without matching passwords", () => {
        // complete the test case similarly
      });

      it("submits the form only with valid data", () => {
        renderComponent();
        screen.getByRole("form", { name: "signup-form" }).onsubmit =
          handleOnSubmitMock;

        // Fill in the form fields with valid data
        fireEvent.change(screen.getByPlaceholderText("Username"), {
          target: { value: "validUsername" },
        });
        fireEvent.change(screen.getByPlaceholderText("Email"), {
          target: { value: "[email protected]" },
        });
        fireEvent.change(screen.getByPlaceholderText("Password"), {
          target: { value: "ValidPassword1!" },
        });
        fireEvent.change(screen.getByPlaceholderText("Confirm Password"), {
          target: { value: "ValidPassword1!" },
        });

        // Submit the form
        fireEvent.click(screen.getByRole("button", { name: "Sign Up" }));

        // Expectations for form submission
        expect(handleOnSubmitMock).toHaveBeenCalled();
      });
    });

And my Form component is not taking any props-

    import React, { useState } from "react";

    interface FormData {
      username: string;
      email: string;
      password: string;
      confirmPassword: string;
    }

    const SignupForm: React.FC = () => {
      const [formData, setFormData] = useState<FormData>({
        username: "",
        email: "",
        password: "",
        confirmPassword: "",
      });
      ... ...

      const handleOnSubmit = (e: React.FormEvent): void => {
        ... ...
      };

      return (
        <div className="bg-gray-900 h-screen flex flex-col items-center justify-center">
          <img ... ... />
          <div className="bg-white p-8 rounded-lg shadow-md w-96">
            <h2 className="text-2xl font-semibold mb-4 text-center text-gray-800">
              Register
            </h2>
            <form aria-label="signup-form" onSubmit={handleOnSubmit}>
              <div className="mb-4">
                <input
                  id="username"
                  type="text"
                  name="username"
                  placeholder="Username"
                  required
                />
              </div>

              // other input fields ... ...

              <button
                type="submit">
                Sign Up
              </button>
            </form>
            <div className="mt-4 text-center">
              <p className="text-gray-600">
                Already have an account?{" "}
                <a href="/signin" className="text-blue-500 hover:underline">
                  Sign in
                </a>
              </p>
            </div>
          </div>
        </div>
      );
    };

    export default SignupForm;

You should be able to edit the above examples for your RecipeSearchForm, and it should work.

Floranceflore answered 4/9, 2023 at 5:26 Comment(4)
Won't handleOnSubmitMock() always be called because clicking a form's submit button always triggers an onSubmit event? I would think you need some way to determine if the onSubmit event handler cancels the event, but not sure how you would do that.Hirokohiroshi
In this example, there are certain patterns to be matched for username, email, password for the form to be submitted successfully. (Those codes are trimmed off as quite big). So main point is, if all those patterns match i.e. username, email & pw are valid only then the button click event would call onsubmit represented as handleOnSubmitMock in the expect statement (that takes only Mock function). Otherwise, if any of patterns not matches, expect(handleOnSubmitMock).toHaveBeenCalled() FAILS, or expect(handleOnSubmitMock).not.toHaveBeenCalled()PASSES. Maybe you're seeking the last one.Floranceflore
Many React developers put the data validation code in the onSubmit handler and cancel the event when something doesn't validate. So you might want to make it clear that your approach is using HTML attributes (like require) to prevent form submission.Hirokohiroshi
Good point. I added those info in the 3rd point. Thanks. Although the purpose of this QnA is to only test onSubmit call on button click. With your scenario, handleOnSubmitMock would still be called with invalid data i.e. the toHaveBeenCalled test needs to pass (that'd be the app's normal behavior in that case), and another unit test would be required to test data validation & event cancellation inside onSubmit handler. From TDD perspective, you might not want to call onSubmit with invalid data.Floranceflore
S
0

Did you try selecting the button by text :

 describe('RecipeSearch', () => {
    test('test clicking the button triggers the onSubmit function', () => {
        const onSubmit = jest.fn();
        render(<RecipeSearch onSubmit={onSubmit} />);
        userEvent.selectOptions(screen.getByRole('combobox'), 'Sour');
        userEvent.click(screen.getByText('Search'));
        expect(onSubmit).toHaveBeenCalled();
    });
});

I'm not sure how getByRole handles a second argument in your first try, but getByText should work.

Subsumption answered 9/2, 2021 at 19:36 Comment(1)
thanks for the comment but unfortunately the test still isnt working.Twana
M
0

You can try below, this is working for me

fireEvent.submit(getByTestId('signup-form'));

Your form should have data-testid="signup-form"

Mandelbaum answered 6/12, 2023 at 9:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.