@openui5/sap.ui.core
Version:
OpenUI5 Core Library sap.ui.core
400 lines (354 loc) • 12.7 kB
JavaScript
/*!
* OpenUI5
* (c) Copyright 2026 SAP SE or an SAP affiliate company.
* Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
*/
(function () {
"use strict";
/*global QUnit */
/*eslint no-alert: 0, no-warning-comments: 0 */
const oScript = Array.from(document.getElementsByTagName("script"))
.find((oScript) => /ModuleTracking.js$/.test(oScript.getAttribute("src")));
function clone(o) {
return o && JSON.parse(JSON.stringify(o));
}
//**********************************************************************************************
// Code for tracking "Uncaught (in promise)" for sap.ui.base.SyncPromise inside QUnit tests
//**********************************************************************************************
var Log,
sClassName = "sap.ui.base.SyncPromise",
iNo = 0,
rSearchParamWithoutValue = /(=)(?=&|$)/g,
mUncaughtById = {},
mUncaughtPromise2Reason = new Map();
/**
* 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 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 (Log) {
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 (Log) {
Log.info(`Promise ${oPromise.$id} caught`,
Object.keys(mUncaughtById), sClassName);
}
return;
}
oPromise.$id = iNo++;
oPromise.$error = new Error();
mUncaughtById[oPromise.$id] = oPromise;
if (Log) {
Log.info(`Promise ${oPromise.$id} rejected with ${oPromise.getResult()}`,
Object.keys(mUncaughtById), sClassName);
}
}
//**********************************************************************************************
// Methods dealing with Istanbul
//**********************************************************************************************
// the initial coverage per module (identified by resource name), taken when QUnit starts
let mInitialCoverageByModule;
const mCoverageSnapshotByModule = {};
const bTestsFiltered = /(testId|filter)=/.test(window.location.search);
function filterCoverage() {
if (window.__coverage__ && Object.keys(mCoverageSnapshotByModule).length) {
if (window.location.search.includes("moduleId=")) {
// only keep the coverage for the actually tested modules
window.__coverage__ = mCoverageSnapshotByModule;
} else {
// replace globally measured code coverage by isolated code coverage results
for (const [sModuleName, oCounts] of Object.entries(mCoverageSnapshotByModule)) {
setModuleCoverage(sModuleName, oCounts);
}
}
}
}
function getResourceName(sModuleName) {
if (!window.__coverage__) {
return undefined;
}
if (sModuleName.endsWith(".js")) {
return sModuleName;
}
const sPath = `${sModuleName.replace(/\./g, "/")}.js`;
return Object.keys(window.__coverage__)
.find((sResourceName) => sResourceName.endsWith(sPath));
}
/**
* Replaces the current code coverage counts for branches by the given counts.
*
* @param {object} oCurrent The current branch counts
* @param {object} oCounts The branch counts to be used
*/
function setBranchCounts(oCurrent, oCounts) {
const oCurrentBranchCounts = oCurrent.b;
const oBranchCounts = oCounts.b;
for (const [sKey, aInitialCounts] of Object.entries(oBranchCounts)) {
const aCurrentCounts = oCurrentBranchCounts[sKey];
aInitialCounts.forEach((iCount, i) => { aCurrentCounts[i] = iCount; });
}
}
/**
* Replaces the current code coverage counts by the given counts.
*
* @param {object} oCurrentCounts The current counts
* @param {object} oCounts The counts to be used
*/
function setCounts(oCurrentCounts, oCounts) {
for (const [sKey, iCount] of Object.entries(oCounts)) {
oCurrentCounts[sKey] = iCount;
}
}
/**
* Sets the measured counts to the given values, or to the initial values if isolated counts
* are not set.
*
* @param {string} sModuleName
* The module name
* @param {object} [oIsolatedCounts]
* The measured isolated counts; if not given the initial counts are used
*/
function setModuleCoverage(sModuleName, oIsolatedCounts) {
const sResourceName = getResourceName(sModuleName);
if (sResourceName) {
const oGlobalCounts = window.__coverage__[sResourceName];
const oCounts = oIsolatedCounts || mInitialCoverageByModule[sResourceName];
setBranchCounts(oGlobalCounts, oCounts);
setCounts(oGlobalCounts.f, oCounts.f);
setCounts(oGlobalCounts.s, oCounts.s);
}
}
function saveInitialCoverage() {
// the coverage from the module loading
mInitialCoverageByModule = clone(window.__coverage__);
}
/**
* Takes a snapshot and verify that all branches, statements and functions are covered.
*
* @param {string} sModuleName - The module name
* @returns {boolean} Whether the coverage is bad
*/
function handleModuleCoverage(sModuleName) {
const sResourceName = getResourceName(sModuleName);
if (!sResourceName) {
return false;
}
const oCoverage = clone(window.__coverage__[sResourceName]);
mCoverageSnapshotByModule[sResourceName] = oCoverage;
if (bTestsFiltered) {
return false;
}
return Object.values(oCoverage.b).some((aCounts) => aCounts.some((iCount) => iCount === 0))
|| Object.values(oCoverage.f).some((iCount) => iCount === 0)
|| Object.values(oCoverage.s).some((iCount) => iCount === 0);
}
//**********************************************************************************************
// Intercept QUnit
// Each module shall only be covered when its own tests run. For this purpose we keep the
// initial module states. When a module is started, we revert its coverage to the initial state.
// When it's finished we take a snapshot. In the end we only keep the snapshots.
//**********************************************************************************************
const fnModule = QUnit.module.bind(QUnit);
/**
* 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) {
mHooks = mHooks || {};
const fnAfter = mHooks.after || function () {};
const fnAfterEach = mHooks.afterEach || function () {};
const fnBefore = mHooks.before || function () {};
const fnBeforeEach = mHooks.beforeEach || function () {};
mHooks.after = function (assert) {
if (!this.__ignoreIsolatedCoverage__ && handleModuleCoverage(sTitle)) {
assert.ok(false, `${sTitle}: Coverage below 100%`);
}
return fnAfter.apply(this, arguments);
};
mHooks.afterEach = function (assert) {
const 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) {
return error(oError);
}
};
mHooks.before = function () {
const oResult = fnBefore.apply(this, arguments);
if (!this.__ignoreIsolatedCoverage__) {
setModuleCoverage(sTitle);
}
return oResult;
};
mHooks.beforeEach = function () {
checkUncaught(); // cleans up what happened before
return fnBeforeEach.apply(this, arguments);
};
fnModule(sTitle, mHooks);
}
if (QUnit.module !== module) {
QUnit.module = module;
sap.ui.require([
"sap/base/Log",
"sap/ui/base/SyncPromise"
], function (Log0, SyncPromise) {
if (Log0.isLoggable(Log0.Level.INFO, sClassName)) {
Log = Log0;
}
SyncPromise.listener = listener;
});
QUnit.begin(() => {
// allow easier module selection: larger list, one click selection
document.body.style.overflow = "scroll"; // always show scrollbar, avoid flickering
document.getElementById("qunit-modulefilter-dropdown-list").style.maxHeight = "none";
document.getElementById("qunit-modulefilter-dropdown")
.addEventListener("click", function (oMouseEvent) {
if (oMouseEvent.target.tagName === "LABEL"
&& oMouseEvent.target.innerText !== "All modules") {
setTimeout(function () {
// click on label instead of checkbox triggers "Apply" automatically
document.getElementById("qunit-modulefilter-actions").firstChild
.dispatchEvent(new MouseEvent("click"));
});
}
});
const oURLSearchParams = new URLSearchParams(document.location.search);
// add a button to clear the filter
if (oURLSearchParams.has("filter")) {
const oClearFilter = document.createElement("button");
oClearFilter.type = "button";
oClearFilter.innerText = "\u2715"; // X
oClearFilter.addEventListener("click", (ev) => {
oURLSearchParams.delete("filter");
document.location.search = oURLSearchParams.toString()
.replace(rSearchParamWithoutValue, '');
});
document.querySelector(".qunit-filter button")
.insertAdjacentElement("beforeBegin", oClearFilter);
}
// Add a hover effect to buttons (colors inspired by QUnit's own CSS)
const oStyle = document.createElement('style');
oStyle.innerText = `
button:hover {
background-color: #DDD !important;
}
button:focus {
box-shadow: 0 0 0 2px rgba(94, 116, 11, 0.5) !important;
}
`;
document.head.appendChild(oStyle);
// remember which lines have been covered initially, at load time
saveInitialCoverage();
});
QUnit.done(() => {
filterCoverage();
// "Run all tests" in "Rerunning selected tests" case: Same href as document, but remove
// testId
const oURLSearchParams = new URLSearchParams(document.location.search);
if (oURLSearchParams.has("testId")) {
const oClearFilter = document.querySelector("#qunit-clearFilter");
if (oClearFilter) {
oURLSearchParams.delete("testId");
const oRunModulesURL = new URL(document.location.href);
oRunModulesURL.search = oURLSearchParams.toString()
.replace(rSearchParamWithoutValue, '');
oClearFilter.href = oRunModulesURL;
}
}
});
}
}());