@openui5/sap.ui.core
Version:
OpenUI5 Core Library sap.ui.core
407 lines (368 loc) • 14.9 kB
JavaScript
/*!
* 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 + "'" : ""));
}
});
});