This is still an issue for many people — and it's the top answer in Google — so I wanted to provide some context from the future ;)
does it keep a single instance of JSDOM across all test runs
Yes, the jsdom
instance remains the same across all test runs within the same file
If so, how can I control it?
Long story short: you'll need to manage DOM cleanup yourself.
There is a helpful Github issue on facebook/jest
that provides more context and solutions. Here's a summary:
- if you want a new
jsdom
instance then separate your tests into separate files. This is not ideal for obvious reasons...
- you can set
.innerHTML = ''
on the HTML element as mentioned in the accepted answer. That will resolve most issues but the window
object will remain the same. Window properties (like event listeners) can persist in subsequent tests and cause unexpected errors.
- cleanup the
jsdom
instance between tests. The jsdom
cleanup function doesn't do anything magic — it's basically resetting global properties. Here's an example directly from the Github issue:
const sideEffects = {
document: {
addEventListener: {
fn: document.addEventListener,
refs: [],
},
keys: Object.keys(document),
},
window: {
addEventListener: {
fn: window.addEventListener,
refs: [],
},
keys: Object.keys(window),
},
};
// Lifecycle Hooks
// -----------------------------------------------------------------------------
beforeAll(async () => {
// Spy addEventListener
['document', 'window'].forEach(obj => {
const fn = sideEffects[obj].addEventListener.fn;
const refs = sideEffects[obj].addEventListener.refs;
function addEventListenerSpy(type, listener, options) {
// Store listener reference so it can be removed during reset
refs.push({ type, listener, options });
// Call original window.addEventListener
fn(type, listener, options);
}
// Add to default key array to prevent removal during reset
sideEffects[obj].keys.push('addEventListener');
// Replace addEventListener with mock
global[obj].addEventListener = addEventListenerSpy;
});
});
// Reset JSDOM. This attempts to remove side effects from tests, however it does
// not reset all changes made to globals like the window and document
// objects. Tests requiring a full JSDOM reset should be stored in separate
// files, which is only way to do a complete JSDOM reset with Jest.
beforeEach(async () => {
const rootElm = document.documentElement;
// Remove attributes on root element
[...rootElm.attributes].forEach(attr => rootElm.removeAttribute(attr.name));
// Remove elements (faster than setting innerHTML)
while (rootElm.firstChild) {
rootElm.removeChild(rootElm.firstChild);
}
// Remove global listeners and keys
['document', 'window'].forEach(obj => {
const refs = sideEffects[obj].addEventListener.refs;
// Listeners
while (refs.length) {
const { type, listener, options } = refs.pop();
global[obj].removeEventListener(type, listener, options);
}
// Keys
Object.keys(global[obj])
.filter(key => !sideEffects[obj].keys.includes(key))
.forEach(key => {
delete global[obj][key];
});
});
// Restore base elements
rootElm.innerHTML = '<head></head><body></body>';
});
For those interested, this is the soft-reset I'm using in "jest.setup-tests.js" which does the following:
- Removes event listeners added to
document
and window
during tests
- Removes keys added to
document
and window
object during tests
- Remove attributes on
<html>
element
- Removes all DOM elements
- Resets
document.documentElement
HTML to <head></head><body></body>
— @jhildenbiddle