Skip to content

Instantly share code, notes, and snippets.

@ndemengel
Last active November 13, 2016 19:59
Show Gist options
  • Save ndemengel/227ae0624bec8601af5995b2f8368849 to your computer and use it in GitHub Desktop.
Save ndemengel/227ae0624bec8601af5995b2f8368849 to your computer and use it in GitHub Desktop.
(part of) Hopwork's front JS test harness
const path = require('path');
const _ = require('lodash');
const AssertionError = require('assertion-error');
const chai = require('chai');
const chaiAsPromised = require('chai-as-promised');
const jsdom = require('jsdom');
const Q = require('q');
const sinonChai = require('sinon-chai');
const jsp = require('./jsp');
// convenience pre-configuration for all tests
chai.use(chaiAsPromised);
chai.use(sinonChai);
const THIS_MODULE_ROOT = path.resolve(__dirname, '../../../..');
const COMMON_JSP_PATH = path.resolve(THIS_MODULE_ROOT, 'src/main/resources/META-INF/resources/WEB-INF/jsp');
let appJspDir = path.resolve(THIS_MODULE_ROOT, 'src/main/resources/META-INF/resources/WEB-INF/jsp');
let scriptsDir = path.resolve(THIS_MODULE_ROOT, 'src/main/js');
function reconfigured(newCfg) {
appJspDir = newCfg.jspDir;
scriptsDir = newCfg.scriptsDir;
return this;
}
function fail(message) {
throw new AssertionError(message);
}
function noop() {
}
function loadHopModule(scriptPath) {
trashCacheAndRequire(path.join(scriptsDir, scriptPath));
$('[data-jsobj]').removeClass('jsinit');
global.window.Hop.control.init(document);
}
function trashCacheAndRequire(scriptPath) {
delete require.cache[require.resolve(scriptPath)];
return require(scriptPath);
}
function resolveJsp(jspDir, filePath) {
if (!/\.jsp$/.test(filePath)) {
filePath = filePath + '.jsp';
}
return path.resolve(jspDir, filePath);
}
function readJsp(jspDir, filePath, cfg) {
return jsp.resolveFile(resolveJsp(jspDir, filePath), cfg.attributes || {}, _.assign({
ignoredInclusions: (cfg.ignoreLayoutIncludes ?
['common/headblock.jsp', 'common/menu.jsp', 'common/private-submenu.jsp', 'common/footer.jsp'] :
[]).concat(cfg.ignoredInclusions),
jspRoot: jspDir
}, cfg));
}
/**
* Decorates testFn to create a DOM document with the optionally given HTML before calling it.
*
* <p>testFn will be given:</p>
* <ul>
* <li>{Window} window the window hosting the created document
* <li>{Document} document the created document
* <li>{Function} query a convenience reference to document.querySelector
* <li>{Function} queryAll a convenience reference to document.querySelectorAll
* </ul>
* <p>Additionally, if testFn is asynchronous, it is expected to declare a
* callback as its last parameter, or to return a promise.</p>
*
* @param {String/Object} maybeCfgOrHtml (optional) the HTML document content,
* or a config object for jsdom
* @param {Function} testFn the function to call when the document is ready
* @returns {Function} a valid test function, ready to be called by Mocha.
*/
function withDocument(maybeCfgOrHtml, testFn) {
const noConfigGiven = arguments.length === 1;
return function testWithDocument() {
let jsdomCfg = {};
if (noConfigGiven) {
testFn = maybeCfgOrHtml;
} else if (typeof maybeCfgOrHtml === 'string') {
jsdomCfg.html = maybeCfgOrHtml;
} else {
jsdomCfg = maybeCfgOrHtml;
}
if (jsdomCfg.file) {
jsdomCfg.file = path.resolve(jsdomCfg.file);
}
if (jsdomCfg.commonJsp) {
jsdomCfg.html = readJsp(COMMON_JSP_PATH, jsdomCfg.commonJsp, jsdomCfg);
}
if (jsdomCfg.jsp) {
jsdomCfg.html = readJsp(appJspDir, jsdomCfg.jsp, jsdomCfg);
}
if (!jsdomCfg.file && !jsdomCfg.html) {
jsdomCfg.html = '<body></body>';
}
// for convenience, return a promise to tell Mocha when the test is over
return Q.Promise((resolve, reject) => {
// when DOM is ready
jsdomCfg.done = (errors, window) => {
if (errors) {
throw new Error(errors);
}
const $ = require('jquery')(window);
// make expected browser globals available
global.window = window;
global.document = window.document;
global.Element = window.Element;
global.navigator = window.navigator;
global.jQuery = $;
global.$ = $;
window.location.href = 'https://tests.hopwork.com';
// jsdom lack some features, let's fix that
require('../../../main/js/polyfills/customEvent');
require('../../../main/js/polyfills/array.findIndex');
require('../../../main/js/polyfills/element.closest');
// skipped: load some core dependencies...
// skipped: mock and instrument some of our core functions to ease testing...
const testFnArgs = [$, window, window.document];
let doneCallbackGiven = false;
if (jsdomCfg.hopModules) {
let hopModules = jsdomCfg.hopModules;
if (typeof hopModules === 'string') {
hopModules = [hopModules];
}
hopModules.forEach(loadHopModule);
}
// async testFn, case 1: testFn expects a callback to call once done
if (testFn.length === testFnArgs.length + 1) {
testFnArgs.push(function done(err) {
if (err) reject(err);
else resolve();
});
doneCallbackGiven = true;
}
$(window.document).ready(() => {
try {
const maybePromise = testFn.apply(undefined, testFnArgs);
// async testFn, case 2: testFn returns a promise
if (maybePromise && maybePromise.then) {
maybePromise.then(resolve, reject);
}
else if (!doneCallbackGiven) {
resolve();
}
}
catch (e) {
reject(e);
}
});
};
jsdom.env(jsdomCfg);
});
};
}
// Utility functions for the rare cases where something happens asynchronously
// in the DOM. To be used within promise chains.
function waitFor(predicate) {
return () => {
return new Promise(resolve => {
(function testPredicate() {
if (predicate()) {
return resolve();
}
setTimeout(testPredicate, 10);
})();
});
};
}
function waitForDelay(delay) {
return () => {
return new Promise(resolve => {
setTimeout(resolve, delay);
});
};
}
// for when you're not in a promise chain but want to use previous functions nonetheless
function immediately(promiseFactory) {
return promiseFactory();
}
module.exports = {
changeUrl: jsdom.changeURL.bind(jsdom),
expect: chai.expect,
fail,
immediately,
loadHopModule,
noop,
reconfigured,
waitFor,
waitForDelay,
withDocument
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment