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.