aotrautils
Version:
A library for vanilla javascript utils (client-side) used in aotra javascript CMS
1,585 lines (1,293 loc) • 532 kB
JavaScript
/*utils COMMONS library associated with aotra version : «1_29072022-2359 (11/11/2025-18:19:16)»*/
/*-----------------------------------------------------------------------------*/
/* ## Utility global methods in a javascript, at least console (nodejs) server, or vanilla javascript with no browser environment.
*
* This set of methods gathers utility generic-purpose methods usable in any JS project.
* Several authors of snippets published freely on the Internet contributed to this library.
* Feel free to use/modify-enhance/publish them under the terms of its license.
*
* # Library name : «aotrautils»
* # Library license : HGPL(Help Burma) (see aotra README information for details : https://alqemia.com/aotra.js )
* # Author name : Jérémie Ratomposon (massively helped by his native country free education system)
* # Author email : info@alqemia.com
* # Organization name : Alqemia
* # Organization email : admin@alqemia.com
* # Organization website : https://alqemia.com
*
*
*/
// COMPATIBILITY browser javascript / nodejs environment :
if(typeof(window)==="undefined") window=global;
//=========================================================================
// GLOBAL CONSTANTS :
PERFORM_TESTS_ON_LIBRARY=false;
//FLATTEN / UNFLATTEN JSON CONSTANTS :
const ROOT_UUID="00000000-0000-0000-0000-000000000000";
const DEFAULT_UUID_ATTR_NAME="JSONid";/* Yet Another UUID */
const DEFAULT_CLASSNAME_ATTR_NAME="JSONtype";/* Yet Another Classname */
const DEFAULT_POINTER_TO_ATTR_NAME="JSONref";/* Yet Another PointerTo */
//=========================================================================
// - console logging :
// NOT AOTESTABLE !
// DEPRECATED :
window.log=function(errorMessage,/*OPTIONAL*/trace,/*OPTIONAL*/failToAlert){
// To log message :
if(typeof console !== "undefined"){
if(console.log && !trace){
console.log(errorMessage);
return errorMessage;
}
} else if(failToAlert && typeof alert !== "undefined"){
alert(errorMessage);
return errorMessage;
}
// To add an error stack if we want to display it :
// CAUTION : THE FUNCTION WILL NOT RETURN THE ERROR MESSAGE IN THIS CASE !
setTimeout(function(){
throw new Error(errorMessage);
// throw new Exception(errorMessage);
}, 1);
return errorMessage;
}
window.lognow=function(errorMessage,object=null,/*OPTIONAL*/trace=false,/*OPTIONAL*/failToAlert=false){
const LOG_SEPARATOR=":";
// To log message :
if(typeof console !== "undefined"){
if(console.log && !trace){
if(object) console.log(getNow()+LOG_SEPARATOR+errorMessage,object);
else console.log(getNow()+LOG_SEPARATOR+errorMessage);
return errorMessage;
}
} else if(failToAlert && typeof alert !== "undefined"){
if(object) alert(getNow()+LOG_SEPARATOR+errorMessage+LOG_SEPARATOR+object);
else alert(getNow()+LOG_SEPARATOR+errorMessage);
return errorMessage;
}
// To add an error stack if we want to display it :
// CAUTION : THE FUNCTION WILL NOT RETURN THE ERROR MESSAGE IN THIS CASE !
setTimeout(function(){
if(object) throw new Error(getNow()+LOG_SEPARATOR+errorMessage+LOG_SEPARATOR+object);
else throw new Error(getNow()+LOG_SEPARATOR+errorMessage);
// throw new Exception(errorMessage);
}, 1);
return errorMessage;
}
//================= aotra andrana (means «trial, try» in malagasy) sub-component =================
// unit and integration testing framework
//================================================================================================
// (Duplicated doc with aotrautils-jquery)
// ============================== COMPLETE AOTEST DOCUMENTATION BELOW ==============================
//
// Unit tests framework embedded in aotra (called "aotest") : (inherits this utils library license)
// «aotest» is a short name used in code for «aotest», which is the real name.
// - LAUNCH INFORMATION :
// All tests are registered as javascript declaring function is executed.
// To execute registered tests, simply execute aotest.run(); after the tests registration code is executed.
// - DEPENDENCIES INFORMATION :
// (DEPENDENCY : the nothing(...) function in utils library)
// (DEPENDENCY : the log(...) function in utils library)
// (DEPENDENCY : the isArray(...) function in utils library)
// (DEPENDENCY : the getFunctionName(...) function in utils library)
// (DEPENDENCY : the getHashedString(...) function in utils library)
// (DEPENDENCY : the getUniqueIdWithDate(...) function in utils library)
// (DEPENDENCY : the getURLParameter(...) function in utils library)
// (DEPENDENCY : jquery methods and functions : if you want to run UI user tests, to simulate UI events and test the outcome... -examples below-)
// - MISCELLANEOUS :
// (Note to developer : functions / methods used in function tracking CANNOT be tested with aotest framework ! or else there will be calls overflow errors. Trust me. I tried.)
// - UNIT TESTS EXAMPLES :
//// Data and treatments testing :
//// Examples :
//var addPoints2DCoordinates=window.aotest(
//// Tests definition
//{
// // Tested function/method name :
// name : "addPoints2DCoordinates"
// // Dummies :
// ,
// dummies : function(){
// var result=new Object();
// result.pDummy1=new Object();
// result.pDummy1.x=20;
// result.pDummy1.y=10;
// result.pDummy2=new Object();
// result.pDummy2.x=60;
// result.pDummy2.y=40;
//
// return result;
// }
// // Normal scenarii :
// , scenarioNormal1 : [ [ {
// dummy : "pDummy1"
// }, {
// dummy : "pDummy2"
// } ], function(result){
// return result.x === 80 && result.y === 50;
// } ],
// scenarioNormal2 : [ [ {
// dummy : "pDummy1"
// }, {
// dummy : "pDummy2"
// } ], {
// asserts : "success"
// } ]
// // Abnormal scenarii :
// , scenarioParameter1IsNull : [ [ null, {
// dummy : "pDummy2"
// } ], {
// asserts : "fail"
// } ],
// scenarioParameter2IsNull : [ [ {
// dummy : "pDummy1"
// }, null ], {
// asserts : "fail"
// } ]
//}
//// Function definition
//, function(p1, p2){
// var result=new Object();
// aotest.assert(p1 === null, "fail");
// aotest.assert(p2 === null, "fail");
// result.x=p1.x + p2.x;
// result.y=p1.y + p2.y;
// return result;
//});
//
//// (Function can share the same dummies because dummies are global)
//var invertPoints2DCoordinates=window.aotest(
//
//// Tests definition
//{
// // Tested function/method name :
// name : "invertPoints2DCoordinates"
//
// // Dummies :
// , dummies : null
// // Normal scenarii :
// , scenarioNormal1 : [ [ {
// dummy : "pDummy1"
// } ], function(result){
// return result.x === -20 && result.y === -10;
// } ],
// scenarioNormal2 : [ [ {
// dummy : "pDummy2"
// } ], function(result){
// return result.x === -60 && result.y === -40;
// } ],
// scenarioNormal3 : [ [ {
// dummy : "pDummy1"
// } ], {
// asserts : "success"
// } ]
// // Abnormal scenarii :
// , scenarioParameterIsNull : [ [ null ], {
// asserts : "fail"
// } ]
//}
//// Function definition
//, function(p1){
// var result=new Object();
// aotest.assert(p1 === null, "fail");
// result.x=-p1.x;
// result.y=-p1.y;
// return result;
//});
//// Advanced unit testing :
//function less(a, b){
// if(!a || !b)
// throw "Arguments must be non-null !";
// return a - b;
//}
//function add(a, b){
// return a + b;
//}
//function multiply(a, b){
// var result=0;
// for (var i=0; i<b; i++)
// result += a;
// return result;
//}
//
//// - Calls and errors throws monitored tests :
//
//window.aotest({
// name : "multiply",
// scenario1 : {
// values : [ [ 2, 5 ], 10 ],
// calls : {
// multiply : 1,
// add : 1,
// less : 0
// }
// },
// scenario2 : {
// values : [ [ 2, null ], {
// asserts : "fail"
// } ],
// calls : {
// multiply : 1,
// add : "throw",
// less : 0
// }
// }
//}, multiply);
//window.aotest({
// name : "less",
// scenario1 : {
// values : [ [ 2, 5 ], -3 ],
// calls : {
// multiply : 0,
// add : 0,
// less : 1
// }
// }
//}, less);
//// Another case, to illustrate that when null paramaters passed, instead of empty array, mean we don't want to execute the tested function / method as the main testing routine
//less=window.aotest({}, less);
//multiply=window.aotest({}, multiply);
//add=window.aotest({
// scenarAddTrack1 : {
// values : [ null, function(){
// return multiply(2, 10) === 20;
// } ],
// calls : {
// multiply : 1,
// add : 10,
// less : 0
// }
// },
// scenarAddTrack2 : {
// values : [ null, function(){
// return multiply(2, 10) === 20;
// } ],
// calls : {
// multiply : 1,
// add : 10,
// less : 0
// }
// },
// scenarAddTrack3 : [ null, function(){
// return true;
// } ]
//}, add);
//
//function less(a, b){
// if(!a || !b)
// throw "Arguments must be non-null !";
// return a - b;
//}
//function add(a, b){
// aotest.assert(a !== null && b !== null, "success");
// return a + b;
//}
//function multiply(a, b){
// var result=0;
// for (var i=0; i<b; i++)
// result=add(result, a);
// return result;
//}
//// CAUTION : If no assert is declared and your test expect asserts to fail, then you test will fail. But, if you have no assert in your test and expect asserts to success, then your test will succeed.
//// It is because you can see asserts as obstacles to your test success : So if there is no obstacle to its success, then there is will be no reason for it not to succeed !
//// - Calls and errors throws monitored
//tests: less=window.aotest({}, less);
//add=window.aotest({}, add);
//window.aotest({
// // ,scenario1:{values:[[2,20],40],calls:{multiply:1,add:20,less:0}}
// // ,scenario2:{values:[[null,2],{asserts:"fail"}],calls:{multiply:1,add:"throw",less:0}}
// scenario3 : {
// values : [ [ 4, 3 ], {
// asserts : "success"
// } ],
// calls : {
// multiply : 1,
// add : 3,
// less : 0
// }
// }
//}, multiply);
//// Yet another example for calls and throws monitoring :
//function less(a, b){
// if(!a || !b)
// throw "Arguments must be non-null !";
// return a - b;
//}
//function add(a, b){
// aotest.assert(a !== null && b !== null, "success");
// return a + b;
//}
//function multiply(a, b){
// var result=0;
// for (var i=0; i<b; i++)
// result=add(result, a);
// return result;
//} // - Calls and errors throws monitored
//tests: less=window.aotest({}, less);
//add=window.aotest({}, add);
//window.aotest({
// // ,scenario1:{values:[[2,20],40],calls:{multiply:1,add:20,less:0}}
// // ,scenario2:{values:[[null,2],{asserts:"fail"}],calls:{multiply:1,add:"throw",less:0}}
// scenario3 : {
// values : [ [ 4, 3 ], {
// asserts : "success"
// } ],
// calls : {
// multiply : 1,
// add : 3,
// less : 0
// }
// }
//}, multiply);
//// -Imbriqued tests :
//window.aotest({
// name : "multiplyNested",
// scenario1Nested : {
// values : [ [ 2, 5 ], 10 ],
// subs : {
// scenario1Sub : [ [ 100, 5 ], 500 ],
// scenario2Sub : {
// values : [ [ 10, 10 ], 100 ],
// subs : {
// scenario1SubSub : [ [ 1000, 1000 ], 1000000 ]
// }
// }
// }
// }
//}, multiply);
//// User UI testing :
//window.aotest({
// name : "getAotraScreen",
// scenario1 : [ [], function(){
// return getAotraScreen() !== null;
// } ]
//});
//window.aotest({
// name : "editionProvider",
// scenarioCreateBubble : [ [], function(){
// var aotraScreen=getAotraScreen();
//
// var activePlane=aotraScreen.getActivePlane();
// aotest.assert(activePlane === null, "fail");
//
// var oldBubblesNumber=activePlane.getBubbles().length;
//
// aotest.assert(typeof jQuery === "undefined", "fail");
//
// var createButtonS=jQuery(".icon.addBubbleBtn");
// aotest.assert(empty(createButtonS.get()), "fail");
// createButtonS.click();
//
// aotest.assert(typeof aotraScreen.mainDiv === "undefined", "fail");
//
// var mainDivS=jQuery(aotraScreen.mainDiv);
// aotest.assert(empty(mainDivS.get()), "fail");
// mainDivS.click();
//
// var applyButtonS=jQuery("#buttonSaveAndCloseBubble");
// aotest.assert(empty(applyButtonS.get()), "fail");
// applyButtonS.click();
//
// var newBubblesNumber=activePlane.getBubbles().length;
//
// return newBubblesNumber === oldBubblesNumber + 1;
// } ],
// scenarioAsserts : [ [], {
// asserts : "success"
// } ]
//});
// AOTEST (headless) Unit Tests :
// Initialization :
if(!window.aotestMethods)
window.aotestMethods=new Object();
if(!window.aotestAllTestsManager){
window.aotestAllTestsManager={
isEmpty:function(){
let result=true;
foreach(window.aotestAllTestsManager,(test,testNameLocal)=>{
result=false;
return "break";
},(test,testNameLocal)=>{ return !window.aotestMethods.aotratestKeyInPredefinedParameters(testNameLocal); });
return result;
}
};
if(!window.aotestAllTestsManager.dummies){
window.aotestAllTestsManager.dummies=new Object();
}
window.aotestAllTestsManager.activateAsserts=false;
window.aotestAllTestsManager.currentRunningScenario=null;
};
// Utility methods for aotest framework :
// UI/CLI ONLY :
window.aotestMethods.getParentTestByScenarioNameInTestsArray=function(testsArray, scenarioName){
return foreach(testsArray,test=>{
return window.aotestMethods.iterateOverScenariiInSingleTest(test, (parentTest, scenario)=>{
if(scenario.name===scenarioName)
return parentTest;
});
});
}
window.aotestMethods.getParentTestByScenarioName=function(testsByMethod, scenarioName){
return foreach(testsByMethod,testsForOneMethod=>{
return foreach(testsForOneMethod,(test,methodName)=>{
return window.aotestMethods.iterateOverScenariiInSingleTest(test, (parentTest, scenario)=>{
if(scenario.name===scenarioName)
return parentTest;
});
});
});
}
window.aotestMethods.getScenarioByName=function(allTests, scenarioName){
const result=window.aotestMethods.iterateOverScenarii(allTests, null, null, null, scenario=>scenario.name===scenarioName);
if(result) return result;
return null;
}
window.aotestMethods.getScenariiByName=function(allTests, scenariiNames){
const results=[];
window.aotestMethods.iterateOverScenarii(allTests, (scenario, test)=>{
results.push(scenario);
}, null, null, scenario=>contains(scenariiNames,scenario.name));
return results;
}
window.aotestMethods.getNumberOfScenariiInGlobal=function(allTestsBagForClientReadOnly, testsType){
let currentScenariiNumber=0;
window.aotestMethods.iterateOverScenarii(allTestsBagForClientReadOnly, (scenario)=>{
currentScenariiNumber++;
}, testsType);
return currentScenariiNumber;
}
window.aotestMethods.iterateOverScenarii=function(allTests, doOnIteration, testsType=null, filterFunction=null, mustTerminateFunction=null){
if(!testsType || testsType==="*"){
return foreach(allTests,(allTestsByType,t)=>{
const loopResult=window.aotestMethods.doForAllTestsByType(allTestsByType, doOnIteration, filterFunction, mustTerminateFunction);
if(loopResult)
return loopResult;
});
}
const allTestsByType=allTests[testsType];
return window.aotestMethods.doForAllTestsByType(allTestsByType, doOnIteration, filterFunction, mustTerminateFunction);
};
/*private*/window.aotestMethods.doForAllTestsByType=function(allTestsByType, doOnIteration, filterFunction=null, mustTerminateFunction=null){
const loopResultI=foreach(allTestsByType,(testsByMethod,methodName)=>{
const loopResultJ=foreach(testsByMethod,(test,executionCoupleName)=>{ // (execution couple IS test)
const scenarii=aotestMethods.getScenariiInSingleTest(test);
const loopResultK=foreach(scenarii,(scenario, scenarioName)=>{
if(doOnIteration) doOnIteration(scenario, test);
if(mustTerminateFunction && mustTerminateFunction(scenario))
return scenario;
},(scenario, scenarioName)=>!filterFunction || filterFunction(scenario));
if(loopResultK) return loopResultK;
});
if(loopResultJ) return loopResultJ;
});
return loopResultI;
}
// ----------------
// UI/CLI ONLY :
window.aotestMethods.getScenarioByNameInTestsArray=function(testsArray, scenarioName){
return foreach(testsArray,(test)=>{
return aotestMethods.getScenarioInSingleTest(test, scenarioName);
});
}
// ----
window.aotestMethods.getScenarioInSingleTest=function(test, scenarioName=null){
if(!scenarioName) return null;
return window.aotestMethods.iterateOverScenariiInSingleTest(test,(t,s,sName)=>{
if(sName===scenarioName){
return s;
}
});
};
window.aotestMethods.getScenariiInSingleTest=function(test, scenariiNames=null){
let results={};
window.aotestMethods.iterateOverScenariiInSingleTest(test,(t,s,sName)=>{
if(!scenariiNames || contains(scenariiNames, sName)){
results[sName]=s;
}
});
return results;
};
window.aotestMethods.iterateOverScenariiInSingleTest=function(test, doOnEachIteration=null){
if(!doOnEachIteration) return;
return foreach(test,(scenario,scenarioName)=>{
const result=doOnEachIteration(test,scenario,scenarioName);
if(!nothing(result)) return result;
},(scenario,scenarioName)=>{ return window.aotestMethods.isScenarioName(scenarioName); });
};
/*public*/window.aotestMethods.iterateOverValuesOnClonedObject=function(scenarioParam, doOnIterationForValue, visited=[], valuePath="", isValueFunction=null){
// CAUTION : We ABSOLUTELY need to iterate over a deep copy, or else it will create weird incomprehensible bugs
// if we do another recursive browsing on the initial object !!!
const scenario=parseJSON(stringifyObject(scenarioParam));
const basePath=valuePath;
foreach(scenario,(item, itemNameOrIndex)=>{
if(contains(visited, item)) return "continue";
visited.push(item);
valuePath=basePath+"."+itemNameOrIndex;
if(isValueFunction){
if(isValueFunction(item)){
scenario[itemNameOrIndex]=doOnIterationForValue(item, itemNameOrIndex, valuePath);
}else{
if(isArray(item)){
window.aotestMethods.iterateOverValuesOnClonedObject(item, doOnIterationForValue, visited, valuePath, isValueFunction);
}else if(isObject(item)){
window.aotestMethods.iterateOverValuesOnClonedObject(item, doOnIterationForValue, visited, valuePath, isValueFunction);
}else{
scenario[itemNameOrIndex]=item;
}
}
}else{
if(isArray(item)){
window.aotestMethods.iterateOverValuesOnClonedObject(item, doOnIterationForValue, visited, valuePath, isValueFunction);
}else if(isObject(item)){
window.aotestMethods.iterateOverValuesOnClonedObject(item, doOnIterationForValue, visited, valuePath, isValueFunction);
}else{
scenario[itemNameOrIndex]=doOnIterationForValue(item, itemNameOrIndex, valuePath);
}
}
});
};
window.aotestMethods.isScenarioName=function(scenarioName){
return contains(scenarioName,"_scenario");
};
// (a little utility function for the unitary tests framework aotest :)
window.aotestMethods.aotratestKeyInPredefinedParameters=function(key){
return contains(
["activateAsserts","currentRunningScenario","name","dummies","prerequisite","clean","methodName",
"mainInstance","nature","isEmpty"],
key);
};
window.aotestMethods.registerScenarioInfos=function(scenariiListParam, name, scenarioInfos){
// Scenario attriburtes :
scenarioInfos["name"]=name;
scenariiListParam[name]=scenarioInfos;
// Sub-scenarii registering :
var subScenariiList=null;
if(!isArray(scenarioInfos))
subScenariiList=scenarioInfos["subs"];
if(subScenariiList && typeof subScenariiList === "object"){
foreach(subScenariiList, (subScenarioInfos,key)=>{
subScenarioInfos["parentScenarioName"]=name;
window.aotestMethods.registerScenarioInfos(subScenariiList, key, subScenarioInfos);
});
}
};
// UNUSED AND BOGUS :
// TODO : FIXME : On today, tracking is deactivated for aotest, because random unresolved bugs occur :
//
///*private*/window.aotestMethods.trackFunction=function(func, methodArgs){
//
// // DBG
// console.log("!!! func:",func);
// console.log("!! func.prototype:",func.prototype);
// console.log("!! func.prototype.constructor:",func.prototype.constructor);
//
// DOES NOT WORK : func.prototype IS SOMETIMES UNDEFINED !!
//
// // TODO : FIXME : Not sure if this is the right objet to call the function upon....
// var callerObject=func.prototype.constructor.caller;
//
//
// var currentRunningScenario=window.aotestAllTestsManager.currentRunningScenario;
// // If we are not in a running test scenario execution context, then we skip
// // tracking :
// if(!currentRunningScenario)
// return func.apply(callerObject, argsAsArray);
//
// var functionName=getFunctionName(func);
// // UNUSED attribute :
// currentRunningScenario.functionName=functionName;
// var functionResult=null;
// if(!currentRunningScenario.callsCounts)
// currentRunningScenario.callsCounts=new Object();
// var callsCounts=currentRunningScenario.callsCounts[functionName];
// if(!callsCounts)
// currentRunningScenario.callsCounts[functionName]=0;
// if(!currentRunningScenario.throwsCounts)
// currentRunningScenario.throwsCounts=new Object();
// var throwsCounts=currentRunningScenario.throwsCounts[functionName];
// if(!throwsCounts)
// currentRunningScenario.throwsCounts[functionName]=0;
// currentRunningScenario.callsCounts[functionName]++;
// try {
// functionResult=func.apply(callerObject, argsAsArray);
// } catch (e){
// currentRunningScenario.throwsCounts[functionName]++;
// throw e;
// }
//
// return functionResult;
//};
// UNUSED AND BOGUS :
// TODO : FIXME : On today, tracking is deactivated for aotest, because random unresolved bugs occur :
//
//window.aotestMethods.getTrackedFunctionDefinition=function(/* NULLABLE */functionDefinition=null){
// if(!functionDefinition)
// return null;
//
// var trackedFunctionDefinition=function(){
// // IMPORTANT : «arguments» here is a system variable containing all
// // arguments for this function call...
// return window.aotestMethods.trackFunction(functionDefinition, arguments);
// };
//
// return trackedFunctionDefinition;
//};
// Main method to initialize (ie. register) a test on a function/method :
window.aotest=function(parameters,functionDefinition=null,ignoreTest=false,CLASSNAME_ATTR_NAME=DEFAULT_CLASSNAME_ATTR_NAME){
// (If we skip test, then there is no need to add tracking on function /
// method :)
if(ignoreTest)
return functionDefinition;
const DEBUG_TRACE=true;
// Parameters :
// Tested function/method name :
const methodNameParam=parameters.methodName;
if(!functionDefinition){
let mainInstance=parameters.mainInstance;
if(!mainInstance)
functionDefinition=window[methodNameParam];
else{
functionDefinition=mainInstance[methodNameParam];
if(!functionDefinition && mainInstance[CLASSNAME_ATTR_NAME]){
let instanceTypeName=mainInstance[CLASSNAME_ATTR_NAME];
// DBG
console.log("ERROR : Could not find method «"+methodNameParam+"» on instance of the following 'alleged type' : «"+instanceTypeName+"». Test registration will be skipped.");
}
}
}
if(!functionDefinition){
// TRACE
console.log("ERROR : No function definition found for function named «"+nonull(methodNameParam, "<unknown name function>")+"», aborting test registration.");
return null;
}
let methodName=getFunctionName(functionDefinition);
// Current method context initialization :
// Test parameters population :
var currentTest=new Object();
var nameParam=parameters["name"] ? parameters["name"] : (methodName ? methodName : ("noNameTest_" + getUniqueIdWithDate()));
// TRACE
// if(DEBUG_TRACE) log("DEBUG : Registering test «"+nameParam+"»");
// Required name :
currentTest.name=nameParam;
// (optional) Func (ie. function name) :
let func=parameters["methodName"];
if(!!func /*(forced to boolean)*/){
currentTest["methodName"]=func;
}
// We add to the global context :
window.aotestAllTestsManager[currentTest.name]=currentTest;
// Dummies :
var dummiesParam=parameters["dummies"];
var dummiesLocal=new Object();
if(typeof (dummiesParam) === "function"){
dummiesLocal=dummiesParam(new Object());
} else if(typeof (dummiesParam) === "object"){
dummiesLocal=dummiesParam;
// }else{
// // TRACE
// if(DEBUG_TRACE){
// var displayTestName="Unit test for function «"+nameParam+"»";
// log("DEBUG : WARN : No dummies for test «"+displayTestName+"»");
// }
}
// We add to the global context :
foreach(dummiesLocal, (dummy,key)=>{
window.aotestAllTestsManager.dummies[key]=dummy;
});
// (optional) Prerequisite function :
var prerequisite=parameters["prerequisite"];
if(prerequisite && typeof prerequisite === "function"){
currentTest["prerequisite"]=prerequisite;
}
// (optional) Clean function :
var clean=parameters["clean"];
if(clean && typeof clean === "function"){
currentTest["clean"]=clean;
}
// (optional) main Instance present or not :
var mainInstance=parameters["mainInstance"];
if(!!mainInstance /*(forced to boolean)*/){
currentTest["mainInstance"]=mainInstance;
}
// Scenarii (last parameters !) :
var scenariiListLocal={};
var scenariiCount=0;
foreach(parameters, (scenarioInfos,key)=>{
if(window.aotestMethods.aotratestKeyInPredefinedParameters(key))
return "continue";
// if(DEBUG_TRACE) log("DEBUG : Registering test «"+nameParam+"» : scenario
// «"+key+"»");
window.aotestMethods.registerScenarioInfos(scenariiListLocal, key, scenarioInfos);
scenariiCount++;
});
if(DEBUG_TRACE)
console.log(("DEBUG : (" + scenariiCount + ") test scenarii registered for test «" + nameParam + "»."));
currentTest.scenariiList=scenariiListLocal;
// TODO : FIXME : On today, tracking is deactivated for aotest, because random unresolved bugs occur :
// We add the method to the global tracking context :
// functionDefinition=window.aotestMethods.getTrackedFunctionDefinition(functionDefinition);
window.aotestAllTestsManager[currentTest.name].functionDefinition=functionDefinition;
return functionDefinition;
};
// Tests running :
// CAUTION : If no assert is declared and your test expect asserts to fail, then
// your test will fail.
// But, if you have no assert in your test and expect asserts to success, then
// your test will succeed.
// It is because you can see asserts as obstacles to your test success :
// So if there is no obstacle to its success, then there is will be no reason
// for it not to succeed !
aotest.assert=function(booleanCondition, behavior){
if(!window.aotestAllTestsManager.activateAsserts)
return;
if(booleanCondition && behavior === "fail")
throw "(Assert failed : evaluated condition succeeded instead of failed.)";
if(!booleanCondition && behavior === "success")
throw "(Assert failed : evaluated condition failed instead of succeeded.)";
};
/* STANDARD AOTEST RESULT :
* This method returns a tests results report object of the following structure :
*
[name]:
-global
-type:string="testsSuiteResult"
-childrenFailed : number
-childrenRan: number
-isFailed: boolean
-message : string
-children
[name] :
...(IDEM)... with : -type="testResult"
-children
[name] :
...(IDEM)... with : -type="scenarioResult"
(-children // (sub-scenarii :)
[name] :
...(IDEM)... with : -type="scenarioResult" )*
*
* */
aotest.run=function(testName=null,scenarioName=null){
// CONSTANTS :
var FAIL_IF_CHILDREN_FAILED=true;
// Result report variable:
var testsResultsObj={};
// DBG
console.log(" window.aotestAllTestsManager",window.aotestAllTestsManager);
if(window.aotestAllTestsManager.isEmpty()){
// TRACE
console.log("WARN : No registered javascript tests to run. Aborting.");
const emptyTestsSuiteResult={
testName: testName,
scenarioName : scenarioName,
nature: "htmlsj",
global:{
type:"testsSuiteResult", childrenFailed:0, childrenRan:0,
isFailed:false,
message:"No registered javascript tests to run.",
expected:null,
actual:null,
},
children:{},
};
testsResultsObj["emptyJavascriptTestsSuite"]=emptyTestsSuiteResult;
return testsResultsObj;
}
aotest.isRunning=true;
// Function 1, case run one test>one scenario
const runOneTestOneScenario=function(chosenTestParam, chosenScenarioParam){
let scenarioResultObjLocal={
testName:chosenTestParam.name,
scenarioName:chosenScenarioParam.name,
nature: "htmlsj",
global:{
type:"scenarioResult",
childrenFailed:0,
childrenRan:0,
isFailed:false,
message:""
},
children:{}
};
var functionParameters=null;
// Which scenario is currently running :
window.aotestAllTestsManager.currentRunningScenario=chosenScenarioParam;
// Functions calls counting :
var isChosenScenarioParamArray=isArray(chosenScenarioParam);
if(!isChosenScenarioParamArray) functionParameters=chosenScenarioParam.methodArgs;
else functionParameters=chosenScenarioParam[0];
// CAUTIOM : At this point, functionParameters is an associative array (parameterName : parameterValue)
let mainInstance=nonull(chosenTestParam.mainInstance,null); // To ensure we have null and not undefined
// CAUTION : functionParameters can be null, meaning we want no execution of
// the function / method !
// (a different case if we want to execute it with no parameters !)
var functionParametersPopulated=null; // ...if this array is null, then we don't want to execute the function !!
if(functionParameters){
functionParametersPopulated=new Array();
// We populate dummies here :
foreach(functionParameters,(fp)=>{
if(fp){
if(fp["dummy"]){
var dummyName=fp["dummy"];
var dummy=window.aotestAllTestsManager.dummies[dummyName];
functionParametersPopulated.push(dummy);
} else {
functionParametersPopulated.push(fp);
}
} else {
functionParametersPopulated.push(null);
}
});
}
let currentExecutedTest=window.aotestAllTestsManager[chosenTestParam.name];
var functionDefinition=currentExecutedTest.functionDefinition;
var expectedResult=null;
if(!isChosenScenarioParamArray) expectedResult=chosenScenarioParam.result;
else expectedResult=chosenScenarioParam[1];
// We copy as flat structures and store all the possible function context objects before its execution :
currentExecutedTest.oldResultFlat=(expectedResult!=null?getAsFlatStructure(expectedResult):null);
// NEW
// (special, scenario instance has priority over the main, test instance, for the function application :)
let actualInstance=nonull(chosenScenarioParam.specialInstance,mainInstance);
// DBG
console.log("!!! functionDefinition:",functionDefinition);
console.log("!!! functionParametersPopulated:",functionParametersPopulated);
// CAUTION : functionParameters can be null, meaning we want no execution of
// the function / method !
// (a different case if we want to execute it with no parameters ! in that case we will have an empty array of arguments)
if(functionDefinition && functionParametersPopulated){
// Execution measurment start :
scenarioResultObjLocal.global.executionStartTime=getNow();
// DBG
console.log("²²²²²² expectedResult",expectedResult);
var functionResult=null;
if(expectedResult){
if(typeof (expectedResult) === "function"){
// If we test according to result check only : (Here we only verify the existence of a result or not)
window.aotestAllTestsManager.activateAsserts=false;
functionResult=functionDefinition.apply(actualInstance, functionParametersPopulated);
if(!expectedResult(functionResult)){
// TRACE
scenarioResultObjLocal.global.message="FAIL: No result detected after function execution but one expected.";
scenarioResultObjLocal.global.isFailed=true;
}
} else if(expectedResult.asserts){
// If we test according to asserts only :
window.aotestAllTestsManager.activateAsserts=true;
try {
functionResult=functionDefinition.apply(actualInstance, functionParametersPopulated);
} catch (e){
// TRACE
scenarioResultObjLocal.global.message="FAIL: Scenario interrupted ! Assert result : «" + e + "».";
scenarioResultObjLocal.global.isFailed=true;
}
// If we expect that asserts will fail (at least one of them), then
// the test is passed though !
// Normal logic if we expect, at the contrary, all asserts to succeed
// :
// (ie. «at least one assert must fail» or «all asserts must succeed»)
// expected result.
if(expectedResult.asserts === "fail"){
// TRACE
scenarioResultObjLocal.global.message="FAIL: Assert scenario was in «fail» expectation logic.";
scenarioResultObjLocal.global.isFailed=!scenarioResultObjLocal.global.isFailed;
}
} else {
// If we test according to result check only :
window.aotestAllTestsManager.activateAsserts=false;
functionResult=functionDefinition.apply(actualInstance, functionParametersPopulated);
if(!expectedResult.notResult){
if(expectedResult !== functionResult){
// TRACE
scenarioResultObjLocal.global.message="FAIL: Expected result does not match function execution result.";
scenarioResultObjLocal.global.isFailed=true;
}
}else{
if(expectedResult.notResult === functionResult){
// TRACE
scenarioResultObjLocal.global.message="FAIL: Expected result matches unwanted function execution result.";
scenarioResultObjLocal.global.isFailed=true;
}
}
}
} else {
// If we test according to asserts only, and not caring about the
// function returned result :
window.aotestAllTestsManager.activateAsserts=true;
try {
functionResult=functionDefinition.apply(actualInstance, functionParametersPopulated);
} catch (e){
// TRACE
scenarioResultObjLocal.global.message="FAIL: Scenario interrupted ! Assert result : «" + e + "».";
scenarioResultObjLocal.global.isFailed=true;
}
}
// *****************************************
// We check function/methods calls here :
// (overrides all other previous test result calculations)
// TRACE
var chosenScenarioParamName=null;
if(!isChosenScenarioParamArray) chosenScenarioParamName=chosenScenarioParam.name;
// UNUSED :
// TODO : FIXME : On today, tracking is deactivated for aotest, because random unresolved bugs occur :
// var callsJSONConfig=null;
// if(!isChosenScenarioParamArray){
// callsJSONConfig=chosenScenarioParam.calls;
// }
//
//
//
// if( // (to avoid calculating more if test has already failed :)
// !scenarioResultObjLocal.global.isFailed && callsJSONConfig){
// var currentRunningScenarioLocal=window.aotestAllTestsManager.currentRunningScenario;
//
// // TRACE
// console.log("LOG: Tracking results for scenario «" + chosenScenarioParam.name + "»");
//
//
// foreach(currentRunningScenarioLocal.callsCounts, (calculatedCount,key)=>{
//
// // console.log("callsCounts «"+key+"»: "+calculatedCount+" ; EXPECTED :
// // "+callsJSONConfig[key]);
// if(callsJSONConfig[key] && isNumber(callsJSONConfig[key])){
// if(callsJSONConfig[key] != calculatedCount){
//
// // TRACE
// scenarioResultObjLocal.global.message="FAIL: Scenario called function the wrong amount of times.";
// scenarioResultObjLocal.global.isFailed=true;
//
// }
// }
// // We reset count :
// currentRunningScenarioLocal.callsCounts[key]=0;
// });
//
// foreach(currentRunningScenarioLocal.throwsCounts, (calculatedCount, key)=>{
//
// // console.log("throwsCounts «"+key+"»: "+calculatedCount+" ; EXPECTED :
// // "+callsJSONConfig[key]);
// if(callsJSONConfig[key] && callsJSONConfig[key] === "throw"){
// if(calculatedCount === 0){
//
// // TRACE
// scenarioResultObjLocal.global.message="FAIL: Scenario expected an exception throw on function the wrong and recorded none.";
// scenarioResultObjLocal.global.isFailed=true;
//
// }
// }
// // We reset count :
// currentRunningScenarioLocal.throwsCounts[key]=0;
// });
//
// }
// Execution measurment end :
scenarioResultObjLocal.global.executionEndTime=getNow();
} else { // IF NOT (functionDefinition && functionParametersPopulated)
// If no function is the actual object of the test, but if all is in the expected
// result function (example : for UI tests !) :
// (reminder : if functionParametersPopulated array is NULL, then we don't
// want to execute the function !!)
if(expectedResult){
if(typeof (expectedResult) === "function"){
// In this special case, asserts are activated within this function,
// expectedResult() (UI tests):
window.aotestAllTestsManager.activateAsserts=true;
// But in this mode, we don't have the choice to set the asserts logic
// (i.e. «at least one assert must fail» or «all asserts must
// succeed») expected result.
try {
// If we test according to result check only : (Here we only verify the existence of a result or not)
if(!expectedResult()){
// TRACE
scenarioResultObjLocal.global.message="FAIL: No result detected after function execution but one expected.";
scenarioResultObjLocal.global.isFailed=true;
}
} catch (e){
// TRACE
scenarioResultObjLocal.global.message="FAIL: Scenario interrupted ! Assert result : «" + e + "».";
scenarioResultObjLocal.global.isFailed=true;
}
}
}
// There can be no other case if we are in UI tests case.
}
// States comparison test handling :
// (overrides all other previous test result calculations)
if( // (to avoid calculating more if test has already failed :)
!scenarioResultObjLocal.global.isFailed
&& !isChosenScenarioParamArray
&& chosenScenarioParam.after){
let beforeResultFlat=currentExecutedTest.oldResultFlat; // expected result
// At this point, we already have applied the method to the input object, in order to compare it with the output object, if necessary.
let actualInstanceFlat=(actualInstance!=null?getAsFlatStructure(actualInstance):null); // actual instance (after execution)
let actualArgsFlat=(functionParametersPopulated!=null?getAsFlatStructure(functionParametersPopulated):null); // actual arguments (after execution)
let afterInstanceFlat=null;
let afterArgsFlat=null;
let afterResultFlat=null;
if(chosenScenarioParam.after.alreadyFlat){
afterInstanceFlat=chosenScenarioParam.after.afterInstance; // EXPECTED instance
afterArgsFlat=chosenScenarioParam.after.methodArgs; // EXPECTED arguments
afterResultFlat=functionResult; // actual result
}else{
afterInstanceFlat=getAsFlatStructure(chosenScenarioParam.after.afterInstance); // EXPECTED instance
afterArgsFlat=getAsFlatStructure(chosenScenarioParam.after.methodArgs); // EXPECTED arguments
afterResultFlat=getAsFlatStructure(functionResult); // actual result (after execution)
}
let areEquivalentInstances=(blank(actualInstanceFlat) && blank(afterInstanceFlat)) || areEquivalentFlatMaps(actualInstanceFlat,afterInstanceFlat);
// // DBG
// console.log("---------------------------------------------");
// console.log(" actualInstanceFlat:",actualInstanceFlat);
// console.log(" afterInstanceFlat:",afterInstanceFlat);
// console.log("-----");
// console.log(" beforeResultFlat:",beforeResultFlat);
// console.log(" afterResultFlat:",afterResultFlat);
// console.log("-----");
// console.log(" actualArgsFlat:",actualArgsFlat);
// console.log(" afterArgsFlat:",afterArgsFlat);
// console.log("---------------------------------------------");
let areEquivalentArgs=(blank(actualArgsFlat) && blank(afterArgsFlat))
|| areEquivalentFlatMaps(actualArgsFlat,afterArgsFlat);
let areEquivalentResults=!!(blank(beforeResultFlat) && blank(afterResultFlat)
|| areEquivalentFlatMaps(beforeResultFlat,afterResultFlat) ) /*(forced to boolean)*/;
if( !areEquivalentInstances
|| !areEquivalentArgs
|| !areEquivalentResults){
scenarioResultObjLocal.global.message=
"FAIL: Scenario failed !"
+(areEquivalentInstances?"":"(instances are not equivalent) ")
+(areEquivalentArgs?"":"(functions arguments are not equivalent) ")
+(areEquivalentResults?"":"(results are not equivalent) ")
;
scenarioResultObjLocal.global.isFailed=true;
}
// DBG
console.log("³³³³³³ areEquivalentInstances",areEquivalentInstances);
console.log("³³³³³³ areEquivalentArgs",areEquivalentArgs);
console.log("³³³³³³ areEquivalentResults",areEquivalentResults);
}
// We check sub-scenarii here :
var subScenariiList=null;
if(!isChosenScenarioParamArray)
subScenariiList=chosenScenarioParam.subs;
let oneChildFailed=false;
if(subScenariiList && typeof subScenariiList === "object"){
foreach(subScenariiList, (subScenario,subScenarioNameLocal)=>{
// DBG
console.log("WE RUN SUB SCENARIO !!!");
// Recursive call :
let childrenResultObj=runOneTestOneScenario(chosenTestParam, subScenario);
if(childrenResultObj.global.isFailed){
oneChildFailed=true;
scenarioResultObjLocal.global.childrenFailed++;
}
scenarioResultObjLocal.global.childrenRan++;
// We add the sub-scenario result to the children array:
scenarioResultObjLocal.children[subScenarioNameLocal]=childrenResultObj;
});
}
if(FAIL_IF_CHILDREN_FAILED){
if(oneChildFailed){
// TRACE
scenarioResultObjLocal.global.message="FAIL: This scenario failed only because some of its children failed.";
scenarioResultObjLocal.global.isFailed=true;
}
}
// DBG
console.log("&&&&&& chosenScenarioParamName:",chosenScenarioParamName);
if(!scenarioResultObjLocal.global.isFailed){
// TRACE
scenarioResultObjLocal.global.message="SUCCESS: Scenario «"+chosenScenarioParamName+"» passed successfully.";
scenarioResultObjLocal.global.isFailed=false;
}
return scenarioResultObjLocal;
};
// Function 2, case run one test>all scenarii
const runOneTestAllScenarii=function(chosenTestParam){
// We init a new empty test result object :
let testResultObjLocal={
testName:chosenTestParam.name,
nature: "htmlsj",
global:{
type:"testResult",
childrenFailed:0,
childrenRan:0,
isFailed:false,
message:""
},
children:{}
};
if(chosenTestParam["prerequisite"]){
// TRACE
console.log("LOG: Running prerequisite for «" + chosenTestParam.name + "»...");
// Prerequisite function running :
chosenTestParam.prerequisite();
}
// TRACE
console.log("LOG: Running all test «" + chosenTestParam.name + "» scenarii...");
const scenariiListLocal=chosenTestParam.scenariiList;
// // DBG
// console.log("chosenTestParam",chosenTestParam);
foreach(aotestMethods.getScenariiInSingleTest(scenariiListLocal), (scenario,scenarioNameLocal)=>{
// DBG
console.log("WE RUN ONE SCENARIO !!!");
let scenarioResultObjLocal=runOneTestOneScenario(chosenTestParam, scenariiListLocal[scenarioNameLocal]);
if(scenarioResultObjLocal.global.isFailed){
// TRACE
testResultObjLocal.global.message="FAIL: This test failed only because some of its children failed.";
testResultObjLocal.global.isFailed=true;
testResultObjLocal.global.childrenFailed++;
}
testResultObjLocal.global.childrenRan++;
// We add the scenario result to the children array:
testResultObjLocal.children[scenarioNameLocal]=scenarioResultObjLocal;
});
if(chosenTestParam.clean){
// TRACE
console.log("LOG: Running clean for «" + chosenTestParam.name + "»...");
// Clean function running :
chosenTestParam.clean();
}
return testResultObjLocal;
};
// Function 3, case run all tests>all scenarii
const runAllTestsAllScenarii=function(){
// TRACE
console.log("LOG: Running all tests...");
// We init a new empty all tests result object :
let allTestsResultObjLocal={
nature: "htmlsj",
global:{
type:"testsSuiteResult",
childrenFailed:0,
childrenRan:0,
isFailed:false,
message:""
},
children:{}
};
var allTestsLocal=window.aotestAllTestsManager;
foreach(allTestsLocal,(test,testNameLocal)=>{
let testResultObj=runOneTestAllScenarii(allTestsLocal[testNameLocal]);
if(testResultObj.global.isFailed){
// TRACE
allTestsResultObjLocal.global.message="FAIL: This all-tests run suite failed only because some of its children failed.";
allTestsResultObjLocal.global.isFailed=true;
allTestsResultObjLocal.global.childrenFailed++;
}
allTestsResultObjLocal.global.childrenRan++;
// We add the scenario result to the children array:
allTestsResultObjLocal.children[testNameLocal]=testResultObj;
},(test,testNameLocal)=>{ return !window.aotestMethods.aotratestKeyInPredefinedParameters(testNameLocal); });
return allTestsResultObjLocal;
};
// ------
// «MAIN»
// ------
// We init a new empty all tests result object :
let allTestsResultObj={
testName:testName,
nature: "htmlsj",
global:{
type:"testsSuiteResult",
childrenFailed:0,
childrenRan:0,
isFailed:false,
message:""
},
children:{}
};
if(testName && window.aotestAllTestsManager[testName]){
var chosenTest=window.aotestAllTestsManager[testName];
if(scenarioName && chosenTest.scenariiList[scenarioName]){
var chosenScenario=chosenTest.scenariiList[scenarioName];
// DBG
console.log("Case run one test -> one scenario");
// We create the single child of this tests suite in the children array:
allTestsResultObj.children[testName]={
testName:testName,
scenarioName:scenarioName,
nature: "htmlsj",
global:{
type:"testResult",
childrenFailed:0,
childrenRan:0,
isFailed:false,
message:""
},
children:{}
};
let testResultObj=allTestsResultObj.children[testName];
// Case run one test -> one scenario (1->1)
let scenarioResultObj=runOneTestOneScenario(chosenTest, chosenScenario);
if(scenarioResultObj.global.isFailed){
// TRACE
testResultObj.global.message="FAIL: This all-tests run suite failed only because some of its children (scenario) failed.";
testResultObj.global.isFailed=true;
testResultObj.global.childrenFailed++;
}
testResultObj.global.childrenRan++;
// We add the scenario result to the children array of the only child of this tests suite:
testResultObj.children[scenarioName]=scenarioResultObj;
} else {
// DBG
console.log("Case run one test -> all scenarii");
// Case run one test -> all scenarii (1->*)
let testResultObj=runOneTestAllScenarii(chosenTest);
if(testResultObj.global.isFailed){
// TRACE
allTestsResultObj.global.message="FAIL: This all-tests run suite failed only because some of its children (test) failed.";
allTestsResultObj.global.isFailed=true;
allTestsResultObj.global.childrenFailed++;
}
allTestsResultObj.global.childrenRan++;
// We add the test result to the children array:
allTestsResultObj.children[testName]=testResultObj;
}
} else {
// DBG
console.log("Case run all tests -> all scenarii");
// Case run all tests -> all scenarii (*->*)
let testsSuiteResultObj=runAllTestsAllScenarii();
// The test suite is the executed test suite :
allTestsResultObj=testsSuiteResultObj;
}
var buildHRReport=function(resultObj){
let str="";
let resultsFailed=getCumulatedInt(resultObj,"childrenFailed");
let resultsRan=getCumulatedInt(resultObj,"childrenRan");
// TRACE
str+="@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@\n";
str+=" AOTEST REPORT \n";
str+="REPORT: "+resultsFailed + "/" + resultsRan + " scenarii failed.\n";
str+="REPORT: "+(resultsRan - resultsFailed) + "/" + resultsRan + " scenarii succeeded.\n";
str+="REPORT: "+resultsRan + " scenarii ran.\n";
str+="@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@\n";
if(0<resultsRan){
if(!resultObj.global.isFailed){
// TRACE
str+="LOG: RESULT :>>>>>> All tests SUCCEEDED ! :D\n";
} else {
// TRACE
str+="LOG: RESULT :>>>>>> Some tests FAILED... :(\n";
}
} else {
// TRACE
str+="LOG: RESULT :>>>>>> No tests ran... :|\n";
}
return str;
};
var getCumulatedInt=function(resultObj, attrName){
let result=0;
result+=resultObj.global[attrName];
foreach(resultObj.children,(child)=>{
result+=getCumulatedInt(child, attrName);
});
return result;
};
// TRACE
console.log( buildHRReport(allTestsResultObj));
// DBG
console.log("TOTAL RESULT :",allTestsResultObj);
aotest.isRunning=false;
return allTestsResultObj;
};
aotest.profile=function(rootObject,methodName,visited=[]){
//if(!rootObject || !isObject(rootObject) || contains(visited,rootObject)) return;
if( !rootObject || isPrimitive(rootObject) || isFunction(rootObject)
|| contains(visi