How to mock window/document with mocha/chai - javascript

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.

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.

Is it possible to have these functions exposed for testing?

Given that I'm extending an existing module and it uses the module.exports in the way shown below, can I even call the start methods from (mocha) tests?
I suspect that there's no decent way to tap into it - and that's ok. I'd just rather test these if I am able to and would love to know how to do it if possible.
const NodeHelper = require("node_helper");
module.exports = NodeHelper.create({
start: function() {
//do stuff
};
});
Edit: NodeHelper returns a function that appears to be "extended":
NodeHelper.create = function(moduleDefinition) {
return NodeHelper.extend(moduleDefinition);
};
Looking closer at the linked code, it uses Resig's class.js so you probably need to create an instance to call the prototype methods, ie
const YourNodeHelper = require('path/to/your/module')
const yourNodeHelper = new YourNodeHelper() // create instance here
yourNodeHelper.start()

Jest import of plain javascript results in unexpected token

Firstly: as far as I can tell, this is not a duplicate. The other questions with similar problems are all slightly different, e.g. use a transformation like babel or have problems with transitive imports. In my case I have no transformation, I have one test file and one file imported file that will be tested. I just started using jest and use the default setting, so there is no configuration file to post.
When I try to run my tests I get the error message:
Test suite failed to run
Jest encountered an unexpected token
This usually means that you are trying to import a file which Jest cannot parse, e.g. it's not plain JavaScript.
The tested file:
export function showTooltip(x, y, content) {
const infoElement = document.getElementById('info');
infoElement.style.left = `${x}px`;
infoElement.style.top = `${y}px`;
infoElement.style.display = 'block';
infoElement.innerText = createTooltipText(content);
}
function createTooltipText(object) {
return Object.keys(object)
.filter(key => key != 'id')
.map(key => `${key} : ${object[key]}`)
.join('\n');
}
export function hideTooltip() {
const infoElement = document.getElementById('info');
infoElement.style.display = 'none';
}
The test:
import {showTooltip, hideTooltip} from '../../../src/public/javascripts/tooltip.js';
const TOOLTIP_DUMMY = {
style: {
left: 0,
top: 0,
display: '',
innerText: ''
}
};
test('showTooltip accesses the element with the id \'info\'', () => {
const getElementByIdMock = jest.fn(() => TOOLTIP_DUMMY);
document.getElementById = getElementByIdMock;
showTooltip(0, 0, {});
expect(getElementByIdMock).toHaveBeenCalledWith('info');
});
test('hideTooltip accesses the element with the id \'info\'', () => {
const getElementByIdMock = jest.fn(() => TOOLTIP_DUMMY);
document.getElementById = getElementByIdMock;
hideTooltip();
expect(getElementByIdMock).toHaveBeenCalledWith('info');
});
As you can see I am using plain javascript so I am not sure what to do here. The error message gives further hints about Babel which does not really apply to my case.
Sidenote: My test might be flawed. I am currently trying to figure out how to use mocks to avoid interaction with the document and I am not sure if that is the way. However this is not the point of this question as it should not affect the ability of the tests to run, but I am very open for suggestions.
EDIT: Why this is not a duplicate to this question: It kind of is, but I feel that question and the accepted answer were not really helpful for me and hopefully someone will profit from this one.
I have found the solution to my problem:
As suggested in this answer, you need to use Babel. This can be done as suggested here, but I used #babel/env-preset as it is suggested on the Babel website.
This left me with the problem that jest internally uses babel-core#6.26.3, but at least babel 7 was required. This problem is described here. I used the temporary fix of manually copying and overwriting babel-core from my node-modules directory to the node-modules directories of jest-config and jest-runtime. This dirty fix is also described in the previous link.
I have yet to find a clean solution, but at least this works.
Use global.document.getElementById = getElementByIdMock; In some configurations Jest doesn't have access to document object directly.

Come up with Better Module Pattern for Node/ES6 Module

