Troubleshooting DOM Leakage Between Tests with React Testing Library

Dana Scheider (he/they)
4 min readMar 13, 2023

In the ancient times, in June of 2020, GitHub user ljosberinn opened an issue against react-testing-library, then on version 10.3.0. They were having an issue where DOM state from one test in a single test file was persisting into the next test case in that file, despite use of Testing Library’s cleanup function. I, too was having the same problem, as were a number of other people, and despite much conversation on the issue, the problem did not end up getting resolved. Until now.

The Original Issue

Original Stack

User ljosberinn reported using the following tech stack:

  • @testing-library/react version 10.3.0
  • Jest 26.0.1
  • JSDOM 16.2.2

The Problem

ljosberinn reported that, when they had multiple tests in a single file, DOM changes from one test would still be present in the tests after it, resulting in failures when elements that should not be present were, or vice versa.

I joined this conversation a little later when I had a similar issue, thinking my issue had something to do with Mock Service Worker. (Spoiler alert: MSW was innocent.) I added my comment and implemented the only known solution at the time: breaking the leaky tests up into multiple files. Needless to say, this was not optimal for someone with a lot of tests.

Some time later, I began a rewrite of my application and changed up my tech stack, most notably switching from Jest to Vitest. I forgot about this problem and thought it would no longer be an issue until it reared its ugly head again.

The New Solution

Almost two years after the original issue was open, after much pain and toil, I stumbled across a working solution to this problem that has eliminated the need to split tests up across multiple files to maintain clean DOM state. By that time, my tech stack had diverged considerably from ljosberinn’s, but I think the solution will work, or at least can be modified to work, for slightly different tool sets.

My Tech Stack

  • @testing-library/react 14.0.0
  • Vitest (not Jest) 0.29.2
  • JSDOM 21.1.1
  • TypeScript 4.9.5

My Solution

It turned out that the solution was to create an isolated DOM for each test. This was easier than it sounds.

When you set up Vitest (or Jest) with Testing Library, you usually have some setup file that is used, often called setupTests.ts or something similar. I modified this file to export a render function similar to Testing Library’s that used a new instance of JSDOM each time. My setupTests.ts file looked like this:

import { type ReactElement } from 'react' // Only required if using TypeScript
import { JSDOM } from 'jsdom'
import { render as originalRender } from '@testing-library/react'

// Global type declarations, can be skipped if not using TypeScript
declare global {
namespace NodeJS {
interface Global {
document: Document
window: Window
}
}
}

// Function to create a new DOM with JSDOM and use it as the source
// of global `window` and `document` objects
const setDom = () => {
// Note that you can set the empty object to any options you would like
// for your JSDOM instance. You could also make it configurable by allowing
// the options to be passed in as an argument to the `setDom` function.
const dom = new JSDOM('<!doctype html><html><body></body></html>', {})

// The `as` and anything after it can be omitted if you aren't using
// TypeScript.
global.window = dom.window as unknown as Window & typeof globalThis
global.document = dom.window.document
}

// If not using TypeScript, you can just use `(ui) => {`. If you'd like to
// include render options to pass through, you could give this function a
// second optional 'options' argument.
export const render = (ui: ReactElement) => {
setDom()

return originalRender(ui)
}

// ... whatever else you need in this file ...

Now, in my test files, I can render components using this render function instead of the one from Testing Library, and it will ensure my DOM is clean from previous tests.

import { describe, test, expect } from 'vitest'
import { act } from '@testing-library/react'
import { render } from '../../setupTests' // or whatever the relative path is
import MyComponent from './myComponent.tsx'

describe('MyComponent', () => {
test('this test adds something to the DOM', () => {
const wrapper = render(<MyComponent />)

const linkToAddElement = wrapper.getByText('Click here')

act(() => linkToAddElement.click())

expect(wrapper.getByText('new element')).toBeTruthy()
})

test('this test needs that something NOT in the DOM', () => {
const wrapper = render(<MyComponent />)

expect(wrapper.queryByText('new element')).toBeFalsy()
})
})

Note that, if we were using the render function from Testing Library, the element with text 'new element' would still be present in the DOM when the second test ran, causing it to fail. This would be true even if we used Testing Library’s cleanup function in an afterEach hook. However, using our own render function, the DOM will be clean and both tests will pass.

Another thing to notice is that we are calling getByText and queryByText on the wrapper object — we are not using the screen object provided by Testing Library. Scoping our expectations to wrapper is what ensures that we’re only checking what’s inside that wrapper to determine what’s present or not present in the DOM.

I added a note on this to the comments on the original issue as well. I hope this helps someone else who’s had trouble isolating their tests!

--

--

Dana Scheider (he/they)

Senior Engineer at CashApp, formerly at Envato & New Relic