HeaderTrim
âŦ…ī¸

Anatomy of a Click

Introduction

So here's the story of how I lost a few hours of my life figuring out some issues with some tests.

Over at Threads Styling we use react-native to write reusable components for both native, and web, using react-native-web. This leads to a pretty sweey setup where we can write our components once and use them where ever we need them.

Together with this we also use Testing Library's @testing-library/react to write tests for our browser based applications.

With React Native the way that you allow interactions with components is generally by wrapping them with some form of Touchable (there's a number of different flavours, TouchableOpacity, TouchableWithoutFeedback, etc) and using the onPress prop instead of onClick like you would on the web.

While writing some tests I ran into an interesting problem. I couldn't get the onPress handler to trigger. 🤔 Pretty odd. The test looked pretty simple, essentially:

test("button should do thing", () => {
// Render our component and get the button
const { getByText } = render(<Screen />);
// Doing something other things here
const btn = getByText("Click me");
// Click the button
fireEvent.click(btn);
// Something updated telling us we clicked the button
getByText("Clicked!");
});

But the test was unable to find something Clicked.

👨‍đŸ’ģ

I recently converted to querying by actual text instead of test IDs and the likes, due to reading this great blog post by Kent C. Dodds, Common mistakes with React Testing Library.

Time to dig a bit deeper.

Debugging

Was I looking for the wrong thing maybe?

Some steps had to happen before we got to our button we were trying to click, so I decided to start by checking the DOM before and after the click.

When getByText throws, it includes the state of the DOM, but only 7000 by default. You can whack that right up with the DEBUG_PRINT_LIMIT environment variable, doing something like DEBUG_PRINT_LIMIT=10000 yarn test to get a bit more.

But I also wanted the state before we even tried to click the button, and I used debug from the render call to do this, like so:

test("button should do thing", () => {
// Render our component and get the button
const { getByText, debug } = render(<Screen />);
// Doing something other things here
debug();
const btn = getByText("Click me");
// Click the button
fireEvent.click(btn);
// Something updated telling us we clicked the button
getByText("Clicked!");
});
⚠ī¸

Here I show how not to do it, ideally you should use debug from the screen given by render, as seen in debug. But I only found out about this afterwards.

After doing this, the DOM looked to be fine before we tried to click, and after clicking, nothing happened, so I'd gotten nowhere. But I was more confident on what wasn't going on, so that's something.

I then resorted to the tried and tested method of debugging - throwing console.log everywhere I could get at and seeing what came out.

And I noticed a curious thing.

The onPress wasn't getting called.

But I was definitely firing an event clicking on it. How could this be?

You'll remember that we use React Native to write out components, and for the web we use React Native for Web to "convert" these components and let us use them on the web for free.

Was something going wrong there?

I opened up React Native for Web and did some digging.

In the main file for dealing with Touchables I found a hint.

/**
* Invoked when the item is "selected" - meaning the interaction ended by
* letting up while the item was either in the state
* `RESPONDER_ACTIVE_PRESS_IN` or `RESPONDER_INACTIVE_PRESS_IN`.
*
* @abstract
* touchableHandlePress: function
*/

Handling presses in React Native for Web uses a gesture responder system for these Touchables. And the gesture responder system doesn't care about the click events. To make the presses feel more like "presses", it responds to the "mousedown" and "mouseup" events in a click.

To get what I mean here, let's dig into the anatomy of a click.

Here's a quick interactive example of a button, and if you click on it it will tell you the events that are fired from it. You can see the source of the example here.

For a click, you should see something like:

mousedown
then
focus
then
mouseup
then
click

If you're on mobile you'll see some touchstart and touchend events in there too.

ℹī¸

I've omitted some events like mousemove etc that fire often and aren't that useful for what I'm trying to show.

So React Native for Web doesn't care about our click events we're firing at it.

The Fix

At the time, I wrote a quick util to handle firing clicks that were React Native for Web compatible, and moved on.

export default function click(element) {
fireEvent.mousedown(element);
fireEvent.mouseup(element);
}

It got us where we needed to be.

But really, this isn't a great solution.

Since doing this, I've started reading more around testing React, and I became aware of @testing-library/user-event, a library to handle all the quirks of events, that the simple fireEvent doesn't aim to solve. It's still work in progress, and looking for help, but the project is doing an admirable attempt to simulate events in a way much more closer to what actually happens in the browser.

This is a much better solution, and using it should save time from these kinds of bugs in the future.