UNPKG

@openui5/sap.ui.core

Version:

OpenUI5 Core Library sap.ui.core

675 lines (581 loc) 27.6 kB
/*! * OpenUI5 * (c) Copyright 2009-2021 SAP SE or an SAP affiliate company. * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. */ /*global window */ //wrapper for loading signals and hasher if module is defined (function (global) { "use strict"; var original; if (global.module) { original = global.module; global.module = undefined; } sap.ui.define([ 'sap/ui/thirdparty/jquery', 'sap/ui/base/Object', 'sap/ui/core/Element', 'sap/ui/core/mvc/View', 'sap/ui/test/matchers/Ancestor', 'sap/ui/test/matchers/MatcherFactory', 'sap/ui/test/pipelines/MatcherPipeline', 'sap/ui/test/_OpaLogger' ], function ($, UI5Object, UI5Element, View, Ancestor, MatcherFactory, MatcherPipeline, _OpaLogger) { /** * @class A Plugin to search UI5 controls. * * @public * @alias sap.ui.test.OpaPlugin * @author SAP SE * @since 1.22 */ var OpaPlugin = UI5Object.extend("sap.ui.test.OpaPlugin", /** @lends sap.ui.test.OpaPlugin.prototype */ { constructor : function() { this._oLogger = _OpaLogger.getLogger("sap.ui.test.Opa5"); this._oMatcherFactory = new MatcherFactory(); }, /** * Gets all the controls or elements of a certain type that are currently instantiated. * If the type is omitted, all controls and elements are returned. * * @param {Function} [fnConstructorType] the control type, e.g: sap.m.CheckBox * @param {string} [sControlType] optional control type name, e.g: "sap.m.CheckBox" * @returns {Array} an array of the found controls (can be empty) * @public */ getAllControls : function (fnConstructorType, sControlType) { var aControls = UI5Element.registry.filter( makeTypeFilterFn(fnConstructorType) ); this._oLogger.debug("Found " + aControls.length + " controls" + (fnConstructorType ? " of type '" + (sControlType || fnConstructorType) + "'" : "") + " in page"); return aControls; }, /** * Returns the view with a specific name. The result should be a unique view. * If there are multiple visible views with that name, none will be returned. * * @param {string} sViewName the name of the view * @returns {sap.ui.core.mvc.View} or undefined * @public */ getView: function (sViewName) { var aViews = this.getAllControls(View, "View"); var aMatchingViews = aViews.filter(function (oViewInstance) { return oViewInstance.getViewName() === sViewName; }); this._oLogger.debug("Found " + aMatchingViews.length + " views with viewName '" + sViewName + "'"); if (aMatchingViews.length > 1) { aMatchingViews = aMatchingViews.filter(function (oViewInstance) { var oViewDomRef = oViewInstance.$(); return oViewDomRef.length > 0 && oViewDomRef.is(":visible") && oViewDomRef.css("visibility") !== "hidden"; }); this._oLogger.debug("Found " + aMatchingViews.length + " visible views with viewName '" + sViewName + "'"); if (aMatchingViews.length !== 1) { this._oLogger.debug("Cannot identify controls uniquely. Please provide viewId to locate the exact view."); aMatchingViews = []; } } return aMatchingViews[0]; }, // find view by ID and/or viewName _getMatchingView: function (oOptions) { var oView = null; var sViewName; if (oOptions.viewName) { var sOptionsViewName = (oOptions.viewNamespace || "") + "." + (oOptions.viewName || ""); sViewName = sOptionsViewName.replace(/\.+/g,'.').replace(/^\.|\.$/g, ""); } if (oOptions.viewId) { var oCoreElement = UI5Element.registry.get(oOptions.viewId); if (oCoreElement instanceof View && (!sViewName || oCoreElement.getViewName() === sViewName)) { oView = oCoreElement; } } else { oView = this.getView(sViewName); } this._oLogger.debug("Found " + (oView ? "" : "no ") + "view with ID '" + oOptions.viewId + "' and viewName '" + sViewName + "'"); return oView; }, /** * Gets a control inside the view (same as calling oView.byId) * Returns all matching controls inside a view (also nested views and their children).<br/> * The view can be specified by viewName, viewNamespace, viewId, and any combination of three. * eg : { id : "foo" } will search globally for a control with the ID foo<br/> * eg : { id : "foo" , viewName : "bar" } will search for a control with the ID foo inside the view with the name bar<br/> * eg : { viewName : "bar" } will return all the controls inside the view with the name bar<br/> * eg : { viewName : "bar", controlType : sap.m.Button } will return all the Buttons inside a view with the name bar<br/> * eg : { viewName : "bar", viewNamespace : "baz." } will return all the Controls in the view with the name baz.bar<br/> * eg : { viewId : "viewBar" } will return all the controls inside the view with the ID viewBar<br/> * * @param {object} oOptions can contain a viewName, viewNamespace, viewId, fragmentId, id and controlType properties. * oOptions.id can be string, array or regular expression * @returns {sap.ui.core.Element|sap.ui.core.Element[]|null} * If oOptions.id is a string, will return the control with such an ID or null.<br/> * If the view is not found or no control matches the given criteria, will return an empty array <br/> * Otherwise, will return an array of matching controls * @public */ getControlInView : function (oOptions) { var oView = this._getMatchingView(oOptions); var bSearchForSingleControl = typeof oOptions.id === "string"; if (!oView) { return bSearchForSingleControl ? null : []; } var sViewName = oView.getViewName(); var sFragmentPrefix = oOptions.fragmentId ? oOptions.fragmentId + OpaPlugin.VIEW_ID_DELIMITER : ""; if ($.isArray(oOptions.id)) { var aControls = []; var aUnmatchedIds = []; oOptions.id.map(function (sId) { return sFragmentPrefix + sId; }).forEach(function (sId) { var oControl = oView.byId(sId); if (oControl) { aControls.push(oControl); } else { aUnmatchedIds.push(sId); } }); var sUnmatchedLog = aUnmatchedIds.length ? ". Found no controls matching the subset of IDs " + aUnmatchedIds : ""; this._oLogger.debug("Found " + aControls.length + " controls with ID contained in " + oOptions.id + " in view '" + sViewName + "'" + sUnmatchedLog); return aControls; } if (bSearchForSingleControl) { var sId = sFragmentPrefix + oOptions.id; var oControl = oView.byId(sId) || null; this._oLogger.debug("Found " + (oControl ? "" : "no ") + "control with ID '" + sId + "' in view '" + sViewName + "'"); return oControl; } var aAllControlsOfTheView = this.getAllControlsWithTheParent(oView, oOptions.controlType, oOptions.sOriginalControlType); var bMatchById = this._isRegExp(oOptions.id); if (bMatchById) { aAllControlsOfTheView = aAllControlsOfTheView.filter(function (oControl) { var sUnprefixedControlId = this._getUnprefixedControlId(oControl.getId(), oView.getId(), oOptions.fragmentId); return oOptions.id.test(sUnprefixedControlId); }.bind(this)); } this._oLogger.debug("Found " + aAllControlsOfTheView.length + " controls of type " + oOptions.sOriginalControlType + (bMatchById ? " with ID matching " + oOptions.id : "") + " in view '" + sViewName + "'"); return aAllControlsOfTheView; }, // get all child controls of a certain control type // the parent is a control and can be an indirect ancestor getAllControlsWithTheParent : function (oParent, fnControlType, sControlType) { var ancestorMatcher = new Ancestor(oParent); return this._filterUniqueControlsByCondition(this.getAllControls(fnControlType, sControlType), ancestorMatcher); }, // get all child controls of a certain control type // the parents are controls whose roots are under the DOM node $Container getAllControlsInContainer : function ($Container, fnControlType, sControlType, sContainerDescription) { var hasExpectedType = makeTypeFilterFn(fnControlType), aControls = this._filterUniqueControlsByCondition(this._getControlsInContainer($Container), hasExpectedType); this._oLogger.debug("Found " + aControls.length + " controls in " + (sContainerDescription ? sContainerDescription : "container") + " with controlType '" + sControlType + "'"); return aControls; }, // get control in static area that matches a control type, ID (string, array, regex), viewId, viewName, fragmentId _getControlsInStaticArea: function (oOptions) { var oStaticArea = $(sap.ui.getCore().getStaticAreaRef()); var vControls = this._getControlsInContainer(oStaticArea) || []; if (oOptions.id) { vControls = this._filterUniqueControlsByCondition(vControls, function (oControl) { var sUnprefixedControlId = oControl.getId(); var oView = this._getMatchingView(oOptions); if (oView) { // the view could be set globally or from page object. in this case, search inside open dialogs should take priority: // - if the control is actually inside the view - the control ID will be considered view-relative // - otherwise, the control ID will be considered global if (this._isControlInView(oControl, oView.getViewName())) { sUnprefixedControlId = this._getUnprefixedControlId(oControl.getId(), oView.getId(), oOptions.fragmentId); } } var bIdMatches = false; if (typeof oOptions.id === "string") { bIdMatches = sUnprefixedControlId === oOptions.id; } if (this._isRegExp(oOptions.id)) { bIdMatches = oOptions.id.test(sUnprefixedControlId); } if ($.isArray(oOptions.id)) { bIdMatches = oOptions.id.filter(function (sId) { return sId === sUnprefixedControlId; }).length > 0; } return bIdMatches; }.bind(this)); this._oLogger.debug("Found " + (vControls.length ? vControls.length : "no") + " controls in the static area with ID matching '" + oOptions.id + "'" + (oOptions.fragmentId ? " and fragmentId: '" + oOptions.fragmentId + "'" : "")); } if (vControls.length && oOptions.controlType) { var hasExpectedType = makeTypeFilterFn(oOptions.controlType); vControls = this._filterUniqueControlsByCondition(vControls, hasExpectedType); this._oLogger.debug("Found " + (vControls.length ? vControls.length : "no") + " controls in the static area with control type matching '" + oOptions.controlType + "'"); } if (oOptions.id && typeof oOptions.id === "string") { return vControls[0] || null; } else { return vControls; } }, // get controls whose roots are in the subtree of oJQueryElement _getControlsInContainer: function (oJQueryElement) { var aAllControls = oJQueryElement.find("*").control(); var aResult = []; aAllControls.forEach(function (oControl) { var bUnique = !aResult.filter(function (oUniqueControl) { return oUniqueControl.getId() === oControl.getId(); }).length; if (bUnique) { aResult.push(oControl); } }); return aResult; }, _isControlInView: function (oControl, sViewName) { if (!oControl) { return false; } if (oControl.getViewName && oControl.getViewName() === sViewName) { return true; } else { return this._isControlInView(oControl.getParent(), sViewName); } }, _isRegExp: function (rRegExp) { // can't use instanceof because the regexp may be created in the parent frame return Object.prototype.toString.call(rRegExp) === "[object RegExp]"; }, /** * Find a control matching the provided options * @param {object} [oOptions] a map of options used to describe the control you are looking for. * @param {string} [oOptions.viewName] Controls will only be searched inside this view (ie: the view (as a control) has to be an ancestor of the control) * If a control ID is given, the control will be found using the byId function of the view. * @param {string} [oOptions.viewId] @since 1.62 Controls will only be searched inside this view (ie: the view (as a control) has to be an ancestor of the control) * If a control ID is given, the control will be found using the byId function of the view. * @param {string|string[]} [oOptions.id] The ID of one or multiple controls. This can be a global ID or an ID used together with viewName. See the documentation of this parameter. * @param {boolean} [oOptions.visible=true] should the control have a visible DOM reference * @param {boolean} [oOptions.interactable=false] @since 1.34 should the control be interactable and enabled. * When true, only interactable and enabled controls will be matched. For details, see the {@link sap.ui.test.matchers.Interactable} matcher. * @param {boolean} [oOptions.enabled=false] @since 1.66 should the control be enabled. * If interactable is true, enabled will also be true, unless declared otherwise. * @param {boolean} [oOptions.editable=false] @since 1.80 should the control be editable. * @param {boolean} [oOptions.searchOpenDialogs] Only controls in the static UI area of UI5 are searched. * @param {string|function} [oOptions.controlType] @since 1.40 match all controls of a certain type * It is usually combined with viewName or searchOpenDialogs. If no control matches the type, an empty array will be returned. Examples: * <pre> * // will return an array of all visible buttons * new OpaPlugin().getMatchingControls({ * controlType: "sap.m.Button" * }); * * // control type will also return controls that extend the control type * // this will return an array of visible sap.m.List and sap.m.Table since both extend List base * new OpaPlugin().getMatchingControls({ * controlType: "sap.m.ListBase" * }); * * // control type is often combined with viewName - only controls that are inside of the view * // and have the correct type will be returned * // here all sap.m.Inputs inside of a view called 'my.View' will be returned * new OpaPlugin().getMatchingControls({ * viewName: "my.View" * controlType: "sap.m.Input" * }); * </pre> * @returns {sap.ui.core.Element|sap.ui.core.Element[]|null} * <ul> * <li>if a oOptions.id is a string, will return the single matching control or null if no controls match</li> * <li>otherwise, will return an array of matching controls, or an empty array, if no controls match</li> * </ul> * * @public */ getMatchingControls : function (oOptions) { var vResult = null; oOptions = oOptions || {}; var bHasValidControlType = this._modifyControlType(oOptions); if (!bHasValidControlType) { return typeof oOptions.id === "string" ? vResult : []; } if (oOptions.searchOpenDialogs) { vResult = this._getControlsInStaticArea(oOptions); } else if (oOptions.viewName || oOptions.viewId) { vResult = this.getControlInView(oOptions); } else if (oOptions.id) { vResult = this.getControlByGlobalId(oOptions); } else if (oOptions.controlType) { vResult = this.getAllControls(oOptions.controlType, oOptions.sOriginalControlType); } else { vResult = this.getAllControls(); } if (!vResult) { return vResult; } var oStateMatchers = this._oMatcherFactory.getStateMatchers({ visible: oOptions.visible, // true by default interactable: oOptions.interactable, // false by default enabled: typeof oOptions.enabled === "undefined" ? oOptions.interactable : oOptions.enabled, // by default, true when interactable, false elsewise editable: typeof oOptions.editable === "undefined" ? false : oOptions.editable // false by default }); var vPipelineResult = OpaPlugin._oMatcherPipeline.process({ control: vResult, matchers: oStateMatchers }); // all controls are filtered out if (!vPipelineResult) { // backwards compatible - return empty array in this case if ($.isArray(vResult)) { return []; } // Single control - return null if (vResult) { return null; } // anything else return vResult; } // Return the matched controls return vPipelineResult; }, /** * retrieve controls with getMatchingControls and then pass them through the matcher pipeline * @param {object} oOptions a map of options used to describe the control you are looking for. * @returns {object|array|string} can return a single control or array of controls depending on options * returns constant FILTER_FOUND_NO_CONTROLS if nothing is found * @private */ _getFilteredControls : function(oOptions) { var vControl = this._filterControlsByCondition(oOptions); var oFilterOptions = $.extend({}, oOptions); // when on the root level of oOptions, these options are already processed (see _filterControlsByCondition) and should not be processed again, // as this results in error when no controls are passed to the matcher pipeline (see _filterControlsByMatchers) // - the pipeline should still be executed because there could be custom matchers ["interactable", "visible", "enabled", "editable"].forEach(function (sProp) { delete oFilterOptions[sProp]; }); return vControl === OpaPlugin.FILTER_FOUND_NO_CONTROLS ? OpaPlugin.FILTER_FOUND_NO_CONTROLS : this._filterControlsByMatchers(oFilterOptions, vControl); }, // filter result of getMatchingControls and maps it to FILTER_FOUND_NO_CONTROLS when no controls are found _filterControlsByCondition: function (oOptions) { var vControl = null; var bPluginLooksForControls = this._isLookingForAControl(oOptions); if (bPluginLooksForControls) { vControl = this.getMatchingControls(oOptions); } // conditions in which no control was found and return value should be the special marker FILTER_FOUND_NO_CONTROLS var aControlsNotFoundConditions = [ typeof oOptions.id === "string" && !vControl, // search for single control by string ID this._isRegExp(oOptions.id) && !vControl.length, // search by regex ID $.isArray(oOptions.id) && (!vControl || vControl.length !== oOptions.id.length), // search by array of IDs oOptions.controlType && $.isArray(vControl) && !vControl.length, // search by control type globally !oOptions.id && (oOptions.viewName || oOptions.viewId || oOptions.searchOpenDialogs) && !vControl.length // search by control type in view or staic area ]; return aControlsNotFoundConditions.some(Boolean) ? OpaPlugin.FILTER_FOUND_NO_CONTROLS : vControl; }, // instantiate any matchers with declarative syntax and run controls through matcher pipeline _filterControlsByMatchers: function (oOptions, vControl) { var oOptionsWithMatchers = $.extend({}, oOptions); var aMatchers = this._oMatcherFactory.getFilteringMatchers(oOptionsWithMatchers); var bPluginLooksForControls = this._isLookingForAControl(oOptions); var vResult = null; /* * If the plugin does not look for controls execute matchers even if vControl is falsy. * This is used when you smuggle in values to success through matchers: * matchers: function () {return "foo";}, * success: function (sFoo) {} */ if ((vControl || !bPluginLooksForControls) && aMatchers.length) { vResult = OpaPlugin._oMatcherPipeline.process({ matchers: aMatchers, control: vControl }); if (!vResult) { return OpaPlugin.FILTER_FOUND_NO_CONTROLS; } } else { vResult = vControl; } return vResult; }, /** * Find a control by its global ID * * @param {object} oOptions a map of match conditions. Must contain an id property * @param {string|string[]} [oOptions.id] required - ID to match. Can be string, regex or array * @param {string|function} [oOptions.controlType] optional - control type to match * @returns {sap.ui.core.Element|sap.ui.core.Element[]} all matching controls * <ul> * <li>if a oOptions.id is a string, will return the single matching control or null if no controls match</li> * <li>otherwise, will return an array of matching controls, or an empty array, if no controls match</li> * </ul> * * @param oOptions must contain ID property of type string, regex or array of strings; optionally it can contain a controlType property. * @returns {sap.ui.core.Element|sap.ui.core.Element[]|null} all controls matched by the regex or the control matched by the string or null * @public */ getControlByGlobalId : function (oOptions) { var hasExpectedType = makeTypeFilterFn(oOptions.controlType); if (typeof oOptions.id === "string") { var oControl = UI5Element.registry.get(oOptions.id) || null; if (oControl && !hasExpectedType(oControl)) { this._oLogger.error("A control with global ID '" + oOptions.id + "' is found but does not have required controlType '" + oOptions.sOriginalControlType + "'. Found control is '" + oControl + "' but null is returned instead"); return null; } this._oLogger.debug("Found " + (oControl ? "" : "no ") + "control with the global ID '" + oOptions.id + "'"); return oControl; } var aMatchIds = []; var bMatchById = this._isRegExp(oOptions.id); if (bMatchById) { //Performance critical UI5Element.registry.forEach(function(oElement, sId) { if (oOptions.id.test(sId)) { aMatchIds.push(sId); } }); } else if ($.isArray(oOptions.id)) { aMatchIds = oOptions.id; } var aMatchingControls = []; var aUnmatchedIds = []; aMatchIds.forEach(function (sId) { var oControl = UI5Element.registry.get(sId); // only return defined controls if (oControl && hasExpectedType(oControl) && !oControl.bIsDestroyed) { aMatchingControls.push(oControl); } else { aUnmatchedIds.push(sId); } }); var sUnmatchedLog = !bMatchById && aUnmatchedIds.length ? ". Found no controls of matching the subset of IDs " + aUnmatchedIds : ""; this._oLogger.debug("Found " + aMatchingControls.length + " controls of type " + oOptions.sOriginalControlType + (bMatchById ? " with ID matching '" : " with ID contained in '") + oOptions.id + sUnmatchedLog); return aMatchingControls; }, /** * Gets the constructor function of a certain controlType * * @param {string} sControlType the name of the type eg: "sap.m.Button" * @returns {null|function} When the type is loaded, the contstructor is returned, if it is a lazy stub or not yet loaded, null will be returned and there will be a log entry. * @public */ getControlConstructor : function (sControlType) { if (sap.ui.lazyRequire._isStub(sControlType)) { this._oLogger.debug("The control type " + sControlType + " is currently a lazy stub."); return null; } var fnControlType = $.sap.getObject(sControlType); // no control type if (!fnControlType) { this._oLogger.debug("The control type " + sControlType + " is undefined."); return null; } // some control types only have static methods and cannot be instanciated (e.g.: sap.m.MessageToast) if (typeof fnControlType !== "function") { this._oLogger.debug("The control type " + sControlType + " must be a function."); return null; } return fnControlType; }, /** * Checks if oOptions contains conditions that would provoke control search * @param {object} oOptions a map of match conditions * @returns {boolean} true if oOptions contains required conditions * @private */ _isLookingForAControl : function (oOptions) { return Object.keys(oOptions).some(function (sKey) { return OpaPlugin._aControlSelectorsForMatchingControls.indexOf(sKey) !== -1 && !!oOptions[sKey]; }); }, // filter controls using a function and return a set of unique controls _filterUniqueControlsByCondition : function (aControls, fnCondition) { return aControls.filter(function (oControl, iPosition, aAllControls) { var bKeepMe = !!fnCondition(oControl); return bKeepMe && aAllControls.indexOf(oControl) === iPosition; }); }, // - if oOptions.controlType is the name of a control type, it will be replaced by the constructor for the control type // and the control type name will be saved in a new option sOriginalControlType // - if oOptions.controlType is not a string, it will be assumed that the control type is a lazy stub (and will not be resolved) // mutates oOptions! _modifyControlType : function (oOptions) { var vControlType = oOptions.controlType; //retrieve the constructor instance if (typeof vControlType !== "string") { if (vControlType && vControlType._sapUiLazyLoader) { // no way of getting the control type's name without actually calling it this._oLogger.debug("The control type is currently a lazy stub"); return false; } // undefined - oOptions has no control type filter that's fine // defined - it is a constructor since we checked that it is no lazy stub return true; } var fnControlConstructor = this.getControlConstructor(vControlType); if (!fnControlConstructor) { return false; } oOptions.sOriginalControlType = vControlType; oOptions.controlType = fnControlConstructor; return true; }, _getUnprefixedControlId: function (sControlId, sViewId, sFragmentId) { // viewID might not be a prefix. strip prefixes only when needed var sUnprefixedControlId = sControlId.replace(sViewId + OpaPlugin.VIEW_ID_DELIMITER, ""); if (sFragmentId) { if (sUnprefixedControlId.startsWith(sFragmentId + OpaPlugin.VIEW_ID_DELIMITER)) { sUnprefixedControlId = sUnprefixedControlId.replace(sFragmentId + OpaPlugin.VIEW_ID_DELIMITER, ""); } else { // don't match control that doesn't have the required fragment ID sUnprefixedControlId = ""; } } return sUnprefixedControlId; } }); /** * Creates a filter function that returns true when a given element * has the type <code>fnControlType</code>. * * When <code>fnControlType</code> is not defined, the returned * filter function will accept any element. * * @param {function} [fnControlType] Constructor to use for <code>instanceof</code> checks or null * @returns {function} Predicate function that returns true when a given element is of the expected type * @private */ function makeTypeFilterFn(fnControlType) { return function (oElement) { if (!fnControlType) { return true; } return oElement instanceof fnControlType; }; } OpaPlugin._oMatcherPipeline = new MatcherPipeline(); OpaPlugin._aControlSelectorsForMatchingControls = [ "id", "viewName", "viewId", "controlType", "searchOpenDialogs" ]; /** * marker for a return type * @private * @type {{}} */ OpaPlugin.FILTER_FOUND_NO_CONTROLS = "FILTER_FOUND_NO_CONTROL"; // delimiter after view or fragment prefix in control IDs OpaPlugin.VIEW_ID_DELIMITER = "--"; return OpaPlugin; }); if (original) { global.module = original; } })(window);