tl;dr - JSDOM has updated so my testing code that uses it has updated as well, ignore the editorial and look at the code.
A while ago, I wrote a bit about some code I often use for integration testing on the front end. Since “integration” can mean a lot of things, to clarify, I am referring to making sure components render the right HTML under certain states (not loading a full page, not testing the render
function). This code centered a lot around jsdom
, an excellent close-enough-to-browser-land tool for running JS. After a while, some changes to jsdom
prompted a revisit of the code, as it stopped working. This post is yet another revisit in the same tradition, responding to updates in JSDOM that broke my tests.
The updates to JSDOM that broke my tests generally were:
renderModuleInJSDOM
which was heavily simplified (and I appreciated)runScripts: "dangerously"
option being added (which I also appreciate, is good for security)should
(which I require
d in node context), which is arguably how it should be.Without any future ado here’s the code:
/**
* Render a component with data provided
*
* @param {Object} Vue - The vue instance to use to render the component
* @param {Object} Component - The component to render
* @param {Object} props - The data to feed in to the component at init
* @returns A promise that resolves to the `window` element of a DOM
*/
exports.renderComponentWithData = function(Vue, Component, props) {
// Create component and mount
var ModifiedVue = Vue.extend(Component);
var vm = new ModifiedVue(props || {});
return new Promise(function(resolve, reject) {
// Render the component
renderer.renderToString(vm, function(err, html) {
if (err) reject(err);
resolve(new jsdom.JSDOM(html).window);
});
});
};
/**
* Render a module (usually a component) in JSDOM, by:
* - Creating a JSPM builder
* - Building a static bundle of the module
* - Loading the bundle with JSDOM
*
* @param {string} path - Path to the module code
* @param {string} name - The name to use for the name of the exported module
* @param {string} [builder] - JSPM builder to re-use
* @returns A promise that resolves with the window of the generated jsdom environment
*/
function renderModuleInJSDOM(path, name, builder) {
// Make a static build of the module
return (builder || new JSPM.Builder())
.buildStatic(path, { minify: true, globalName: "window." + name })
.then(output => new jsdom.JSDOM(`<script>${output.source}</script>`, {runScripts: "dangerously"}).window);
}
exports.renderModuleInJSDOM = renderModuleInJSDOM;
/**
* Instantiate a component into an off-page element from a loaded global module in JSDOM
* The component module is expected to have `vue` and `component` properties.
*
* @param {string} globalName - The name of the loaded component module.
* @param {Object} data - Data to pass to the component on initialization
* @param {Object} data.propsData - Initial props for the component
* @returns A function that operates on a window, and resolves to original window, the vm, and element or the relevant component
*/
function instantiateComponentFromLoadedGlobal(globalName, data) {
return function(window) {
if (typeof window[globalName] === "undefined" || window[globalName] == null) {
throw new Error("Incorrect/Invalid global name, failed to find window[" + globalName + "]");
}
// Grab the loaded element, component, and vue fn
var vueFn = window[globalName].default.vue;
var comp = vueFn.extend(window[globalName].default.component);
// Create and mount component into off-page element
var vm = (new comp(data)).$mount();
var elem = vm.$el;
// NOTE - the element is NOT on the page at this point, it"s off-page
// The structure below is referred to as an ElemInfo object frequently.
return {vm, elem, vueFn, comp, window};
};
}
exports.instantiateComponentFromLoadedGlobal = instantiateComponentFromLoadedGlobal;
function isValidElemInfoObj(obj) {
return obj.vm && obj.elem && obj.window && obj.comp;
}
/**
* Add component"s element (expeted to be off-page) to the body of the HTML document. Usually called after `instantiateComponentFromLoadedGlobal`
*
* @param {ElemInfo} info - Information about element/env see `instantiateComponentFromLoadedGlobal`
* @returns A an object (elemInfo) that contains the window, the vm, and element or the relevant component. see `instantiateComponentFromLoadedGlobal`
*/
function addComponentElementToBody(info) {
if (!isValidElemInfoObj(info)) throw new Error("Invalid ElemInfo object:", info);
// Add elem to document body
info.window
.document
.documentElement
.getElementsByTagName("body")[0]
.appendChild(info.elem);
// NOTE - the element is NOT on the page at this point, it"s off-page
return info;
}
exports.addComponentElementToBody = addComponentElementToBody;
/**
* Simulate a click in JSDOM
*
* @param {EventTarget} tgt - The element that should be the target for the click
* @param {Window} window - The JSDOM-provided window of the page
* @param {object} options - Click event options (by default an cancelable, event that bubbles, with the given element as the view)
* @param {Function} tgt.dispatchEvent - The element should be able to dispatch events.
*/
function jsdomClick(tgt, window, options) {
tgt.dispatchEvent(
new window.MouseEvent("click", options || { bubbles: true, cancelable: true, view: tgt })
);
}
exports.jsdomClick = jsdomClick;
A few notes to go with this:
Vue
’s click handler so I left that function in just in case others have the same issue (maybe it’s changed/fixed now)Here’s an example of one of these bad boys in action, using tape
(which is what I use for all my testing now).
test("login-page component should render properly with no options", t => {
TestUtil.renderModuleInJSDOM(COMPONENT_JS_PATH, "Component")
.then(TestUtil.instantiateComponentFromLoadedGlobal("Component"))
.then(elemInfo => {
t.assert( elemInfo.vm , "VM should be load properly in JSDOM");
t.end();
})
.catch(TestUtil.failAndEnd(t));
});
There’s not a crazy amount there being tested, but it’s enough to demonstrate how I have used the functions defined above. Of course, you can go lots of places from here, using more of the items that elemInfo
provides (vm
is the component, elem
being the dom element attached to the component, window
, etc).