How do I trigger the window load event in this Jest test? - javascript

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.

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.

Stackblitz: onClick doesn't find my function

I have as basic onClick in a button that outputs an alert
function test(){
alert('here');
}
<button onClick="test()">Press</button>
In StackBlitz this doesn't work - https://stackblitz.com/edit/js-umarwe?embed=1&file=index.js&hideNavigation=1
Is this because of ES6
How does this work in ES6
Why it doesn't work
The reasons it is not working is that behind Stackblitz there is an asset building system which treats your Javascript code as modules.
That means that variables defined in those modules are only available inside those modules and do not become attached to the global namespace (as you expected and seem to be used to).
Minimum required to fix it
To achieve that, you need to explicitly attach those variables to the global object, which inside a browser happens to be window.
Simply adding the following line at the end of your index.js file makes your code work:
window.test = test;
A better way
Please note that using inline event handlers directly on the element (like onclick) is considered bad practice (and does have practical disadvantages, but that would lead too far). Instead, you should use Javascripts Element.prototype.addEventlistener() function. Steps to get there:
Add an id to your button so your Javascript can find it:
<button id="testButton">Press</button>
Next, put that element in a variable:
const button = document.getElementById('testButton');
Last step: Add the event listener for the click event:
button.addEventListener('click', test)
Here's the full index.js for that refactored version:
// Import stylesheets
import './style.css';
function test(){
alert('here');
}
const button = document.getElementById('testButton');
button.addEventListener('click', test);
How to make it even safer and better
One more note: If you place the script tag loading the Javascript in the head section of the document, you either need to add a defer attribute on the tag, or wrap the part of the code that needs to access the DOM in a DOMContentLoaded event handler:
// Import stylesheets
import './style.css';
function test(){
alert('here');
}
document.addEventListener('DOMContentLoaded', function () {
const button = document.getElementById('testButton');
button.addEventListener('click', test);
}
Otherwise the HTML has not yet been parsed by the browser when your Javascript tries to find the button and attach the event listener.
Modify test function declaration in index.js file:
window.test = function(){
alert('here');
}
It doesn't recognize test function on click event because test function you declared is not in the same scope.
You can do this.
window.test = () =>{
alert('here');
}
I don't know stackBlitz..
but, I understand loading index.js
so window.test = () => {} ...

"ReferenceError: Can't find variable: Mustache" in Unit Tests?

im running a rails app which im running Unit tests on the Javascript side (Using Teaspoon/Jasmine).
the funny thing is, on the function I call I KNOW Mustache.render function is working (Im able to console.log it's return value (which is the Mustache.render function) and see that it is working. However when I call that function from my unit tests im getting a:
Failure/Error: ReferenceError: Can't find variable: Mustache.
For reference I don't actually call the Mustache render function directly im simply calling the function that uses it and grabbing it's return value to check again.
I've been able to successfully grab and use various other functions and use them just fine, this one is just giving me trouble. Can the Mustache.render object not exist outside it's own file or scope or something?
Edit: Example code:
_makeSomething: function viewMakeSomething(data) {
const template = templates.something;
return Mustache.render(document.querySelector(template).innerHTML, data);
}
and my test code is simply:
it('_makeSomething object', function() {
let obj = {id: 1234, content: "_makeSomething Assertion", author: "Test User"}
let $something = _makeSomething(obj);
});
(Right now im just capturing it before I assert anything or split it up/etc...., but it's just calling it at all)
Problem is that you teaspoon doesn't have access to your dev/production assets pipelene. You should specify what JS to load for your tests. This is necessary to prevent loading all files from manifest to test some feature. Because this is unit testing.
From example:
//= require mustache
describe("My great feature", function() {
it("will change the world", function() {
expect(true).toBe(true);
expect(Mustache).toBeDefined();
});
});

Understanding jQuery & Pub Sub Pattern with this example

I use jQuery for some time, but that is usually very simple jQuery. I just watched some video tutorial in which the author uses something called Pub Sub Pattern. I've never heard of it before, so I have searched on Stackoverflow and Google for explanations:
Why would one use the Publish/Subscribe pattern (in JS/jQuery)?
But it's still not clear to me, especially because of the code that is used by the author of the above mentioned tutorial. So, I will paste this code here and if you can give me explanations:
1. Here is the first .js file named pubsub.js, and I don't understand it:
(function($) {
var o = $({}); // ??? what is this ???
$.subscribe = function() { // ??? and this ???
o.on.apply(o, arguments); // ??? o.on.apply(o, arguments) ???
};
$.unsubscribe = function() { // ??? and this ???
o.off.apply(o, arguments); // ??
};
$.publish = function() { // ??? and this ???
o.trigger.apply(o, arguments); // ?? o.trigger.apply(o, arguments); ??
};
}(jQuery));
I know that with jQuery you can use $( document ).ready() or $(function() but I've never seen (function($) { ... }(jQuery)); - what does this mean/do? Also, I don't understand the rest of the code...
2. The next file is app.js and it contains:
(function() {
$.subscribe('form.submitted', function() {
$('.flash').fadeIn(500).delay(1000).fadeOut(500);
})
});
What does this actually do? Again, what (function() { ... }); means/do? And as for the rest of code, can you explain to me $.subscribe('form.submitted', function() {?
3. Finally, we have something like this:
$.publish('form.submitted', form); // publish?
This also is not clear to me.
I understand that all this is a basic implementation of PubSub Pattern with jQuery, but I still don't get why would someone do in this way (by using this pattern), I have read that answer on Stackoverflow, but it's still unclear to me... I guess that if I understand this code, then it would become clearer to me why and when to use this pattern.
In the case of (function($) { ... }(jQuery));, the author is passing the jQuery instance in as a parameter. Inside the function (which has it's own scope), the $ is a reference to the jQuery instance that was passed in.
"Pub Sub" is just another term for Event Management, or Event Handling. All you're saying is "When [this] happens, do [that]".
When you "subscribe", you are passing in 2 parameters, the "event" that you are listening for, and the code you want to run when the event "fires".
When you "publish", you are "firing" (or triggering) that event.
Think of it like the onclick event. When you set something up on the onclick event, you are subscribing to that event. When you click, you are publishing that event.

How do I get Jasmine to always show the spec list?

If my Jasmine test has failures, it only shows those by default. I have to click "Spec List" to see all of the tests that were run.
Can I somehow get it to always show the spec list by default?
I am using jasmine 2.1.3 with require.js as outlined in this stackoverflow question:
Getting requirejs to work with Jasmine
and this was bugging me too.
I am also using jquery so I added an event trigger after the .execute() like so:
require(specs, function (spec) {
jasmineEnv.execute();
$('.spec-list-menu').click();
});
I couldn't find any configuration for setting the default, but you can see in the jasmine-html.js file:
find('.failures-menu').onclick = function() {
setMenuModeTo('failure-list');
};
find('.spec-list-menu').onclick = function() {
setMenuModeTo('spec-list');
};
setMenuModeTo('failure-list');
if you changed it to:
find('.failures-menu').onclick = function() {
setMenuModeTo('failure-list');
};
find('.spec-list-menu').onclick = function() {
setMenuModeTo('spec-list');
};
setMenuModeTo('spec-list');
It will also set the default.
I don't really like editing libraries like that since I usually forget what I have changed when I update the library.
That was the reason I went with the jquery route.

Categories