@openui5/sap.ui.core
Version:
OpenUI5 Core Library sap.ui.core
496 lines (451 loc) • 16.5 kB
JavaScript
/*!
* OpenUI5
* (c) Copyright 2026 SAP SE or an SAP affiliate company.
* Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
*/
/*
* IMPORTANT: This is a private module, it must not be listed as a direct module dependency,
* nor must it be required by code outside this package.
*/
/*global QUnit, sinon */
sap.ui.define([
"sap/base/util/fetch",
"./_utils"
], function(fetch, utils) {
"use strict";
function makeArray(arg) {
return Array.isArray(arg) ? arg : [arg];
}
function requireP(deps) {
return new Promise(function(resolve, reject) {
sap.ui.require(makeArray(deps), function() {
resolve(Array.prototype.slice.call(arguments));
}, reject);
});
}
function copyFiltered(target, source, filter) {
if ( source ) {
for ( var key in source ) {
if ( Object.hasOwn(filter, key) ) {
target[key] = source[key];
}
}
}
return target;
}
function ensureDOM() {
function insertDIV(id) {
if ( document.body.querySelector("#" + id) == null ) {
var elem = document.createElement("div");
elem.id = id;
document.body.insertBefore(elem, document.body.firstChild);
}
}
return utils.whenDOMReady()
.then(() => requireP("sap/ui/events/FocusEventFix"))
.then(function() {
/* Prepare body: Add QUnit DOM if missing, add CSS, ... */
insertDIV("qunit");
insertDIV("qunit-fixture");
});
}
function onCSPViolation(e) {
var location = e.sourceFile + ":" + e.lineNumber + ":" + e.columnNumber,
msg = "Security policy violation: directive '" + e.violatedDirective + "'";
if ( e.blockedURI ) {
msg += " violated by '" + String(e.blockedURI).slice(0, 20) + "'";
}
if ( QUnit.config.current ) {
QUnit.pushFailure(msg, location);
} else {
// should be caught and reported by QUnit's global error handler
// if not, it will only fail the current task for the securityviolationevent
throw new Error(msg + " at " + location);
}
}
/*
* @const
*/
var QUNIT_KNOWN_OPTIONS = {
altertitle: 1,
collapse: 1,
filter: 1,
fixture: 1,
hidepassed: 1,
maxDepth: 1,
module: 1,
moduleId: 1,
notrycatch: 1,
noglobals: 1,
seed: 1,
reorder: 1,
requireExpects: 1,
testId: 1,
testTimeout: 1,
scrolltop: 1
};
/*
* Retrieves information about the active version of an external component (e.g. qunit or sinon).
*
* The <code>config.version</code> property determines, which version of the component should be used.
* It can have one of the following values
* <ul>
* <li>an object value is assumed to contain the component info and is returned "as is"</li>
* <li>a falsy value (false, 0, null or "") disables the component and <code>null</code> is returned</li>
* <li>any other value must be the name of an entry in the <code>config.versions</code> map</li>
* </ul>
*
* If an entry in the <code>config.versions</code> map is not an object, it is assumed to be the name
* of another entry (aliasing). E.g. version "edge" usually will point to a concrete version like "2".
*
* <b>Note:</b> Aliasing can create infinite loops. No measure are taken to prevent or detect this.
*
* @param {object} config Configuration for all versions of the component
* @param {Object<string,object>} config.versions Map of configurations keyed by a version string
* @param {string|boolean|number|object} config.version Config to be used or version to be used or an alias or a boolean (enabled flag)
* @param {string} name Name of the external component (for error reporting only)
* @returns object|null Configuration t use or null if component is disabled
* @throws {TypeError}
*/
function getActiveVersion(componentConfig, componentName) {
var versionsMap = componentConfig.versions;
var version = componentConfig.version || null;
while ( typeof version !== "object" ) {
if ( !Object.hasOwn(versionsMap, version) ) {
throw new TypeError("unsupported " + componentName + " version " + componentConfig.version);
}
version = versionsMap[version];
}
return version;
}
function initTestModule(oConfig) {
var pAfterLoader, pQUnit, pSinon, pSinonQUnitBridge, pSinonConfig, pCoverage, pTestEnv,
oQUnitConfig;
document.title = oConfig.title;
// first configure the loader if needed
if ( oConfig.loader ) {
sap.ui.loader.config(oConfig.loader);
}
if ( oConfig.runAfterLoader ) {
pAfterLoader = requireP( oConfig.runAfterLoader );
} else {
pAfterLoader = Promise.resolve();
}
oQUnitConfig = getActiveVersion(oConfig.qunit, "qunit");
if ( oQUnitConfig != null ) {
// QUnit configuration can be set in advance, we always disable the autostart
window.QUnit = window.QUnit || {};
QUnit.config = QUnit.config || {};
if ( oConfig.qunit != null && typeof oConfig.qunit === 'object' ) {
copyFiltered(QUnit.config, oConfig.qunit, QUNIT_KNOWN_OPTIONS);
}
QUnit.config.autostart = false;
// now load qunitPause, QUnit, its CSS + the reporter bridge
pQUnit = pAfterLoader.then(function () {
return requireP("sap/ui/test/qunitPause");
}).then(function () {
utils.addStylesheet(oQUnitConfig.css);
return requireP(oQUnitConfig.module);
}).then(function() {
return requireP("sap/ui/qunit/qunit-junit");
});
}
var oSinonConfig = getActiveVersion(oConfig.sinon, "sinon");
if ( oSinonConfig != null ) {
pSinon = pAfterLoader.then(function() {
return requireP(oSinonConfig.module);
}).then(function([sinonModule]) {
// If the sinon module provides an export, we also apply it globally
// to make it available to the tests and the sinon-qunit-bridge.
if (sinonModule) {
globalThis.sinon = sinonModule;
}
});
if ( oConfig.sinon.qunitBridge && pQUnit ) {
pSinonQUnitBridge = Promise.all([
pQUnit,
pSinon
]).then(function() {
return requireP(oSinonConfig.bridge);
});
}
// sinon configuration must be applied only after sinon AND bridge have been loaded,
// they both set their own defaults
if ( oConfig.sinon != null && typeof oConfig.sinon === 'object' ) {
pSinonConfig = Promise.all([
pSinon,
pSinonQUnitBridge
]).then(function() {
// copy only settings that are listed in sinon.defaultConfig
// Note: newer versions of sinon do not support config / defaultConfig anymore
if (typeof sinon.defaultConfig === "object") {
sinon.config = copyFiltered(sinon.config || {}, oConfig.sinon, sinon.defaultConfig);
}
return arguments;
});
}
} else if ( oQUnitConfig != null ) {
// shim dependencies for the bridges, based on the selected QUnit version
// might be needed if tests load the bridge on their own
sap.ui.loader.config({
shim: {
"sap/ui/thirdparty/sinon-qunit": {
deps: [oQUnitConfig.module, "sap/ui/thirdparty/sinon"]
},
"sap/ui/qunit/sinon-qunit-bridge": {
deps: [oQUnitConfig.module, "sap/ui/thirdparty/sinon-4"]
}
}
});
}
pCoverage = pQUnit
.then(function () {
// QUnit.urlParams.coverage is a "boolean" queryString, if present in the url, regardless of the value,
// then it should be treated as truthy value. Otherwise, it'd be simply undefined.
if ( QUnit.urlParams.coverage === undefined ) {
return {};
}
if ( oConfig.coverage.instrumenter === "blanket") {
return { instrumenter: "blanket" };
}
return fetch("/.ui5/coverage/ping").then(function (oData) {
if (oData.status >= 400 && oConfig.coverage.instrumenter !== "istanbul") {
// Default fallback to blanket.
// Instrumenter is either not defined explicitly or set to "auto" and the middleware is not available
return { instrumenter: "blanket" };
} else if ( oData.status >= 400 ) {
// There's no middleware and the instrumenter is "istanbul"
return { instrumenter: null, error: "Istanbul is set as instrumenter, but there's no middleware" };
} else {
// There's a middleware available and the instrumenter is either "auto" or "istanbul"
return { instrumenter: "istanbul" };
}
});
})
.then(function(oSettings) {
if ( !oSettings.instrumenter ) {
return oSettings;
}
// Blanket coverage requires sync loading of the assets.
// By default the assets are being loaded asynchronously and we'd need to enforce reload in order to configure
// properly the _configureLoader.js
if (
(QUnit.urlParams["coverage-mode"] || oSettings.instrumenter === "blanket") &&
QUnit.urlParams["coverage-mode"] !== oSettings.instrumenter
) {
var oCoverageUrl = new URL(window.location.href);
// Needs to be explicitly reset as the URL builder creates ..&coverage=&..
// and this gets resolved to a variable coverage === "", which leads to a falsy value
oCoverageUrl.searchParams.set("coverage", "true");
oCoverageUrl.searchParams.set("coverage-mode", oSettings.instrumenter);
window.location = oCoverageUrl.toString();
}
return oSettings;
})
.then(function(oSettings) {
if ( oSettings.instrumenter === "blanket" ) {
// when coverage has been activated in a QUnit page via checkbox,
// then load blanket, configure it, then load the QUnit plugin
return requireP("sap/ui/thirdparty/blanket").then(function() {
if ( oConfig.coverage && window.blanket ) {
if (oConfig.coverage.only != null) {
window.blanket.options("sap-ui-cover-only", oConfig.coverage.only);
}
if (oConfig.coverage.never != null) {
window.blanket.options("sap-ui-cover-never", oConfig.coverage.never);
}
if (oConfig.coverage.branchTracking) {
window.blanket.options("branchTracking", true);
}
}
return requireP("sap/ui/qunit/qunit-coverage");
}).then(function() {
// when coverage is active, qunit-coverage sets autostart to true again
QUnit.config.autostart = false;
});
} else if (oSettings.instrumenter === "istanbul") {
return requireP("sap/ui/qunit/qunit-coverage-istanbul").then(function() {
var _adjustHtmlAttrValues = function(filter) {
return Array.isArray(filter) ?
JSON.stringify(filter) : filter;
};
// Creates a list of flat arrays from a config object.
// For example:
// {watermarks: {branches: [10, 20], functions: [10, 20]}}
// becomes:
// [
// [watermarks, branches, [10, 20]],
// [watermarks, functions, [10, 20]],
// ]
var _serializeConfig = function (oCoverageObj) {
var aRecords = [];
for (var [sKey, vValue] of Object.entries(oCoverageObj)) {
if (Object.prototype.toString.call(vValue) === "[object Object]") {
var aSubRecords = _serializeConfig(vValue);
// eslint-disable-next-line no-loop-func
aRecords = aRecords.concat(aSubRecords.map(function (aEntry) {
return [sKey].concat(aEntry);
}));
} else {
aRecords.push([sKey, vValue]);
}
}
return aRecords;
};
if (oConfig.coverage) {
var oScript = document.querySelector('script[src$="qunit/qunit-coverage-istanbul.js"]');
if (oScript && oConfig.coverage != null) {
var aConfig = _serializeConfig(oConfig.coverage);
aConfig.forEach(function(aEntry) {
var vValue = aEntry.pop();
if (vValue !== null) {
oScript.setAttribute("data-sap-ui-cover-" + aEntry.join("-"), _adjustHtmlAttrValues(vValue));
}
});
}
}
});
} else if (oSettings.instrumenter === null && oSettings.error) { // No middleware
QUnit.test("There's an error with the instrumentation setup or configuration", function (assert) {
assert.ok(false, oSettings.error);
});
}
})
.then(function() {
// add a QUnit configuration option in the Toolbar to enable/disable
// client-side instrumentation
var bHasCoverageCheckbox = QUnit.config.urlConfig.some(function (oConf) {
return oConf.id === "coverage";
});
if (!bHasCoverageCheckbox) {
QUnit.config.urlConfig.push({
id: "coverage",
label: "Enable coverage",
tooltip: "Enable code coverage."
});
}
});
pCoverage = pCoverage.then(function() {
// QUnit option CSP
if ( QUnit.urlParams["sap-ui-xx-csp-policy"] ) {
document.addEventListener("securitypolicyviolation", onCSPViolation);
QUnit.done(function() {
document.removeEventListener("securitypolicyviolation", onCSPViolation);
});
}
QUnit.config.urlConfig.push({
id: "sap-ui-xx-csp-policy",
label: "CSP",
value: {
"sap-target-level-1:report-only": "Level 1",
"sap-target-level-2:report-only": "Level 2"
},
tooltip: "What Content-Security-Policy should the server send"
});
// QUnit option repeat-to-failure
if ( QUnit.urlParams["rtf"] || QUnit.urlParams["repeat-to-failure"]) {
QUnit.done(function(results) {
if (results.failed === 0) {
setTimeout(function() {
location.reload();
}, 100);
}
});
}
QUnit.config.urlConfig.push({
id: "repeat-to-failure",
label: "Repeat",
value: false,
tooltip: "Whether this test should auto-repeat until it fails"
});
});
pTestEnv = Promise.all([
pAfterLoader,
pQUnit,
pSinon,
pSinonQUnitBridge,
pSinonConfig,
pCoverage
]);
if ( oConfig.beforeBootstrap ) {
pTestEnv = pTestEnv.then(function() {
return requireP(oConfig.beforeBootstrap);
});
}
// copy UI5 configuration
window["sap-ui-config"] = oConfig.ui5 || {};
if ( Array.isArray(window["sap-ui-config"].libs) ) {
window["sap-ui-config"].libs = window["sap-ui-config"].libs.join(",");
}
// write test config to window
// can be read by specific tests, e.g. tests in the generic test collection
window["sap-ui-test-config"] = oConfig.testConfig || {};
if ( oConfig.bootCore ) {
pTestEnv = pTestEnv.then(function() {
return new Promise(function(resolve, reject) {
sap.ui.require(["sap/ui/core/Core"], function(core) {
core.boot?.(); // method no longer exists with new bootstrap
core.ready(resolve);
}, reject);
});
});
}
return pTestEnv.then(function() {
if (oConfig.autostart) {
// first load the tests, then ensure DOM then start tests
return requireP( oConfig.module ). // Note: accepts single module or array
then(function(aTestModules) {
return Promise.all(aTestModules);
}).then(function() {
return ensureDOM();
}).then(function() {
// When using xx-waitForTheme=init the test starter also
// takes care of waiting for additional stylesheets e.g. caused by
// implicit loading of libs via test module dependencies.
// Note: config option is internally converted to lowercase
if (oConfig.ui5["xx-waitfortheme"] === "init") {
return new Promise(function(resolve, reject) {
sap.ui.require(["sap/ui/test/utils/waitForThemeApplied"], resolve, reject);
}).then(function(waitForThemeApplied) {
return waitForThemeApplied();
});
}
}).then(function() {
QUnit.start();
});
} else {
// first ensure the DOM then load tests as tests will start QUnit already
return ensureDOM().then(function() {
return requireP( oConfig.module ). // Note: accepts single module or array
then(function(aTestModules) {
return Promise.all(aTestModules);
});
});
}
});
}
utils.registerResourceRoots();
var oParams = new URLSearchParams(window.location.search),
sSuiteName = utils.getAttribute('data-sap-ui-testsuite') || oParams.get("testsuite"),
sTestName = utils.getAttribute('data-sap-ui-test') || oParams.get("test");
utils.getSuiteConfig(sSuiteName).then(function(oSuiteConfig) {
var oTestConfig = oSuiteConfig.tests[sTestName];
if (!oTestConfig) {
throw new TypeError("Invalid test name");
}
return initTestModule(oTestConfig);
}).catch(function(oErr) {
console.error(oErr.stack || oErr); // eslint-disable-line no-console
if ( typeof QUnit !== "undefined" ) {
QUnit.test("Test Starter", function() {
throw oErr;
});
QUnit.start();
} else {
utils.whenDOMReady().then(function() {
document.body.style.color = "red";
document.body.innerHTML = "<pre>" + utils.encode(oErr.stack || oErr.message || String(oErr)) + "</pre>";
});
}
});
});