Testing global event listeners within a React component

window.addEventListener not triggered by Enzyme simulated events
Headphone in a yellow background

Although many developers working with React consider it an anti-pattern, occasionally, there is the need to listen and react to an event globally within a React component. As every good developer out there, you probably want to provide a unit test for this functionality. However, while implementing the test, you might face some troubles… Here comes this article trying to help you on your way through.


The React component

In my particular case, I have a React component which renders an entire page of my app. I want to listen when the user hits the Enter key and do something after this event happens.

Therefore, on componentDidMount I register the event listener to the global window object and on componentWillUnmount I unregister it. You can see part of the code of my component here:

componentDidMount() {
  window.addEventListener('keyup', this.handleKeyUp);
}

componentWillUnmount() {
  window.removeEventListener('keyup', this.handleKeyUp);
}

handleKeyUp(event) {
  if (event.key === 'Enter') {
    this.handleEnterKey();
  }
}

It works perfectly as expected! When the user hits the Enter key, the component listens to it and executes some operations.

The Test

Now, I want to provide a unit test for this functionality.

My testing stack includes Enzyme. Hence, I think of using the simulate() feature to mimic the hit of the key. However, the event handler I registered previously in my component is not being triggered.

After some research on the Web, the community comes rescuing me and I find the solution! I’ll try to summarise and explain it here.

The Problem & the Solution

Let’s start by specifying and making it really clear what the goal is that you want to achieve. I’ll do this by quoting from the linked GitHub issue above:

Your goal here is effectively to make sure the event is bound, and that when it’s fired something happens in your component.

Let’s also quote a statement from one of the Enzyme’s maintainers. It specifies a really important aspect of the testing utility which will help us understand the nature of the issue.

Enzyme is meant to test React components and attaching an event listener to the document with addEventListener means the event is not being handled by React’s synthetic event system. Our simulate method for mount is a thin wrapper around ReactTestUtils.Simulate, which only deals with React’s synthetic event system.

You see now why in this case using the simulate() method of Enzyme is not having any effect at the React component level.

To have any effect, keeping in mind the goal of our test, what you need to simulate is the mechanism of window.addEventListener. That is, you need to create a binding between an event name and callback function.

const map = {};
window.addEventListener = jest.fn((event, cb) => {
  map[event] = cb;
});

Now, when you mount the React component and componentDidMount is executed, the binding is created through the map object defined above.

const component = mount(<MyComponent {...props} />);

At this point, instead of using the simulate() Enzyme method to fire the event, you can simulate the typing of the Enter key by executing the following line of code

// simulate event
map.keyup({ key: 'Enter' })

Now, you can assert that when this event is triggered, the callback binder to this event through window.addEventListener is executed:

expect(component.handleEnterKey).toHaveBeenCalled()

and voilá!

Test is green!

I hope this small article will be helpful for anyone ending up in this same situation and that it will help to understand the why and how of the approach adopted!

Thanks to timoxley to raise the issue, to awaery to follow up and to blainekasten to provide a great solution!

Want to join our Engineering team?
Apply today!
Share: