@openui5/sap.ui.core
Version:
OpenUI5 Core Library sap.ui.core
771 lines (692 loc) • 23.7 kB
JavaScript
/*!
* OpenUI5
* (c) Copyright 2009-2021 SAP SE or an SAP affiliate company.
* Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
*/
(function () {
"use strict";
/*global _$blanket, blanket, falafel, Map, QUnit */
/*eslint no-alert: 0, no-warning-comments: 0 */
var aFileNames = [], // maps a file's index to its name
oScript = getScriptTag(),
aStatistics = [], // maps a file's index to its "hits" array (and statistics record)
iThreshold,
rWordChar = /\w/; // a "word" (= identifier) character
/**
* Keep track of branch coverage.
*
* @param {number} iFileIndex
* The current file's index
* @param {number} iBranchIndex
* The current branch's index
* @param {any} vCondition
* The current branch's condition
* @param {number} [iLine]
* The current branch's line number (for statement coverage)
* @returns {any}
* <code>vCondition</code>
*/
function branchTracking(iFileIndex, iBranchIndex, vCondition, iLine) {
if (iBranchIndex >= 0) {
if (vCondition) {
aStatistics[iFileIndex].branchTracking[iBranchIndex].truthy += 1;
} else {
aStatistics[iFileIndex].branchTracking[iBranchIndex].falsy += 1;
}
}
if (iLine >= 0) {
lineTracking(iFileIndex, iLine);
}
return vCondition;
}
/**
* Returns the element's attribute as an integer.
*
* @param {Element} oElement The element
* @param {string} sAttributeName The attribute name
* @param {number} iDefault The default value
* @returns {number} The attribute value or the default value if the attribute value is not a
* positive number
*/
function getAttributeAsInteger(oElement, sAttributeName, iDefault) {
var iValue = parseInt(oElement.getAttribute(sAttributeName));
// Note: if the value is not a number, the result is NaN which is not greater than 0
return iValue > 0 ? iValue : iDefault;
}
/**
* Determines the script tag that loaded this script.
*
* @return {Element}
* The script tag that loaded this script
*/
function getScriptTag() {
var oScript,
aScripts = document.getElementsByTagName("script"),
i, n;
for (i = 0, n = aScripts.length; i < n; i += 1) {
oScript = aScripts[i];
if (/BranchTracking.js$/.test(oScript.getAttribute("src"))) {
return oScript;
}
}
}
/**
* Replacement for Blanket.js' <code>instrument</code> function.
*
* @param {object} oConfiguration
* Configuration object
* @param {string} oConfiguration.inputFile
* The current file's original source code
* @param {string} oConfiguration.inputFileName
* The current file's name
* @param {function} fnSuccess
* Success handler, called with resulting instrumented source code
*/
function instrument(oConfiguration, fnSuccess){
var bBranchTracking = blanket.options("branchTracking"),
bComment = false, // interested in meta comments?
Device,
iFileIndex = aFileNames.length,
sFileName = oConfiguration.inputFileName,
iNoOfOutputLines,
sScriptInput = oConfiguration.inputFile,
sScriptOutput;
if (sScriptInput.indexOf("// sap-ui-cover-browser msie") >= 0) {
bComment = true; // needed by isDeviceSpecificBlock(), no matter which device
Device = sap.ui.require("sap/ui/Device");
if (Device && Device.browser.msie) {
// no need to call isChildOfIgnoredNode()
Device = undefined;
}
}
aFileNames.push(sFileName);
aStatistics[iFileIndex] = _$blanket[sFileName] = []; // hits
if (bBranchTracking) {
_$blanket[sFileName].branchTracking = [];
}
_$blanket[sFileName].source = sScriptInput.split("\n");
_$blanket[sFileName].source.unshift(""); // line 0 does not exist!
_$blanket[sFileName].warnings = [];
sScriptOutput = "" + falafel(sScriptInput, {
attachComment : bComment,
comment : bComment,
loc : true,
range : true,
source : sScriptInput // is simply attached to each Location
// tokens : false,
// tolerant : false
}, visit.bind(null, bBranchTracking, iFileIndex, Device));
iNoOfOutputLines = sScriptOutput.split("\n").length + 1; // account for line 0 here as well
if (iNoOfOutputLines !== _$blanket[sFileName].source.length) {
warn(sFileName, "Line length mismatch! " + _$blanket[sFileName].source.length + " vs. "
+ iNoOfOutputLines);
}
fnSuccess(sScriptOutput);
}
/**
* Returns whether the given node or one of its ancestors is device-specific for a device
* other than what the given <code>Device</code> indicates.
*
* @param {sap.ui.Device} Device
* Device
* @param {object} oNode
* AST node
* @returns {boolean}
* Whether the given node or one of its ancestors is device-specific for another device
*/
function isChildOfIgnoredNode(Device, oNode) {
if (!("$ignored" in oNode)) {
if (oNode.parent && isChildOfIgnoredNode(Device, oNode.parent)) {
oNode.$ignored = true;
} else { // ignore device-specific code on other devices
oNode.$ignored = oNode.type === "BlockStatement"
&& isDeviceSpecificBlock(Device, oNode);
}
}
return oNode.$ignored;
}
/**
* Returns whether the given block statement node is device-specific (in general, or for a
* device other than what the given <code>Device</code> indicates).
*
* @param {sap.ui.Device} [Device]
* Optional device API; without it, the meta comment alone counts
* @param {object} oNode
* AST node
* @returns {boolean}
* Whether the given block statement node is device-specific for another device
*/
function isDeviceSpecificBlock(Device, oNode) {
/*
* Tells whether the given comment is a meta comment for device-specific code (in general,
* if no <code>Device</code> is available, or for a device other than what the available
* <code>Device</code> indicates).
*
* @param {string} oComment
* A single block or end-of-line comment
* @returns {boolean}
* Whether the given comment is a meta comment for device-specific code (see above)
*/
function isNotForDevice(oComment) {
return oComment.type === "Line" && oComment.value === " sap-ui-cover-browser msie"
&& !(Device && Device.browser.msie);
}
return oNode.body[0] && oNode.body[0].leadingComments
&& oNode.body[0].leadingComments.some(isNotForDevice);
}
/**
* Keep track of statement coverage.
*
* @param {number} iFileIndex
* The current file's index
* @param {number} iLine
* The current line number
*/
function lineTracking(iFileIndex, iLine) {
aStatistics[iFileIndex][iLine] += 1;
}
/**
* Visit the given node, maybe instrument it.
*
* Note: "The recursive walk is a pre-traversal, so children get called before their parents."
*
* Note: We make no attempt to check that the global variable <code>blanket</code> is not
* redefined. We don't rely on <code>window</code>. No support for labeled statements
* (see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/label).
* We rely on ESLint, e.g. http://eslint.org/docs/rules/curly.
*
* @param {boolean} bBranchTracking
* Whether branch tracking is on
* @param {number} iFileIndex
* The current file's index
* @param {sap.ui.Device} [Device]
* Device
* @param {object} oNode
* AST node
* @returns {boolean}
* Whether <code>oNode.update()</code> has been used (Note: works only once!). This is just
* meant to keep track for future internal usage and is actually ignored by Falafel's
* <code>walk</code>.
*/
function visit(bBranchTracking, iFileIndex, Device, oNode) {
var aHits = aStatistics[iFileIndex],
aBranchTracking = aHits.branchTracking,
iLine = oNode.loc.start.line,
sNewSource;
/*
* Adds line tracking instrumentation to the current node.
*
* @returns {boolean}
* <code>true</code> because <code>oNode.update()</code> has been used
*/
function addLineTracking() {
oNode.update("blanket.$l(" + iFileIndex + ", " + iLine + "); " + oNode.source());
initHits();
return true;
}
/*
* Initialize hits count for current line, double check for duplicates.
*/
function initHits() {
if (iLine in aHits) {
warn(iFileIndex, "Multiple statements on same line detected"
+ " – minified code not supported! Line number " + iLine);
}
aHits[iLine] = 0;
}
/*
* Preserve operator's source code incl. comments and line breaks, but avoid leading closing
* or trailing opening parentheses.
*
* Note: outer parentheses are absorbed by operators and do not appear in operand's source!
*
* @returns {string}
*/
function operator() {
var sSource = oNode.loc.source.slice(oNode.left.range[1], oNode.right.range[0]);
if (sSource[0] === ")") {
sSource = sSource.slice(1);
}
if (sSource.slice(-1) === "(") {
sSource = sSource.slice(0, -1);
}
return sSource;
}
if (Device && isChildOfIgnoredNode(Device, oNode)) {
return false;
}
switch (oNode.type) {
case "FunctionDeclaration":
case "FunctionExpression":
if (oNode.body.body[0] && iLine === oNode.body.body[0].loc.start.line) {
warn(iFileIndex, "Function body must not start on same line! Line number "
+ iLine);
// aHits[iLine] = NaN; //TODO find an easy way to mark this line as "missed"
}
break;
default:
}
switch (oNode.type) {
case "AssignmentExpression":
case "ArrayExpression":
case "BlockStatement":
case "BinaryExpression":
case "Block": // block comment
case "CallExpression":
case "CatchClause": //TODO coverage for empty blocks?
case "DebuggerStatement":
case "EmptyStatement": //TODO coverage?!
case "FunctionExpression":
case "Identifier":
case "Line": // end-of-line comment
case "Literal":
case "MemberExpression":
case "NewExpression":
case "ObjectExpression":
case "Program":
case "Property":
case "SequenceExpression":
case "SwitchCase": //TODO (statement) coverage!
case "ThisExpression":
case "UnaryExpression":
case "UpdateExpression":
case "VariableDeclarator":
return false;
case "ConditionalExpression":
if (!bBranchTracking) {
return false;
}
oNode.test.update("blanket.$b(" + iFileIndex + ", " + aBranchTracking.length + ", "
+ oNode.test.source() + ")"
);
aBranchTracking.push({
alternate : oNode.alternate.loc,
consequent : oNode.consequent.loc,
falsy : 0,
truthy : 0
});
return true;
case "ExpressionStatement":
if (oNode.expression.type === "Literal"
&& oNode.expression.value === "use strict") {
return false; // do not instrument "use strict"; it would break it!
}
// fall through
case "DoWhileStatement":
case "ForInStatement":
case "ForStatement":
case "WhileStatement":
case "WithStatement":
// Note: we assume block statements only (@see blanket._blockifyIf)
case "BreakStatement":
case "ContinueStatement":
case "FunctionDeclaration":
case "ReturnStatement":
case "SwitchStatement":
case "ThrowStatement":
case "TryStatement":
return addLineTracking(oNode);
case "IfStatement":
if (isDeviceSpecificBlock(undefined, oNode.consequent)) {
// Note: if "then" is device-specific, we cannot expect branch coverage of "if"
bBranchTracking = false;
}
// Note: we assume block statements only (@see blanket._blockifyIf)
oNode.test.update("blanket.$b(" + iFileIndex + ", "
+ (bBranchTracking ? aBranchTracking.length : -1) + ", "
+ oNode.test.source() + ", " + iLine + ")"
);
initHits();
if (bBranchTracking) {
aBranchTracking.push({
// Note: in case of missing "else" we blame it on the condition
alternate : (oNode.alternate || oNode.test).loc,
consequent : oNode.consequent.loc,
falsy : 0,
truthy : 0
});
} // else would like to fall through to line tracking, but that does not work for
// "else if" unless s.th. like blanket._blockifyIf is used!
return true;
case "LogicalExpression":
if (!bBranchTracking) {
return false;
}
if (oNode.operator === "||" || oNode.operator === "&&") {
// Note: (...) around right source!
sNewSource = "blanket.$b(" + iFileIndex + ", " + aBranchTracking.length + ", "
+ oNode.left.source() + ") "
+ operator() + " (" + oNode.right.source() + ")";
if (!rWordChar.test(oNode.loc.source[oNode.range[0]])) {
// Note: handle minified code like "return!x||y;"
sNewSource = " " + sNewSource;
}
oNode.update(sNewSource);
aBranchTracking.push({
alternate : oNode.operator === "&&"
? oNode.left.loc
: oNode.right.loc,
consequent : oNode.operator === "&&"
? oNode.right.loc
: oNode.left.loc,
falsy : 0,
truthy : 0
});
}
return true;
case "VariableDeclaration":
if (oNode.parent.type === "ForInStatement"
|| oNode.parent.type === "ForStatement") {
return false;
}
return addLineTracking(oNode);
case "LabeledStatement":
default:
throw new Error(oNode.source());
}
}
/**
* Logs the given message related to the given file both as a warning on console and as a
* warning to be reported inside QUnit.module's "before" hook.
*
* @param {number|string} vFile - the affected file's index or name
* @param {string} sMessage - a message
*/
function warn(vFile, sMessage) {
var sFileName = typeof vFile === "string"
? vFile
: aFileNames[vFile];
jQuery.sap.log.warning(sMessage, sFileName, "sap.ui.test.BranchTracking");
_$blanket[sFileName].warnings.push(sMessage);
}
/**
* Listens on QUnit and delivers a function that returns the tested modules.
*
* @returns {function} A function that delivers the tested modules or <code>undefined</code> if
* all modules have been tested
*/
function listenOnQUnit() {
var mModules = {},
aTestedModules = [],
iTotalModules;
QUnit.begin(function (oDetails) {
iTotalModules = oDetails.modules.length;
oDetails.modules.forEach(function (oModule) {
mModules[oModule.name] = oModule;
});
});
QUnit.moduleStart(function (oModule) {
// Why, oh why, is the module name different here?
aTestedModules = aTestedModules.concat(
Object.keys(mModules).filter(function (sModuleName) {
return mModules[sModuleName].tests === oModule.tests;
})
);
});
return function () {
return aTestedModules.length < iTotalModules ? aTestedModules : undefined;
};
}
if (window.blanket) {
window._$blanket = {}; // maps a file's name to its statistics array
blanket.$b = branchTracking;
blanket.$l = lineTracking;
blanket.instrument = instrument; // self-made "plug-in" ;-)
var fnGetTestedModules = listenOnQUnit(),
iLinesOfContext = getAttributeAsInteger(oScript, "data-lines-of-context", 3);
iThreshold = Math.min(getAttributeAsInteger(oScript, "data-threshold", 0), 100);
// Note: instrument() MUST have been replaced before!
sap.ui.require(["sap/ui/test/BlanketReporter"], function (BlanketReporter) {
blanket.options("reporter",
BlanketReporter.bind(null, iLinesOfContext, iThreshold, fnGetTestedModules));
});
}
//**********************************************************************************************
// Code for tracking "Uncaught (in promise)" for sap.ui.base.SyncPromise inside QUnit tests
// and for checking isolated code coverage (that is, each "class" by its corresponding test)
//**********************************************************************************************
var bInfo,
sClassName = "sap.ui.base.SyncPromise",
mFileName2InitialHits = {},
sFilter,
fnModule,
iNo = 0,
sTestId,
mUncaughtById = {},
mUncaughtPromise2Reason = new Map();
/**
* Check isolated line/branch coverage for the given test.
*
* @param {object} oTest - QUnit's test environment
* @param {object} assert - QUnit's object with the assertion methods
*/
function checkIsolatedCoverage(oTest, assert) {
var aBranchesWithUnchangedHits,
aHits = _$blanket[oTest.$currentFileName],
aInitialHits = mFileName2InitialHits[oTest.$currentFileName],
aLinesWithUnchangedHits;
if (oTest.$oldHits) {
aLinesWithUnchangedHits = Object.keys(aHits).filter(function (iLine) {
return !(aInitialHits && aInitialHits[iLine])
&& aHits[iLine] === oTest.$oldHits[iLine];
});
assert.notOk(aLinesWithUnchangedHits.length,
"Some lines have not been covered by this module in isolation: "
+ aLinesWithUnchangedHits);
}
if (oTest.$oldBranchTracking) {
aBranchesWithUnchangedHits = Object.keys(aHits.branchTracking).filter(function (i) {
return aHits.branchTracking[i].falsy === oTest.$oldBranchTracking[i].falsy
|| aHits.branchTracking[i].truthy === oTest.$oldBranchTracking[i].truthy;
});
assert.notOk(aBranchesWithUnchangedHits.length,
"Some branches have not been fully covered by this module in isolation: "
+ aBranchesWithUnchangedHits);
}
}
/**
* Check for uncaught errors in sync promises and provide an appropriate report to the given
* optional reporter.
*
* @param {function} [fnReporter]
* Optional reporter to receive a report
*/
function checkUncaught(fnReporter) {
var sId,
iLength = Object.keys(mUncaughtById).length
+ (mUncaughtPromise2Reason ? mUncaughtPromise2Reason.size : 0),
sMessage = "Uncaught (in promise): " + iLength + " times\n",
oPromise,
vReason,
oResult,
itValues;
if (iLength) {
for (sId in mUncaughtById) {
oPromise = mUncaughtById[sId];
if (oPromise.getResult() && oPromise.getResult().stack) {
sMessage += oPromise.getResult().stack;
} else {
sMessage += oPromise.getResult();
}
if (oPromise.$error.stack) {
sMessage += "\n>>> SyncPromise rejected with above reason...\n"
+ oPromise.$error.stack.split("\n").slice(2).join("\n"); // hide listener
}
sMessage += "\n\n";
}
mUncaughtById = {};
//TODO once IE is gone: for (let vReason of mUncaughtPromise2Reason.values()) {...}
if (mUncaughtPromise2Reason && mUncaughtPromise2Reason.size) {
itValues = mUncaughtPromise2Reason.values();
for (;;) {
oResult = itValues.next();
if (oResult.done) {
break;
}
vReason = oResult.value;
sMessage += (vReason && vReason.stack || vReason) + "\n\n";
}
mUncaughtPromise2Reason.clear();
}
if (fnReporter) {
fnReporter(sMessage);
} else if (bInfo) {
jQuery.sap.log.info("Clearing " + iLength + " uncaught promises", sMessage,
sClassName);
}
}
}
if (oScript.getAttribute("data-uncaught-in-promise") !== "true") {
/*
* Listener for "unhandledrejection" events to keep track of "Uncaught (in promise)".
*/
window.addEventListener("unhandledrejection", function (oEvent) {
if (oEvent.reason && oEvent.reason.$uncaughtInPromise) { // ignore exceptional cases
return;
}
if (mUncaughtPromise2Reason) {
mUncaughtPromise2Reason.set(oEvent.promise, oEvent.reason);
oEvent.preventDefault(); // do not report on console
} else { // QUnit already done
alert("Uncaught (in promise) " + oEvent.reason);
}
});
/*
* Listener for "rejectionhandled" events to keep track of "Uncaught (in promise)".
*/
window.addEventListener("rejectionhandled", function (oEvent) {
if (mUncaughtPromise2Reason) {
mUncaughtPromise2Reason.delete(oEvent.promise);
}
});
}
/**
* Listener for sync promises which become (un)caught.
*
* @param {sap.ui.base.SyncPromise} oPromise
* A sync promise
* @param {boolean} bCaught
* Tells whether the given sync promise became caught
*/
function listener(oPromise, bCaught) {
if (bCaught) {
delete mUncaughtById[oPromise.$id];
if (bInfo) {
jQuery.sap.log.info("Promise " + oPromise.$id + " caught",
Object.keys(mUncaughtById), sClassName);
}
return;
}
oPromise.$id = iNo++;
oPromise.$error = new Error();
mUncaughtById[oPromise.$id] = oPromise;
if (bInfo) {
jQuery.sap.log.info("Promise " + oPromise.$id + " rejected with "
+ oPromise.getResult(), Object.keys(mUncaughtById), sClassName);
}
}
/**
* Wrapper for <code>QUnit.module</code> to check for uncaught errors in sync promises and
* for 100% isolated test coverage.
*
* @param {string} sTitle
* The module's title
* @param {object} [mHooks]
* Optional map of hooks, e.g. "beforeEach"
*/
function module(sTitle, mHooks) {
var fnAfter, fnAfterEach, fnBefore, fnBeforeEach;
mHooks = mHooks || {};
fnAfter = mHooks.after || function () {};
fnAfterEach = mHooks.afterEach || function () {};
fnBefore = mHooks.before || function () {};
fnBeforeEach = mHooks.beforeEach || function () {};
mHooks.after = function (assert) {
if (window.blanket && !sFilter && !sTestId && !this.__ignoreIsolatedCoverage__
&& iThreshold >= 100 && !assert.test.module.stats.bad) {
checkIsolatedCoverage(this, assert);
}
return fnAfter.apply(this, arguments);
};
mHooks.afterEach = function (assert) {
var fnCheckUncaught = checkUncaught.bind(null, assert.ok.bind(assert, false));
function error(oError) {
fnCheckUncaught();
throw oError;
}
function success(vResult) {
if (vResult && typeof vResult.then === "function") {
return vResult.then(success, error);
}
fnCheckUncaught();
return vResult;
}
try {
return success(fnAfterEach.apply(this, arguments));
} catch (oError) {
error(oError);
}
};
mHooks.before = function (assert) {
var aHits;
this.$currentFileName = jQuery.sap.getResourceName(assert.test.module.name);
aHits = window.blanket && _$blanket[this.$currentFileName];
if (aHits) {
this.$oldHits = aHits.slice();
if (aHits.branchTracking) {
this.$oldBranchTracking = JSON.parse(
JSON.stringify(aHits.branchTracking, ["falsy", "truthy"]));
}
aHits.warnings.forEach(function (sMessage) {
assert.ok(false, sMessage);
});
}
return fnBefore.apply(this, arguments);
};
mHooks.beforeEach = function (assert) {
checkUncaught(); // cleans up what happened before
return fnBeforeEach.apply(this, arguments);
};
fnModule(sTitle, mHooks);
}
if (QUnit.module !== module) {
fnModule = QUnit.module.bind(QUnit);
QUnit.module = module;
sap.ui.require([
"sap/base/Log",
"sap/base/util/UriParameters",
"sap/ui/base/SyncPromise"
], function (Log, UriParameters, SyncPromise) {
var oUriParameters = UriParameters.fromQuery(window.location.search);
bInfo = Log.isLoggable(Log.Level.INFO, sClassName);
sFilter = oUriParameters.get("filter");
sTestId = oUriParameters.get("testId");
SyncPromise.listener = listener;
});
// allow easier module selection: larger list, one click selection
QUnit.begin(function () {
var sFileName, aHits;
jQuery("#qunit-modulefilter-dropdown-list").css("max-height", "none");
jQuery("#qunit-modulefilter-dropdown").on("click", function (oMouseEvent) {
if (oMouseEvent.target.tagName === "LABEL") {
setTimeout(function () {
// click on label instead of checkbox triggers "Apply" automatically
jQuery("#qunit-modulefilter-actions").children().first().trigger("click");
});
}
});
if (window.blanket) {
// remember which lines have been covered initially, at load time
for (sFileName in _$blanket) {
aHits = _$blanket[sFileName];
mFileName2InitialHits[sFileName] = aHits.slice();
}
// Note: for SyncPromise, a lot of lines are already covered!
}
});
QUnit.done(function () {
mUncaughtPromise2Reason = null; // no use to keep track anymore
});
}
}());
//TODO add tooltips to highlighting to explain rules