React Testing Library: Checkboxes and events

Published See discussion on Bluesky
TL;DR:

If you're testing a checkbox input element with React Testing Library, you'll want to fire a click event on the checkbox not a change event!

Read on to find out how I discovered this!

I was recently refactoring about 70 individual Enzyme-based unit tests across our design system codebase at work to swap it for React Testing Library, and encountered an odd quirk that I figured I'd document on my blog.

I was converting some tests for one of our Checkbox Table components, a component that allows users to select individual rows or the entire page of results shown within a table. The previous tests were quite extensive, the entire file was well over 1200 lines of code covering many different and difficult edge cases.

As I went through I converted one of the tests that was asserting on an onChange handler being properly called when one of the checkboxes was selected, the previous test code looked something like:

1const handleChange = jest.fn();
2const wrapper = mount(<CheckboxTable onChange={handleChange} />);
3
4wrapper
5 .find(`[data-enzyme-id="header-checkbox"]`)
6 .simulate('change', { target: { checked: true } });
7
8expect(handleChange).toHaveBeenCalledTimes(1);
1const handleChange = jest.fn();
2const wrapper = mount(<CheckboxTable onChange={handleChange} />);
3
4wrapper
5 .find(`[data-enzyme-id="header-checkbox"]`)
6 .simulate('change', { target: { checked: true } });
7
8expect(handleChange).toHaveBeenCalledTimes(1);

So I started converting as I had other tests within the codebase:

1const handleChange = jest.fn();
2render(<CheckboxTable onChange={handleChange} />);
3
4fireEvent.change(screen.getByTestId('header-checkbox'), {
5 target: { checked: true },
6});
7
8expect(handleChange).toHaveBeenCalledTimes(1);
1const handleChange = jest.fn();
2render(<CheckboxTable onChange={handleChange} />);
3
4fireEvent.change(screen.getByTestId('header-checkbox'), {
5 target: { checked: true },
6});
7
8expect(handleChange).toHaveBeenCalledTimes(1);

However, the test started failing!

I started scratching my head after debugging this for about 30 minutes without making much traction on the failing test, in fact I was about to comment the test out and return to it in a follow up ticket but then I did one last google search and stumbled upon this stackoverflow result, linked from there was this issue comment noting that for checkbox elements, we actually want to fire a click event instead of a change event!

So I went back to my test and updated it one last time to the following:

1const handleChange = jest.fn();
2render(<CheckboxTable onChange={handleChange} />);
3
4fireEvent.click(screen.getByTestId('header-checkbox'));
5
6expect(handleChange).toHaveBeenCalledTimes(1);
1const handleChange = jest.fn();
2render(<CheckboxTable onChange={handleChange} />);
3
4fireEvent.click(screen.getByTestId('header-checkbox'));
5
6expect(handleChange).toHaveBeenCalledTimes(1);

and the test started passing!