Question
JQuery is initialized on import
. If window
and window.document
exist (and module
doesn’t), JQuery saves the references and uses them thereafter.
Is there a way I can "reinitialize" or "reset" JQuery after it’s been import
ed to give it a different reference to window
and document
?
A failing testcase
Project Structure
.
├── .eslintrc.json
├── .prettierrc
├── index.spec.js
├── package.json
├── README.md
├── spec/
│ └── support/
│ ├── jasmine-spec.json
│ ├── logger.js
│ ├── slow-spec-reporter.js
│ └── type-check.js
├── template1.html
└── template2.html
./index.spec.js
import { JSDOM } from 'jsdom';
describe('jquery', () => {
it('uses the currently available document', async () => {
const { document: template1Document, jquery: template1Jquery } = await parseHtml('template1.html');
expect(template1Document.querySelector('p').textContent).toEqual('Hello world');
expect(template1Jquery.find('p').text()).toEqual('Hello world');
const { document: template2Document, jquery: template2Jquery } = await parseHtml('template2.html');
expect(template2Document.querySelector('p').textContent).toEqual('Goodbye world');
expect(template2Jquery.find('p').text()).toEqual('Goodbye world'); // !!!
// Expected 'Hello world' to equal 'Goodbye world'.
});
});
async function parseHtml(fileName) {
const dom = await JSDOM.fromFile(fileName, {
url: 'http://localhost',
runScripts: 'dangerously',
resources: 'usable',
pretendToBeVisual: true,
});
const window = dom.window;
const document = window.document;
globalThis.window = window;
globalThis.document = document;
const dynamicImport = await import('jquery');
const $ = dynamicImport.default;
return {
document: document,
jquery: $(`html`),
};
}
./template1.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>template1.html</title>
</head>
<body>
<p>Hello world</p>
</body>
</html>
./template2.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>template2.html</title>
</head>
<body>
<p>Goodbye world</p>
</body>
</html>
This is an excerpt of the code, but you can find a complete github repository here.
Context
I’m working on a frontend-only, single page app project. Even though the project runs exclusively in the browser, the automated tests run in Node.JS. The tests load the html in JSDOM and execute portions of the production code.1
On creation, JSDOM returns a DOM API that works in Node.JS, including a window
object. Without this, jQuery will error on import because modern versions have code like this:
(function(global, factory) {
"use strict";
if (typeof module === "object" && typeof module.exports === "object") {
// For CommonJS and CommonJS-like environments where a proper `window`
// is present, execute the factory and get jQuery.
// For environments that do not have a `window` with a `document`
// (such as Node.js), expose a factory as module.exports.
// This accentuates the need for the creation of a real `window`.
// e.g. var jQuery = require("jquery")(window);
// See ticket trac-14549 for more info.
module.exports = global.document ?
factory(global, true) :
function(w) {
if (!w.document) {
throw new Error("jQuery requires a window with a document");
}
return factory(w);
};
} else {
factory(global);
}
// Pass this if window is not defined yet
})(typeof window !== "undefined" ? window : this, function(window, noGlobal) {
console.log(`window`, window);
console.log(`noGlobal`, noGlobal);
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
As you can see, this executes as a side effect of importing jQuery. This has caused considerable headache because it isn’t always straightforward to create JSDOM before importing jQuery.
For example, if the production code imports jQuery and the test imports the production code, JSDOM (and therefore window
) won’t exist yet. This throws an error.
But here’s a more important use case: I want to be able to use different HTML files in my tests, but this is limiting me to one per test run.
Notes
For the record, I’m using jQuery 3.7.1, but stackoverflow code snippets don’t give me the option to pick that version. I think that’s fine because, as far as I can tell, this code is the same in both.
1: Unfortunately, that means going against the official JSDOM advice. But in this context, I can’t see a way around that.
2
Answers
Everything is in your code already:
This means that if there is no
window
with adocument
available on the global scope, the import will provide the factory it uses instead of executing it with the available window object and returning the instantiated jQuery.If you change your parseHTML code to leverage this:
If you do not make the window object available in the global scope, you’ll receive the jQuery factory. You’ll then be able to instantiate it the way you need, by passing the window object that you care about.
More details on why your solution was not working
When you import code/modules, the parsing and execution of the imported file is only done once (on the first import) and the resulting value is kept inside a cache so later imports do not need to work as much, but this causes your import value to always be the same.
The note clearly reads:
JSDOM gives you access to the
window
, pass the window to the jQuery factory to create a jQuery object attached to that window. Here is a minimal example: