Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a way to reinitialize jQuery?

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 part 1

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.

It's not enough to reinitialize one or some of the imported jquery's, all the jquery references need to be reinitialized. For example, if a production file starts with import jQuery from 'jquery', that jQuery needs to be a reference to the reinitialized jquery. This is hard to explain so I'll show what I mean with another failing testcase:

A failing testcase part 2

Project Structure

.
├── .eslintrc.json
├── .prettierrc
├── index.spec.js
├── package.json
├── prod-code.js
├── README.md
├── spec/
│   └── support/
│       ├── jasmine-spec.json
│       ├── logger.js
│       ├── slow-spec-reporter.js
│       └── type-check.js
├── template1.html
├── template2.html
└── test-setup.js

./index.spec.js

import * as testSetup from './test-setup.js';
import * as prodCode from './prod-code.js';

describe('jquery', () => {
  it('dynamic imports affect static imports template1', async () => {
    await testSetup.parseHtmlWithDynamicImport('template1.html');

    const { document: template1Document, jquery: template1Jquery } =
      await prodCode.parseHtmlWithJQueryStaticImport('template1.html'); // !!!
    // Error: jQuery requires a window with a document
    expect(template1Document.querySelector('p').textContent).toEqual(
      'Hello world'
    );
    expect(template1Jquery.find('p').text()).toEqual('Hello world');
  });

  it('dynamic imports affect static imports template2', async () => {
    await testSetup.parseHtmlWithDynamicImport('template2.html');

    const { document: template2Document, jquery: template2Jquery } =
      await prodCode.parseHtmlWithJQueryStaticImport('template2.html'); // !!!
    // Error: jQuery requires a window with a document
    expect(template2Document.querySelector('p').textContent).toEqual(
      'Goodbye world'
    );
    expect(template2Jquery.find('p').text()).toEqual('Goodbye world');
  });
});

./test-setup.js

import { JSDOM} from 'jsdom'

export async function parseHtmlWithDynamicImport(fileName) {
    const dom = await JSDOM.fromFile(fileName, {
      url: 'http://localhost',
      runScripts: 'dangerously',
      resources: 'usable',
      pretendToBeVisual: true,
    });

    const dynamicImport = await import('jquery');
    const $ = dynamicImport.default(dom.window);
    return {
      document: dom.window.document,
      jquery: $(`html`),
    };
  }

./prod-code.js

import { JSDOM } from 'jsdom';
import jQuery from 'jquery';

export async function parseHtmlWithJQueryStaticImport(fileName) {
  const dom = await JSDOM.fromFile(fileName, {
    url: 'http://localhost',
    runScripts: 'dangerously',
    resources: 'usable',
    pretendToBeVisual: true,
  });


  return {
    document: dom.window.document,
    jquery: 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.~~ And when it's later used, window won't be set.

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.

Someday, I'd like to release this test code as a test framework to the public. Some people may explicitly import jQuery from 'jquery' or import $ from 'jquery' or use a jQuery/$ in the global scope. Ideally, they wouldn't have to modify their production code to use my framework.


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.

like image 338
Daniel Kaplan Avatar asked Oct 18 '25 11:10

Daniel Kaplan


2 Answers

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.

like image 190
Salketer Avatar answered Oct 21 '25 10:10

Salketer


Read the note again:

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

Inside your html parsing function, you get reference to jQuery factory which you can use to create a jQuery Object.

That being said, you can use something along the following lines to initialize jQuery in both scenario described in OP:

  let jQuery_;
  if (typeof jQuery !== 'undefined') {
    // jQuery already imported statically using
    // import jQuery from 'jquery'
    jQuery_ = jQuery(dom.window);
  } else {
    // use dynamic import as described in
    // the comment inside jQuery code
    const dynamicImport = await import('jquery');
    jQuery_ = dynamicImport.default(dom.window);
  }
  // either way, we use jQuery factory to create jQuery
  // object bound to the document inside dom.window

  // PS: jQuery is different from jQuery('html')
  // you can do jQuery.ajax() and jQuery.browser
  // but you can't do jQuery("html").ajax() or jQuery.browser
  return {
    document: dom.window.document,
    jquery: jQuery_('html'),
  };
like image 42
Salman A Avatar answered Oct 21 '25 12:10

Salman A