testing library: async fireevents

2021-01-06

 | 

~3 min read

 | 

575 words

When testing Javascript code in the DOM, there are few libraries I like as much as Testing Library. It’s really just a superb experience, though there are some limitations.1

To handle asynchronous code, the testing library has wait (which is deprecated) and waitFor.

In our example below, we are testing the effect of clicking on our <Counter/>, a button that shows the number of times it’s been clicked. This is a special counter. It’s count is not managed in state, but on the server. Each click sends a request to the server which we must await before asserting against the UI.2

In such a situation, it’s common to see code like this in a test as a result:

import * as Preact from "preact"
import { getQueriesForElement, fireEvent, waitFor } from "@testing-library/dom"

test("renders a counter", async () => {
  const { getByText } = render(<Counter />)
  const counter = getByText("0")
  //highlight-start
  fireEvent.click(counter)
  await waitFor(() => expect(counter).toHaveTextContent("1"))

  fireEvent.click(counter)
  await waitFor(() => expect(counter).toHaveTextContent("2"))
  //highlight-end
})

Ét voila! We have successfully waited for the call to resolve and seen that the UI has updated. Unfortunately, this is rather cumbersome, particularly if we have to do it all over the place. Kent C. Dodds offered a clever solution in his Testing Javascript course: fireEventAsync.

The API is the exact same as fireEvent, but it will wrap each call in a Promise. That means we can eliminate our waitFor calls and write much more streamlined code:

test("renders a counter", async () => {
  const { getByText } = render(<Counter />)
  const counter = getByText("0")

  await fireEventAsync.click(counter)
  expect(counter).toHaveTextContent("1")

  await fireEventAsync.click(counter)
  expect(counter).toHaveTextContent("2")
})

How does this work? Let’s look at the code (which I updated to use waitFor instead of wait):

const { fireEvent, waitFor } = require("@testing-library/dom")

const fireEventAsync = {}

Object.entries(fireEvent).reduce((obj, [key, val]) => {
  obj[key] = async (...args) => {
    return await waitFor(() => val(...args))
  }
  return obj
}, fireEventAsync)

export { fireEventAsync }

fireEvent is a collection of all the different events with mock functions that are useful for testing purposes. This works by taking each one of those and redefining it as an asynchronous function that takes any number of arguments and returns an awaited invocation wrapped in waitFor.

Thus, if you were to inspect the fireEventAsync method, you’d see nearly the exact same thing as in the fireEvent. All of the events and their associated functions would be present. In the case of fireEventAsync, however, they’d all return Promises.

It’s a really clever solution which makes perfect sense once I took the time to step through what was happening. The really great thing, of course, is that there’s nothing unique about fireEvent. This same pattern could be used for any collection of methods that need to be converted into asynchronous variants.

Thanks Kent!

Footnotes

  • 1 For example, the fireEvent API focuses exclusively on the actual event and doesn’t fire some of the other events that would occur in the wild based on user interaction. I wrote more about the issue and how to solve it in Testing Library: User Events..
  • 2 Interesting note for Preact users. Unlike React, Preact does not render synchronously, waiting for the next tick in the event loop instead. Consequently, a test that observes UI changes as a result of any effect must be handled asynchronously with Preact. This can be done in the same way: wrapping each assertion with waitFor.


Hi there and thanks for reading! My name's Stephen. I live in Chicago with my wife, Kate, and dog, Finn. Want more? See about and get in touch!