Testing Something Special

Frontend development is so exciting nowadays! There is a wide spectrum of opportunities in each particular project. There can be tons of different interesting cases related to feature development, bug fixing and testing! A lot of new components, configurations, and flows.

The stack

TypeScript, React.js

Interesting facts

Today we will take a look at one of the two most interesting and progressive parts of web development - Forms & Tables. 100% NO, huge NO, totally NO They are kind of the biggest regret and disappointment foundation in web development. Almost all projects are built by adding forms & tables. Some of them can even include only forms & tables. All of them are mostly the same - layout, logic & use cases, but with different styling.

Welcome the guest of the evening

We definitely should at least try to live in peace with them, though, and walk hand in hand as they are a big part of the West Web world. So today’s pick is testing <form /> submissions.

Here we go again…

This story begins with this issue in one of the most popular form libraries in the React.js world, Formik. The problem was in continuously submitting a form by holding the Enter key. This behavior is native to browsers, so the solution chosen was to work around it with a submitting flag - allowSubmit. Submissions would be allowed only when the flag is in its true state. Changing allowSubmit to false after submission and back to true after the keyUp event was fired by the enter key works perfectly for resolving this issue. Toggling this behavior was handled by adding a new boolean property that was checked by onSubmit & onKeyUp handlers.

Issue solution

    const ENTER_KEY_CODE = 13;
    const allowSubmit = React.useRef(true);
    const handleKeyUp = ({ keyCode }: React.KeyboardEvent<HTMLFormElement>) => {
      // If submit event was fired & prevent prop was passed unblock the submission
      if (keyCode === ENTER_KEY_CODE && props.preventStickingSubmissions) {
        allowSubmit.current = true;
      }
    };
    // Wrapper for the submit event handler
    const submitWrap = (ev: React.FormEvent<HTMLFormElement>) => {
      // Check if the form was not already submitted
      if (allowSubmit.current) {
        // Call the original submit handler
        handleSubmit(ev);
        // If prevent prop was passed -> change flag's state
        if (props.preventStickingSubmissions) {
          allowSubmit.current = false;
        }
      } else {
        // Prevent the default browser submit event behavior
        ev.preventDefault();
      }
    };
    return (
      <form
        onKeyUp={handleKeyUp}
        onSubmit={submitWrap}
        // The rest of the props go here
        {...props}
      />
    );

Rewards and congratulations were so close…

At the top of the definition of the <Form /> component a TODO was placed with the call for implementing tests for this component. I’m positioning myself as somebody who believes in karma - The code you leave for others is the same code you will get from others. So I decided to add tests for my implementation right away.

1001 night

I created a new file for tests, in which I decided to cover previously implemented logic with a couple of tests. But right after I defined the describe & it functions I got stuck. I realized that I had no idea how to mock sticking submission behavior.

My attempts looked something like this:

    const form = getByRole('form');
    act(() => {
      fireEvent.keyDown(form, { keyCode: ENTER_KEY_CODE });
      fireEvent.keyDown(form, { keyCode: ENTER_KEY_CODE });
      fireEvent.keyUp(form, { keyCode: ENTER_KEY_CODE });
      fireEvent.keyDown(form, { keyCode: ENTER_KEY_CODE });
      fireEvent.keyDown(form, { keyCode: ENTER_KEY_CODE });
    });
    await waitFor(() => expect(onSubmit).toBeCalledTimes(1));

Or this:

    const form = getByRole('form');
    const event = new KeyboardEvent('keydown', {
      keyCode: ENTER_KEY_CODE
    });
    act(() => {
      form.dispatchEvent(event);
      form.dispatchEvent(event);
      fireEvent.keyUp(form, { keyCode: ENTER_KEY_CODE });
      form.dispatchEvent(event);
      form.dispatchEvent(event);
    });
    await waitFor(() => expect(onSubmit).toBeCalledTimes(2));

After running these tests, I expected that they would at least fail with incorrect called times count, but the result was worse - the onSubmit function wasn’t called at all. I tried tons of different combinations. So after my first unsuccessful attempt, I left it for a couple of days with the decision to push the code and create a draft PR stating my desire to add tests, but that I was stuck and still working on them, so maybe somebody would leave a useful comment that would help me figure out what was wrong with the logic for testing this feature.

…Three Days Grace… umm, later…

Opening the file with the tests, I decided to take a step back and rethink my idea for testing this feature. I started to think about which result I was trying to achieve and in which way so, in my mind, I was thinking like an ordinary user of the service - I keep holding the pressed Enter button and the form is submitted a couple of times. Then I started to think about what is going on under the hood when keeping that button pressed. I debugged my code a couple of times and understood that I do not care about the keydown event. The event that fires continuously when I’m holding the button is submit, the keydown event fires only once. After realizing that, everything fell into place. The solution was in the surface the whole time.

The one thing that needed to be changed in the previous code was the keydown event. After changing it to submit my tests started to run successfully!

The final code

    it('should submit form two times when pressing enter key, then unpressing it & repeat', async () => {
      const onSubmit = jest.fn();
      const { getByRole } = render(
        <Formik initialValues= preventStickingSubmissions onSubmit={onSubmit}>
          <Form name="Form" />
        </Formik>
      );
      const form = getByRole('form');
      act(() => {
        fireEvent.submit(form);
        fireEvent.submit(form);
        fireEvent.submit(form);
        fireEvent.keyUp(form, { keyCode: ENTER_KEY_CODE });
        fireEvent.submit(form);
        fireEvent.submit(form);
        fireEvent.keyUp(form, { keyCode: ENTER_KEY_CODE });
      });
      await waitFor(() => expect(onSubmit).toBeCalledTimes(2));
    });

Conclusion

Solving this problem reminded me of the main truth - there is no magic in software development. If I don’t understand why something happens in a certain way - the best choice, in that case, is diving deep into the source of the problem/implementation.