Better Test User Interactions in JavaScript Apps with Emulated Events

By  Jesse Jordan

 29 Aug 2022

For over a decade, single-page web applications have been on the rise and continue to be a popular medium for modern web experiences today. Digital products such as Twitter, Gmail, LinkedIn, and Netflix, as well as our dashboard at Meroxa, are such JavaScript applications and are served to billions of users every day.

To guarantee the delivery of high-quality software, engineering teams implementing modern web applications must not only dedicate time to the development of new features or the maintenance of already existing code, but also to the verification of application behavior through manual and automated testing.

When it comes to automated testing of JavaScript applications, it is key to verify the application state is changing as expected over time. The state of JavaScript applications is defined by a continuous sequence of user interactions and browser events. In some cases, the actual user interaction can not be replicated in a test easily — but the underlying events may be.

By emulating the DOM (Document Object Model) events in our automated tests, we get closer to mimicking our app’s behavior accurately. In this article, you’ll learn what DOM events are and how you can leverage them in your testing approach for more reliable test coverage.

What is a DOM event?

A DOM event signals occurrences in a web app, such as a user interaction (e.g. a user hovering over a button element) or another event-triggering action, that is unrelated to user behavior (e.g. the browser finishing to load a web page). DOM events can be used to run a single or a set of multiple functions inside of a JavaScript application, at a specific point in time — specifically whenever the associated DOM event is triggered.

User interactions prompt many of the DOM events in a JavaScript application’s lifecycle. For example, a user may use a mouse or keyboard device to activate a <button> element, triggering many DOM events while doing so, including, but not limited to the click event.

Here’s a typical sequence of the DOM events sent whenever a button is clicked using a mouse device:

  • pointerover
  • pointerenter
  • mouseover
  • pointerrawupdate
  • pointermove
  • mousemove
  • pointerdown
  • mousedown
  • focus
  • pointerup
  • mouseup
  • click
  • blur

In a test environment, e.g. when running our JavaScript application in the context of an automated Jest or QUnit test run, it may be helpful for us to emulate such user interactions to verify that the event-driven features we have implemented are working as expected.

But how can you assert the sequence of events and associated app state changes in your JavaScript tests? Let’s take a look at an example component.

Example: Automated testing of file uploads

Imagine we were building an amazing file upload component that allows users to click a button to browse for a file on their machine, select it and then subsequently upload its content to the app.

If we wrote our app using EmberJS, as we’re doing for our open-source component library mx-ui-components at Meroxa, the component may be structured similarly to this:

And in a similar fashion, we may want to build such a component in a React library like this:

In a production environment, a user can now upload their files using the <Upload> component by clicking the Browse file button and selecting their file from their local machine.

findafile-demo

In our test suite, natively executing the full user interaction would be impossible: when our tests run in our continuous integration workflow, we won’t have easy access to the file directory of the remote machine from which a file is supposed to be selected for upload.

Instead of uploading a real file in our automated test, we can emulate the file upload event that results from the user interaction; this way, we can test if any associated event listeners and subsequent state changes in our JavaScript app are working as expected.

While building a web application using a JavaScript framework, you may benefit from the comfort of using compatible testing libraries, such as QUnit, @ember/test-helpers or Jest in combination with @testing-library/react, which will make emulating custom events even easier.

Let’s leverage Ember ’s triggerEvent function to test the file upload behavior of our <Upload /> component shown earlier:

In a React app on the other hand, we can assert the same user flow using the handy helper methods from @testing-library in a similar manner:

Other approaches for testing events in your tests

If you don’t have a developer-friendly testing library for your use case, you can create your own testing helper library for easy reuse in many different JavaScript-based projects.

Mimicking common user actions in plain JavaScript

Many HTML elements have built-in methods for programmatically triggering common user interactions on them, which makes testing user flows more straightforward.

For example, if we wanted to mock a user clicking a button element, we could emulate this as follows in our integration test:

Sometimes we would like to assert an application state change in our automated test that is elicited by a DOM event for which there is no corresponding DOM element method, such as element.click.

What if we updated our app state anytime a user was starting and stopping to hover over the button mentioned in the example above, regardless if the button was clicked or not? In that case, we might want to emulate the mouseenter and mouseleave events instead and verify that our JavaScript application is still behaving as expected.

Emulating any DOM event

For such test scenarios based on less common DOM events, we can leverage the dispatchEvent API:

The dispatchEvent() method of the EventTarget sends an Event to the object, (synchronously) invoking the affected EventListeners in the appropriate order.

from the MDN docs on dispatchEvent

Any DOM event can be programmatically triggered where needed, by calling the dispatchEvent method on the target element:

In our file upload component example from above, we could write our own test helper to emulate the feature functionality. Using the dispatchEvent method in combination with the change event in our test helper util already does the trick:

That’s a wrap!

Whether you’re using a framework or plain old JavaScript to build out your web apps and components, testing event-driven behavior has never been easier. By using testing libraries or comprehensive Web APIs, emulating events in unit and integration tests is a breeze.

Have thoughts, questions or recommendations on how you can test events in JavaScript? Let us know in the Meroxa community or on Twitter at @meroxadata!

References

Jesse Jordan

Jesse Jordan

Staff Software Engineer-Frontend