UNPKG

@openui5/sap.ui.core

Version:

OpenUI5 Core Library sap.ui.core

350 lines (316 loc) 13.1 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 FocusEvent, DragEvent, FileList, DataTransfer, DataTransferItemList, 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" ], function (ManagedObject, QUnitUtils, Opa5, Device, jQueryDOM, _OpaLogger) { "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 {@link sap.ui.test.Press.controlAdapters} 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 = jQueryDOM(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 (jQueryDOM.isFunction(vAdapter)) { 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 isAlreadyFocused = this._isFocused(oDomRef); var bIsIE11 = Device.browser.msie && Device.browser.version < 12; var bIsNewFF = Device.browser.firefox && Device.browser.version >= 60; if (isAlreadyFocused || bIsIE11 || 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?"); 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 * @private */ _createAndDispatchMouseEvent: function (sName, oDomRef) { // 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 iLeftMouseButtonIndex = 0; var oMouseEvent; if (Device.browser.msie && Device.browser.version < 12) { oMouseEvent = document.createEvent("MouseEvent"); oMouseEvent.initMouseEvent(sName, true, true, window, 0, 0, 0, 0, 0, false, false, false, false, iLeftMouseButtonIndex, oDomRef); } else { oMouseEvent = new MouseEvent(sName, { bubbles: true, cancelable: true, identifier: 1, target: oDomRef, radiusX: 1, radiusY: 1, rotationAngle: 0, button: iLeftMouseButtonIndex, type: sName // include the type so jQuery.event.fixHooks can copy properties properly }); } oDomRef.dispatchEvent(oMouseEvent); }, _createAndDispatchFocusEvent: function (sName, oDomRef) { var oFocusEvent, bBubbles = ["focusin", "focusout", "activate", "deactivate"].indexOf(sName) !== -1; if (Device.browser.msie && (Device.browser.version < 12)) { oFocusEvent = document.createEvent("FocusEvent"); oFocusEvent.initFocusEvent(sName, bBubbles, false, window, 0, oDomRef); } else { 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) { // calculate drop position based on user input // determines where the source will be dropped: before, after or in place of the target if (Device.browser.msie && Device.browser.version < 12) { // drag and drop is not supported in IE11. // IE11's support for HTML5 drag and drop is questionable.. // when an event is initialized, dataTransfer is nullified. later this causes a null reference error in sap/ui/core/dnd/DragDropInfo // (Unable to set property 'effectAllowed' of undefined or null reference). // Another difference is that DataTransfer is an object in IE11, and would be instantiate like: oDataTransfer = new DataTransfer.constructor() return; } var mCoordinates = this._getEventCoordinates(oDomRef, oOptions); var oDataTransfer = new DataTransfer(); var oDragEvent; if (Device.browser.edge) { oDragEvent = document.createEvent("DragEvent"); oDragEvent.initDragEvent(sName, true, true, window, 0, mCoordinates.x, mCoordinates.y, mCoordinates.x, mCoordinates.y, false, false, false, false, 1, oDomRef, oDataTransfer); } else { 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); }, _getEventCoordinates: function (oDomRef, oOptions) { var $domRef = jQueryDOM(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; } }); });