@openui5/sap.ui.core
Version:
OpenUI5 Core Library sap.ui.core
614 lines (556 loc) • 25 kB
JavaScript
/*!
* OpenUI5
* (c) Copyright 2026 SAP SE or an SAP affiliate company.
* Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
*/
sap.ui.define(['sap/ui/core/Core', "sap/ui/VersionInfo", "sap/ui/core/Lib"],
function(oCore, VersionInfo, Lib) {
"use strict";
/**
* <code>sap.ui.qunit.utils.ControlIterator</code> is a utility for collecting all available controls across libraries in order to e.g. run tests on each of them.
*
* It is used by calling the static <code>run</code> function with a callback function as parameter. This function will be called for each control
* (and provide control name and class and more information about the control as arguments), so a test could be executed for this control.
*
* The second parameter of the <code>run</code> function can be used to configure several options, e.g. to define a hard-coded list of control libraries
* to test (otherwise all available libraries are discovered), to exclude certain controls or libraries, to state whether subclasses of sap.ui.core.Element should
* also be tested, etc.
* Check the documentation of the <code>run</code> function parameters to understand which Controls/Elements are used by default and which ones are excluded.
*
* The <code>run</code> function does NOT execute synchronously! In case a QUnit test function is written that embeds a call to <code>run</code>
* (as opposed to creating a test function inside each callback), this test function needs to be asynchronous and needs to use the <code>done</code>
* callback option (called when all control tests have been executed) to call <code>done()</code> (the function returned by <code>assert.async()</code>).
*
* Usage example:
* <code>
* QUnit.config.autostart = false;
*
* sap.ui.require(["sap/ui/qunit/utils/ControlIterator"], function(ControlIterator) {
*
* ControlIterator.run(function(sControlName, oControlClass, oInfo) { // loop over all controls
*
* QUnit.test("Testing control " + sControlName, function(assert) { // create one test per control
* assert.ok(true, sControlName + " would be tested now");
* // e.g. create a control instance: oControl = new oControlClass()
* });
*
* },{
* librariesToTest: ["sap.ui.someLibrary"], // optionally limit the test scope or do other settings
* done: function(oResult) {
* // do something when the above function has been executed for all controls (here: all control tests have been created)
*
* QUnit.start(); // tell QUnit that all tests have now been created (due to autostart=false)
* }
* });
*
* });
* </code>
*
* This module is independent from QUnit, so it could be used for other purposes than unit tests.
*
* @namespace
*
* @author SAP SE
* @version 1.147.0
*
* @public
* @since 1.48.0
* @alias sap.ui.qunit.utils.ControlIterator
*/
var ControlIterator = {};
var aControlsThatCannotBeRenderedGenerically = [
"sap.chart.Chart",
"sap.m.FacetFilterItem",
"sap.m.internal.NumericInput",
"sap.m.IconTabBarSelectList",
"sap.m.LightBox",
"sap.m.Menu",
"sap.m.NotificationListBase",
"sap.m.NotificationListItem",
"sap.m.QuickViewBase",
"sap.m.QuickViewGroup",
"sap.m.QuickViewGroupElement",
"sap.m.TabStripItem",
"sap.m.TimePickerSlider",
"sap.m.TimePickerSliders",
"sap.m.UploadCollectionToolbarPlaceholder",
"sap.m.Wizard",
"sap.makit.Chart",
"sap.me.TabContainer",
"sap.suite.ui.microchart.InteractiveBarChart",
"sap.suite.ui.microchart.InteractiveDonutChart",
"sap.tnt.NavigationList",
"sap.ui.comp.smartform.Group",
"sap.ui.comp.smartform.GroupElement",
"sap.ui.comp.valuehelpdialog.ValueHelpDialog",
/**
* @deprecated since 1.108
*/
"sap.ui.core.mvc.HTMLView",
/**
* @deprecated since 1.120
*/
"sap.ui.core.mvc.JSONView",
/**
* @deprecated since 1.90
*/
"sap.ui.core.mvc.JSView",
/**
* @deprecated since 1.56
*/
"sap.ui.core.mvc.TemplateView",
"sap.ui.core.mvc.View",
"sap.ui.core.mvc.XMLView",
/**
* @deprecated since 1.120
*/
"sap.ui.core.mvc.XMLAfterRenderingNotifier",
/**
* @deprecated since 1.56
*/
"sap.ui.core.tmpl.Template",
"sap.ui.core.UIComponent",
"sap.ui.core.util.Export",
"sap.ui.documentation.BorrowedList",
"sap.ui.documentation.LightTable",
"sap.ui.integration.cards.AnalyticalContent", // requires an associated card instance in onAfterRendering
"sap.ui.integration.cards.AnalyticsCloudContent", // requires an associated card instance in onAfterRendering
"sap.ui.integration.cards.CalendarContent", // requires a model in onBeforeRendering
"sap.ui.layout.BlockLayoutRow",
"sap.ui.layout.form.ResponsiveGridLayoutPanel", // control not for stand alone usage. Only inside ResponsiveGridLayout
"sap.ui.layout.form.ResponsiveLayoutPanel", // control not for stand alone usage. Only inside ResponsiveLayout
"sap.ui.mdc.chart.ChartTypeButton", // requires a chart
"sap.ui.richtexteditor.RichTextEditor",
"sap.ui.richtexteditor.ToolbarWrapper",
"sap.ui.rta.AddElementsDialog",
"sap.ui.rta.ContextMenu",
"sap.ui.suite.TaskCircle",
"sap.ui.table.ColumnMenu",
"sap.ui.unified.Menu",
"sap.ui.ux3.ActionBar",
"sap.ui.ux3.ExactList.LB",
"sap.ui.ux3.NotificationBar",
"sap.uiext.inbox.composite.InboxTaskTitleControl",
"sap.uiext.inbox.InboxFormattedTextView",
"sap.uiext.inbox.InboxTaskDetails",
"sap.uiext.inbox.InboxToggleTextView",
"sap.uiext.inbox.SubstitutionRulesManager",
"sap.uxap.AnchorBar",
"sap.uxap.BlockBase",
"sap.uxap.BreadCrumbs",
"sap.uxap.ObjectPageHeader",
"sap.uxap.ObjectPageSubSection",
"sap.viz.ui5.controls.common.BaseControl",
"sap.viz.ui5.controls.VizRangeSlider",
"sap.viz.ui5.controls.VizTooltip",
"sap.viz.ui5.core.BaseChart"
];
function controlCanBeRendered(sControlName, fnControlClass) {
if (!controlCanBeInstantiated(sControlName, fnControlClass)) {
return false;
}
// controls which are known not to work standalone - some of them cannot work, some might need to be improved
if (aControlsThatCannotBeRenderedGenerically.indexOf(sControlName) > -1) { // known to be untestable
return false;
}
return true;
}
var aControlsThatCannotBeInstantiated = [
"sap.makit.Chart",
"sap.ui.commons.SearchField", // can be instantiated, but fails before rendering
"sap.ui.commons.SearchField.CB", // a MESS!
"sap.ui.commons.SearchFieldCB",
"sap.ui.commons.Tab",
"sap.ui.comp.transport.TransportDialog",
"sap.ui.core.ComponentContainer",
/**
* @deprecated since 1.108
*/
"sap.ui.core.mvc.HTMLView",
/**
* @deprecated since 1.120
*/
"sap.ui.core.mvc.JSONView",
/**
* @deprecated since 1.90
*/
"sap.ui.core.mvc.JSView",
/**
* @deprecated since 1.56
*/
"sap.ui.core.mvc.TemplateView",
"sap.ui.core.mvc.View",
"sap.ui.core.mvc.XMLView",
/**
* @deprecated since 1.120
*/
"sap.ui.core.mvc.XMLAfterRenderingNotifier",
/**
* @deprecated since 1.56
*/
"sap.ui.core.XMLComposite",
"sap.ui.mdc.BaseControl", // should be abstract?
"sap.ui.mdc.odata.v4.microchart.MicroChart", //The control only runs in views with XML pre-processor. The test can't provide this environment
"sap.ui.mdc.ValueHelpDialog", //The control only runs in views with XML pre-processor. The test can't provide this environment
"sap.ui.mdc.XMLComposite", //The control only runs in views with XML pre-processor. The test can't provide this environment
"sap.ui.rta.AddElementsDialog",
"sap.ui.rta.ContextMenu"
];
function controlCanBeInstantiated(sControlName, fnControlClass) {
// controls which are known not to work standalone
if (aControlsThatCannotBeInstantiated.indexOf(sControlName) > -1) { // known to be not instantiable
return false;
}
if (!fnControlClass) {
return false;
}
var oMetadata = fnControlClass.getMetadata();
if (oMetadata.isAbstract()) {
return false;
}
return true;
}
ControlIterator.aKnownOpenUI5Libraries = [
"sap.f", "sap.m", "sap.tnt", "sap.ui.codeeditor", "sap.ui.commons", "sap.ui.core",
"sap.ui.documentation", "sap.ui.dt", "sap.ui.fl", "sap.ui.integration",
"sap.ui.layout", "sap.ui.rta", "sap.ui.suite", "sap.ui.support", "sap.ui.table",
"sap.ui.unified", "sap.ui.ux3", "sap.uxap"
];
ControlIterator.aKnownRuntimeLayerLibraries = ControlIterator.aKnownOpenUI5Libraries.concat([
"sap.chart", "sap.makit", "sap.me", "sap.ndc", "sap.suite.ui.microchart", "sap.ui.comp",
"sap.ui.generic.app", "sap.ui.generic.template", "sap.ui.mdc", "sap.ui.richtexteditor",
"sap.viz"]);
ControlIterator.isKnownRuntimeLayerLibrary = function(sLibName) {
return ControlIterator.aKnownRuntimeLayerLibraries.indexOf(sLibName) > -1;
};
function nop() {
}
function alwaysTrue() {
return true;
}
function alwaysFalse() {
return false;
}
function toName(oLibrary) {
return oLibrary.name;
}
function getAllLibraries(fnFilter) {
fnFilter = fnFilter || alwaysTrue;
// discover what is available in order to also test other libraries than those loaded in bootstrap
return VersionInfo.load()
.then(function(oInfo) {
return oInfo.libraries.map(toName).filter(fnFilter);
})
.then(function(aLibraries) {
return Promise.all(
aLibraries.map(function(sLibName) {
// ignore load errors. This happens for e.g. "themelib_sap_bluecrystal"...
return oCore.loadLibrary(sLibName, {async: true}).catch(nop);
})
);
})
.then(function() {
// get a shallow copy the loaded library metadata
var mLibraries = Lib.all();
// filter libraries out that have not been requested
for (var sLibName in mLibraries) {
if (!fnFilter(sLibName)) {
delete mLibraries[sLibName];
}
}
return mLibraries;
});
}
/**
* Asynchronously loads the module for the class with the given name and returns the export of that module
* @param {string} sClassName name of the class to load
*/
function loadControlClass(sClassName) {
var sModuleName = sClassName.replace(/\./g, "/");
return new Promise(function(resolve, reject) {
sap.ui.require([sModuleName], function(FNClass) {
resolve(FNClass);
}, function(oErr) {
reject(new Error("failed to load class " + sModuleName + ":" + oErr));
});
});
}
/**
* Creates a filter function for libraries, taking the given parameters into account.
*
* When a list of libraries is given (<code>aLibrariesToTest</code>), the returned filter
* function will match exactly the given libraries (allowlist).
* Alternatively, a list of libraries to exclude can be given (<code>aExcludedLibraries</code>,
* blocklist) which will then not be matched by the returned filter function. In the case of
* the blocklist, the filter is additionally restricted to openui5 and sapui5.runtime libraries
* unless <code>bIncludeDistLayer</code> is set to true.
*
* @param {string[]} [aLibrariesToTest] List of libraries to load
* @param {string[]} [aExcludedLibraries] List of libraries to exclude
* @param {boolean} [bIncludeDistLayer] whether the list of libraries should be restricted to
* known runtime-layer libraries (superset of the OpenUI5 libraries) or include any
* dist-layer libraries
* @returns {function(string):boolean} A filter function for library names
*/
function makeLibraryFilter(aLibrariesToTest, aExcludedLibraries, bIncludeDistLayer) {
if ( aLibrariesToTest ) {
return function(sLibName) {
return aLibrariesToTest.indexOf(sLibName) >= 0;
};
} else if ( bIncludeDistLayer ) {
return function(sLibName) {
return aExcludedLibraries.indexOf(sLibName) < 0;
};
}
return function(sLibName) {
return aExcludedLibraries.indexOf(sLibName) < 0 &&
(bIncludeDistLayer || ControlIterator.isKnownRuntimeLayerLibrary(sLibName));
};
}
ControlIterator.loadLibraries = function(vLibraries) {
if ( vLibraries === "openui5" ) {
vLibraries = ControlIterator.aKnownOpenUI5Libraries;
} else if ( vLibraries === "sapui5.runtime" ) {
vLibraries = ControlIterator.aKnownRuntimeLayerLibraries;
}
var fnFilter;
if ( Array.isArray(vLibraries) ) {
fnFilter = makeLibraryFilter(vLibraries);
} else if ( typeof vLibraries === "function" ) {
fnFilter = vLibraries;
} else if ( vLibraries == null ) {
fnFilter = alwaysTrue;
} else {
throw new TypeError("unexpected filter " + vLibraries);
}
return getAllLibraries(fnFilter);
};
function checkLibraries(mLibraries, QUnit) {
QUnit.test("Should load at least one library and some controls", function(assert) {
assert.expect(2);
var bLibFound = false;
for (var sLibName in mLibraries) {
if (mLibraries[sLibName]) {
if (!bLibFound) {
assert.ok(mLibraries[sLibName], "Should have loaded at least one library");
bLibFound = true;
}
var iControls = mLibraries[sLibName].controls ? mLibraries[sLibName].controls.length : 0;
if (iControls > 0) {
assert.ok(iControls > 0, "Should find at least 10 controls in a library");
break;
}
}
}
});
}
/**
* Checks whether the control is not among the explicitly excluded controls and is not excluded due to its
* rendering/instantiation capabilities.
*
* The returned promise resolves with an info object describing the control's capabilities when the class
* should be included in the iterator or with a falsy value otherwise.
*
* @param {string} sControlName Qualified name (dot notation) of the control to test
* @param {string[]} aControlsToTest List of controls to include in the tests (allowlist)
* @param {string[]} aExcludedControls List of controls to exclude from the tests (blocklist)
* @param {boolean} bIncludeNonRenderable Whether the iterator should include controls that can't be rendered
* @param {boolean} bIncludeNonInstantiable Whether the iterator should include controls that can't be instantiated
* @returns Promise<({name:string,class:function,canBeInstantiated:boolean,canBeRendered:boolean}|false)>
* A promise on an info object or <code>false</code> if the class should not be tested
*/
var shouldTestControl = function(sControlName, aControlsToTest, aExcludedControls, bIncludeNonRenderable, bIncludeNonInstantiable) {
if (!sControlName
|| aControlsToTest.length && aControlsToTest.indexOf(sControlName) < 0 // only test specific controls
|| aExcludedControls.indexOf(sControlName) >= 0 ) {
return Promise.resolve(false);
}
return loadControlClass(sControlName).then(function(FNControlClass) {
var oInfo = {
name: sControlName,
"class": FNControlClass,
canBeInstantiated: controlCanBeInstantiated(sControlName, FNControlClass),
canBeRendered: controlCanBeRendered(sControlName, FNControlClass)
};
if (!bIncludeNonInstantiable && !oInfo.canBeInstantiated) {
return false;
}
if (!bIncludeNonRenderable && !oInfo.canBeRendered) {
return false;
}
return oInfo;
}, alwaysFalse);
};
/**
* Calls the callback function for all controls in the given array, unless they are explicitly excluded
*/
var loopControlsInLibrary = function(aControls, aControlsToTest, aExcludedControls, bIncludeNonRenderable, bIncludeNonInstantiable, fnCallback) {
return new Promise(function(resolve, reject){
var iControlCountInLib = 0;
var loop = function(i) {
if (i < aControls.length) {
var sControlName = aControls[i];
handleControl(sControlName, aControlsToTest, aExcludedControls, bIncludeNonRenderable, bIncludeNonInstantiable, fnCallback).then(function(bCountThisControl){
if (bCountThisControl) {
iControlCountInLib++;
}
loop(i + 1);
});
} else {
resolve(iControlCountInLib);
}
};
loop(0);
});
};
function handleControl(sControlName, aControlsToTest, aExcludedControls, bIncludeNonRenderable, bIncludeNonInstantiable, fnCallback) {
return shouldTestControl(sControlName, aControlsToTest, aExcludedControls, bIncludeNonRenderable, bIncludeNonInstantiable)
.then(function(oControlInfo) {
if ( oControlInfo ) {
fnCallback(sControlName, oControlInfo.class, {
canInstantiate: oControlInfo.canBeInstantiated,
canRender: oControlInfo.canBeRendered
});
}
return new Promise(function(resolve) {
// give the UI the chance to be responsive to user interaction and to update information about the currently handled control
setTimeout(function(){
resolve(!!oControlInfo);
}, 0);
});
});
}
/**
* Triggers the ControlIterator to collect all Controls (considering the given options) and then call the <code>fnCallback</code> function for each one.
* This function executes asynchronously.
*
* @param {function} fnCallback function(sControlName, oControlClass, oInfo) called for every single control.
* Arguments passed into the callback function are: the control name, the control class and an object with further information about the control.
* This object has the following boolean flags: canRender, canInstantiate, which describe if the control can be instantiated/rendered (some cannot).
*
* @param {object} [mOptions] optional settings for the test run
* @param {object.string[]} [mOptions.librariesToTest] which control libraries to test, e.g. <code>["sap.ui.core"]</code>. When set, exactly these libraries will be tested and the options excludedLibraries and includeDistLayer will be ignored. Otherwise, the module will try to discover all available libraries.
* @param {object.string[]} [mOptions.excludedLibraries=undefined] which control libraries to exclude from testing, e.g. <code>["sap.ui.core"]</code>. Only used when librariesToTest is not set.
* @param {object.string[]} [mOptions.controlsToTest=undefined] which controls to test, e.g. <code>["sap.m.Button"]</code>. When set, exactly these controls will be tested (IF they are found in the available/tested libraries) and the option excludedControls will be ignored. Otherwise, the module will try to discover all available controls.
* @param {object.string[]} [mOptions.excludedControls=undefined] which controls to exclude from testing, e.g. <code>["sap.m.Button"]</code>.
* @param {object.boolean} [mOptions.includeDistLayer=false] whether to include dist-layer libraries in the test. Only used when librariesToTest is not set.
* @param {object.boolean} [mOptions.includeElements=false] whether to include all entities inheriting from sap.ui.core.Element in the test. Otherwise only those inheriting from sap.ui.core.Control are tested.
* @param {object.boolean} [mOptions.includeNonRenderable=true] whether to include entities that cannot be generically rendered (some controls fail when they are not configured in a specific way).
* @param {object.boolean} [mOptions.includeNonInstantiable=false] whether to include entities that cannot be generically instantiated.
* @param {object.object} [mOptions.qunit=undefined] optionally, the QUnit object can be given here, so this module can do some internal sanity checks.
* @param {object.function} [mOptions.done] the callback function to call once all tests have been executed. This function receives an object as sole argument with the following properties: testedControlCount, testedLibraryCount
*
* @since 1.48.0
* @public
*/
ControlIterator.run = function(fnCallback, mOptions) {
window.setTimeout(function() { // fake async because sync loading libraries might not be possible in the future
_run(fnCallback, mOptions);
}, 1);
};
/**
* Called by run() with a 1:1 parameter forwarding
*/
function _run(fnCallback, mOptions) {
if (!mOptions) {
mOptions = {};
}
var fnDone = mOptions.done || nop;
var aLibrariesToTest = mOptions.librariesToTest || undefined;
var aExcludedLibraries = mOptions.excludedLibraries || [];
var aControlsToTest = mOptions.controlsToTest || [];
var aExcludedControls = mOptions.excludedControls || [];
var bIncludeDistLayer = (mOptions.includeDistLayer !== undefined) ? mOptions.includeDistLayer : false;
var bIncludeElements = (mOptions.includeElements !== undefined) ? mOptions.includeElements : false;
var bIncludeNonRenderable = (mOptions.includeNonRenderable !== undefined) ? mOptions.includeNonRenderable : true;
var bIncludeNonInstantiable = (mOptions.includeNonInstantiable !== undefined) ? mOptions.includeNonInstantiable : false;
var QUnit = mOptions.qunit;
if (QUnit) { // verify it's good
QUnit.test("Checking the given QUnit object", function(assert) {
assert.ok(true, "The given QUnit should be able to assert");
});
} else { // otherwise create a mock QUnit that can be used for assert.ok() only
var assert = {
ok: function(bCondition, sText) {
if (!bCondition) {
throw new Error(sText);
}
},
expect: nop
};
QUnit = {
module: nop,
test: function (text, fnCallback) {
fnCallback(assert);
}
};
}
// check given parameters
QUnit.test("Checking the given options", function(assert) {
assert.ok(mOptions.librariesToTest === undefined || Array.isArray(mOptions.librariesToTest), "The given librariesToTest must be undefined or an array, but is: " + mOptions.librariesToTest);
assert.ok(mOptions.excludedLibraries === undefined || Array.isArray(mOptions.excludedLibraries), "The given excludedLibraries must be undefined or an array, but is: " + mOptions.excludedLibraries);
assert.ok(mOptions.excludedControls === undefined || Array.isArray(mOptions.excludedControls), "The given excludedControls must be undefined or an array, but is: " + mOptions.excludedControls);
assert.ok(mOptions.includeDistLayer === undefined || typeof mOptions.includeDistLayer === "boolean", "The given includeDistLayer must be undefined or a boolean, but is: " + mOptions.includeDistLayer);
assert.ok(mOptions.includeElements === undefined || typeof mOptions.includeElements === "boolean", "The given includeElements must be undefined or a boolean, but is: " + mOptions.includeElements);
assert.ok(mOptions.includeNonRenderable === undefined || typeof mOptions.includeNonRenderable === "boolean", "The given includeNonRenderable must be undefined or a boolean, but is: " + mOptions.includeNonRenderable);
assert.ok(mOptions.includeNonInstantiable === undefined || typeof mOptions.includeNonInstantiable === "boolean", "The given includeNonInstantiable must be undefined or a boolean, but is: " + mOptions.includeNonInstantiable);
assert.ok(fnDone === undefined || typeof fnDone === "function", "The given done callback must be undefined or a function, but is: " + fnDone);
});
// get the libraries we are interested in
var fnFilter = makeLibraryFilter(aLibrariesToTest, aExcludedLibraries, bIncludeDistLayer);
return getAllLibraries(fnFilter)
.then(function(mLibraries) {
checkLibraries(mLibraries, QUnit);
return mLibraries;
})
.then(function(mLibraries) {
return loopLibraries(mLibraries, bIncludeElements, aControlsToTest, aExcludedControls, bIncludeNonRenderable, bIncludeNonInstantiable, fnCallback).then(function(aResults){
fnDone({
testedControlCount: aResults[0],
testedLibraryCount: aResults[1]
});
});
});
}
function loopLibraries(mLibraries, bIncludeElements, aControlsToTest, aExcludedControls, bIncludeNonRenderable, bIncludeNonInstantiable, fnCallback) {
return new Promise(function(resolve) {
// loop over all libs and controls and call the callback for each
var aLibraryNames = Object.keys(mLibraries),
iControlCount = 0,
iLibCount = 0;
var loop = function(i) {
if (i < aLibraryNames.length) {
var sLibName = aLibraryNames[i];
handleLibrary(mLibraries, sLibName, bIncludeElements, aControlsToTest, aExcludedControls, bIncludeNonRenderable, bIncludeNonInstantiable, fnCallback).then(function(aResult){
iControlCount += aResult[0];
if (aResult[1]) {
iLibCount++;
}
loop(i + 1);
});
} else {
resolve([iControlCount, iLibCount]);
}
};
loop(0);
});
}
function handleLibrary(mLibraries, sLibName, bIncludeElements, aControlsToTest, aExcludedControls, bIncludeNonRenderable, bIncludeNonInstantiable, fnCallback) {
return new Promise(function(resolve) {
var oLibrary = mLibraries[sLibName];
if (!oLibrary) { // in case removed from the map
resolve([0, false]);
return;
}
// we may need a concatenated array of Controls and Elements
var aControls = oLibrary.controls;
if (bIncludeElements) {
aControls = aControls.concat(oLibrary.elements.slice());
}
loopControlsInLibrary(aControls, aControlsToTest, aExcludedControls, bIncludeNonRenderable, bIncludeNonInstantiable, fnCallback).then(function(iAnalyzedControls){
resolve([iAnalyzedControls, true]);
});
});
}
return ControlIterator;
}, /* bExport= */true);