I'm struggling to come up with a pattern that will satisfy both my tests and ability for Travis to run my script.
I'll start off by saying that the way I have Travis running my script is that I specify the script to be run via node-babel command in my travis.yml as so:
script:
- babel-node ./src/client/deploy/deploy-feature-branch.js
That means when babel-node runs this, I need a method to auto run in deploy-feature-branch.js which I have. That's the line let { failure, success, payload } = deployFeatureBranch(). That forces deployFeatureBranch() to run because it's set to a destructure command.
In there I also have an options object:
let options = {
localBuildFolder: 'build',
domain: 'ourdomain',
branch: process.env.TRAVIS_PULL_REQUEST_BRANCH
}
During a PR build, travis automatically sets the value for process.env.TRAVIS_PULL_REQUEST_BRANCH. That's great! However the way I've set up this module doesn't work so well for tests. The problem I have is that if I try to set options from my test, for some reason the options object isn't being set.
I guess the problem I want to address is first and foremost, why options isn't being set when I try to set them from my test. And then is there a better way to design this module overall.
Test
import {options, deployFeatureBranch } from '../../../client/deploy/deploy-feature-branch'
it.only('creates a S3 test environment for a pull request', async () => {
options.branch = 'feature-100'
options.domain = 'ourdomain'
options.localDeployFolder = 'build'
const result = await deployFeatureBranch()
expect(result.success).to.be.true
})
})
When deployFeatureBranch() runs above in my test, the implementation of
tries to reference options.branch but it ends up being undefined even though I set it to be 'feature-100'. branch is defaulted to process.env.TRAVIS_PULL_REQUEST_BRANCH but I want to be able to override that and set it from tests.
deploy-feature-branch.js
import * as deployApi from './deployApi'
let options = {
localBuildFolder: 'build',
domain: 'ourdomain',
branch: process.env.TRAVIS_PULL_REQUEST_BRANCH
}
const deployFeatureBranch = async (options) => {
console.log(green(`Deploying feature branch: ${options.branch}`))
let { failure, success, payload } = await deployApi.run(options)
return { failure, success, payload }
}
let { failure, success, payload } = deployFeatureBranch(options)
export {
options,
deployFeatureBranch
}
I can't really think of a better way to structure this and also to resolve the setting options issue. I'm also not limited to using Node Modules either, I would be fine with ES6 exports too.
Instead of exporting options and modifying it, just pass in your new options object when calling the function in your test:
import {deployFeatureBranch } from '../../../client/deploy/deploy-feature-branch'
it.only('creates a S3 test environment for a pull request', async () => {
const options = {
branch: 'feature-100',
domain: 'ourdomain',
localDeployFolder: 'build'
};
const result = await deployFeatureBranch(options)
expect(result.success).to.be.true
})
});
The reason it isn't working is because your deployFeatureBranch() function expects options to be passed in when you call it, which you aren't doing.
Also, exporting and changing an object, while it might work, is also really weird and should be avoided. Creating a new object (or cloning the exported object) is definitely the way to go.

javascript patch an existing external dependency module method with a mocked version in unit test

I'm looking for something like this in python: http://fgimian.github.io/blog/2014/04/10/using-the-python-mock-library-to-fake-regular-functions-during-tests/
but for javascript/node.js to mock out a class method, or non-class method, etc. used inside code to be tested, like a black box, e.g. external dependencies. Is there something currently available for node.js like that? Or how to emulate that python unit test behavior? I'm currently using mocha & chai for node.js unit testing. Here's example of the intended test to illustrate:
var chai = require('chai');
var expect = chai.expect; // we are using the "expect" style of Chai
//SUT module inherits/extends BasicBolt from: https://github.com/apache/storm/blob/master/storm-multilang/javascript/src/main/resources/resources/storm.js
var SUT = require('./../my_code_under_test.js');
var storm = require("./../storm.js");
var fs = require('fs');
var path = require('path');
//...
it('component tests my code, somewhat like a black box', function() {
var myBolt = new SUT.MyCustomBolt();
var cfg = {}, context = {}, done = function() { return; };
myBolt.initialize(cfg,context,done);
var input = fs.readFileSync(path.resolve(__dirname, "input/data.json"),{'encoding':'utf8'});
var tup = new storm.Tuple(1,"default","default",1,["foo",input]);
myBolt.process(tup,done);
//myBolt.process makes HTTP GET call via node.js sync-request module
//myBolt.process then ends by calling a "self.emit()", e.g. BasicBolt.emit()
//for testing purposes, need to mock out request('GET',url) from sync-request, so not need HTTP endpoint to return result
//and need to mock emit such that it simply stores a copy of the emitted data to a (global) variable we can assert against, then reset the variable at end of each test. myBolt.process() itself does not return data to assert against.
expect(theEmittedData).to.equal('some value');
});
//...
I was able to do this approach in python, using the referenced link. I hope can do same in javascript. Or is the javascript best practice done differently?
FYI, am writing tests to test the code without having to run it within Apache Storm topology/infrastructure, just with the client libraries. As the bolt is rather simple, doesn't make sense to try to break down further the code inside myBolt.process() just to avoid having to mock the external dependencies, and I want to test around/with the storm library it's coupled with.
The 2 libraries you should consider would be sinon and testdouble.

Categories