nodejs + jsdom, jQuery strange behavior - javascript

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

Related

JSDOM script element being overwritten in Mocha

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.

Cannot instantiate a class when script is injected into document dynamically

I am experimenting with Classes available in javascript and have setup a simple test case as follows:
<script src="myclass.js></script>
<script>
var test = new MyClass();
</script>
myclass.js contains the following:
class MyClass {
constructor() {
}
}
This works as expected.
However, if I dynamically load "myclass.js" using jQuery.getScript() function the browser returns the following error:
Uncaught ReferenceError: MyClass is not defined
Things I have double checked are:
The code to instantiate the class is placed within the success callback of the getScript function
And also that the script is actually being loaded and executed (with a simple console log)
However I seem to have a brick wall with this. Is there any reason why a class cannot be instantiated from if the file containing the class is loaded from a javascript file using the jQuery.getScript function?
This is the code which does not work:
<script>
$(document).ready(function() {
$.getScript('myclass.js', function () {
var test = new MyClass();
});
});
</script>
Testing on Chrome Version 71.0.3578.98 (64-bit)
Have a look at this question and its answers as well as the documentation such says the success callback is run after loading but not necessarily after executing the script.
To sum up, it might suffice to run your code by appending a then (or done) handler:
$.getScript(url).then(function() {
var test = new MyClass();
});
If this is not enough you should fall back to use a setInterval-triggered check for the existence of the class (stop the interval after finding the class).
This way you are avoiding any dependency on the specific browser behavior when the script gets executed after loading it.
function afterScriptExecuted() {
var test = new MyClass();
}
function testScriptExecuted() {
return window.MyClass !== undefined;
}
$.getScript(url).then(function() {
var id = setInterval(function() {
if (testScriptExecuted()) {
clearInterval(id);
afterScriptExecuted();
}
}, 50);
});
In the end, only the following approach worked for me (rather than using jQuery.getScript)
var script = document.createElement('script');
script.onload = function () {
var test = new MyClass();
};
script.src = '/myclass.js';
document.head.appendChild(script);

Additional url attribute at nightwatch page object

I was trying to add an additional url attribute as a function to my page-object while using nightwatchjs.
Like:
module.exports = {
url: function() {
return this.api.launchUrl + '/content/site1.xhtml';
},
cancelUrl: function() {
return this.api.launchUrl + '/content/cancel_site1.xhtml';
}
}
Anyhow nightwatch is not able to get that 2nd attribute cancelUrl, ie undefined.
Why is that so? Shouldn't nightwatch be able to access that attribute as it is nothing more than a function call returning a string or am I misunderstanding a javascript or special page-object concept?
--
I am aware that there should be a page-object for each site so there should not be a 2nd site. Anyhow I would like to understand why this is not working technically.
Not sure I can answer the "why" (other than to say that when nightwatch loads up your page objects as globally available it must be wrapping your js file and filtering on 'known' functions) but I can offer a solution: add a command to your page object with the desired function. For example:
let pageCommands = {
cancelUrl: function() {
return this.api.launchUrl + '/content/cancel_site1.xhtml';
}
};
module.exports = {
commands: [pageCommands],
...
}
It's not the typical use of page commands, but your test would then be able to access the cancelUrl function on the page object instance.
More on page commands here

How is this JavaScript object created for this bookmarklet?

I am trying to reverse engineer a bookmarklet that uses CasperJS.
It creates an object called __utils__ that I can execute console commands with.
The link to the bookmarklet is here:-
http://casperjs.org/api.html#bookmarklet
Which references this JavaScript file:-
https://raw.github.com/n1k0/casperjs/master/modules/clientutils.js
I have searched through the whole source code and I cannot find a reference to how this object is created.
Any pointers would be appreciated.
The bookmarklet simply runs a small snippet of JavaScript code that appends a link to the clientutils.js to the document's end. After that, it will run an anonymous function every 50 milliseconds that checks if the script has loaded (and has made the ClientUtils function available), and if it has, it stops running the function and creates window.__utils__, thus making it available in the console. Here's the actual bookmarklet code in a more readable format. It should be pretty straightforward to understand:
(function () {
void(function () {
if (!document.getElementById('CasperUtils')) {
var CasperUtils = document.createElement('script');
CasperUtils.id = 'CasperUtils';
CasperUtils.src = 'https://raw.github.com/n1k0/casperjs/master/modules/clientutils.js';
document.documentElement.appendChild(CasperUtils);
var interval = setInterval(function () {
if (typeof ClientUtils === 'function') {
window.__utils__ = new window.ClientUtils();
clearInterval(interval);
}
}, 50);
}
}());
})();
Look at the source of api.html. After Just drag this link look at the JS in the href attribute. Near the end it contains:
window.__utils__=new%20window.clientUtils();

Javascript: console logging

I use console.log in my JS files to trace the application.
The problem: logs are in production environment.
How can I remove lines like console.log from code?
P.S. Please do not advice text solutions like find + xargs + grep -v.
For my significant projects, I have my own logging function that internally uses console.log(), but there are no console.log() calls in my code except for the one place in this function. I can then enable or disable logging by changing one variable.
My function is actually a little more involved than this with options to put the output into places other than just the console, but conceptually, it looks like this:
// change this variable to false to globally turn off all logging
var myLoggingEnabled = true;
function myLog() {
if (myLoggingEnabled) {
if (window.console && console.log) {
console.log.apply(this, arguments);
}
}
}
You can then use code like this to log:
myLog(foo);
FYI, for deployed code compactness and performance optimization, I also have a minimization step that removes all calls to myLog() from my code. This is an optimization that I've chosen to take advantage of. Perhaps you could share why you wouldn't also consider this type of optimization.
Well, you can disable them with
console.log=function(){}
But the lines will be there unsless you delete them manually.
If you use Grunt you can add a task so as to remove/comment the console.log statements.
Therefore the console.log are no longer called.
https://www.npmjs.org/package/grunt-remove-logging-calls
Yeah, I had a similar situation, I posted about it here. http://bhavinsurela.com/naive-way-of-overriding-console-log/
This is the gist of the code.
var domainNames =["fiddle.jshell.net"]; // we replace this by our production domain.
var logger = {
force:false,
original:null,
log:function(obj)
{
var hostName = window.location.hostname;
if(domainNames.indexOf(hostName) > -1)
{
if(window.myLogger.force === true)
{
window.myLogger.original.apply(this,arguments);
}
}else {
window.myLogger.original.apply(this,arguments);
}
},
forceLogging:function(force){
window.myLogger.force = force;
},
original:function(){
return window.myLogger.original;
},
init:function(){
window.myLogger.original = console.log;
console.log = window.myLogger.log;
}
}
window.myLogger = logger;
console.log("this should print like normal");
window.myLogger.init();
console.log("this should not print");
window.myLogger.forceLogging(true);
console.log("this should print now");

Categories