UNPKG

@openui5/sap.ui.core

Version:

OpenUI5 Core Library sap.ui.core

407 lines (368 loc) 14.9 kB
/*! * OpenUI5 * (c) Copyright 2026 SAP SE or an SAP affiliate company. * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. */ /*global FocusEvent, DragEvent, DataTransfer, MouseEvent, document */ sap.ui.define([ 'sap/ui/base/ManagedObject', 'sap/ui/qunit/QUnitUtils', 'sap/ui/test/Opa5', 'sap/ui/Device', "sap/ui/thirdparty/jquery", "sap/ui/test/_OpaLogger", "sap/ui/test/_FocusListener" ], function (ManagedObject, QUnitUtils, Opa5, Device, jQuery, _OpaLogger, _FocusListener) { "use strict"; /** * @class Actions for Opa5 - needs to implement an executeOn function that should simulate a user interaction on a control * @abstract * @extends sap.ui.base.ManagedObject * @public * @name sap.ui.test.actions.Action * @author SAP SE * @since 1.34 */ return ManagedObject.extend("sap.ui.test.actions.Action", { metadata : { properties: { /** * Use this only if the target property or the default of the action does not work for your control. * The id suffix of the DOM Element the press action will be executed on. * For most of the controls you do not have to specify this, since the Control Adapters will find the correct DOM Element. * But some controls have multiple DOM elements that could be target of your Action. * Then you should set this property. * For a detailed documentation of the suffix see {@link sap.ui.core.Element#$} * * @since 1.38 */ idSuffix: { type: "string" } }, publicMethods : [ "executeOn" ] }, /** * Checks if the matcher is matching - will get an instance of sap.ui.core.Control as parameter * Should be overwritten by subclasses * * @param {sap.ui.core.Control} element the {@link sap.ui.core.Element} or a control (extends element) the action will be executed on * @protected * @name sap.ui.test.actions.Action#executeOn * @function */ executeOn : function () { return true; }, /** * Finds the most suitable jQuery element to execute an action on. * A control may have many elements in its DOM representation. The most suitable one is chosen by priority: * <ol> * <li>If the user provided an idSuffix, return the element that matches it, or null</li> * <li>If there is a control adapter for the action - return the element that matches it. See <code>controlAdapters</code> at {@link sap.ui.test.Press} for an example</li> * <li>If there is no control adapter, or it matches no elements, return the focusDomRef of the control. Note that some controls may not have a focusDomRef.</li> * </ol> * @param {object} oControl the control to execute an action on * @returns {jQuery} the jQuery element which is most suitable for the action * @protected */ $: function (oControl) { var $ActionDomRef; var sErrorMessage = ""; if (this.getIdSuffix()) { // if user requested an ID suffix, it should be used -- no fallback $ActionDomRef = oControl.$(this.getIdSuffix()); sErrorMessage = $ActionDomRef.length ? "" : "DOM representation of control '" + oControl + "' has no element with user-provided ID suffix '" + this.getIdSuffix() + "'"; } else { var sAdapter = this._getAdapter(oControl); if (sAdapter) { $ActionDomRef = oControl.$(sAdapter); sErrorMessage = $ActionDomRef.length ? "" : "DOM representation of control '" + oControl + "' has no element with ID suffix '" + sAdapter + "' which is the default adapter for '" + this.getMetadata().getName() + "'"; } if (!$ActionDomRef || !$ActionDomRef.length) { // if no adapter is set or no element is found for it -- fallback to control focus dom ref $ActionDomRef = jQuery(oControl.getFocusDomRef()); if (!$ActionDomRef.length) { $ActionDomRef = oControl.$(); if (!$ActionDomRef.length) { sErrorMessage += "DOM representation of control '" + oControl + "' has no focus DOM reference"; } } } } if ($ActionDomRef.length) { this.oLogger.info("Found a DOM reference for the control '" + oControl + "'. Executing '" + this.getMetadata().getName() + "' on the DOM element with ID '" + $ActionDomRef[0].id + "'"); return $ActionDomRef; } else { // the control has no dom ref of any kind - action has no target this.oLogger.error(sErrorMessage); throw new Error(sErrorMessage); } }, /** * Returns the QUnitUtils * @returns {sap.ui.test.qunit.QUnitUtils} QUnit utils of the current window or the OPA frame * @protected */ getUtils : function () { return Opa5.getUtils() || QUnitUtils; }, init: function () { this.controlAdapters = {}; this.oLogger = _OpaLogger.getLogger(this.getMetadata().getName()); }, dropPosition: { BEFORE: "BEFORE", AFTER: "AFTER", CENTER: "CENTER" }, /** * Traverses the metadata chain of a control and looks for action adapters. * An action adapter is a suffix part of the ID of a DOM element, where * the DOM element is part of a control DOM representation and should be used as a target for events * @param {object} oControl the control for which to find an action adapter * @returns {string|null} the ID suffix of the DOM element to use as event target * @private */ _getAdapter : function (oControl) { var fnGetAdapterByMeta = function (oMetadata) { var vAdapter = this.controlAdapters[oMetadata.getName()]; if (vAdapter) { if (typeof vAdapter === "function") { return vAdapter(oControl); } if (typeof vAdapter === "string") { return vAdapter; } } var oParentMetadata = oMetadata.getParent(); if (oParentMetadata) { return fnGetAdapterByMeta(oParentMetadata); } return null; }.bind(this); return fnGetAdapterByMeta(oControl.getMetadata()); }, _tryOrSimulateFocusin: function ($DomRef, oControl) { var oDomRef = $DomRef[0]; var bFireArtificialEvents = false; var bSimulateFocusout = false; var isAlreadyFocused = this._isFocused(oDomRef); var bIsNewFF = Device.browser.firefox && Device.browser.version >= 60; var oLastFocusedElement; if (isAlreadyFocused || bIsNewFF) { // 1. If the event is already focused, make sure onfocusin event of the control will be properly fired when executing this action, // otherwise the next blur will not be able to safely remove the focus. // 2. In IE11 (and often in Firefox v61.0/v60.0 ESR), if the focus action fails and focusin is dispatched, onfocusin will be called twice. // To avoid this, directly dispatch the artificial events bFireArtificialEvents = true; } else { $DomRef.trigger("focus"); var bWasFocused = this._isFocused(oDomRef); // if focus was successful, skip the artificial events and thus avoid recieving onfocusin twice. // else, fire the artificial events because we still want onfocusin to work. bFireArtificialEvents = !bWasFocused; } if (bFireArtificialEvents) { this.oLogger.debug("Control " + oControl + " could not be focused - maybe you are debugging?"); // since we are simulating a focus shift a *new* element // ensure we need to simulate the focusout/blur of the *old* focused as well // unless the *old* was already blurred in a previous step, so we check that as well: oLastFocusedElement = _FocusListener.getLastFocusedElement(); bSimulateFocusout = oLastFocusedElement && oLastFocusedElement !== oDomRef && oLastFocusedElement !== _FocusListener.getLastBlurredElement(); if (bSimulateFocusout) { this._simulateFocusout(oLastFocusedElement); } this._createAndDispatchFocusEvent("focusin", oDomRef); this._createAndDispatchFocusEvent("focus", oDomRef); this._createAndDispatchFocusEvent("activate", oDomRef); } if (!this._isFocused(oDomRef)) { this.oLogger.trace("Control " + oControl + " could not be focused-in correctly. This may lead to lost interactions with the control"); } }, _simulateFocusout: function (oDomRef) { this._createAndDispatchFocusEvent("focusout", oDomRef); this._createAndDispatchFocusEvent("blur", oDomRef); this._createAndDispatchFocusEvent("deactivate", oDomRef); }, /** * Create the correct event object for a mouse event. * * @param {string} sName Name of the mouse event * @param {Element} oDomRef DOM element on which the event is going to be triggered * @param {boolean} bShiftKey Indicates whether the shift key is down in addition * @param {boolean} bAltKey Indicates whether the alt key is down in addition * @param {boolean} bCtrlKey Indicates whether the ctrl key is down in addition * @param {number} iClientX X coordinate for the event * @param {number} iClientY Y coordinate for the event * @param {number} iButton Mouse button index (0=left, 1=middle, 2=right) * @private */ _createAndDispatchMouseEvent: function (sName, oDomRef, bShiftKey, bAltKey, bCtrlKey, iClientX, iClientY, iButton) { // ignore scrolled down stuff (client X, Y not set) // and assume stuff is over the whole screen (screen X, Y not set) // See file jquery.sap.events.js for some insights to the magic var iMouseButton = iButton !== undefined ? iButton : 0; var oMouseEvent; oMouseEvent = new MouseEvent(sName, { bubbles: true, cancelable: true, identifier: 1, target: oDomRef, radiusX: 1, radiusY: 1, rotationAngle: 0, button: iMouseButton, buttons: iMouseButton === 2 ? 2 : 1, // buttons bitmask: 1=primary, 2=secondary/right type: sName, // include the type so jQuery.event.fixHooks can copy properties properly shiftKey: bShiftKey, altKey: bAltKey, ctrlKey: bCtrlKey, clientX: iClientX, clientY: iClientY }); oDomRef.dispatchEvent(oMouseEvent); }, _createAndDispatchFocusEvent: function (sName, oDomRef) { var oFocusEvent, bBubbles = ["focusin", "focusout", "activate", "deactivate"].indexOf(sName) !== -1; oFocusEvent = new FocusEvent(sName, { type: sName, target: oDomRef, curentTarget: oDomRef, bubbles: bBubbles }); oDomRef.dispatchEvent(oFocusEvent); this.oLogger.info("Dispatched focus event: '" + sName + "'"); }, _createAndDispatchDragEvent: function (sName, oDomRef, oOptions) { var mCoordinates = this._getEventCoordinates(oDomRef, oOptions); var oDataTransfer = new DataTransfer(); var oDragEvent; oDragEvent = new DragEvent(sName, { type: sName, // include the type so jQuery.event.fixHooks can copy properties properly eventPhase: 3, bubbles: true, cancelable: true, defaultPrevented: false, composed: true, returnValue: true, cancelBubble: false, target: oDomRef, toElement: oDomRef, srcElement: oDomRef, radiusX: 1, radiusY: 1, rotationAngle: 0, // coordinates are needed to infer drop elem. e.g. in the control handlers, the drop target can be recalculated using document.elementfromPoint. // even if set, pageXY and screenXY are zeroed. if clientXY is set, then xy and pageXY will be = clientXY, and offsetXY will be calculated correctly. // this may cause problems for controls outside the client area // in this case, users should scroll before the drag event clientX: mCoordinates.x, clientY: mCoordinates.y, // dataTransfer should be at least an empty object, to avoid undefined error dataTransfer: oDataTransfer }); oDomRef.dispatchEvent(oDragEvent); }, _createAndDispatchScrollEvent: function (oDomRef, oOptions) { if (oOptions.x != null) { oDomRef.scrollLeft = oOptions.x; } if (oOptions.y != null) { oDomRef.scrollTop = oOptions.y; } var oScrollEvent = new Event("scroll", { bubbles: false, cancelBubble: false, cancelable: false, composed: false, currentTarget: null, defaultPrevented: false, eventPhase: 0, isTrusted: true, returnValue: true, target: oDomRef, srcElement: oDomRef, type: "scroll" }); oDomRef.dispatchEvent(oScrollEvent); }, _getEventCoordinates: function (oDomRef, oOptions) { var $domRef = jQuery(oDomRef); var offset = $domRef.offset(); var mCenterCoordinates = { x: offset.left + $domRef.outerWidth() / 2, y: offset.top + $domRef.outerHeight() / 2 }; if (!oOptions) { return mCenterCoordinates; } switch (oOptions.position) { case this.dropPosition.BEFORE: // coords of upper left corner return { x: offset.left, y: offset.top }; case this.dropPosition.AFTER: // coords of bottom right corner return { x: offset.left + $domRef.outerWidth(), y: offset.top + $domRef.outerHeight() }; case this.dropPosition.CENTER: default: return mCenterCoordinates; } }, _isFocused: function (oDomRef) { // This check returns false if: // 1. you have the focus in the dev tools console, or a background tab, or the browser is not focused at all. (bDocumentHasFocus will be false) // 2. in IE11 and Firefox, because the document.activeElement wasn't updated. (bIsActiveElement will be false) var bIsActiveElement = oDomRef === document.activeElement; var bDocumentHasFocus = !document.hasFocus || document.hasFocus(); var bIsElemInBody = !!(oDomRef.type || oDomRef.href || ~oDomRef.tabIndex); return bIsActiveElement && bDocumentHasFocus && bIsElemInBody; }, /** * Creates and dispatches a keyboard event on the given DOM element. * * @param {string} sName Name of the keyboard event (e.g., "keydown", "keyup") * @param {Element} oDomRef DOM element on which the event is going to be triggered * @param {object} [oOptions] Options for the keyboard event * @param {string} [oOptions.key] The key value of the key pressed (e.g., "Enter", "ArrowUp", "ArrowDown") * @param {number} [oOptions.keyCode] The keyCode of the key pressed * @param {boolean} [oOptions.shiftKey] Indicates whether the shift key is down in addition * @param {boolean} [oOptions.altKey] Indicates whether the alt key is down in addition * @param {boolean} [oOptions.ctrlKey] Indicates whether the ctrl key is down in addition * @private */ _createAndDispatchKeyboardEvent: function (sName, oDomRef, oOptions) { oOptions = oOptions || {}; var oKeyboardEvent = new KeyboardEvent(sName, { bubbles: true, cancelable: true, key: oOptions.key || "", code: oOptions.code || "", keyCode: oOptions.keyCode || 0, which: oOptions.keyCode || 0, shiftKey: !!oOptions.shiftKey, altKey: !!oOptions.altKey, ctrlKey: !!oOptions.ctrlKey, metaKey: !!oOptions.metaKey, type: sName, target: oDomRef }); oDomRef.dispatchEvent(oKeyboardEvent); this.oLogger.info("Dispatched keyboard event: '" + sName + "'" + (oOptions.key ? " with key '" + oOptions.key + "'" : "")); } }); });