/* eslint-disable no-nested-ternary */ /** * EventUtils provides some utility methods for creating and sending DOM events. * * When adding methods to this file, please add a performance test for it.
*/
// Certain functions assume this is loaded into browser window scope. // This is modifiable because certain chrome tests create their own gBrowser. /* global gBrowser:true */
// This file is used both in privileged and unprivileged contexts, so we have to // be careful about our access to Components.interfaces. We also want to avoid // naming collisions with anything that might be defined in the scope that imports // this script. // // Even if the real |Components| doesn't exist, we might shim in a simple JS // placebo for compat. An easy way to differentiate this from the real thing // is whether the property is read-only or not. The real |Components| property // is read-only. /* global _EU_Ci, _EU_Cc, _EU_Cu, _EU_ChromeUtils, _EU_OS */
window.__defineGetter__("_EU_Ci", function () { var c = Object.getOwnPropertyDescriptor(window, "Components"); return c && c.value && !c.writable ? Ci : SpecialPowers.Ci;
});
window.__defineGetter__("_EU_Cc", function () { var c = Object.getOwnPropertyDescriptor(window, "Components"); return c && c.value && !c.writable ? Cc : SpecialPowers.Cc;
});
window.__defineGetter__("_EU_Cu", function () { var c = Object.getOwnPropertyDescriptor(window, "Components"); return c && c.value && !c.writable ? Cu : SpecialPowers.Cu;
});
window.__defineGetter__("_EU_ChromeUtils", function () { var c = Object.getOwnPropertyDescriptor(window, "ChromeUtils"); return c && c.value && !c.writable ? ChromeUtils : SpecialPowers.ChromeUtils;
});
function _EU_isLinux(aWindow = window) { if (window._EU_OS) { return window._EU_OS == "linux";
} if (aWindow) { try { return aWindow.navigator.platform.startsWith("Linux");
} catch (ex) {}
} return navigator.platform.startsWith("Linux");
}
function _EU_isAndroid(aWindow = window) { if (window._EU_OS) { return window._EU_OS == "android";
} if (aWindow) { try { return aWindow.navigator.userAgent.includes("Android");
} catch (ex) {}
} return navigator.userAgent.includes("Android");
}
function _EU_maybeWrap(o) { // We're used in some contexts where there is no SpecialPowers and also in // some where it exists but has no wrap() method. And this is somewhat // independent of whether window.Components is a thing... var haveWrap = false; try {
haveWrap = SpecialPowers.wrap != undefined;
} catch (e) { // Just leave it false.
} if (!haveWrap) { // Not much we can do here. return o;
} var c = Object.getOwnPropertyDescriptor(window, "Components"); return c && c.value && !c.writable ? o : SpecialPowers.wrap(o);
}
function _EU_maybeUnwrap(o) { var haveWrap = false; try {
haveWrap = SpecialPowers.unwrap != undefined;
} catch (e) { // Just leave it false.
} if (!haveWrap) { // Not much we can do here. return o;
} var c = Object.getOwnPropertyDescriptor(window, "Components"); return c && c.value && !c.writable ? o : SpecialPowers.unwrap(o);
}
function _EU_getPlatform() { if (_EU_isWin()) { return"windows";
} if (_EU_isMac()) { return"mac";
} if (_EU_isAndroid()) { return"android";
} if (_EU_isLinux()) { return"linux";
} return"unknown";
}
function _EU_roundDevicePixels(aMaybeFractionalPixels) { return Math.floor(aMaybeFractionalPixels + 0.5);
}
/** * promiseElementReadyForUserInput() dispatches mousemove events to aElement * and waits one of them for a while. Then, returns "resolved" state when it's * successfully received. Otherwise, if it couldn't receive mousemove event on * it, this throws an exception. So, aElement must be an element which is * assumed non-collapsed visible element in the window. * * This is useful if you need to synthesize mouse events via the main process * but your test cannot check whether the element is now in APZ to deliver * a user input event.
*/
async function promiseElementReadyForUserInput(
aElement,
aWindow = window,
aLogFunc = null
) { if (typeof aElement == "string") {
aElement = aWindow.document.getElementById(aElement);
}
function waitForMouseMoveForHittest() { returnnew Promise(resolve => {
let timeout; const onHit = () => { if (aLogFunc) {
aLogFunc("mousemove received");
}
aWindow.clearInterval(timeout);
resolve(true);
};
aElement.addEventListener("mousemove", onHit, {
capture: true,
once: true,
});
timeout = aWindow.setInterval(() => { if (aLogFunc) {
aLogFunc("mousemove not received in this 300ms");
}
aElement.removeEventListener("mousemove", onHit, {
capture: true,
});
resolve(false);
}, 300);
synthesizeMouseAtCenter(aElement, { type: "mousemove" }, aWindow);
});
} for (let i = 0; i < 20; i++) { if (await waitForMouseMoveForHittest()) { return Promise.resolve();
}
} thrownew Error("The element or the window did not become interactive");
}
function getElement(id) { returntypeof id == "string" ? document.getElementById(id) : id;
}
/** * Send a mouse event to the node aTarget (aTarget can be an id, or an * actual node) . The "event" passed in to aEvent is just a JavaScript * object with the properties set that the real mouse event object should * have. This includes the type of the mouse event. Pretty much all those * properties are optional. * E.g. to send an click event to the node with id 'node' you might do this: * * ``sendMouseEvent({type:'click'}, 'node');``
*/ function sendMouseEvent(aEvent, aTarget, aWindow) { if (
![ "click", "contextmenu", "dblclick", "mousedown", "mouseup", "mouseover", "mouseout",
].includes(aEvent.type)
) { thrownew Error( "sendMouseEvent doesn't know about event type '" + aEvent.type + "'"
);
}
if (!aWindow) {
aWindow = window;
}
if (typeof aTarget == "string") {
aTarget = aWindow.document.getElementById(aTarget);
}
let event =
aEvent.type == "click" || aEvent.type == "contextmenu"
? new aWindow.PointerEvent(aEvent.type, dict)
: new aWindow.MouseEvent(aEvent.type, dict);
// If documentURIObject exists or `window` is a stub object, we're in // a chrome scope, so don't bother trying to go through SpecialPowers. if (!window.document || window.document.documentURIObject) { return aTarget.dispatchEvent(event);
} return SpecialPowers.dispatchEvent(aWindow, aTarget, event);
}
function isHidden(aElement) { var box = aElement.getBoundingClientRect(); return box.width == 0 && box.height == 0;
}
/** * Send a drag event to the node aTarget (aTarget can be an id, or an * actual node) . The "event" passed in to aEvent is just a JavaScript * object with the properties set that the real drag event object should * have. This includes the type of the drag event.
*/ function sendDragEvent(aEvent, aTarget, aWindow = window) { if (
![ "drag", "dragstart", "dragend", "dragover", "dragenter", "dragleave", "drop",
].includes(aEvent.type)
) { thrownew Error( "sendDragEvent doesn't know about event type '" + aEvent.type + "'"
);
}
if (typeof aTarget == "string") {
aTarget = aWindow.document.getElementById(aTarget);
}
/* * Drag event cannot be performed if the element is hidden, except 'dragend' * event where the element can becomes hidden after start dragging.
*/ if (aEvent.type != "dragend" && isHidden(aTarget)) { var targetName = aTarget.nodeName; if ("id" in aTarget && aTarget.id) {
targetName += "#" + aTarget.id;
} thrownew Error(`${aEvent.type} event target ${targetName} is hidden`);
}
var event = aWindow.document.createEvent("DragEvent");
var typeArg = aEvent.type; var canBubbleArg = true; var cancelableArg = true; var viewArg = aWindow; var detailArg = aEvent.detail || 0; var screenXArg = aEvent.screenX || 0; var screenYArg = aEvent.screenY || 0; var clientXArg = aEvent.clientX || 0; var clientYArg = aEvent.clientY || 0; var ctrlKeyArg = aEvent.ctrlKey || false; var altKeyArg = aEvent.altKey || false; var shiftKeyArg = aEvent.shiftKey || false; var metaKeyArg = aEvent.metaKey || false; var buttonArg = computeButton(aEvent); var relatedTargetArg = aEvent.relatedTarget || null; var dataTransfer = aEvent.dataTransfer || null;
if (aEvent._domDispatchOnly) { return aTarget.dispatchEvent(event);
}
var utils = _getDOMWindowUtils(aWindow); return utils.dispatchDOMEventViaPresShellForTesting(aTarget, event);
}
/** * Send the char aChar to the focused element. This method handles casing of * chars (sends the right charcode, and sends a shift key for uppercase chars). * No other modifiers are handled at this point. * * For now this method only works for ASCII characters and emulates the shift * key state on US keyboard layout.
*/ function sendChar(aChar, aWindow) { var hasShift; // Emulate US keyboard layout for the shiftKey state. switch (aChar) { case"!": case"@": case"#": case"$": case"%": case"^": case"&": case"*": case"(": case")": case"_": case"+": case"{": case"}": case":": case'"': case"|": case"<": case">": case"?":
hasShift = true; break; default:
hasShift =
aChar.toLowerCase() != aChar.toUpperCase() &&
aChar == aChar.toUpperCase(); break;
}
synthesizeKey(aChar, { shiftKey: hasShift }, aWindow);
}
/** * Send the string aStr to the focused element. * * For now this method only works for ASCII characters and emulates the shift * key state on US keyboard layout.
*/ function sendString(aStr, aWindow) { for (let i = 0; i < aStr.length; ++i) { // Do not split a surrogate pair to call synthesizeKey. Dispatching two // sets of keydown and keyup caused by two calls of synthesizeKey is not // good behavior. It could happen due to a bug, but a surrogate pair should // be introduced with one key press operation. Therefore, calling it with // a surrogate pair is the right thing. // Note that TextEventDispatcher will consider whether a surrogate pair // should cause one or two keypress events automatically. Therefore, we // don't need to check the related prefs here. if (
(aStr.charCodeAt(i) & 0xfc00) == 0xd800 &&
i + 1 < aStr.length &&
(aStr.charCodeAt(i + 1) & 0xfc00) == 0xdc00
) {
sendChar(aStr.substring(i, i + 2), aWindow);
i++;
} else {
sendChar(aStr.charAt(i), aWindow);
}
}
}
/** * Send the non-character key aKey to the focused node. * The name of the key should be the part that comes after ``DOM_VK_`` in the * KeyEvent constant name for this key. * No modifiers are handled at this point.
*/ function sendKey(aKey, aWindow) { var keyName = "VK_" + aKey.toUpperCase();
synthesizeKey(keyName, { shiftKey: false }, aWindow);
}
/** * Parse the key modifier flags from aEvent. Used to share code between * synthesizeMouse and synthesizeKey.
*/ function _parseModifiers(aEvent, aWindow = window) { var nsIDOMWindowUtils = _EU_Ci.nsIDOMWindowUtils; var mval = 0; if (aEvent.shiftKey) {
mval |= nsIDOMWindowUtils.MODIFIER_SHIFT;
} if (aEvent.ctrlKey) {
mval |= nsIDOMWindowUtils.MODIFIER_CONTROL;
} if (aEvent.altKey) {
mval |= nsIDOMWindowUtils.MODIFIER_ALT;
} if (aEvent.metaKey) {
mval |= nsIDOMWindowUtils.MODIFIER_META;
} if (aEvent.accelKey) {
mval |= _EU_isMac(aWindow)
? nsIDOMWindowUtils.MODIFIER_META
: nsIDOMWindowUtils.MODIFIER_CONTROL;
} if (aEvent.altGrKey) {
mval |= nsIDOMWindowUtils.MODIFIER_ALTGRAPH;
} if (aEvent.capsLockKey) {
mval |= nsIDOMWindowUtils.MODIFIER_CAPSLOCK;
} if (aEvent.fnKey) {
mval |= nsIDOMWindowUtils.MODIFIER_FN;
} if (aEvent.fnLockKey) {
mval |= nsIDOMWindowUtils.MODIFIER_FNLOCK;
} if (aEvent.numLockKey) {
mval |= nsIDOMWindowUtils.MODIFIER_NUMLOCK;
} if (aEvent.scrollLockKey) {
mval |= nsIDOMWindowUtils.MODIFIER_SCROLLLOCK;
} if (aEvent.symbolKey) {
mval |= nsIDOMWindowUtils.MODIFIER_SYMBOL;
} if (aEvent.symbolLockKey) {
mval |= nsIDOMWindowUtils.MODIFIER_SYMBOLLOCK;
}
return mval;
}
/** * Synthesize a mouse event on a target. The actual client point is determined * by taking the aTarget's client box and offseting it by aOffsetX and * aOffsetY. This allows mouse clicks to be simulated by calling this method. * * aEvent is an object which may contain the properties: * `shiftKey`, `ctrlKey`, `altKey`, `metaKey`, `accessKey`, `clickCount`, * `button`, `type`. * For valid `type`s see nsIDOMWindowUtils' `sendMouseEvent`. * * If the type is specified, an mouse event of that type is fired. Otherwise, * a mousedown followed by a mouseup is performed. * * aWindow is optional, and defaults to the current window object. * * Returns whether the event had preventDefault() called on it.
*/ function synthesizeMouse(aTarget, aOffsetX, aOffsetY, aEvent, aWindow) { var rect = aTarget.getBoundingClientRect(); return synthesizeMouseAtPoint(
rect.left + aOffsetX,
rect.top + aOffsetY,
aEvent,
aWindow
);
}
/** * Synthesize one or more touches on aTarget. aTarget can be either Element * or Array of Elements. aOffsetX, aOffsetY, aEvent.id, aEvent.rx, aEvent.ry, * aEvent.angle, aEvent.force, aEvent.tiltX, aEvent.tiltY and aEvent.twist can * be either Number or Array of Numbers (can be mixed). If you specify array * to synthesize a multi-touch, you need to specify same length arrays. If * you don't specify array to them, same values (or computed default values for * aEvent.id) are used for all touches. * * @param {Element | Element[]} aTarget The target element which you specify * relative offset from its top-left. * @param {Number | Number[]} aOffsetX The relative offset from left of aTarget. * @param {Number | Number[]} aOffsetY The relative offset from top of aTarget. * @param {Object} aEvent * type: The touch event type. If undefined, "touchstart" and "touchend" will * be synthesized at same point. * * id: The touch id. If you don't specify this, default touch id will be used * for first touch and further touch ids are the values incremented from the * first id. * * rx, ry: The radii of the touch. * * angle: The angle in degree. * * force: The force of the touch. If the type is "touchend", this should be 0. * If unspecified, this is default to 0 for "touchend" or 1 for the others. * * tiltX, tiltY: The tilt of the touch. * * twist: The twist of the touch. * @param {Window} aWindow Default to `window`. * @returns true if and only if aEvent.type is specified and default of the * event is prevented.
*/ function synthesizeTouch(
aTarget,
aOffsetX,
aOffsetY,
aEvent = {},
aWindow = window
) {
let rectX, rectY; if (Array.isArray(aTarget)) {
let lastTarget, lastTargetRect;
aTarget.forEach(target => { const rect =
target == lastTarget ? lastTargetRect : target.getBoundingClientRect();
rectX.push(rect.left);
rectY.push(rect.top);
lastTarget = target;
lastTargetRect = rect;
});
} else { const rect = aTarget.getBoundingClientRect();
rectX = [rect.left];
rectY = [rect.top];
} const offsetX = (() => { if (Array.isArray(aOffsetX)) {
let ret = [];
aOffsetX.forEach((value, index) => {
ret.push(value + rectX[Math.min(index, rectX.length - 1)]);
}); return ret;
} return aOffsetX + rectX[0];
})(); const offsetY = (() => { if (Array.isArray(aOffsetY)) {
let ret = [];
aOffsetY.forEach((value, index) => {
ret.push(value + rectY[Math.min(index, rectY.length - 1)]);
}); return ret;
} return aOffsetY + rectY[0];
})(); return synthesizeTouchAtPoint(offsetX, offsetY, aEvent, aWindow);
}
/** * Return the drag service. Note that if we're in the headless mode, this * may return null because the service may be never instantiated (e.g., on * Linux).
*/ function getDragService() { try { return _EU_Cc["@mozilla.org/widget/dragservice;1"].getService(
_EU_Ci.nsIDragService
);
} catch (e) { // If we're in the headless mode, the drag service may be never // instantiated. In this case, an exception is thrown. Let's ignore // any exceptions since without the drag service, nobody can create a // drag session. returnnull;
}
}
/** * End drag session if there is. * * TODO: This should synthesize "drop" if necessary. * * @param left X offset in the viewport * @param top Y offset in the viewport * @param aEvent The event data, the modifiers are applied to the * "dragend" event. * @param aWindow The window. * @return true if handled. In this case, the caller should not * synthesize DOM events basically.
*/ function _maybeEndDragSession(left, top, aEvent, aWindow) {
let utils = _getDOMWindowUtils(aWindow); const dragSession = utils.dragSession; if (!dragSession) { returnfalse;
} // FIXME: If dragSession.dragAction is not // nsIDragService.DRAGDROP_ACTION_NONE nor aEvent.type is not `keydown`, we // need to synthesize a "drop" event or call setDragEndPointForTests here to // set proper left/top to `dragend` event. try {
dragSession.endDragSession(false, _parseModifiers(aEvent, aWindow));
} catch (e) {} returntrue;
}
/* * Synthesize a mouse event at a particular point in aWindow. * * aEvent is an object which may contain the properties: * `shiftKey`, `ctrlKey`, `altKey`, `metaKey`, `accessKey`, `clickCount`, * `button`, `type`. * For valid `type`s see nsIDOMWindowUtils' `sendMouseEvent`. * * If the type is specified, an mouse event of that type is fired. Otherwise, * a mousedown followed by a mouseup is performed. * * aWindow is optional, and defaults to the current window object.
*/ function synthesizeMouseAtPoint(left, top, aEvent, aWindow = window) { if (aEvent.allowToHandleDragDrop) { if (aEvent.type == "mouseup" || !aEvent.type) { if (_maybeEndDragSession(left, top, aEvent, aWindow)) { returnfalse;
}
} elseif (aEvent.type == "mousemove") { if (_maybeSynthesizeDragOver(left, top, aEvent, aWindow)) { returnfalse;
}
}
}
var utils = _getDOMWindowUtils(aWindow); var defaultPrevented = false;
if (utils) { var button = computeButton(aEvent); var clickCount = aEvent.clickCount || 1; var modifiers = _parseModifiers(aEvent, aWindow); var pressure = "pressure" in aEvent ? aEvent.pressure : 0;
// aWindow might be cross-origin from us. var MouseEvent = _EU_maybeWrap(aWindow).MouseEvent;
// Default source to mouse. var inputSource = "inputSource" in aEvent
? aEvent.inputSource
: MouseEvent.MOZ_SOURCE_MOUSE; // Compute a pointerId if needed. var id; if ("id" in aEvent) {
id = aEvent.id;
} else { var isFromPen = inputSource === MouseEvent.MOZ_SOURCE_PEN;
id = isFromPen
? utils.DEFAULT_PEN_POINTER_ID
: utils.DEFAULT_MOUSE_POINTER_ID;
}
// FYI: nsIDOMWindowUtils.sendMouseEvent takes floats for the coordinates. // Therefore, don't round/truncate the fractional values. var isDOMEventSynthesized = "isSynthesized" in aEvent ? aEvent.isSynthesized : true; var isWidgetEventSynthesized = "isWidgetEventSynthesized" in aEvent
? aEvent.isWidgetEventSynthesized
: false; if ("type" in aEvent && aEvent.type) {
defaultPrevented = utils.sendMouseEvent(
aEvent.type,
left,
top,
button,
clickCount,
modifiers, false,
pressure,
inputSource,
isDOMEventSynthesized,
isWidgetEventSynthesized,
computeButtons(aEvent, utils),
id
);
} else {
utils.sendMouseEvent( "mousedown",
left,
top,
button,
clickCount,
modifiers, false,
pressure,
inputSource,
isDOMEventSynthesized,
isWidgetEventSynthesized,
computeButtons(Object.assign({ type: "mousedown" }, aEvent), utils),
id
);
utils.sendMouseEvent( "mouseup",
left,
top,
button,
clickCount,
modifiers, false,
pressure,
inputSource,
isDOMEventSynthesized,
isWidgetEventSynthesized,
computeButtons(Object.assign({ type: "mouseup" }, aEvent), utils),
id
);
}
}
return defaultPrevented;
}
/** * Synthesize one or more touches at the points. aLeft, aTop, aEvent.id, * aEvent.rx, aEvent.ry, aEvent.angle, aEvent.force, aEvent.tiltX, aEvent.tiltY * and aEvent.twist can be either Number or Array of Numbers (can be mixed). * If you specify array to synthesize a multi-touch, you need to specify same * length arrays. If you don't specify array to them, same values are used for * all touches. * * @param {Element | Element[]} aTarget The target element which you specify * relative offset from its top-left. * @param {Number | Number[]} aOffsetX The relative offset from left of aTarget. * @param {Number | Number[]} aOffsetY The relative offset from top of aTarget. * @param {Object} aEvent * type: The touch event type. If undefined, "touchstart" and "touchend" will * be synthesized at same point. * * id: The touch id. If you don't specify this, default touch id will be used * for first touch and further touch ids are the values incremented from the * first id. * * rx, ry: The radii of the touch. * * angle: The angle in degree. * * force: The force of the touch. If the type is "touchend", this should be 0. * If unspecified, this is default to 0 for "touchend" or 1 for the others. * * tiltX, tiltY: The tilt of the touch. * * twist: The twist of the touch. * @param {Window} aWindow Default to `window`. * @returns true if and only if aEvent.type is specified and default of the * event is prevented.
*/ function synthesizeTouchAtPoint(aLeft, aTop, aEvent = {}, aWindow = window) {
let utils = _getDOMWindowUtils(aWindow); if (!utils) { returnfalse;
}
if (
Array.isArray(aLeft) &&
Array.isArray(aTop) &&
aLeft.length != aTop.length
) { thrownew Error(`aLeft and aTop should be same length array`);
}
// Call synthesizeMouse with coordinates at the center of aTarget. function synthesizeMouseAtCenter(aTarget, aEvent, aWindow) { var rect = aTarget.getBoundingClientRect(); return synthesizeMouse(
aTarget,
rect.width / 2,
rect.height / 2,
aEvent,
aWindow
);
} function synthesizeTouchAtCenter(aTarget, aEvent = {}, aWindow = window) { var rect = aTarget.getBoundingClientRect();
synthesizeTouchAtPoint(
rect.left + rect.width / 2,
rect.top + rect.height / 2,
aEvent,
aWindow
);
}
/** * Synthesize a wheel event without flush layout at a particular point in * aWindow. * * aEvent is an object which may contain the properties: * shiftKey, ctrlKey, altKey, metaKey, accessKey, deltaX, deltaY, deltaZ, * deltaMode, lineOrPageDeltaX, lineOrPageDeltaY, isMomentum, * isNoLineOrPageDelta, isCustomizedByPrefs, expectedOverflowDeltaX, * expectedOverflowDeltaY * * deltaMode must be defined, others are ok even if undefined. * * expectedOverflowDeltaX and expectedOverflowDeltaY take integer value. The * value is just checked as 0 or positive or negative. * * aWindow is optional, and defaults to the current window object.
*/ function synthesizeWheelAtPoint(aLeft, aTop, aEvent, aWindow = window) { var utils = _getDOMWindowUtils(aWindow); if (!utils) { return;
}
/** * Synthesize a wheel event on a target. The actual client point is determined * by taking the aTarget's client box and offseting it by aOffsetX and * aOffsetY. * * aEvent is an object which may contain the properties: * shiftKey, ctrlKey, altKey, metaKey, accessKey, deltaX, deltaY, deltaZ, * deltaMode, lineOrPageDeltaX, lineOrPageDeltaY, isMomentum, * isNoLineOrPageDelta, isCustomizedByPrefs, expectedOverflowDeltaX, * expectedOverflowDeltaY * * deltaMode must be defined, others are ok even if undefined. * * expectedOverflowDeltaX and expectedOverflowDeltaY take integer value. The * value is just checked as 0 or positive or negative. * * aWindow is optional, and defaults to the current window object.
*/ function synthesizeWheel(aTarget, aOffsetX, aOffsetY, aEvent, aWindow) { var rect = aTarget.getBoundingClientRect();
synthesizeWheelAtPoint(
rect.left + aOffsetX,
rect.top + aOffsetY,
aEvent,
aWindow
);
}
const _FlushModes = {
FLUSH: 0,
NOFLUSH: 1,
};
function _sendWheelAndPaint(
aTarget,
aOffsetX,
aOffsetY,
aEvent,
aCallback,
aFlushMode = _FlushModes.FLUSH,
aWindow = window
) { var utils = _getDOMWindowUtils(aWindow); if (!utils) { return;
}
if (utils.isMozAfterPaintPending) { // If a paint is pending, then APZ may be waiting for a scroll acknowledgement // from the content thread. If we send a wheel event now, it could be ignored // by APZ (or its scroll offset could be overridden). To avoid problems we // just wait for the paint to complete.
aWindow.waitForAllPaintsFlushed(function () {
_sendWheelAndPaint(
aTarget,
aOffsetX,
aOffsetY,
aEvent,
aCallback,
aFlushMode,
aWindow
);
}); return;
}
var onwheel = function () {
SpecialPowers.wrap(window).removeEventListener("wheel", onwheel, {
mozSystemGroup: true,
});
// Wait one frame since the wheel event has not caused a refresh observer // to be added yet.
setTimeout(function () {
utils.advanceTimeAndRefresh(1000);
if (!aCallback) {
utils.advanceTimeAndRefresh(0); return;
}
var waitForPaints = function () {
SpecialPowers.Services.obs.removeObserver(
waitForPaints, "apz-repaints-flushed"
);
aWindow.waitForAllPaintsFlushed(function () {
utils.restoreNormalRefresh();
aCallback();
});
};
// Listen for the system wheel event, because it happens after all of // the other wheel events, including legacy events.
SpecialPowers.wrap(aWindow).addEventListener("wheel", onwheel, {
mozSystemGroup: true,
}); if (aFlushMode === _FlushModes.FLUSH) {
synthesizeWheel(aTarget, aOffsetX, aOffsetY, aEvent, aWindow);
} else {
synthesizeWheelAtPoint(aOffsetX, aOffsetY, aEvent, aWindow);
}
}
/** * This is a wrapper around synthesizeWheel that waits for the wheel event * to be dispatched and for the subsequent layout/paints to be flushed. * * This requires including paint_listener.js. Tests must call * DOMWindowUtils.restoreNormalRefresh() before finishing, if they use this * function. * * If no callback is provided, the caller is assumed to have its own method of * determining scroll completion and the refresh driver is not automatically * restored.
*/ function sendWheelAndPaint(
aTarget,
aOffsetX,
aOffsetY,
aEvent,
aCallback,
aWindow = window
) {
_sendWheelAndPaint(
aTarget,
aOffsetX,
aOffsetY,
aEvent,
aCallback,
_FlushModes.FLUSH,
aWindow
);
}
/** * Similar to sendWheelAndPaint but without flushing layout for obtaining * ``aTarget`` position in ``aWindow`` before sending the wheel event. * ``aOffsetX`` and ``aOffsetY`` should be offsets against aWindow.
*/ function sendWheelAndPaintNoFlush(
aTarget,
aOffsetX,
aOffsetY,
aEvent,
aCallback,
aWindow = window
) {
_sendWheelAndPaint(
aTarget,
aOffsetX,
aOffsetY,
aEvent,
aCallback,
_FlushModes.NOFLUSH,
aWindow
);
}
function synthesizeNativeTap(
aTarget,
aOffsetX,
aOffsetY,
aLongTap = false,
aCallback = null,
aWindow = window
) {
let utils = _getDOMWindowUtils(aWindow); if (!utils) { return;
}
let scale = aWindow.devicePixelRatio;
let rect = aTarget.getBoundingClientRect();
let x = _EU_roundDevicePixels(
(aWindow.mozInnerScreenX + rect.left + aOffsetX) * scale
);
let y = _EU_roundDevicePixels(
(aWindow.mozInnerScreenY + rect.top + aOffsetY) * scale
);
let observer = {
observe: (subject, topic, data) => { if (aCallback && topic == "mouseevent") {
aCallback(data);
}
},
};
utils.sendNativeTouchTap(x, y, aLongTap, observer);
}
/** * Similar to synthesizeMouse but generates a native widget level event * (so will actually move the "real" mouse cursor etc. Be careful because * this can impact later code as well! (e.g. with hover states etc.) * * @description There are 3 mutually exclusive ways of indicating the location of the * mouse event: set ``atCenter``, or pass ``offsetX`` and ``offsetY``, * or pass ``screenX`` and ``screenY``. Do not attempt to mix these. * * @param {object} aParams * @param {string} aParams.type "click", "mousedown", "mouseup" or "mousemove" * @param {Element} aParams.target Origin of offsetX and offsetY, must be an element * @param {Boolean} [aParams.atCenter] * Instead of offsetX/Y, synthesize the event at center of `target`. * @param {Number} [aParams.offsetX] * X offset in `target` (in CSS pixels if `scale` is "screenPixelsPerCSSPixel") * @param {Number} [aParams.offsetY] * Y offset in `target` (in CSS pixels if `scale` is "screenPixelsPerCSSPixel") * @param {Number} [aParams.screenX] * X offset in screen (in CSS pixels if `scale` is "screenPixelsPerCSSPixel"), * Neither offsetX/Y nor atCenter must be set if this is set. * @param {Number} [aParams.screenY] * Y offset in screen (in CSS pixels if `scale` is "screenPixelsPerCSSPixel"), * Neither offsetX/Y nor atCenter must be set if this is set. * @param {String} [aParams.scale="screenPixelsPerCSSPixel"] * If scale is "screenPixelsPerCSSPixel", devicePixelRatio will be used. * If scale is "inScreenPixels", clientX/Y nor scaleX/Y are not adjusted with screenPixelsPerCSSPixel. * @param {Number} [aParams.button=0] * Defaults to 0, if "click", "mousedown", "mouseup", set same value as DOM MouseEvent.button * @param {Object} [aParams.modifiers={}] * Active modifiers, see `_parseNativeModifiers` * @param {Window} [aParams.win=window] * The window to use its utils. Defaults to the window in which EventUtils.js is running. * @param {Element} [aParams.elementOnWidget=target] * Defaults to target. If element under the point is in another widget from target's widget, * e.g., when it's in a XUL <panel>, specify this.
*/ function synthesizeNativeMouseEvent(aParams, aCallback = null) { const {
type,
target,
offsetX,
offsetY,
atCenter,
screenX,
screenY,
scale = "screenPixelsPerCSSPixel",
button = 0,
modifiers = {},
win = window,
elementOnWidget = target,
} = aParams; if (atCenter) { if (offsetX != undefined || offsetY != undefined) { throw Error(
`atCenter is specified, but offsetX (${offsetX}) and/or offsetY (${offsetY}) are also specified`
);
} if (screenX != undefined || screenY != undefined) { throw Error(
`atCenter is specified, but screenX (${screenX}) and/or screenY (${screenY}) are also specified`
);
} if (!target) { throw Error("atCenter is specified, but target is not specified");
}
} elseif (offsetX != undefined && offsetY != undefined) { if (screenX != undefined || screenY != undefined) { throw Error(
`offsetX/Y are specified, but screenX (${screenX}) and/or screenY (${screenY}) are also specified`
);
} if (!target) { throw Error( "offsetX and offsetY are specified, but target is not specified"
);
}
} elseif (screenX != undefined && screenY != undefined) { if (offsetX != undefined || offsetY != undefined) { throw Error(
`screenX/Y are specified, but offsetX (${offsetX}) and/or offsetY (${offsetY}) are also specified`
);
}
} const utils = _getDOMWindowUtils(win); if (!utils) { return;
}
const rect = target?.getBoundingClientRect();
let resolution = 1.0; try {
resolution = _getDOMWindowUtils(win.top).getResolution();
} catch (e) { // XXX How to get mobile viewport scale on Fission+xorigin since // window.top access isn't allowed due to cross-origin?
} const scaleValue = (() => { if (scale === "inScreenPixels") { return 1.0;
} if (scale === "screenPixelsPerCSSPixel") { return win.devicePixelRatio;
} throw Error(`invalid scale value (${scale}) is specified`);
})(); // XXX mozInnerScreen might be invalid value on mobile viewport (Bug 1701546), // so use window.top's mozInnerScreen. But this won't work fission+xorigin // with mobile viewport until mozInnerScreen returns valid value with // scale. const x = _EU_roundDevicePixels(
(() => { if (screenX != undefined) { return screenX * scaleValue;
}
let winInnerOffsetX = win.mozInnerScreenX; try {
winInnerOffsetX =
win.top.mozInnerScreenX +
(win.mozInnerScreenX - win.top.mozInnerScreenX) * resolution;
} catch (e) { // XXX fission+xorigin test throws permission denied since win.top is // cross-origin.
} return (
(((atCenter ? rect.width / 2 : offsetX) + rect.left) * resolution +
winInnerOffsetX) *
scaleValue
);
})()
); const y = _EU_roundDevicePixels(
(() => { if (screenY != undefined) { return screenY * scaleValue;
}
let winInnerOffsetY = win.mozInnerScreenY; try {
winInnerOffsetY =
win.top.mozInnerScreenY +
(win.mozInnerScreenY - win.top.mozInnerScreenY) * resolution;
} catch (e) { // XXX fission+xorigin test throws permission denied since win.top is // cross-origin.
} return (
(((atCenter ? rect.height / 2 : offsetY) + rect.top) * resolution +
winInnerOffsetY) *
scaleValue
);
})()
); const modifierFlags = _parseNativeModifiers(modifiers);
function promiseNativeMouseEventAndWaitForEvent(aParams) { returnnew Promise(resolve =>
synthesizeNativeMouseEventAndWaitForEvent(aParams, resolve)
);
}
/** * This is a wrapper around synthesizeNativeMouseEvent that waits for the mouse * event to be dispatched to the target content. * * This API is supposed to be used in those test cases that synthesize some * input events to chrome process and have some checks in content.
*/ function synthesizeAndWaitNativeMouseMove(
aTarget,
aOffsetX,
aOffsetY,
aCallback,
aWindow = window
) {
let browser = gBrowser.selectedTab.linkedBrowser;
let mm = browser.messageManager;
let { ContentTask } = _EU_ChromeUtils.importESModule( "resource://testing-common/ContentTask.sys.mjs"
);
/** * Synthesize a key event. It is targeted at whatever would be targeted by an * actual keypress by the user, typically the focused element. * * @param {String} aKey * Should be either: * * - key value (recommended). If you specify a non-printable key name, * prepend the ``KEY_`` prefix. Otherwise, specifying a printable key, the * key value should be specified. * * - keyCode name starting with ``VK_`` (e.g., ``VK_RETURN``). This is available * only for compatibility with legacy API. Don't use this with new tests. * * @param {Object} [aEvent] * Optional event object with more specifics about the key event to * synthesize. * @param {String} [aEvent.code] * If you don't specify this explicitly, it'll be guessed from aKey * of US keyboard layout. Note that this value may be different * between browsers. For example, "Insert" is never set only on * macOS since actual key operation won't cause this code value. * In such case, the value becomes empty string. * If you need to emulate non-US keyboard layout or virtual keyboard * which doesn't emulate hardware key input, you should set this value * to empty string explicitly. * @param {Number} [aEvent.repeat] * If you emulate auto-repeat, you should set the count of repeat. * This method will automatically synthesize keydown (and keypress). * @param {*} aEvent.location * If you want to specify this, you can specify this explicitly. * However, if you don't specify this value, it will be computed * from code value. * @param {String} aEvent.type * Basically, you shouldn't specify this. Then, this function will * synthesize keydown (, keypress) and keyup. * If keydown is specified, this only fires keydown (and keypress if * it should be fired). * If keyup is specified, this only fires keyup. * @param {Number} aEvent.keyCode * Must be 0 - 255 (0xFF). If this is specified explicitly, * .keyCode value is initialized with this value. * @param {Window} aWindow * Is optional and defaults to the current window object. * @param {Function} aCallback * Is optional and can be used to receive notifications from TIP. * * @description * ``accelKey``, ``altKey``, ``altGraphKey``, ``ctrlKey``, ``capsLockKey``, * ``fnKey``, ``fnLockKey``, ``numLockKey``, ``metaKey``, ``scrollLockKey``, * ``shiftKey``, ``symbolKey``, ``symbolLockKey`` * Basically, you shouldn't use these attributes. nsITextInputProcessor * manages modifier key state when you synthesize modifier key events. * However, if some of these attributes are true, this function activates * the modifiers only during dispatching the key events. * Note that if some of these values are false, they are ignored (i.e., * not inactivated with this function). *
*/ function synthesizeKey(aKey, aEvent = undefined, aWindow = window, aCallback) { const event = aEvent === undefined || aEvent === null ? {} : aEvent;
let dispatchKeydown =
!("type" in event) || event.type === "keydown" || !event.type; const dispatchKeyup =
!("type" in event) || event.type === "keyup" || !event.type;
if (dispatchKeydown && aKey == "KEY_Escape") {
let eventForKeydown = Object.assign({}, JSON.parse(JSON.stringify(event)));
eventForKeydown.type = "keydown"; if (
_maybeEndDragSession( // TODO: We should set the last dragover point instead
0,
0,
eventForKeydown,
aWindow
)
) { if (!dispatchKeyup) { return;
} // We don't need to dispatch only keydown event because it's consumed by // the drag session.
dispatchKeydown = false;
}
}
var TIP = _getTIP(aWindow, aCallback); if (!TIP) { return;
} var KeyboardEvent = _getKeyboardEvent(aWindow); var modifiers = _emulateToActivateModifiers(TIP, event, aWindow); var keyEventDict = _createKeyboardEventDictionary(aKey, event, TIP, aWindow); var keyEvent = new KeyboardEvent("", keyEventDict.dictionary);
try { if (dispatchKeydown) {
TIP.keydown(keyEvent, keyEventDict.flags); if ("repeat" in event && event.repeat > 1) {
keyEventDict.dictionary.repeat = true; var repeatedKeyEvent = new KeyboardEvent("", keyEventDict.dictionary); for (var i = 1; i < event.repeat; i++) {
TIP.keydown(repeatedKeyEvent, keyEventDict.flags);
}
}
} if (dispatchKeyup) {
TIP.keyup(keyEvent, keyEventDict.flags);
}
} finally {
_emulateToInactivateModifiers(TIP, modifiers, aWindow);
}
}
/** * This is a wrapper around synthesizeKey that waits for the key event to be * dispatched to the target content. It returns a promise which is resolved * when the content receives the key event. * * This API is supposed to be used in those test cases that synthesize some * input events to chrome process and have some checks in content.
*/ function synthesizeAndWaitKey(
aKey,
aEvent,
aWindow = window,
checkBeforeSynthesize,
checkAfterSynthesize
) {
let browser = gBrowser.selectedTab.linkedBrowser;
let mm = browser.messageManager;
let keyCode = _createKeyboardEventDictionary(aKey, aEvent, null, aWindow)
.dictionary.keyCode;
let { ContentTask } = _EU_ChromeUtils.importESModule( "resource://testing-common/ContentTask.sys.mjs"
);
let keyRegisteredPromise = new Promise(resolve => {
mm.addMessageListener("Test:KeyRegistered", function processed() {
mm.removeMessageListener("Test:KeyRegistered", processed);
resolve();
});
}); // eslint-disable-next-line no-shadow
let keyReceivedPromise = ContentTask.spawn(browser, keyCode, keyCode => { returnnew Promise(resolve => {
addEventListener("keyup", function onKeyEvent(e) { if (e.keyCode == keyCode) {
removeEventListener("keyup", onKeyEvent);
resolve();
}
});
sendAsyncMessage("Test:KeyRegistered");
});
});
keyRegisteredPromise.then(() => { if (checkBeforeSynthesize) {
checkBeforeSynthesize();
}
synthesizeKey(aKey, aEvent, aWindow); if (checkAfterSynthesize) {
checkAfterSynthesize();
}
}); return keyReceivedPromise;
}
function _parseNativeModifiers(aModifiers, aWindow = window) {
let modifiers = 0; if (aModifiers.capsLockKey) {
modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CAPS_LOCK;
} if (aModifiers.numLockKey) {
modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_NUM_LOCK;
} if (aModifiers.shiftKey) {
modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_SHIFT_LEFT;
} if (aModifiers.shiftRightKey) {
modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_SHIFT_RIGHT;
} if (aModifiers.ctrlKey) {
modifiers |=
SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CONTROL_LEFT;
} if (aModifiers.ctrlRightKey) {
modifiers |=
SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CONTROL_RIGHT;
} if (aModifiers.altKey) {
modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_ALT_LEFT;
} if (aModifiers.altRightKey) {
modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_ALT_RIGHT;
} if (aModifiers.metaKey) {
modifiers |=
SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_COMMAND_LEFT;
} if (aModifiers.metaRightKey) {
modifiers |=
SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_COMMAND_RIGHT;
} if (aModifiers.helpKey) {
modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_HELP;
} if (aModifiers.fnKey) {
modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_FUNCTION;
} if (aModifiers.numericKeyPadKey) {
modifiers |=
SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_NUMERIC_KEY_PAD;
}
// Mac: Any unused number is okay for adding new keyboard layout. // When you add new keyboard layout here, you need to modify // TISInputSourceWrapper::InitByLayoutID(). // Win: These constants can be found by inspecting registry keys under // HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Keyboard Layouts
/** * synthesizeNativeKey() dispatches native key event on active window. * This is implemented only on Windows and Mac. Note that this function * dispatches the key event asynchronously and returns immediately. If a * callback function is provided, the callback will be called upon * completion of the key dispatch. * * @param aKeyboardLayout One of KEYBOARD_LAYOUT_* defined above. * @param aNativeKeyCode A native keycode value defined in * NativeKeyCodes.js. * @param aModifiers Modifier keys. If no modifire key is pressed, * this must be {}. Otherwise, one or more items * referred in _parseNativeModifiers() must be * true. * @param aChars Specify characters which should be generated * by the key event. * @param aUnmodifiedChars Specify characters of unmodified (except Shift) * aChar value. * @param aCallback If provided, this callback will be invoked * once the native keys have been processed * by Gecko. Will never be called if this * function returns false. * @return True if this function succeed dispatching * native key event. Otherwise, false.
*/
function synthesizeNativeKey(
aKeyboardLayout,
aNativeKeyCode,
aModifiers,
aChars,
aUnmodifiedChars,
aCallback,
aWindow = window
) { var utils = _getDOMWindowUtils(aWindow); if (!utils) { returnfalse;
} var nativeKeyboardLayout = null; if (_EU_isMac(aWindow)) {
nativeKeyboardLayout = aKeyboardLayout.Mac;
} elseif (_EU_isWin(aWindow)) {
nativeKeyboardLayout = aKeyboardLayout.Win;
} if (nativeKeyboardLayout === null) { returnfalse;
}
/** * Indicate that an event with an original target of aExpectedTarget and * a type of aExpectedEvent is expected to be fired, or not expected to * be fired.
*/ function _expectEvent(aExpectedTarget, aExpectedEvent, aTestName) { if (!aExpectedTarget || !aExpectedEvent) { returnnull;
}
_gSeenEvent = false;
var type =
aExpectedEvent.charAt(0) == "!"
? aExpectedEvent.substring(1)
: aExpectedEvent; var eventHandler = function (event) { var epassed =
!_gSeenEvent &&
event.originalTarget == aExpectedTarget &&
event.type == type;
is(
epassed, true,
aTestName + " " + type + " event target " + (_gSeenEvent ? "twice" : "")
);
_gSeenEvent = true;
};
/** * Check if the event was fired or not. The event handler aEventHandler * will be removed.
*/ function _checkExpectedEvent(
aExpectedTarget,
aExpectedEvent,
aEventHandler,
aTestName
) { if (aEventHandler) { var expectEvent = aExpectedEvent.charAt(0) != "!"; var type = expectEvent ? aExpectedEvent : aExpectedEvent.substring(1);
aExpectedTarget.removeEventListener(type, aEventHandler); var desc = type + " event"; if (!expectEvent) {
desc += " not";
}
is(_gSeenEvent, expectEvent, aTestName + " " + desc + " fired");
}
_gSeenEvent = false;
}
/** * Similar to synthesizeMouse except that a test is performed to see if an * event is fired at the right target as a result. * * aExpectedTarget - the expected originalTarget of the event. * aExpectedEvent - the expected type of the event, such as 'select'. * aTestName - the test name when outputing results * * To test that an event is not fired, use an expected type preceded by an * exclamation mark, such as '!select'. This might be used to test that a * click on a disabled element doesn't fire certain events for instance. * * aWindow is optional, and defaults to the current window object.
*/ function synthesizeMouseExpectEvent(
aTarget,
aOffsetX,
aOffsetY,
aEvent,
aExpectedTarget,
aExpectedEvent,
aTestName,
aWindow
) { var eventHandler = _expectEvent(aExpectedTarget, aExpectedEvent, aTestName);
synthesizeMouse(aTarget, aOffsetX, aOffsetY, aEvent, aWindow);
_checkExpectedEvent(aExpectedTarget, aExpectedEvent, eventHandler, aTestName);
}
/** * Similar to synthesizeKey except that a test is performed to see if an * event is fired at the right target as a result. * * aExpectedTarget - the expected originalTarget of the event. * aExpectedEvent - the expected type of the event, such as 'select'. * aTestName - the test name when outputing results * * To test that an event is not fired, use an expected type preceded by an * exclamation mark, such as '!select'. * * aWindow is optional, and defaults to the current window object.
*/ function synthesizeKeyExpectEvent(
key,
aEvent,
aExpectedTarget,
aExpectedEvent,
aTestName,
aWindow
) { var eventHandler = _expectEvent(aExpectedTarget, aExpectedEvent, aTestName);
synthesizeKey(key, aEvent, aWindow);
_checkExpectedEvent(aExpectedTarget, aExpectedEvent, eventHandler, aTestName);
}
function disableNonTestMouseEvents(aDisable) { var domutils = _getDOMWindowUtils();
domutils.disableNonTestMouseEvents(aDisable);
}
function _getDOMWindowUtils(aWindow = window) { // Leave this here as something, somewhere, passes a falsy argument
--> --------------------
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.