Last active
November 13, 2016 19:59
-
-
Save ndemengel/227ae0624bec8601af5995b2f8368849 to your computer and use it in GitHub Desktop.
(part of) Hopwork's front JS test harness
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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