I have two nearly identical JS files that I cannot change that I want to add tests for.
file 1:
const url = "https://file-1.js";
(function () {
"use strict";
window.onload = () => {
const script = document.createElement("script");
script.src = url;
document.head.appendChild(script);
};
})();
file 2:
const url = "https://file-2.js";
(function () {
"use strict";
window.onload = () => {
const script = document.createElement("script");
script.src = url;
document.head.appendChild(script);
};
})();
Then test 1:
const chai = require("chai");
const { expect } = chai;
const jsdom = require("jsdom");
const { JSDOM } = jsdom;
const { window } = new JSDOM(`<!DOCTYPE html><head></head><p>Fake document</p>`, {
resources: "usable",
});
global.document = window.document;
global.window = window;
const myFile = require("../src/myFile");
describe("Test 1", function () {
it("Loads a file from an external source", function (done) {
console.log(window.document.head.children); // See what's going on
expect(window.document.head.children[0].src).to.equal("https://file-1.js");
});
});
test 2:
const chai = require("chai");
const { expect } = chai;
const jsdom = require("jsdom");
const { JSDOM } = jsdom;
const myFile2 = require("../src/myFile2");
describe("Test 2", function () {
it("Loads a file from an external source", function (done) {
console.log(window.document.head.children); // See what's going on
expect(window.document.head.children[0].src).to.equal("https://file-2.js");
});
});
Test 2 passes but test 1 fails. The value of both console.logs is:
HTMLCollection { '0': HTMLScriptElement {} }
And console.log(window.document.head.children[0].src) produces:
https://file-2.js
I'd expect there to be two children in window.document.head but there's only 1, per the above. It appears Mocha is loading all the required files in all tests first, and the appendChild in the 2nd file is overwriting the value from the first.
Is there a way around this? I experimented with done() or moving around where the require is called but it results in the same outcome.
After reviewing the repo in the answer from Christian I realized I needed to fire the window.onload event after importing each file.
Also, I do not want to run ('dangerously') the scripts, just ensure that they are appended a document as a script element. That's all.
The following works:
const chai = require("chai");
const { expect } = chai;
const jsdom = require("jsdom");
const { JSDOM } = jsdom;
const { window } = new JSDOM(`<!DOCTYPE html><head></head><p>Fake document</p>`, {
resources: "usable",
});
global.document = window.document;
global.window = window;
const downloaderPopup = require("../src/MyFile");
window.dispatchEvent(new window.Event("load"));
const downloaderMain = require("../src/MyFile2");
window.dispatchEvent(new window.Event("load"));
describe("Both tests", function () {
describe("Test 1", function () {
it("Dynamocally loads file 1", function () {
expect(window.document.head.children[1].src).to.equal("https://file-1.js");
});
});
describe("Test 2", function () {
it("Dynamically loads file 2", function () {
expect(window.document.head.children[0].src).to.equal("https://file-2.js");
});
});
});
I created a repo for you to look at. A few notes:
We need to set the runScripts: "dangerously" flag for JSDOM if we want to load external scripts (see this issue).
We need to manually re-fire the load event ourselves - basically, by the time your script is executed, the status of document.readyState is "complete", i.e., the load event has already fired.
What's happening here is that window is ready as soon as JSDOM is done compiling the HTML script we pass it on initialization. We can import what we need and then fire the load event manually - as long as we do not pass any scripts to the initial JSDOM call, we can be sure that we will not be triggering anything twice.
When the load event fires, the generated <script> tags are actually added to the DOM, but since they contain dummy URLs with nothing to actually load, the process throws: Error: Could not load script: "https://file-1.js/". I changed those URLs to the jQuery library and Hammer.js for the sake of testing, and you will need to add logic to make sure that URL is safe.
Since both scripts set window.onload = function() {...}, if we run them both and then fire the load event (which we would normally do), only the last one will be triggered because each window.onload set overwrites the former.
We can get around this, but only because we know what the script contains. See the test files for the workaround: just require, fire the onload, and then use delete window.onload. I used dispatchEvent just to show the form for that, but since the overwrite issue isn't a problem for window.addEventListener (just for naively setting the window.onload property), it would probably be better to call window.onload() and then deleting it. It's hairy but it's not unmanageable.
I have actually been working on something close to this for the past few days, and have recently put up two packages to help with similar scenarios: enable-window-document (which exposes window and document globals) and enable-browser-mode (which aims to completely simulate the browser runtime, setting the global object to window and exposing a window.include function to evaluate an imported script in the global context, i.e. include('jquery.min.js'), with no errors).
For this situation (and the low complexity of the test scripts), enable-window-document will suffice. When running in full browser compatibility mode, we actually get failures because of the const url = ... declaration in both scripts - those are evaluated in the global context when full browser compatibility is enabled, which results in trying to re-set the window.url variable which is declared as const. Simply setting the window and document globals will work for this use case, but if you start to load complex scripts you may run into issues.
What I would recommend is to use enable-browser-mode if your scripts could run in the browser (i.e., no conflicting global const variables), and then replace any require calls to browser JS (test1.js and test2.js) with include(). This will make sure that window refers to the global object and your average wild browser JS will execute as expected.
After loading all the scripts and hacking around the onload conflicts, we run the tests:
// inside index.js
...
console.log(document.head.outerHTML);
console.log("jQuery:", window.$);
$ node .
RUNNING TESTS...
<head><script src="https://code.jquery.com/jquery-3.5.1.min.js"></script><script src="https://hammerjs.github.io/dist/hammer.min.js"></script></head>
jQuery: undefined
And weirdly we can't access window.jQuery at runtime. Yet, in the console:
$ node
Welcome to Node.js v14.4.0.
Type ".help" for more information.
> require('.')
RUNNING TESTS...
<head><script src="https://code.jquery.com/jquery-3.5.1.min.js"></script><script src="https://hammerjs.github.io/dist/hammer.min.js"></script></head>
jQuery: undefined
{}
> window.jQuery
<ref *1> [Function: S] {
fn: S {
jquery: '3.5.1',
constructor: [Circular *1],
length: 0,
toArray: [Function: toArray]
...
So I would recommend toying around to see what you can and cannot get to work.
Footnote: Jest is hot Facebook garbage and I'm not going to concern myself with debugging it (claims window global doesn't exist in myFile.js and so on). What we're doing here is pretty hacky and seems out of the suite's scope, or else conflicts with its native JSDOM interfacing somehow, though I might be missing something. If you want to spend time debugging it, be my guest: I left the project structure so that you can run jest and see what it's complaining about, but you'll need to uncomment out the describe statements etc.
Anyway, hope this helped.
Related
I'm trying to set up my first DOM-manipulation/JQuery Jest test in my Rails project. In its essential form at the moment, I'm just trying to clear a basic hurdle of 'imported Javascript functions do anything at all'.
To that end, I have the following code in PledgeFormUpdates.js.test:
'use strict';
import pledge_form_updates from '../../app/javascript/components/PledgeFormUpdates.js';
test('Displays GDPR checkbox on EU country selection', () => {
// Set up our document body
document.body.innerHTML =
'<select id="pledge_pledgor_home_country" class="country-select"></select>'// +
pledge_form_updates();
const $ = require('jquery');
$(window).trigger('load');
$("#pledge_pledgor_home_country").trigger('change');
});
And in PledgeFormUpdates.js I have the following:
export default function() {
console.log('hello world');
window.addEventListener("load", function() {
console.log('mellow curled');
$("#pledge_pledgor_home_country").change(function() {
console.log('yellow twirled')
});
});
};
So when I run the test, I'm expecting to see printout including 'hello world', 'mellow curled' and 'yellow twirled'. But in practice it's stopping after the first output, and so presumably the window load event isn't actually getting triggered (which I confirmed by commenting out the window.addEventListener... line, then seeing all three printouts).
What am I doing wrong here - or rather, how should I trigger the load event? (I also tried $(window).load();, but that just raises a TypeError)
I found a workaround for this, though it seems quite hacky. In the tested file, if I substitute the line
window.addEventListener("load", function() {,
with
$(window).on("load", function() {
then running the test (including the $(window).trigger('load'); statement) prints all of the log lines.
I must be missing something, because I can't believe that Jest would require JQuery to trigger a page load event, but for now at least this works.
I am dynamically adding a <script> tag to the document <head> on page load based on the environment.
The Function:
export const loadScript = () => {
// load script tag into head
const HEAD = document.getElementsByTagName('head')
const SCRIPT_TAG = document.createElement('script')
SCRIPT_TAG.setAttribute('src', process.env.SCRIPT_SRC)
SCRIPT_TAG.setAttribute('async', true)
HEAD[0].append(SCRIPT_TAG)
}
I want to write a test that checks if once the loadScript() function is run that the <script> tag made it into the head. Our environment is set up with Jest, and I haven't found a satisfactory example that demonstrates how to do it, or works.
I am new to testing, and would appreciate any solutions, or hints offered.
I suppose the easiest way to test it would be something like this:
test('loadScript', () => {
process.env.SCRIPT_SRC = 'the-src';
loadScript();
expect(document.head.innerHTML).toBe('<script src="the-src" async="true"></script>');
});
This works because the default test environment for Jest is jsdom which simulates the document.
test('loadScript', () => {
loadScript();
const script = document.getElementsByTagName('script')[0];
// trigger the callback
script.onreadystatechange(); // or script.onLoad();
expect("something which you have on load").toBe('expected result on load');
});
I am building an Atom Electron app. Right now I have this in the preload.js of one of my webviews:
var { requireTaskPool } = require('electron-remote');
var work = '';
var _ = require('lodash');
work = requireTaskPool(require.resolve('./local/path/to/js/file.js'));
function scriptRun() {
console.log('Preload: Script Started');
// `work` will get executed concurrently in separate background processes
// and resolve with a promise
_.times(1, () => {
work(currentTab).then(result => {
console.log(`Script stopped. Total time running was ${result} ms`);
});
});
}
module.exports = scriptRun;
scriptRun();
It gets a local script and then executes it in a background process.
I want to do the same exact thing, except I want to retrieve the script from an external source like so
work = requireTaskPool(require.resolve('https://ex.com/path/to/js/file.js'));
When I do this, I get errors like:
Uncaught Error: Cannot find module 'https://ex.com/path/to/js/file.js'
How can I load external scripts? And then use the loaded scripts with my work function. My feeling is that require only works with local files. If AJAX is the answer, can I see an example of how to get a script, then pass it into my work without executing it prior?
I was able to load a remote js file and execute the function defined in it, hopefully it will provide you enough to start with...
my remote dummy.js, available online somewhere:
const dummy = () => console.log('dummy works');
my download.js:
const vm = require("vm");
const rp = require('request-promise');
module.exports = {
downloadModule: async () => {
try {
let body = await rp('http://somewhere.online/dummy.js');
let script = vm.createScript(body);
script.runInThisContext();
// this is the actual dummy method loaded from remote dummy.js
// now available in this context:
return dummy;
} catch (err) {
console.log('err', err);
}
return null;
}
};
You need to add the request-promise package.
Then in my main.js I use it like this:
const {downloadModule} = require('./download');
downloadModule().then((dummy) => {
if (dummy) dummy();
else console.log('no dummy');
});
When I run it, this is what I get:
$ electron .
dummy works
I wanted to create an actual module and require it, but I have not had the time to play with this further. If I accomplish that I will add it here.
You have not provided any details on your file.js. But I can give you the general idea.
There are two things that you need at minimum to call your package a module:
file.js (of course you have it) and
package.json
The structure of your file.js should be something like this:
//load your dependencies here
var something = require("something");
//module.exports is necessary to export your code,
//so that you can fetch this code in another file by using require.
module.exports = function() {
abc: function(){
//code for abc function
},
xyz: function(){
//code for xyz function
}
}
Now if you put your package on any website, you can access it as:
npm install https://ex.com/path/to/js/file.js
Now, a copy of your package will be put into node-modules folder.
So, now you can access it as:
var x = require('name-of-your-package-in-node-modules');
Now, you can also do:
var abc = require('name-of-your-package-in-node-modules').abc;
or
var xyz = require('name-of-your-package-in-node-modules').xyz;
When I try to unit test the getElement function
class BarFoo {
getElement() {
return document.querySelector('#barfoo');
}
}
mocha doesn't know anything about document, so I figured that you might do something like this:
beforeEach(() => {
global.document = {
querySelector: () => { ... }
}
}
Although this works, I'm wondering if this is the correct approach and maybe there is a package available to solve this issue, because my approach can get laborious if more browser API's are used ?
There are a few options available to you:
Option 1: Use JSDOM
By adding a DOM to your code, you can unit test much of your client-side code within node.js
Option 2: Use MOCHA on the client
Mocha does run inside the client and you can use separate client-side unit tests. This tends to be my preferred approach as I can test against specific browsers and not a specific JS implantation.
Option 3: Use PhantomJS
PhantomJS allows you to control a headless browser within your testing environment.
Option 4: Headless Chrome
Now that Headless Chrome is out, the PhantomJS maintainer has retired.
I have been writing tests similar to what you proposed when I just needed to mock a certain function on window:
it('html test', function () {
const windowRef = global.window;
global.window = {document: {querySelector: () => null}};
const lib = require('lib-that-uses-queryselector');
assert(true);
global.window = windowRef;
});
I have been using mock-browser in other tests when I wanted a more complete window object:
it('html test', function () {
const windowRef = global.window;
const MockBrowser = require('mock-browser').mocks.MockBrowser;
global.window = new MockBrowser().getWindow();
const lib = require('lib-that-uses-window');
assert(true);
global.window = windowRef;
});
Note that you probably want to restore the window object (global.window = windowRef; above) after messing with globals.
The code below is just a small snippet from my server.js file just to run the test provided by the jsdom documentation.
var window = jsdom.jsdom().createWindow();
jsdom.jQueryify(window, './jq.min.js' , function() {
console.log('inside');
window.$('body').append('<div class="testing">Hello World, It works</div>');
console.log(window.$('.testing').text());
console.log('end');
});
The output I get literally is just inside and then the server hangs and never returns. I've added a debug statement console.log(window); to see if the window object is truly being created, and I do end up with a fairly large output statement detailing the object's contents. One thing I did notice however is that the output does not show that $ is a defined method of the window object and in fact, console.log(window.$); renders undefined.
I understand jsdom is still in dev mode, but is there something I'm missing here?
Just as some background, I have tried several variations of the code, including using the jsdom.env() method and also building a document from existing HTML markup, neither of which rendered expected results either.
I hope this code snippet helps you:
createWindow = function(fn) {
var window = jsdom.jsdom().createWindow(),
script = window.document.createElement('script');
jsdom.jQueryify(window, function() {
script.src = 'file://' + __dirname + '/some.library.js';
script.onload = function() {
if (this.readyState === 'complete') {
fn(window);
}
}
});
}
createWindow(function(window) {
// Do your jQuery stuff:
window.$('body').hide();
});
from here