skip to Main Content

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 imported 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


  1. Everything is in your code already:

      // 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.
    

    This means that if there is no window with a document 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:

    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;
      const $ = dynamicImport.default(dom.window);
      return {
        document: document,
        jquery: $(`html`),
      };
    }
    

    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.

    Login or Signup to reply.
  2. The note clearly reads:

    // 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);
    

    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:

    const { JSDOM } = require("jsdom");
    const jqFactory = require("jquery");
    
    async function parseHtml(fileName) {
        const dom = await JSDOM.fromFile(fileName, {
            url: 'http://localhost'
        });
        const jQuery = jqFactory(dom.window);
        return {
            jQuery: jQuery,
            window: dom.window
        };
    };
    
    (async function() {
        const doc1 = await parseHtml('template1.html');
        const doc2 = await parseHtml('template2.html');
    
        // works
        console.log(doc1.jQuery('p').text());
        console.log(doc2.jQuery('p').text());
    
        // works
        console.log(doc1.window.$('p').text());
        console.log(doc2.window.$('p').text());
    
        // works
        console.log(doc1.window.$.fn.jquery);
        console.log(doc2.window.$.fn.jquery);
    })();
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search