-
-
Notifications
You must be signed in to change notification settings - Fork 6.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Clearing the jsdom environment #1224
Comments
Cleanup doesn't really do much special stuff: https://github.com/rstacruz/jsdom-global/blob/master/index.js#L49 I suggest if you need this, to either:
Does this work for you? At this point I'm quite hesitant to adding new APIs to support jsdom better. |
Wfm |
It seems to me that it'd be best for jest-environment-jsdom to create a new instance of JSDOM for each test. This way less state is maintained between tests. Is there any reason this isn't done currently? |
We do create new instances of jsdom, however we do not load jsdom from scratch which would increase runtime for each test by more than 100ms. |
That's a shame that instantiating jsdom is so expensive. I'll open an issue on the jsdom repo for discussion on either:
|
Just chiming in to say that this would be an absolutely amazing feature to have for the already incredible library that is Jest. On the other hand, perhaps we should petition for a solution over in the jsdom repo? |
(Assuming perf issues could somehow be alleviated, of course.) |
For what it's worth, last time we needed that we used: beforeEach(() => {
document.body.innerHTML = '';
}); |
The issue with doing this is that you have to know which state is modified during each test, which leaves a lot of room for human error. |
The problem with simply clearing the "physical" DOM (i.e. innerHTML) is also that the shared state for virtual objects (window.*) still remains. This can result in very subtle errors (e.g. a single test running fine on its own but failing when running in a suite). This seems to be a common problem when using I'd really vote for a more convenient way in jest-environment-jsdom that allows to reset the entire JSDOM on demand (e.g. something like |
A configuration option for jest-environment-jsdom would work. |
It occurs to me that there's a point at which integration tests are close enough to end-to-end tests that they should just be made into E2E. However, this doesn't apply to the situations in which we're seeing this JSDOM issue on our team. Plus, there are often good reasons to do certain things as integration tests via jest instead of E2E. Just in case anyone was wondering, which they most assuredly were not. |
If you need a fresh environment for every single test, just put them in individual test files. The jsdom env is recreated for each test file. I don't think an API for this makes much sense, as it's quite edge casey |
I'm testing code based on I've managed to solve a fair amount by creating an iframe, using its contentWindow, then throwing that away. But I'm still using the global |
We're writing some tests that inject |
Without this feature, it seems impossible to reset properties like |
Slow and working is better than fast and broken. Jest should offer the ability to reset jsdom on each test and let users determine if the extra 1/10th of a second performance hit per reset is tolerable for them. Examples have been provided here that clearly demonstrate why this is necessary and how simple resets like Optimizing for performance is fine, but neglecting to offer a "slower but safer" path simply because it adds a minuscule amount of time per test is hostile to end users and feels weirdly (and unnecessarily) performance obsessed. |
For those interested, here is the soft-reset I'm using in
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>';
}); This has worked well for me so far. Not as well as a native Jest reset likely would, but... you know. 😉 |
Nice @jhildenbiddle. Also, it is easy to import For example, you can make a function like this: const { JSDOM } = require('jsdom');
function initJSDOM(markup, options = {}) {
const dom = new JSDOM(markup, options);
global.window = dom.window;
global.document = dom.window.document;
global.navigator = dom.window.navigator;
global.location = dom.window.location;
global.XMLHttpRequest = dom.window.XMLHttpRequest;
return dom;
} then use |
@jhildenbiddle overall your idea looks impressive. But I have a question. Is it OK to set the The obvious solution would be to assign to |
Good catch, @elias6. The following line: rootElm.innerHTML = '<html><head></head><body></body></html>'; Should be: rootElm.innerHTML = '<head></head><body></body>'; I've updated the code in the comment above. FYI, here's the explanation for why you were unable to set
Since |
@jhildenbiddle what if there are attributes on the [...document.documentElement.attributes].forEach(attr =>
document.documentElement.removeAttribute(attr.name)
); But I am wondering if that catches everything, or if there is a somehow better way to do what I am doing. |
@elias6 -- I'm doing the same thing in the const rootElm = document.documentElement;
// Remove attributes on root element
[...rootElm.attributes].forEach(attr => rootElm.removeAttribute(attr.name)); No reason to think it wouldn't remove all of the attributes since that's exactly what |
@jhildenbiddle I know that my idea removes all the attributes on the |
This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs. |
Currently between two it() calls, any DOM node will stay, so you have to manually clean it up.
Should we:
Also relevant, when I used mocha, I was using this very simple tool: https://github.com/rstacruz/jsdom-global
The text was updated successfully, but these errors were encountered: