nodegame-widgets
Version:
Collections of useful and reusable javascript / HTML snippets for nodeGame
680 lines (592 loc) • 21.3 kB
JavaScript
/**
* # Requirements
* Copyright(c) 2019 Stefano Balietti <ste@nodegame.org>
* MIT Licensed
*
* Checks a list of requirements and displays the results
*
* TODO: see if we need to reset the state between two
* consecutive calls to checkRequirements (results array).
*
* www.nodegame.org
*/
(function(node) {
"use strict";
node.widgets.register('Requirements', Requirements);
// ## Meta-data
Requirements.version = '0.7.2';
Requirements.description = 'Checks a set of requirements and display the ' +
'results';
Requirements.title = 'Requirements';
Requirements.className = 'requirements';
Requirements.texts.errStr = 'One or more function is taking too long. ' +
'This is likely to be due to a compatibility' +
' issue with your browser or to bad network' +
' connectivity.';
Requirements.texts.testPassed = 'All tests passed.';
// ## Dependencies
Requirements.dependencies = {
JSUS: {},
List: {}
};
/**
* ## Requirements constructor
*
* Instantiates a new Requirements object
*
* @param {object} options
*/
function Requirements(options) {
/**
* ### Requirements.callbacks
*
* Array of all test callbacks
*/
this.requirements = [];
/**
* ### Requirements.stillChecking
*
* Number of tests still pending
*/
this.stillChecking = 0;
/**
* ### Requirements.withTimeout
*
* If TRUE, a maximum timeout to the execution of ALL tests is set
*/
this.withTimeout = options.withTimeout || true;
/**
* ### Requirements.timeoutTime
*
* The time in milliseconds for the timeout to expire
*/
this.timeoutTime = options.timeoutTime || 10000;
/**
* ### Requirements.timeoutId
*
* The id of the timeout, if created
*/
this.timeoutId = null;
/**
* ### Requirements.summary
*
* Span summarizing the status of the tests
*/
this.summary = null;
/**
* ### Requirements.summaryUpdate
*
* Span counting how many tests have been completed
*/
this.summaryUpdate = null;
/**
* ### Requirements.summaryResults
*
* Span displaying the results of the tests
*/
this.summaryResults = null;
/**
* ### Requirements.dots
*
* Looping dots to give the user the feeling of code execution
*/
this.dots = null;
/**
* ### Requirements.hasFailed
*
* TRUE if at least one test has failed
*/
this.hasFailed = false;
/**
* ### Requirements.results
*
* The outcomes of all tests
*/
this.results = [];
/**
* ### Requirements.completed
*
* Maps all tests that have been completed already to avoid duplication
*/
this.completed = {};
/**
* ### Requirements.sayResult
*
* If true, the final result of the tests will be sent to the server
*/
this.sayResults = options.sayResults || false;
/**
* ### Requirements.sayResultLabel
*
* The label of the SAY message that will be sent to the server
*/
this.sayResultsLabel = options.sayResultLabel || 'requirements';
/**
* ### Requirements.addToResults
*
* Callback to add properties to result object sent to server
*/
this.addToResults = options.addToResults || null;
/**
* ### Requirements.onComplete
*
* Callback to be executed at the end of all tests
*/
this.onComplete = null;
/**
* ### Requirements.onSuccess
*
* Callback to be executed at the end of all tests
*/
this.onSuccess = null;
/**
* ### Requirements.onFailure
*
* Callback to be executed at the end of all tests
*/
this.onFailure = null;
/**
* ### Requirements.callbacksExecuted
*
* TRUE, the callbacks have been executed
*/
this.callbacksExecuted = false;
/**
* ### Requirements.list
*
* `List` to render the results
*
* @see nodegame-server/List
*/
// TODO: simplify render syntax.
this.list = new W.List({
render: {
pipeline: renderResult,
returnAt: 'first'
}
});
function renderResult(o) {
var imgPath, img, span, text;
imgPath = '/images/' + (o.content.success ?
'success-icon.png' : 'delete-icon.png');
img = document.createElement('img');
img.src = imgPath;
// Might be the full exception object.
if ('object' === typeof o.content.text) {
o.content.text = extractErrorMsg(o.content.text);
}
text = document.createTextNode(o.content.text);
span = document.createElement('span');
span.className = 'requirement';
span.appendChild(img);
span.appendChild(text);
return span;
}
}
// ## Requirements methods
/**
* ### Requirements.init
*
* Setups the requirements widget
*
* Available options:
*
* - requirements: array of callback functions or objects formatted as
* { cb: function [, params: object] [, name: string] };
* - onComplete: function executed with either failure or success
* - onFailure: function executed when at least one test fails
* - onSuccess: function executed when all tests succeed
* - maxWaitTime: max waiting time to execute all tests (in milliseconds)
*
* @param {object} conf Configuration object.
*/
Requirements.prototype.init = function(conf) {
if ('object' !== typeof conf) {
throw new TypeError('Requirements.init: conf must be object. ' +
'Found: ' + conf);
}
if (conf.requirements) {
if (!J.isArray(conf.requirements)) {
throw new TypeError('Requirements.init: conf.requirements ' +
'must be array or undefined. Found: ' +
conf.requirements);
}
this.requirements = conf.requirements;
}
if ('undefined' !== typeof conf.onComplete) {
if (null !== conf.onComplete &&
'function' !== typeof conf.onComplete) {
throw new TypeError('Requirements.init: conf.onComplete must ' +
'be function, null or undefined. Found: ' +
conf.onComplete);
}
this.onComplete = conf.onComplete;
}
if ('undefined' !== typeof conf.onSuccess) {
if (null !== conf.onSuccess &&
'function' !== typeof conf.onSuccess) {
throw new TypeError('Requirements.init: conf.onSuccess must ' +
'be function, null or undefined. Found: ' +
conf.onSuccess);
}
this.onSuccess = conf.onSuccess;
}
if ('undefined' !== typeof conf.onFailure) {
if (null !== conf.onFailure &&
'function' !== typeof conf.onFailure) {
throw new TypeError('Requirements.init: conf.onFailure must ' +
'be function, null or undefined. Found: ' +
conf.onFailure);
}
this.onFailure = conf.onFailure;
}
if (conf.maxExecTime) {
if (null !== conf.maxExecTime &&
'number' !== typeof conf.maxExecTime) {
throw new TypeError('Requirements.init: conf.onMaxExecTime ' +
'must be number, null or undefined. ' +
'Found: ' + conf.maxExecTime);
}
this.withTimeout = !!conf.maxExecTime;
this.timeoutTime = conf.maxExecTime;
}
};
/**
* ### Requirements.addRequirements
*
* Adds any number of requirements to the requirements array
*
* Callbacks can be asynchronous or synchronous.
*
* An asynchronous callback must call the `results` function
* passed as input parameter to communicate the outcome of the test.
*
* A synchronous callback must return the value immediately.
*
* In both cases the return is an array, where every item is an
* error message. Empty array means test passed.
*
* @see this.requirements
*/
Requirements.prototype.addRequirements = function() {
var i, len;
i = -1, len = arguments.length;
for ( ; ++i < len ; ) {
if ('function' !== typeof arguments[i] &&
'object' !== typeof arguments[i] ) {
throw new TypeError('Requirements.addRequirements: ' +
'requirements must be function or ' +
'object. Found: ' + arguments[i]);
}
this.requirements.push(arguments[i]);
}
};
/**
* ### Requirements.checkRequirements
*
* Asynchronously or synchronously checks all registered callbacks
*
* Can add a timeout for the max execution time of the callbacks, if the
* corresponding option is set.
*
* Results are displayed conditionally
*
* @param {boolean} display If TRUE, results are displayed
*
* @return {array} The array containing the errors
*
* @see this.withTimeout
* @see this.requirements
*/
Requirements.prototype.checkRequirements = function(display) {
var i, len;
var errors, cbName, errMsg;
if (!this.requirements.length) {
throw new Error('Requirements.checkRequirements: no requirements ' +
'to check.');
}
this.updateStillChecking(this.requirements.length, true);
errors = [];
i = -1, len = this.requirements.length;
for ( ; ++i < len ; ) {
// Get Test Name.
if (this.requirements[i] && this.requirements[i].name) {
cbName = this.requirements[i].name;
}
else {
cbName = i + 1;
}
try {
resultCb(this, cbName, i);
}
catch(e) {
this.hasFailed = true;
errMsg = extractErrorMsg(e);
this.updateStillChecking(-1);
errors.push('An error occurred in requirement n.' +
cbName + ': ' + errMsg);
}
}
if (this.withTimeout) this.addTimeout();
if ('undefined' === typeof display ? true : false) {
this.displayResults(errors);
}
if (this.isCheckingFinished()) this.checkingFinished();
return errors;
};
/**
* ### Requirements.addTimeout
*
* Starts a timeout for the max execution time of the requirements
*
* Upon time out results are checked, and eventually displayed.
*
* @see this.stillCheckings
* @see this.withTimeout
* @see this.requirements
*/
Requirements.prototype.addTimeout = function() {
var that = this;
this.timeoutId = setTimeout(function() {
if (that.stillChecking > 0) {
that.displayResults([that.getText('errStr')]);
}
that.timeoutId = null;
that.hasFailed = true;
that.checkingFinished();
}, this.timeoutTime);
};
/**
* ### Requirements.clearTimeout
*
* Clears the timeout for the max execution time of the requirements
*
* @see this.timeoutId
* @see this.stillCheckings
* @see this.requirements
*/
Requirements.prototype.clearTimeout = function() {
if (this.timeoutId) {
clearTimeout(this.timeoutId);
this.timeoutId = null;
}
};
/**
* ### Requirements.updateStillChecking
*
* Updates the number of requirements still running on the display
*
* @param {number} update The number of requirements still running, or an
* increment as compared to the current value
* @param {boolean} absolute TRUE, if `update` is to be interpreted as an
* absolute value
*
* @see this.summaryUpdate
* @see this.stillCheckings
* @see this.requirements
*/
Requirements.prototype.updateStillChecking = function(update, absolute) {
var total, remaining;
this.stillChecking = absolute ? update : this.stillChecking + update;
total = this.requirements.length;
remaining = total - this.stillChecking;
this.summaryUpdate.innerHTML = ' (' + remaining + ' / ' + total + ')';
};
/**
* ### Requirements.isCheckingFinished
*
* Returns TRUE if all requirements have returned
*
* @see this.stillCheckings
* @see this.requirements
*/
Requirements.prototype.isCheckingFinished = function() {
return this.stillChecking <= 0;
};
/**
* ### Requirements.checkingFinished
*
* Clears up timer and dots, and executes final callbacks accordingly
*
* First, executes the `onComplete` callback in any case. Then if no
* errors have been raised executes the `onSuccess` callback, otherwise
* the `onFailure` callback.
*
* @param {boolean} force If TRUE, the function is executed again,
* regardless of whether it was already executed. Default: FALSE
*
* @see this.onComplete
* @see this.onSuccess
* @see this.onFailure
* @see this.stillCheckings
* @see this.requirements
*/
Requirements.prototype.checkingFinished = function(force) {
var results;
// Sometimes, if all requirements are almost synchronous, it
// can happen that this function is called twice (from resultCb
// and at the end of all requirements checkings.
if (this.callbacksExecuted && !force) return;
this.callbacksExecuted = true;
if (this.timeoutId) clearTimeout(this.timeoutId);
this.dots.stop();
if (this.sayResults) {
results = {
success: !this.hasFailed,
results: this.results
};
if (this.addToResults) {
J.mixin(results, this.addToResults());
}
node.say(this.sayResultsLabel, 'SERVER', results);
}
if (this.onComplete) this.onComplete();
if (this.hasFailed) {
if (this.onFailure) this.onFailure();
}
else if (this.onSuccess) {
this.onSuccess();
}
};
/**
* ### Requirements.displayResults
*
* Displays the results of the requirements on the screen
*
* Creates a new item in the list of results for every error found
* in the results array.
*
* If no error was raised, the results array should be empty.
*
* @param {array} results The array containing the return values of all
* the requirements
*
* @see this.onComplete
* @see this.onSuccess
* @see this.onFailure
* @see this.stillCheckings
* @see this.requirements
*/
Requirements.prototype.displayResults = function(results) {
var i, len;
if (!this.list) {
throw new Error('Requirements.displayResults: list not found. ' +
'Have you called .append() first?');
}
if (!J.isArray(results)) {
throw new TypeError('Requirements.displayResults: results must ' +
'be array. Found: ' + results);
}
// No errors.
if (!this.hasFailed && this.stillChecking <= 0) {
// All tests passed.
this.list.addDT({
success: true,
text: this.getText('testPassed')
});
}
else {
// Add the errors.
i = -1, len = results.length;
for ( ; ++i < len ; ) {
this.list.addDT({
success: false,
text: results[i]
});
}
}
// Parse deletes previously existing nodes in the list.
this.list.parse();
};
Requirements.prototype.append = function() {
this.summary = document.createElement('span');
this.summary.appendChild(
document.createTextNode('Evaluating requirements'));
this.summaryUpdate = document.createElement('span');
this.summary.appendChild(this.summaryUpdate);
this.dots = W.getLoadingDots();
this.summary.appendChild(this.dots.span);
this.summaryResults = document.createElement('div');
this.summary.appendChild(document.createElement('br'));
this.summary.appendChild(this.summaryResults);
this.bodyDiv.appendChild(this.summary);
this.bodyDiv.appendChild(this.list.getRoot());
};
Requirements.prototype.listeners = function() {
var that;
that = this;
node.registerSetup('requirements', function(conf) {
if (!conf) return;
if ('object' !== typeof conf) {
node.warn('requirements widget: invalid setup object: ' + conf);
return;
}
// Configure all requirements.
that.init(conf);
// Start a checking immediately if requested.
if (conf.doChecking !== false) that.checkRequirements();
return conf;
});
this.on('destroyed', function() {
node.deregisterSetup('requirements');
});
};
// ## Helper methods.
function resultCb(that, name, i) {
var req, update, res;
update = function(success, errors, data) {
if (that.completed[name]) {
throw new Error('Requirements.checkRequirements: test ' +
'already completed: ' + name);
}
that.completed[name] = true;
that.updateStillChecking(-1);
if (!success) that.hasFailed = true;
if ('string' === typeof errors) errors = [ errors ];
if (errors) {
if (!J.isArray(errors)) {
throw new Error('Requirements.checkRequirements: ' +
'errors must be array or undefined. ' +
'Found: ' + errors);
}
that.displayResults(errors);
}
that.results.push({
name: name,
success: success,
errors: errors,
data: data
});
if (that.isCheckingFinished()) that.checkingFinished();
};
req = that.requirements[i];
if ('function' === typeof req) {
res = req(update);
}
else if ('object' === typeof req) {
res = req.cb(update, req.params || {});
}
else {
throw new TypeError('Requirements.checkRequirements: invalid ' +
'requirement: ' + name + '.');
}
// Synchronous checking.
if (res) update(res.success, res.errors, res.data);
}
function extractErrorMsg(e) {
var errMsg;
if (e.msg) {
errMsg = e.msg;
}
else if (e.message) {
errMsg = e.message;
}
else if (e.description) {
errMsg = errMsg.description;
}
else {
errMsg = e.toString();
}
return errMsg;
}
})(node);