Testing events attached to HTML Document
Tags: javascript, testing, react
Published: 2022-08-21 11:22:14

## Component

Recently I was working on tests for one of our React components, which implements dropdown functionality. With appropriate prop enabled, it allows dropdown items to be collapsed on "outside" (anywhere on the page) click. Code for this part was similar to

```js
useEffect(() => {
  const onOutsideClick = () => setIsOpen(false)

  document.addEventListener("click", onOutsideClick)

  return () => {
    document.removeEventListener("click", onOutsideClick)
  }
}, [])
```

As you can see, event listener is attached directly to `document` object, which is a reference to [HTML Document](https://www.w3schools.com/jsref/dom_obj_document.asp). _It is not a DOM node, which is important._ Overall, addition of event listener to `document` is a wide used practice when you want to ensure your callback be fired on any place on the page.

## Test setup 
We are using React Testing Library to test our components. It encourages to work with components/features as user would've work with them. As a consequence, it's better to avoid doing things which aren't natural to your app user flow.
~~Unfortunately while dealing with `document` it's necessary to break this rule.~~ Main (and only) RTL's method of rendering test markup is `render` which build whole tree starting from dedicated DOM node, `document.body` by default. 

```js

render(<AppComponent />)
// Which equals to
render(<AppComponent/>, { baseElement: document.body })

// Code below won't work because document isn't a node
render(<AppComponent/>, { baseElement: document })

```
You can alter it by adjusting `baseElement` option of `render` method, but still it has to be DOM node. It's not possible to build a tree on top of `document` object itself.
This means opened dropdown won't be closed on click, because `document` object isn't simulated by testing library and no events will be issued.

## Solution
First idea that I was thinking of was to hide dropdown by clicking on its anchor (our implementation allows to do so).
```js
    // Initially options are not mounted in the tree
    expect(getByText('Option three').not.toBeInTheDocument()
    // This is dropdown anchor, click on it shows options to choose from
    fireEvent.click(getByText('Selected: Option one'))
    // Third option is now visible (and mounted)
    expect(getByText('Option three').toBeVisible()
    // "Technical" click, which won't be conducted by user under normal circumstances
    fireEvent.click(getByText('Selected: Option one'))
    // Third option is now unmounted again
    expect(getByText('Option three').not.toBeInTheDocument()

```
Despite being a working solution, this adds some unnatural flow to test and can potentially influence dropdown state in unpredictable manner.

Most preferable workaround (from those left on the table) is to simulate `click` event on the document object. This will cause callbacks to be triggered and won't require any additional user-related actions.
```js
    // Initially options are not mounted in the tree
    expect(getByText('Option three').not.toBeInTheDocument()
    // This is dropdown anchor, click on it shows options to choose from
    fireEvent.click(getByText('Selected: Option one'))
    // Third option is now visible (and mounted)
    expect(getByText('Option three').toBeVisible()
    act(() => {
      // Direct call on document API
      document.dispatchEvent(new Event('click'))
    })
    // Third option is now unmounted again
    expect(getByText('Option three').not.toBeInTheDocument()

```
---

I hope this small investigation will help you in your daily work with RTL :)