Jsdom: How do you load external scripts?

Created on 7 Jul 2017  ·  3Comments  ·  Source: jsdom/jsdom

jsdom can run scripts which are inline inside of the document's <head> tag, but how do you make it run scripts which are linked to in the document head via <script src="">?

For options I'm using:

{
    url: url,
    resources: 'usable',
    runScripts: 'dangerously',
}

Basic info:

  • Node.js version: 8.1.2
  • jsdom version: 11.1.0

Minimal reproduction case:

// index.js
const request = require('request')
const jsdom = require('jsdom')
const {JSDOM} = jsdom
const url = 'http://localhost:8000'

request(url, (error, response, body) => {
  const options = {
    url: url,
    resources: 'usable',
    runScripts: 'dangerously',
  }

  const dom = new JSDOM(body, options)

  console.log(dom.window.document.body.children.length) // Expecting to see `1`
  console.log(dom.window.document.body.innerHTML) // Expecting to see `<h1>Hello world</h1>`
})
// external.js
document.addEventListener('DOMContentLoaded', () => {
  document.write('<h1>Hello world!</h1>')
})
<!-- index.html -->
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>JSDOM test</title>
    <script src="external.js"></script>
  </head>
  <body>
  </body>
</html>

Complete minimum working example: https://github.com/cg433n/jsdom-test

Most helpful comment

The issue here is that DOMContentLoaded is an asynchronous event. You're logging before it has a chance to happen. You need to wait for it to fire; only then will your document.write() happen.

Something like this should probably work:

const dom = new JSDOM(body, options)

dom.window.document.addEventListener('DOMContentLoaded', () => {
  // We need to delay one extra turn because we are the first DOMContentLoaded listener,
  // but we want to execute this code only after the second DOMContentLoaded listener
  // (added by external.js) fires.
  setImmediate(() => {
    console.log(dom.window.document.body.children.length) // Expecting to see `1`
    console.log(dom.window.document.body.innerHTML) // Expecting to see `<h1>Hello world</h1>`
  });
});

All 3 comments

The issue here is that DOMContentLoaded is an asynchronous event. You're logging before it has a chance to happen. You need to wait for it to fire; only then will your document.write() happen.

Something like this should probably work:

const dom = new JSDOM(body, options)

dom.window.document.addEventListener('DOMContentLoaded', () => {
  // We need to delay one extra turn because we are the first DOMContentLoaded listener,
  // but we want to execute this code only after the second DOMContentLoaded listener
  // (added by external.js) fires.
  setImmediate(() => {
    console.log(dom.window.document.body.children.length) // Expecting to see `1`
    console.log(dom.window.document.body.innerHTML) // Expecting to see `<h1>Hello world</h1>`
  });
});

I see! You guys are fantastic - keep up the good work!

Notice: 2 features of the demo above are risky.

FS hack

Alternatively, I recommand using fs to read and load the js codes from files, see Stackoverflow based on @EricRicketts' FS.read() approach in #3023 and @Domenic's addEventListener() in #1914. The working demo I propose includes:

  • use fs.readFileSync to loads external js into html as strings
  • loop to handle several external js files
  • use case of firing external js' functions
  • use case of passing terminal variables to node, then to an external js functions

This is likely to prevent you from doing xhr / API calls and therefore only partially solve the issue.

URLS

I couldn't figure out the way to get url work.
Ideally, an updated minimal JSDOM loading external.js files via url working demo would be welcome. :heart:

Was this page helpful?
0 / 5 - 0 ratings