2 * EventUtils provides some utility methods for creating and sending DOM events.
4 * When adding methods to this file, please add a performance test for it.
7 // Certain functions assume this is loaded into browser window scope.
8 // This is modifiable because certain chrome tests create their own gBrowser.
9 /* global gBrowser:true */
11 // This file is used both in privileged and unprivileged contexts, so we have to
12 // be careful about our access to Components.interfaces. We also want to avoid
13 // naming collisions with anything that might be defined in the scope that imports
16 // Even if the real |Components| doesn't exist, we might shim in a simple JS
17 // placebo for compat. An easy way to differentiate this from the real thing
18 // is whether the property is read-only or not. The real |Components| property
20 /* global _EU_Ci, _EU_Cc, _EU_Cu, _EU_ChromeUtils, _EU_OS */
21 window.__defineGetter__("_EU_Ci", function () {
22 var c = Object.getOwnPropertyDescriptor(window, "Components");
23 return c && c.value && !c.writable ? Ci : SpecialPowers.Ci;
26 window.__defineGetter__("_EU_Cc", function () {
27 var c = Object.getOwnPropertyDescriptor(window, "Components");
28 return c && c.value && !c.writable ? Cc : SpecialPowers.Cc;
31 window.__defineGetter__("_EU_Cu", function () {
32 var c = Object.getOwnPropertyDescriptor(window, "Components");
33 return c && c.value && !c.writable ? Cu : SpecialPowers.Cu;
36 window.__defineGetter__("_EU_ChromeUtils", function () {
37 var c = Object.getOwnPropertyDescriptor(window, "ChromeUtils");
38 return c && c.value && !c.writable ? ChromeUtils : SpecialPowers.ChromeUtils;
41 window.__defineGetter__("_EU_OS", function () {
44 this._EU_OS = _EU_ChromeUtils.import(
45 "resource://gre/modules/AppConstants.jsm"
53 function _EU_isMac(aWindow = window) {
55 return window._EU_OS == "macosx";
59 return aWindow.navigator.platform.indexOf("Mac") > -1;
62 return navigator.platform.indexOf("Mac") > -1;
65 function _EU_isWin(aWindow = window) {
67 return window._EU_OS == "win";
71 return aWindow.navigator.platform.indexOf("Win") > -1;
74 return navigator.platform.indexOf("Win") > -1;
77 function _EU_isLinux(aWindow = window) {
79 return window._EU_OS == "linux";
83 return aWindow.navigator.platform.startsWith("Linux");
86 return navigator.platform.startsWith("Linux");
89 function _EU_isAndroid(aWindow = window) {
91 return window._EU_OS == "android";
95 return aWindow.navigator.userAgent.includes("Android");
98 return navigator.userAgent.includes("Android");
101 function _EU_maybeWrap(o) {
102 // We're used in some contexts where there is no SpecialPowers and also in
103 // some where it exists but has no wrap() method. And this is somewhat
104 // independent of whether window.Components is a thing...
105 var haveWrap = false;
107 haveWrap = SpecialPowers.wrap != undefined;
109 // Just leave it false.
112 // Not much we can do here.
115 var c = Object.getOwnPropertyDescriptor(window, "Components");
116 return c && c.value && !c.writable ? o : SpecialPowers.wrap(o);
119 function _EU_maybeUnwrap(o) {
120 var haveWrap = false;
122 haveWrap = SpecialPowers.unwrap != undefined;
124 // Just leave it false.
127 // Not much we can do here.
130 var c = Object.getOwnPropertyDescriptor(window, "Components");
131 return c && c.value && !c.writable ? o : SpecialPowers.unwrap(o);
134 function _EU_getPlatform() {
141 if (_EU_isAndroid()) {
151 * promiseElementReadyForUserInput() dispatches mousemove events to aElement
152 * and waits one of them for a while. Then, returns "resolved" state when it's
153 * successfully received. Otherwise, if it couldn't receive mousemove event on
154 * it, this throws an exception. So, aElement must be an element which is
155 * assumed non-collapsed visible element in the window.
157 * This is useful if you need to synthesize mouse events via the main process
158 * but your test cannot check whether the element is now in APZ to deliver
159 * a user input event.
161 async function promiseElementReadyForUserInput(
166 if (typeof aElement == "string") {
167 aElement = aWindow.document.getElementById(aElement);
170 function waitForMouseMoveForHittest() {
171 return new Promise(resolve => {
173 const onHit = () => {
175 aLogFunc("mousemove received");
177 aWindow.clearInterval(timeout);
180 aElement.addEventListener("mousemove", onHit, {
184 timeout = aWindow.setInterval(() => {
186 aLogFunc("mousemove not received in this 300ms");
188 aElement.removeEventListener("mousemove", onHit, {
193 synthesizeMouseAtCenter(aElement, { type: "mousemove" }, aWindow);
196 for (let i = 0; i < 20; i++) {
197 if (await waitForMouseMoveForHittest()) {
198 return Promise.resolve();
201 throw new Error("The element or the window did not become interactive");
204 function getElement(id) {
205 return typeof id == "string" ? document.getElementById(id) : id;
208 this.$ = this.getElement;
210 function computeButton(aEvent) {
211 if (typeof aEvent.button != "undefined") {
212 return aEvent.button;
214 return aEvent.type == "contextmenu" ? 2 : 0;
217 function computeButtons(aEvent, utils) {
218 if (typeof aEvent.buttons != "undefined") {
219 return aEvent.buttons;
222 if (typeof aEvent.button != "undefined") {
223 return utils.MOUSE_BUTTONS_NOT_SPECIFIED;
226 if (typeof aEvent.type != "undefined" && aEvent.type != "mousedown") {
227 return utils.MOUSE_BUTTONS_NO_BUTTON;
230 return utils.MOUSE_BUTTONS_NOT_SPECIFIED;
234 * Send a mouse event to the node aTarget (aTarget can be an id, or an
235 * actual node) . The "event" passed in to aEvent is just a JavaScript
236 * object with the properties set that the real mouse event object should
237 * have. This includes the type of the mouse event. Pretty much all those
238 * properties are optional.
239 * E.g. to send an click event to the node with id 'node' you might do this:
241 * ``sendMouseEvent({type:'click'}, 'node');``
243 function sendMouseEvent(aEvent, aTarget, aWindow) {
253 ].includes(aEvent.type)
256 "sendMouseEvent doesn't know about event type '" + aEvent.type + "'"
264 if (typeof aTarget == "string") {
265 aTarget = aWindow.document.getElementById(aTarget);
268 var event = aWindow.document.createEvent("MouseEvent");
270 var typeArg = aEvent.type;
271 var canBubbleArg = true;
272 var cancelableArg = true;
273 var viewArg = aWindow;
276 // eslint-disable-next-line no-nested-ternary
277 (aEvent.type == "click" ||
278 aEvent.type == "mousedown" ||
279 aEvent.type == "mouseup"
281 : aEvent.type == "dblclick"
284 var screenXArg = aEvent.screenX || 0;
285 var screenYArg = aEvent.screenY || 0;
286 var clientXArg = aEvent.clientX || 0;
287 var clientYArg = aEvent.clientY || 0;
288 var ctrlKeyArg = aEvent.ctrlKey || false;
289 var altKeyArg = aEvent.altKey || false;
290 var shiftKeyArg = aEvent.shiftKey || false;
291 var metaKeyArg = aEvent.metaKey || false;
292 var buttonArg = computeButton(aEvent);
293 var relatedTargetArg = aEvent.relatedTarget || null;
295 event.initMouseEvent(
313 // If documentURIObject exists or `window` is a stub object, we're in
314 // a chrome scope, so don't bother trying to go through SpecialPowers.
315 if (!window.document || window.document.documentURIObject) {
316 return aTarget.dispatchEvent(event);
318 return SpecialPowers.dispatchEvent(aWindow, aTarget, event);
321 function isHidden(aElement) {
322 var box = aElement.getBoundingClientRect();
323 return box.width == 0 && box.height == 0;
327 * Send a drag event to the node aTarget (aTarget can be an id, or an
328 * actual node) . The "event" passed in to aEvent is just a JavaScript
329 * object with the properties set that the real drag event object should
330 * have. This includes the type of the drag event.
332 function sendDragEvent(aEvent, aTarget, aWindow = window) {
342 ].includes(aEvent.type)
345 "sendDragEvent doesn't know about event type '" + aEvent.type + "'"
349 if (typeof aTarget == "string") {
350 aTarget = aWindow.document.getElementById(aTarget);
354 * Drag event cannot be performed if the element is hidden, except 'dragend'
355 * event where the element can becomes hidden after start dragging.
357 if (aEvent.type != "dragend" && isHidden(aTarget)) {
358 var targetName = aTarget.nodeName;
359 if ("id" in aTarget && aTarget.id) {
360 targetName += "#" + aTarget.id;
362 throw new Error(`${aEvent.type} event target ${targetName} is hidden`);
365 var event = aWindow.document.createEvent("DragEvent");
367 var typeArg = aEvent.type;
368 var canBubbleArg = true;
369 var cancelableArg = true;
370 var viewArg = aWindow;
371 var detailArg = aEvent.detail || 0;
372 var screenXArg = aEvent.screenX || 0;
373 var screenYArg = aEvent.screenY || 0;
374 var clientXArg = aEvent.clientX || 0;
375 var clientYArg = aEvent.clientY || 0;
376 var ctrlKeyArg = aEvent.ctrlKey || false;
377 var altKeyArg = aEvent.altKey || false;
378 var shiftKeyArg = aEvent.shiftKey || false;
379 var metaKeyArg = aEvent.metaKey || false;
380 var buttonArg = computeButton(aEvent);
381 var relatedTargetArg = aEvent.relatedTarget || null;
382 var dataTransfer = aEvent.dataTransfer || null;
403 if (aEvent._domDispatchOnly) {
404 return aTarget.dispatchEvent(event);
407 var utils = _getDOMWindowUtils(aWindow);
408 return utils.dispatchDOMEventViaPresShellForTesting(aTarget, event);
412 * Send the char aChar to the focused element. This method handles casing of
413 * chars (sends the right charcode, and sends a shift key for uppercase chars).
414 * No other modifiers are handled at this point.
416 * For now this method only works for ASCII characters and emulates the shift
417 * key state on US keyboard layout.
419 function sendChar(aChar, aWindow) {
421 // Emulate US keyboard layout for the shiftKey state.
447 aChar.toLowerCase() != aChar.toUpperCase() &&
448 aChar == aChar.toUpperCase();
451 synthesizeKey(aChar, { shiftKey: hasShift }, aWindow);
455 * Send the string aStr to the focused element.
457 * For now this method only works for ASCII characters and emulates the shift
458 * key state on US keyboard layout.
460 function sendString(aStr, aWindow) {
461 for (let i = 0; i < aStr.length; ++i) {
462 // Do not split a surrogate pair to call synthesizeKey. Dispatching two
463 // sets of keydown and keyup caused by two calls of synthesizeKey is not
464 // good behavior. It could happen due to a bug, but a surrogate pair should
465 // be introduced with one key press operation. Therefore, calling it with
466 // a surrogate pair is the right thing.
467 // Note that TextEventDispatcher will consider whether a surrogate pair
468 // should cause one or two keypress events automatically. Therefore, we
469 // don't need to check the related prefs here.
471 (aStr.charCodeAt(i) & 0xfc00) == 0xd800 &&
472 i + 1 < aStr.length &&
473 (aStr.charCodeAt(i + 1) & 0xfc00) == 0xdc00
475 sendChar(aStr.substring(i, i + 2), aWindow);
478 sendChar(aStr.charAt(i), aWindow);
484 * Send the non-character key aKey to the focused node.
485 * The name of the key should be the part that comes after ``DOM_VK_`` in the
486 * KeyEvent constant name for this key.
487 * No modifiers are handled at this point.
489 function sendKey(aKey, aWindow) {
490 var keyName = "VK_" + aKey.toUpperCase();
491 synthesizeKey(keyName, { shiftKey: false }, aWindow);
495 * Parse the key modifier flags from aEvent. Used to share code between
496 * synthesizeMouse and synthesizeKey.
498 function _parseModifiers(aEvent, aWindow = window) {
499 var nsIDOMWindowUtils = _EU_Ci.nsIDOMWindowUtils;
501 if (aEvent.shiftKey) {
502 mval |= nsIDOMWindowUtils.MODIFIER_SHIFT;
504 if (aEvent.ctrlKey) {
505 mval |= nsIDOMWindowUtils.MODIFIER_CONTROL;
508 mval |= nsIDOMWindowUtils.MODIFIER_ALT;
510 if (aEvent.metaKey) {
511 mval |= nsIDOMWindowUtils.MODIFIER_META;
513 if (aEvent.accelKey) {
514 mval |= _EU_isMac(aWindow)
515 ? nsIDOMWindowUtils.MODIFIER_META
516 : nsIDOMWindowUtils.MODIFIER_CONTROL;
518 if (aEvent.altGrKey) {
519 mval |= nsIDOMWindowUtils.MODIFIER_ALTGRAPH;
521 if (aEvent.capsLockKey) {
522 mval |= nsIDOMWindowUtils.MODIFIER_CAPSLOCK;
525 mval |= nsIDOMWindowUtils.MODIFIER_FN;
527 if (aEvent.fnLockKey) {
528 mval |= nsIDOMWindowUtils.MODIFIER_FNLOCK;
530 if (aEvent.numLockKey) {
531 mval |= nsIDOMWindowUtils.MODIFIER_NUMLOCK;
533 if (aEvent.scrollLockKey) {
534 mval |= nsIDOMWindowUtils.MODIFIER_SCROLLLOCK;
536 if (aEvent.symbolKey) {
537 mval |= nsIDOMWindowUtils.MODIFIER_SYMBOL;
539 if (aEvent.symbolLockKey) {
540 mval |= nsIDOMWindowUtils.MODIFIER_SYMBOLLOCK;
547 * Synthesize a mouse event on a target. The actual client point is determined
548 * by taking the aTarget's client box and offseting it by aOffsetX and
549 * aOffsetY. This allows mouse clicks to be simulated by calling this method.
551 * aEvent is an object which may contain the properties:
552 * `shiftKey`, `ctrlKey`, `altKey`, `metaKey`, `accessKey`, `clickCount`,
554 * For valid `type`s see nsIDOMWindowUtils' `sendMouseEvent`.
556 * If the type is specified, an mouse event of that type is fired. Otherwise,
557 * a mousedown followed by a mouseup is performed.
559 * aWindow is optional, and defaults to the current window object.
561 * Returns whether the event had preventDefault() called on it.
563 function synthesizeMouse(aTarget, aOffsetX, aOffsetY, aEvent, aWindow) {
564 var rect = aTarget.getBoundingClientRect();
565 return synthesizeMouseAtPoint(
566 rect.left + aOffsetX,
572 function synthesizeTouch(aTarget, aOffsetX, aOffsetY, aEvent, aWindow) {
573 var rect = aTarget.getBoundingClientRect();
574 return synthesizeTouchAtPoint(
575 rect.left + aOffsetX,
583 * Return the drag service. Note that if we're in the headless mode, this
584 * may return null because the service may be never instantiated (e.g., on
587 function getDragService() {
589 return _EU_Cc["@mozilla.org/widget/dragservice;1"].getService(
590 _EU_Ci.nsIDragService
593 // If we're in the headless mode, the drag service may be never
594 // instantiated. In this case, an exception is thrown. Let's ignore
595 // any exceptions since without the drag service, nobody can create a
602 * End drag session if there is.
604 * TODO: This should synthesize "drop" if necessary.
606 * @param left X offset in the viewport
607 * @param top Y offset in the viewport
608 * @param aEvent The event data, the modifiers are applied to the
610 * @param aWindow The window.
611 * @return true if handled. In this case, the caller should not
612 * synthesize DOM events basically.
614 function _maybeEndDragSession(left, top, aEvent, aWindow) {
615 const dragService = getDragService();
616 const dragSession = dragService?.getCurrentSession();
620 // FIXME: If dragSession.dragAction is not
621 // nsIDragService.DRAGDROP_ACTION_NONE nor aEvent.type is not `keydown`, we
622 // need to synthesize a "drop" event or call setDragEndPointForTests here to
623 // set proper left/top to `dragend` event.
625 dragService.endDragSession(false, _parseModifiers(aEvent, aWindow));
630 function _maybeSynthesizeDragOver(left, top, aEvent, aWindow) {
631 const dragSession = getDragService()?.getCurrentSession();
635 const target = aWindow.document.elementFromPoint(left, top);
638 createDragEventObject(
642 dragSession.dataTransfer,
644 accelKey: aEvent.accelKey,
645 altKey: aEvent.altKey,
646 altGrKey: aEvent.altGrKey,
647 ctrlKey: aEvent.ctrlKey,
648 metaKey: aEvent.metaKey,
649 shiftKey: aEvent.shiftKey,
650 capsLockKey: aEvent.capsLockKey,
652 fnLockKey: aEvent.fnLockKey,
653 numLockKey: aEvent.numLockKey,
654 scrollLockKey: aEvent.scrollLockKey,
655 symbolKey: aEvent.symbolKey,
656 symbolLockKey: aEvent.symbolLockKey,
667 * Synthesize a mouse event at a particular point in aWindow.
669 * aEvent is an object which may contain the properties:
670 * `shiftKey`, `ctrlKey`, `altKey`, `metaKey`, `accessKey`, `clickCount`,
672 * For valid `type`s see nsIDOMWindowUtils' `sendMouseEvent`.
674 * If the type is specified, an mouse event of that type is fired. Otherwise,
675 * a mousedown followed by a mouseup is performed.
677 * aWindow is optional, and defaults to the current window object.
679 function synthesizeMouseAtPoint(left, top, aEvent, aWindow = window) {
680 if (aEvent.allowToHandleDragDrop) {
681 if (aEvent.type == "mouseup" || !aEvent.type) {
682 if (_maybeEndDragSession(left, top, aEvent, aWindow)) {
685 } else if (aEvent.type == "mousemove") {
686 if (_maybeSynthesizeDragOver(left, top, aEvent, aWindow)) {
692 var utils = _getDOMWindowUtils(aWindow);
693 var defaultPrevented = false;
696 var button = computeButton(aEvent);
697 var clickCount = aEvent.clickCount || 1;
698 var modifiers = _parseModifiers(aEvent, aWindow);
699 var pressure = "pressure" in aEvent ? aEvent.pressure : 0;
701 // aWindow might be cross-origin from us.
702 var MouseEvent = _EU_maybeWrap(aWindow).MouseEvent;
704 // Default source to mouse.
706 "inputSource" in aEvent
708 : MouseEvent.MOZ_SOURCE_MOUSE;
709 // Compute a pointerId if needed.
711 if ("id" in aEvent) {
714 var isFromPen = inputSource === MouseEvent.MOZ_SOURCE_PEN;
716 ? utils.DEFAULT_PEN_POINTER_ID
717 : utils.DEFAULT_MOUSE_POINTER_ID;
720 var isDOMEventSynthesized =
721 "isSynthesized" in aEvent ? aEvent.isSynthesized : true;
722 var isWidgetEventSynthesized =
723 "isWidgetEventSynthesized" in aEvent
724 ? aEvent.isWidgetEventSynthesized
726 if ("type" in aEvent && aEvent.type) {
727 defaultPrevented = utils.sendMouseEvent(
737 isDOMEventSynthesized,
738 isWidgetEventSynthesized,
739 computeButtons(aEvent, utils),
743 utils.sendMouseEvent(
753 isDOMEventSynthesized,
754 isWidgetEventSynthesized,
755 computeButtons(Object.assign({ type: "mousedown" }, aEvent), utils),
758 utils.sendMouseEvent(
768 isDOMEventSynthesized,
769 isWidgetEventSynthesized,
770 computeButtons(Object.assign({ type: "mouseup" }, aEvent), utils),
776 return defaultPrevented;
779 function synthesizeTouchAtPoint(left, top, aEvent, aWindow = window) {
780 var utils = _getDOMWindowUtils(aWindow);
781 let defaultPrevented = false;
784 var id = aEvent.id || utils.DEFAULT_TOUCH_POINTER_ID;
785 var rx = aEvent.rx || 1;
786 var ry = aEvent.ry || 1;
787 var angle = aEvent.angle || 0;
788 var force = aEvent.force || (aEvent.type === "touchend" ? 0 : 1);
789 var tiltX = aEvent.tiltX || 0;
790 var tiltY = aEvent.tiltY || 0;
791 var twist = aEvent.twist || 0;
792 var modifiers = _parseModifiers(aEvent, aWindow);
794 if ("type" in aEvent && aEvent.type) {
795 defaultPrevented = utils.sendTouchEvent(
810 utils.sendTouchEvent(
824 utils.sendTouchEvent(
840 return defaultPrevented;
843 // Call synthesizeMouse with coordinates at the center of aTarget.
844 function synthesizeMouseAtCenter(aTarget, aEvent, aWindow) {
845 var rect = aTarget.getBoundingClientRect();
846 return synthesizeMouse(
854 function synthesizeTouchAtCenter(aTarget, aEvent, aWindow) {
855 var rect = aTarget.getBoundingClientRect();
856 synthesizeTouchAtPoint(
857 rect.left + rect.width / 2,
858 rect.top + rect.height / 2,
865 * Synthesize a wheel event without flush layout at a particular point in
868 * aEvent is an object which may contain the properties:
869 * shiftKey, ctrlKey, altKey, metaKey, accessKey, deltaX, deltaY, deltaZ,
870 * deltaMode, lineOrPageDeltaX, lineOrPageDeltaY, isMomentum,
871 * isNoLineOrPageDelta, isCustomizedByPrefs, expectedOverflowDeltaX,
872 * expectedOverflowDeltaY
874 * deltaMode must be defined, others are ok even if undefined.
876 * expectedOverflowDeltaX and expectedOverflowDeltaY take integer value. The
877 * value is just checked as 0 or positive or negative.
879 * aWindow is optional, and defaults to the current window object.
881 function synthesizeWheelAtPoint(aLeft, aTop, aEvent, aWindow = window) {
882 var utils = _getDOMWindowUtils(aWindow);
887 var modifiers = _parseModifiers(aEvent, aWindow);
889 if (aEvent.isNoLineOrPageDelta) {
890 options |= utils.WHEEL_EVENT_CAUSED_BY_NO_LINE_OR_PAGE_DELTA_DEVICE;
892 if (aEvent.isMomentum) {
893 options |= utils.WHEEL_EVENT_CAUSED_BY_MOMENTUM;
895 if (aEvent.isCustomizedByPrefs) {
896 options |= utils.WHEEL_EVENT_CUSTOMIZED_BY_USER_PREFS;
898 if (typeof aEvent.expectedOverflowDeltaX !== "undefined") {
899 if (aEvent.expectedOverflowDeltaX === 0) {
900 options |= utils.WHEEL_EVENT_EXPECTED_OVERFLOW_DELTA_X_ZERO;
901 } else if (aEvent.expectedOverflowDeltaX > 0) {
902 options |= utils.WHEEL_EVENT_EXPECTED_OVERFLOW_DELTA_X_POSITIVE;
904 options |= utils.WHEEL_EVENT_EXPECTED_OVERFLOW_DELTA_X_NEGATIVE;
907 if (typeof aEvent.expectedOverflowDeltaY !== "undefined") {
908 if (aEvent.expectedOverflowDeltaY === 0) {
909 options |= utils.WHEEL_EVENT_EXPECTED_OVERFLOW_DELTA_Y_ZERO;
910 } else if (aEvent.expectedOverflowDeltaY > 0) {
911 options |= utils.WHEEL_EVENT_EXPECTED_OVERFLOW_DELTA_Y_POSITIVE;
913 options |= utils.WHEEL_EVENT_EXPECTED_OVERFLOW_DELTA_Y_NEGATIVE;
917 // Avoid the JS warnings "reference to undefined property"
918 if (!aEvent.deltaX) {
921 if (!aEvent.deltaY) {
924 if (!aEvent.deltaZ) {
928 var lineOrPageDeltaX =
929 // eslint-disable-next-line no-nested-ternary
930 aEvent.lineOrPageDeltaX != null
931 ? aEvent.lineOrPageDeltaX
933 ? Math.floor(aEvent.deltaX)
934 : Math.ceil(aEvent.deltaX);
935 var lineOrPageDeltaY =
936 // eslint-disable-next-line no-nested-ternary
937 aEvent.lineOrPageDeltaY != null
938 ? aEvent.lineOrPageDeltaY
940 ? Math.floor(aEvent.deltaY)
941 : Math.ceil(aEvent.deltaY);
942 utils.sendWheelEvent(
957 * Synthesize a wheel event on a target. The actual client point is determined
958 * by taking the aTarget's client box and offseting it by aOffsetX and
961 * aEvent is an object which may contain the properties:
962 * shiftKey, ctrlKey, altKey, metaKey, accessKey, deltaX, deltaY, deltaZ,
963 * deltaMode, lineOrPageDeltaX, lineOrPageDeltaY, isMomentum,
964 * isNoLineOrPageDelta, isCustomizedByPrefs, expectedOverflowDeltaX,
965 * expectedOverflowDeltaY
967 * deltaMode must be defined, others are ok even if undefined.
969 * expectedOverflowDeltaX and expectedOverflowDeltaY take integer value. The
970 * value is just checked as 0 or positive or negative.
972 * aWindow is optional, and defaults to the current window object.
974 function synthesizeWheel(aTarget, aOffsetX, aOffsetY, aEvent, aWindow) {
975 var rect = aTarget.getBoundingClientRect();
976 synthesizeWheelAtPoint(
977 rect.left + aOffsetX,
984 const _FlushModes = {
989 function _sendWheelAndPaint(
995 aFlushMode = _FlushModes.FLUSH,
998 var utils = _getDOMWindowUtils(aWindow);
1003 if (utils.isMozAfterPaintPending) {
1004 // If a paint is pending, then APZ may be waiting for a scroll acknowledgement
1005 // from the content thread. If we send a wheel event now, it could be ignored
1006 // by APZ (or its scroll offset could be overridden). To avoid problems we
1007 // just wait for the paint to complete.
1008 aWindow.waitForAllPaintsFlushed(function () {
1022 var onwheel = function () {
1023 SpecialPowers.removeSystemEventListener(window, "wheel", onwheel);
1025 // Wait one frame since the wheel event has not caused a refresh observer
1027 setTimeout(function () {
1028 utils.advanceTimeAndRefresh(1000);
1031 utils.advanceTimeAndRefresh(0);
1035 var waitForPaints = function () {
1036 SpecialPowers.Services.obs.removeObserver(
1038 "apz-repaints-flushed"
1040 aWindow.waitForAllPaintsFlushed(function () {
1041 utils.restoreNormalRefresh();
1046 SpecialPowers.Services.obs.addObserver(
1048 "apz-repaints-flushed"
1050 if (!utils.flushApzRepaints(aWindow)) {
1056 // Listen for the system wheel event, because it happens after all of
1057 // the other wheel events, including legacy events.
1058 SpecialPowers.addSystemEventListener(aWindow, "wheel", onwheel);
1059 if (aFlushMode === _FlushModes.FLUSH) {
1060 synthesizeWheel(aTarget, aOffsetX, aOffsetY, aEvent, aWindow);
1062 synthesizeWheelAtPoint(aOffsetX, aOffsetY, aEvent, aWindow);
1067 * This is a wrapper around synthesizeWheel that waits for the wheel event
1068 * to be dispatched and for the subsequent layout/paints to be flushed.
1070 * This requires including paint_listener.js. Tests must call
1071 * DOMWindowUtils.restoreNormalRefresh() before finishing, if they use this
1074 * If no callback is provided, the caller is assumed to have its own method of
1075 * determining scroll completion and the refresh driver is not automatically
1078 function sendWheelAndPaint(
1098 * Similar to sendWheelAndPaint but without flushing layout for obtaining
1099 * ``aTarget`` position in ``aWindow`` before sending the wheel event.
1100 * ``aOffsetX`` and ``aOffsetY`` should be offsets against aWindow.
1102 function sendWheelAndPaintNoFlush(
1116 _FlushModes.NOFLUSH,
1121 function synthesizeNativeTapAtCenter(
1127 let rect = aTarget.getBoundingClientRect();
1128 return synthesizeNativeTap(
1138 function synthesizeNativeTap(
1146 let utils = _getDOMWindowUtils(aWindow);
1151 let scale = aWindow.devicePixelRatio;
1152 let rect = aTarget.getBoundingClientRect();
1153 let x = (aWindow.mozInnerScreenX + rect.left + aOffsetX) * scale;
1154 let y = (aWindow.mozInnerScreenY + rect.top + aOffsetY) * scale;
1157 observe: (subject, topic, data) => {
1158 if (aCallback && topic == "mouseevent") {
1163 utils.sendNativeTouchTap(x, y, aLongTap, observer);
1167 * Similar to synthesizeMouse but generates a native widget level event
1168 * (so will actually move the "real" mouse cursor etc. Be careful because
1169 * this can impact later code as well! (e.g. with hover states etc.)
1171 * @description There are 3 mutually exclusive ways of indicating the location of the
1172 * mouse event: set ``atCenter``, or pass ``offsetX`` and ``offsetY``,
1173 * or pass ``screenX`` and ``screenY``. Do not attempt to mix these.
1175 * @param {object} aParams
1176 * @param {string} aParams.type "click", "mousedown", "mouseup" or "mousemove"
1177 * @param {Element} aParams.target Origin of offsetX and offsetY, must be an element
1178 * @param {Boolean} [aParams.atCenter]
1179 * Instead of offsetX/Y, synthesize the event at center of `target`.
1180 * @param {Number} [aParams.offsetX]
1181 * X offset in `target` (in CSS pixels if `scale` is "screenPixelsPerCSSPixel")
1182 * @param {Number} [aParams.offsetY]
1183 * Y offset in `target` (in CSS pixels if `scale` is "screenPixelsPerCSSPixel")
1184 * @param {Number} [aParams.screenX]
1185 * X offset in screen (in CSS pixels if `scale` is "screenPixelsPerCSSPixel"),
1186 * Neither offsetX/Y nor atCenter must be set if this is set.
1187 * @param {Number} [aParams.screenY]
1188 * Y offset in screen (in CSS pixels if `scale` is "screenPixelsPerCSSPixel"),
1189 * Neither offsetX/Y nor atCenter must be set if this is set.
1190 * @param {String} [aParams.scale="screenPixelsPerCSSPixel"]
1191 * If scale is "screenPixelsPerCSSPixel", devicePixelRatio will be used.
1192 * If scale is "inScreenPixels", clientX/Y nor scaleX/Y are not adjusted with screenPixelsPerCSSPixel.
1193 * @param {Number} [aParams.button=0]
1194 * Defaults to 0, if "click", "mousedown", "mouseup", set same value as DOM MouseEvent.button
1195 * @param {Object} [aParams.modifiers={}]
1196 * Active modifiers, see `_parseNativeModifiers`
1197 * @param {Window} [aParams.win=window]
1198 * The window to use its utils. Defaults to the window in which EventUtils.js is running.
1199 * @param {Element} [aParams.elementOnWidget=target]
1200 * Defaults to target. If element under the point is in another widget from target's widget,
1201 * e.g., when it's in a XUL <panel>, specify this.
1203 function synthesizeNativeMouseEvent(aParams, aCallback = null) {
1212 scale = "screenPixelsPerCSSPixel",
1216 elementOnWidget = target,
1219 if (offsetX != undefined || offsetY != undefined) {
1221 `atCenter is specified, but offsetX (${offsetX}) and/or offsetY (${offsetY}) are also specified`
1224 if (screenX != undefined || screenY != undefined) {
1226 `atCenter is specified, but screenX (${screenX}) and/or screenY (${screenY}) are also specified`
1230 throw Error("atCenter is specified, but target is not specified");
1232 } else if (offsetX != undefined && offsetY != undefined) {
1233 if (screenX != undefined || screenY != undefined) {
1235 `offsetX/Y are specified, but screenX (${screenX}) and/or screenY (${screenY}) are also specified`
1240 "offsetX and offsetY are specified, but target is not specified"
1243 } else if (screenX != undefined && screenY != undefined) {
1244 if (offsetX != undefined || offsetY != undefined) {
1246 `screenX/Y are specified, but offsetX (${offsetX}) and/or offsetY (${offsetY}) are also specified`
1250 const utils = _getDOMWindowUtils(win);
1255 const rect = target?.getBoundingClientRect();
1256 let resolution = 1.0;
1258 resolution = _getDOMWindowUtils(win.top).getResolution();
1260 // XXX How to get mobile viewport scale on Fission+xorigin since
1261 // window.top access isn't allowed due to cross-origin?
1263 const scaleValue = (() => {
1264 if (scale === "inScreenPixels") {
1267 if (scale === "screenPixelsPerCSSPixel") {
1268 return win.devicePixelRatio;
1270 throw Error(`invalid scale value (${scale}) is specified`);
1272 // XXX mozInnerScreen might be invalid value on mobile viewport (Bug 1701546),
1273 // so use window.top's mozInnerScreen. But this won't work fission+xorigin
1274 // with mobile viewport until mozInnerScreen returns valid value with
1277 if (screenX != undefined) {
1278 return screenX * scaleValue;
1280 let winInnerOffsetX = win.mozInnerScreenX;
1283 win.top.mozInnerScreenX +
1284 (win.mozInnerScreenX - win.top.mozInnerScreenX) * resolution;
1286 // XXX fission+xorigin test throws permission denied since win.top is
1290 (((atCenter ? rect.width / 2 : offsetX) + rect.left) * resolution +
1296 if (screenY != undefined) {
1297 return screenY * scaleValue;
1299 let winInnerOffsetY = win.mozInnerScreenY;
1302 win.top.mozInnerScreenY +
1303 (win.mozInnerScreenY - win.top.mozInnerScreenY) * resolution;
1305 // XXX fission+xorigin test throws permission denied since win.top is
1309 (((atCenter ? rect.height / 2 : offsetY) + rect.top) * resolution +
1314 const modifierFlags = _parseNativeModifiers(modifiers);
1317 observe: (subject, topic, data) => {
1318 if (aCallback && topic == "mouseevent") {
1323 if (type === "click") {
1324 utils.sendNativeMouseEvent(
1327 utils.NATIVE_MOUSE_MESSAGE_BUTTON_DOWN,
1332 utils.sendNativeMouseEvent(
1335 utils.NATIVE_MOUSE_MESSAGE_BUTTON_UP,
1345 utils.sendNativeMouseEvent(
1351 return utils.NATIVE_MOUSE_MESSAGE_BUTTON_DOWN;
1353 return utils.NATIVE_MOUSE_MESSAGE_BUTTON_UP;
1355 return utils.NATIVE_MOUSE_MESSAGE_MOVE;
1357 throw Error(`Invalid type is specified: ${type}`);
1367 function promiseNativeMouseEvent(aParams) {
1368 return new Promise(resolve => synthesizeNativeMouseEvent(aParams, resolve));
1371 function synthesizeNativeMouseEventAndWaitForEvent(aParams, aCallback) {
1372 const listener = aParams.eventTargetToListen || aParams.target;
1373 const eventType = aParams.eventTypeToWait || aParams.type;
1374 listener.addEventListener(eventType, aCallback, {
1378 synthesizeNativeMouseEvent(aParams);
1381 function promiseNativeMouseEventAndWaitForEvent(aParams) {
1382 return new Promise(resolve =>
1383 synthesizeNativeMouseEventAndWaitForEvent(aParams, resolve)
1388 * This is a wrapper around synthesizeNativeMouseEvent that waits for the mouse
1389 * event to be dispatched to the target content.
1391 * This API is supposed to be used in those test cases that synthesize some
1392 * input events to chrome process and have some checks in content.
1394 function synthesizeAndWaitNativeMouseMove(
1401 let browser = gBrowser.selectedTab.linkedBrowser;
1402 let mm = browser.messageManager;
1403 let { ContentTask } = _EU_ChromeUtils.importESModule(
1404 "resource://testing-common/ContentTask.sys.mjs"
1407 let eventRegisteredPromise = new Promise(resolve => {
1408 mm.addMessageListener(
1409 "Test:MouseMoveRegistered",
1410 function processed(message) {
1411 mm.removeMessageListener("Test:MouseMoveRegistered", processed);
1416 let eventReceivedPromise = ContentTask.spawn(
1418 [aOffsetX, aOffsetY],
1419 ([clientX, clientY]) => {
1420 return new Promise(resolve => {
1421 addEventListener("mousemove", function onMouseMoveEvent(e) {
1422 if (e.clientX == clientX && e.clientY == clientY) {
1423 removeEventListener("mousemove", onMouseMoveEvent);
1427 sendAsyncMessage("Test:MouseMoveRegistered");
1431 eventRegisteredPromise.then(() => {
1432 synthesizeNativeMouseEvent({
1440 return eventReceivedPromise;
1444 * Synthesize a key event. It is targeted at whatever would be targeted by an
1445 * actual keypress by the user, typically the focused element.
1447 * @param {String} aKey
1450 * - key value (recommended). If you specify a non-printable key name,
1451 * prepend the ``KEY_`` prefix. Otherwise, specifying a printable key, the
1452 * key value should be specified.
1454 * - keyCode name starting with ``VK_`` (e.g., ``VK_RETURN``). This is available
1455 * only for compatibility with legacy API. Don't use this with new tests.
1457 * @param {Object} [aEvent]
1458 * Optional event object with more specifics about the key event to
1460 * @param {String} [aEvent.code]
1461 * If you don't specify this explicitly, it'll be guessed from aKey
1462 * of US keyboard layout. Note that this value may be different
1463 * between browsers. For example, "Insert" is never set only on
1464 * macOS since actual key operation won't cause this code value.
1465 * In such case, the value becomes empty string.
1466 * If you need to emulate non-US keyboard layout or virtual keyboard
1467 * which doesn't emulate hardware key input, you should set this value
1468 * to empty string explicitly.
1469 * @param {Number} [aEvent.repeat]
1470 * If you emulate auto-repeat, you should set the count of repeat.
1471 * This method will automatically synthesize keydown (and keypress).
1472 * @param {*} aEvent.location
1473 * If you want to specify this, you can specify this explicitly.
1474 * However, if you don't specify this value, it will be computed
1476 * @param {String} aEvent.type
1477 * Basically, you shouldn't specify this. Then, this function will
1478 * synthesize keydown (, keypress) and keyup.
1479 * If keydown is specified, this only fires keydown (and keypress if
1480 * it should be fired).
1481 * If keyup is specified, this only fires keyup.
1482 * @param {Number} aEvent.keyCode
1483 * Must be 0 - 255 (0xFF). If this is specified explicitly,
1484 * .keyCode value is initialized with this value.
1485 * @param {Window} aWindow
1486 * Is optional and defaults to the current window object.
1487 * @param {Function} aCallback
1488 * Is optional and can be used to receive notifications from TIP.
1491 * ``accelKey``, ``altKey``, ``altGraphKey``, ``ctrlKey``, ``capsLockKey``,
1492 * ``fnKey``, ``fnLockKey``, ``numLockKey``, ``metaKey``, ``scrollLockKey``,
1493 * ``shiftKey``, ``symbolKey``, ``symbolLockKey``
1494 * Basically, you shouldn't use these attributes. nsITextInputProcessor
1495 * manages modifier key state when you synthesize modifier key events.
1496 * However, if some of these attributes are true, this function activates
1497 * the modifiers only during dispatching the key events.
1498 * Note that if some of these values are false, they are ignored (i.e.,
1499 * not inactivated with this function).
1502 function synthesizeKey(aKey, aEvent = undefined, aWindow = window, aCallback) {
1503 const event = aEvent === undefined || aEvent === null ? {} : aEvent;
1504 let dispatchKeydown =
1505 !("type" in event) || event.type === "keydown" || !event.type;
1506 const dispatchKeyup =
1507 !("type" in event) || event.type === "keyup" || !event.type;
1509 if (dispatchKeydown && aKey == "KEY_Escape") {
1510 let eventForKeydown = Object.assign({}, JSON.parse(JSON.stringify(event)));
1511 eventForKeydown.type = "keydown";
1513 _maybeEndDragSession(
1514 // TODO: We should set the last dragover point instead
1521 if (!dispatchKeyup) {
1524 // We don't need to dispatch only keydown event because it's consumed by
1525 // the drag session.
1526 dispatchKeydown = false;
1530 var TIP = _getTIP(aWindow, aCallback);
1534 var KeyboardEvent = _getKeyboardEvent(aWindow);
1535 var modifiers = _emulateToActivateModifiers(TIP, event, aWindow);
1536 var keyEventDict = _createKeyboardEventDictionary(aKey, event, TIP, aWindow);
1537 var keyEvent = new KeyboardEvent("", keyEventDict.dictionary);
1540 if (dispatchKeydown) {
1541 TIP.keydown(keyEvent, keyEventDict.flags);
1542 if ("repeat" in event && event.repeat > 1) {
1543 keyEventDict.dictionary.repeat = true;
1544 var repeatedKeyEvent = new KeyboardEvent("", keyEventDict.dictionary);
1545 for (var i = 1; i < event.repeat; i++) {
1546 TIP.keydown(repeatedKeyEvent, keyEventDict.flags);
1550 if (dispatchKeyup) {
1551 TIP.keyup(keyEvent, keyEventDict.flags);
1554 _emulateToInactivateModifiers(TIP, modifiers, aWindow);
1559 * This is a wrapper around synthesizeKey that waits for the key event to be
1560 * dispatched to the target content. It returns a promise which is resolved
1561 * when the content receives the key event.
1563 * This API is supposed to be used in those test cases that synthesize some
1564 * input events to chrome process and have some checks in content.
1566 function synthesizeAndWaitKey(
1570 checkBeforeSynthesize,
1571 checkAfterSynthesize
1573 let browser = gBrowser.selectedTab.linkedBrowser;
1574 let mm = browser.messageManager;
1575 let keyCode = _createKeyboardEventDictionary(aKey, aEvent, null, aWindow)
1576 .dictionary.keyCode;
1577 let { ContentTask } = _EU_ChromeUtils.importESModule(
1578 "resource://testing-common/ContentTask.sys.mjs"
1581 let keyRegisteredPromise = new Promise(resolve => {
1582 mm.addMessageListener("Test:KeyRegistered", function processed(message) {
1583 mm.removeMessageListener("Test:KeyRegistered", processed);
1587 // eslint-disable-next-line no-shadow
1588 let keyReceivedPromise = ContentTask.spawn(browser, keyCode, keyCode => {
1589 return new Promise(resolve => {
1590 addEventListener("keyup", function onKeyEvent(e) {
1591 if (e.keyCode == keyCode) {
1592 removeEventListener("keyup", onKeyEvent);
1596 sendAsyncMessage("Test:KeyRegistered");
1599 keyRegisteredPromise.then(() => {
1600 if (checkBeforeSynthesize) {
1601 checkBeforeSynthesize();
1603 synthesizeKey(aKey, aEvent, aWindow);
1604 if (checkAfterSynthesize) {
1605 checkAfterSynthesize();
1608 return keyReceivedPromise;
1611 function _parseNativeModifiers(aModifiers, aWindow = window) {
1613 if (aModifiers.capsLockKey) {
1614 modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CAPS_LOCK;
1616 if (aModifiers.numLockKey) {
1617 modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_NUM_LOCK;
1619 if (aModifiers.shiftKey) {
1620 modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_SHIFT_LEFT;
1622 if (aModifiers.shiftRightKey) {
1623 modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_SHIFT_RIGHT;
1625 if (aModifiers.ctrlKey) {
1627 SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CONTROL_LEFT;
1629 if (aModifiers.ctrlRightKey) {
1631 SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CONTROL_RIGHT;
1633 if (aModifiers.altKey) {
1634 modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_ALT_LEFT;
1636 if (aModifiers.altRightKey) {
1637 modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_ALT_RIGHT;
1639 if (aModifiers.metaKey) {
1641 SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_COMMAND_LEFT;
1643 if (aModifiers.metaRightKey) {
1645 SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_COMMAND_RIGHT;
1647 if (aModifiers.helpKey) {
1648 modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_HELP;
1650 if (aModifiers.fnKey) {
1651 modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_FUNCTION;
1653 if (aModifiers.numericKeyPadKey) {
1655 SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_NUMERIC_KEY_PAD;
1658 if (aModifiers.accelKey) {
1659 modifiers |= _EU_isMac(aWindow)
1660 ? SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_COMMAND_LEFT
1661 : SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CONTROL_LEFT;
1663 if (aModifiers.accelRightKey) {
1664 modifiers |= _EU_isMac(aWindow)
1665 ? SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_COMMAND_RIGHT
1666 : SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CONTROL_RIGHT;
1668 if (aModifiers.altGrKey) {
1669 modifiers |= _EU_isMac(aWindow)
1670 ? SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_ALT_LEFT
1671 : SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_ALT_GRAPH;
1676 // Mac: Any unused number is okay for adding new keyboard layout.
1677 // When you add new keyboard layout here, you need to modify
1678 // TISInputSourceWrapper::InitByLayoutID().
1679 // Win: These constants can be found by inspecting registry keys under
1680 // HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Keyboard Layouts
1682 const KEYBOARD_LAYOUT_ARABIC = {
1686 hasAltGrOnWin: false,
1688 _defineConstant("KEYBOARD_LAYOUT_ARABIC", KEYBOARD_LAYOUT_ARABIC);
1689 const KEYBOARD_LAYOUT_ARABIC_PC = {
1690 name: "Arabic - PC",
1693 hasAltGrOnWin: false,
1695 _defineConstant("KEYBOARD_LAYOUT_ARABIC_PC", KEYBOARD_LAYOUT_ARABIC_PC);
1696 const KEYBOARD_LAYOUT_BRAZILIAN_ABNT = {
1697 name: "Brazilian ABNT",
1700 hasAltGrOnWin: true,
1703 "KEYBOARD_LAYOUT_BRAZILIAN_ABNT",
1704 KEYBOARD_LAYOUT_BRAZILIAN_ABNT
1706 const KEYBOARD_LAYOUT_DVORAK_QWERTY = {
1707 name: "Dvorak-QWERTY",
1710 hasAltGrOnWin: false,
1712 _defineConstant("KEYBOARD_LAYOUT_DVORAK_QWERTY", KEYBOARD_LAYOUT_DVORAK_QWERTY);
1713 const KEYBOARD_LAYOUT_EN_US = {
1717 hasAltGrOnWin: false,
1719 _defineConstant("KEYBOARD_LAYOUT_EN_US", KEYBOARD_LAYOUT_EN_US);
1720 const KEYBOARD_LAYOUT_FRENCH = {
1722 Mac: 8, // Some keys mapped different from PC, e.g., Digit6, Digit8, Equal, Slash and Backslash
1724 hasAltGrOnWin: true,
1726 _defineConstant("KEYBOARD_LAYOUT_FRENCH", KEYBOARD_LAYOUT_FRENCH);
1727 const KEYBOARD_LAYOUT_FRENCH_PC = {
1729 Mac: 13, // Compatible with Windows
1731 hasAltGrOnWin: true,
1733 _defineConstant("KEYBOARD_LAYOUT_FRENCH_PC", KEYBOARD_LAYOUT_FRENCH_PC);
1734 const KEYBOARD_LAYOUT_GREEK = {
1738 hasAltGrOnWin: true,
1740 _defineConstant("KEYBOARD_LAYOUT_GREEK", KEYBOARD_LAYOUT_GREEK);
1741 const KEYBOARD_LAYOUT_GERMAN = {
1745 hasAltGrOnWin: true,
1747 _defineConstant("KEYBOARD_LAYOUT_GERMAN", KEYBOARD_LAYOUT_GERMAN);
1748 const KEYBOARD_LAYOUT_HEBREW = {
1752 hasAltGrOnWin: true,
1754 _defineConstant("KEYBOARD_LAYOUT_HEBREW", KEYBOARD_LAYOUT_HEBREW);
1755 const KEYBOARD_LAYOUT_JAPANESE = {
1759 hasAltGrOnWin: false,
1761 _defineConstant("KEYBOARD_LAYOUT_JAPANESE", KEYBOARD_LAYOUT_JAPANESE);
1762 const KEYBOARD_LAYOUT_KHMER = {
1766 hasAltGrOnWin: true,
1767 }; // available on Win7 or later.
1768 _defineConstant("KEYBOARD_LAYOUT_KHMER", KEYBOARD_LAYOUT_KHMER);
1769 const KEYBOARD_LAYOUT_LITHUANIAN = {
1773 hasAltGrOnWin: true,
1775 _defineConstant("KEYBOARD_LAYOUT_LITHUANIAN", KEYBOARD_LAYOUT_LITHUANIAN);
1776 const KEYBOARD_LAYOUT_NORWEGIAN = {
1780 hasAltGrOnWin: true,
1782 _defineConstant("KEYBOARD_LAYOUT_NORWEGIAN", KEYBOARD_LAYOUT_NORWEGIAN);
1783 const KEYBOARD_LAYOUT_RUSSIAN = {
1787 hasAltGrOnWin: true, // No AltGr, but Ctrl + Alt + Digit8 introduces a char
1789 _defineConstant("KEYBOARD_LAYOUT_RUSSIAN", KEYBOARD_LAYOUT_RUSSIAN);
1790 const KEYBOARD_LAYOUT_RUSSIAN_MNEMONIC = {
1791 name: "Russian - Mnemonic",
1794 hasAltGrOnWin: true,
1795 }; // available on Win8 or later.
1797 "KEYBOARD_LAYOUT_RUSSIAN_MNEMONIC",
1798 KEYBOARD_LAYOUT_RUSSIAN_MNEMONIC
1800 const KEYBOARD_LAYOUT_SPANISH = {
1804 hasAltGrOnWin: true,
1806 _defineConstant("KEYBOARD_LAYOUT_SPANISH", KEYBOARD_LAYOUT_SPANISH);
1807 const KEYBOARD_LAYOUT_SWEDISH = {
1811 hasAltGrOnWin: true,
1813 _defineConstant("KEYBOARD_LAYOUT_SWEDISH", KEYBOARD_LAYOUT_SWEDISH);
1814 const KEYBOARD_LAYOUT_THAI = {
1818 hasAltGrOnWin: false,
1820 _defineConstant("KEYBOARD_LAYOUT_THAI", KEYBOARD_LAYOUT_THAI);
1823 * synthesizeNativeKey() dispatches native key event on active window.
1824 * This is implemented only on Windows and Mac. Note that this function
1825 * dispatches the key event asynchronously and returns immediately. If a
1826 * callback function is provided, the callback will be called upon
1827 * completion of the key dispatch.
1829 * @param aKeyboardLayout One of KEYBOARD_LAYOUT_* defined above.
1830 * @param aNativeKeyCode A native keycode value defined in
1831 * NativeKeyCodes.js.
1832 * @param aModifiers Modifier keys. If no modifire key is pressed,
1833 * this must be {}. Otherwise, one or more items
1834 * referred in _parseNativeModifiers() must be
1836 * @param aChars Specify characters which should be generated
1838 * @param aUnmodifiedChars Specify characters of unmodified (except Shift)
1840 * @param aCallback If provided, this callback will be invoked
1841 * once the native keys have been processed
1842 * by Gecko. Will never be called if this
1843 * function returns false.
1844 * @return True if this function succeed dispatching
1845 * native key event. Otherwise, false.
1848 function synthesizeNativeKey(
1857 var utils = _getDOMWindowUtils(aWindow);
1861 var nativeKeyboardLayout = null;
1862 if (_EU_isMac(aWindow)) {
1863 nativeKeyboardLayout = aKeyboardLayout.Mac;
1864 } else if (_EU_isWin(aWindow)) {
1865 nativeKeyboardLayout = aKeyboardLayout.Win;
1867 if (nativeKeyboardLayout === null) {
1872 observe(aSubject, aTopic, aData) {
1873 if (aCallback && aTopic == "keyevent") {
1878 utils.sendNativeKeyEvent(
1879 nativeKeyboardLayout,
1881 _parseNativeModifiers(aModifiers, aWindow),
1889 var _gSeenEvent = false;
1892 * Indicate that an event with an original target of aExpectedTarget and
1893 * a type of aExpectedEvent is expected to be fired, or not expected to
1896 function _expectEvent(aExpectedTarget, aExpectedEvent, aTestName) {
1897 if (!aExpectedTarget || !aExpectedEvent) {
1901 _gSeenEvent = false;
1904 aExpectedEvent.charAt(0) == "!"
1905 ? aExpectedEvent.substring(1)
1907 var eventHandler = function (event) {
1910 event.originalTarget == aExpectedTarget &&
1915 aTestName + " " + type + " event target " + (_gSeenEvent ? "twice" : "")
1920 aExpectedTarget.addEventListener(type, eventHandler);
1921 return eventHandler;
1925 * Check if the event was fired or not. The event handler aEventHandler
1928 function _checkExpectedEvent(
1934 if (aEventHandler) {
1935 var expectEvent = aExpectedEvent.charAt(0) != "!";
1936 var type = expectEvent ? aExpectedEvent : aExpectedEvent.substring(1);
1937 aExpectedTarget.removeEventListener(type, aEventHandler);
1938 var desc = type + " event";
1942 is(_gSeenEvent, expectEvent, aTestName + " " + desc + " fired");
1945 _gSeenEvent = false;
1949 * Similar to synthesizeMouse except that a test is performed to see if an
1950 * event is fired at the right target as a result.
1952 * aExpectedTarget - the expected originalTarget of the event.
1953 * aExpectedEvent - the expected type of the event, such as 'select'.
1954 * aTestName - the test name when outputing results
1956 * To test that an event is not fired, use an expected type preceded by an
1957 * exclamation mark, such as '!select'. This might be used to test that a
1958 * click on a disabled element doesn't fire certain events for instance.
1960 * aWindow is optional, and defaults to the current window object.
1962 function synthesizeMouseExpectEvent(
1972 var eventHandler = _expectEvent(aExpectedTarget, aExpectedEvent, aTestName);
1973 synthesizeMouse(aTarget, aOffsetX, aOffsetY, aEvent, aWindow);
1974 _checkExpectedEvent(aExpectedTarget, aExpectedEvent, eventHandler, aTestName);
1978 * Similar to synthesizeKey except that a test is performed to see if an
1979 * event is fired at the right target as a result.
1981 * aExpectedTarget - the expected originalTarget of the event.
1982 * aExpectedEvent - the expected type of the event, such as 'select'.
1983 * aTestName - the test name when outputing results
1985 * To test that an event is not fired, use an expected type preceded by an
1986 * exclamation mark, such as '!select'.
1988 * aWindow is optional, and defaults to the current window object.
1990 function synthesizeKeyExpectEvent(
1998 var eventHandler = _expectEvent(aExpectedTarget, aExpectedEvent, aTestName);
1999 synthesizeKey(key, aEvent, aWindow);
2000 _checkExpectedEvent(aExpectedTarget, aExpectedEvent, eventHandler, aTestName);
2003 function disableNonTestMouseEvents(aDisable) {
2004 var domutils = _getDOMWindowUtils();
2005 domutils.disableNonTestMouseEvents(aDisable);
2008 function _getDOMWindowUtils(aWindow = window) {
2009 // Leave this here as something, somewhere, passes a falsy argument
2010 // to this, causing the |window| default argument not to get picked up.
2015 // If documentURIObject exists or `window` is a stub object, we're in
2016 // a chrome scope, so don't bother trying to go through SpecialPowers.
2017 if (!window.document || window.document.documentURIObject) {
2018 return aWindow.windowUtils;
2021 // we need parent.SpecialPowers for:
2022 // layout/base/tests/test_reftests_with_caret.html
2023 // chrome: toolkit/content/tests/chrome/test_findbar.xul
2024 // chrome: toolkit/content/tests/chrome/test_popup_anchor.xul
2025 if ("SpecialPowers" in window && window.SpecialPowers != undefined) {
2026 return SpecialPowers.getDOMWindowUtils(aWindow);
2028 if ("SpecialPowers" in parent && parent.SpecialPowers != undefined) {
2029 return parent.SpecialPowers.getDOMWindowUtils(aWindow);
2032 // TODO: this is assuming we are in chrome space
2033 return aWindow.windowUtils;
2036 function _defineConstant(name, value) {
2037 Object.defineProperty(this, name, {
2044 const COMPOSITION_ATTR_RAW_CLAUSE =
2045 _EU_Ci.nsITextInputProcessor.ATTR_RAW_CLAUSE;
2046 _defineConstant("COMPOSITION_ATTR_RAW_CLAUSE", COMPOSITION_ATTR_RAW_CLAUSE);
2047 const COMPOSITION_ATTR_SELECTED_RAW_CLAUSE =
2048 _EU_Ci.nsITextInputProcessor.ATTR_SELECTED_RAW_CLAUSE;
2050 "COMPOSITION_ATTR_SELECTED_RAW_CLAUSE",
2051 COMPOSITION_ATTR_SELECTED_RAW_CLAUSE
2053 const COMPOSITION_ATTR_CONVERTED_CLAUSE =
2054 _EU_Ci.nsITextInputProcessor.ATTR_CONVERTED_CLAUSE;
2056 "COMPOSITION_ATTR_CONVERTED_CLAUSE",
2057 COMPOSITION_ATTR_CONVERTED_CLAUSE
2059 const COMPOSITION_ATTR_SELECTED_CLAUSE =
2060 _EU_Ci.nsITextInputProcessor.ATTR_SELECTED_CLAUSE;
2062 "COMPOSITION_ATTR_SELECTED_CLAUSE",
2063 COMPOSITION_ATTR_SELECTED_CLAUSE
2066 var TIPMap = new WeakMap();
2068 function _getTIP(aWindow, aCallback) {
2073 if (TIPMap.has(aWindow)) {
2074 tip = TIPMap.get(aWindow);
2076 tip = _EU_Cc["@mozilla.org/text-input-processor;1"].createInstance(
2077 _EU_Ci.nsITextInputProcessor
2079 TIPMap.set(aWindow, tip);
2081 if (!tip.beginInputTransactionForTests(aWindow, aCallback)) {
2083 TIPMap.delete(aWindow);
2088 function _getKeyboardEvent(aWindow = window) {
2089 if (typeof KeyboardEvent != "undefined") {
2091 // See if the object can be instantiated; sometimes this yields
2092 // 'TypeError: can't access dead object' or 'KeyboardEvent is not a constructor'.
2093 new KeyboardEvent("", {});
2094 return KeyboardEvent;
2097 if (typeof content != "undefined" && "KeyboardEvent" in content) {
2098 return content.KeyboardEvent;
2100 return aWindow.KeyboardEvent;
2103 // eslint-disable-next-line complexity
2104 function _guessKeyNameFromKeyCode(aKeyCode, aWindow = window) {
2105 var KeyboardEvent = _getKeyboardEvent(aWindow);
2107 case KeyboardEvent.DOM_VK_CANCEL:
2109 case KeyboardEvent.DOM_VK_HELP:
2111 case KeyboardEvent.DOM_VK_BACK_SPACE:
2113 case KeyboardEvent.DOM_VK_TAB:
2115 case KeyboardEvent.DOM_VK_CLEAR:
2117 case KeyboardEvent.DOM_VK_RETURN:
2119 case KeyboardEvent.DOM_VK_SHIFT:
2121 case KeyboardEvent.DOM_VK_CONTROL:
2123 case KeyboardEvent.DOM_VK_ALT:
2125 case KeyboardEvent.DOM_VK_PAUSE:
2127 case KeyboardEvent.DOM_VK_EISU:
2129 case KeyboardEvent.DOM_VK_ESCAPE:
2131 case KeyboardEvent.DOM_VK_CONVERT:
2133 case KeyboardEvent.DOM_VK_NONCONVERT:
2134 return "NonConvert";
2135 case KeyboardEvent.DOM_VK_ACCEPT:
2137 case KeyboardEvent.DOM_VK_MODECHANGE:
2138 return "ModeChange";
2139 case KeyboardEvent.DOM_VK_PAGE_UP:
2141 case KeyboardEvent.DOM_VK_PAGE_DOWN:
2143 case KeyboardEvent.DOM_VK_END:
2145 case KeyboardEvent.DOM_VK_HOME:
2147 case KeyboardEvent.DOM_VK_LEFT:
2149 case KeyboardEvent.DOM_VK_UP:
2151 case KeyboardEvent.DOM_VK_RIGHT:
2152 return "ArrowRight";
2153 case KeyboardEvent.DOM_VK_DOWN:
2155 case KeyboardEvent.DOM_VK_SELECT:
2157 case KeyboardEvent.DOM_VK_PRINT:
2159 case KeyboardEvent.DOM_VK_EXECUTE:
2161 case KeyboardEvent.DOM_VK_PRINTSCREEN:
2162 return "PrintScreen";
2163 case KeyboardEvent.DOM_VK_INSERT:
2165 case KeyboardEvent.DOM_VK_DELETE:
2167 case KeyboardEvent.DOM_VK_WIN:
2169 case KeyboardEvent.DOM_VK_CONTEXT_MENU:
2170 return "ContextMenu";
2171 case KeyboardEvent.DOM_VK_SLEEP:
2173 case KeyboardEvent.DOM_VK_F1:
2175 case KeyboardEvent.DOM_VK_F2:
2177 case KeyboardEvent.DOM_VK_F3:
2179 case KeyboardEvent.DOM_VK_F4:
2181 case KeyboardEvent.DOM_VK_F5:
2183 case KeyboardEvent.DOM_VK_F6:
2185 case KeyboardEvent.DOM_VK_F7:
2187 case KeyboardEvent.DOM_VK_F8:
2189 case KeyboardEvent.DOM_VK_F9:
2191 case KeyboardEvent.DOM_VK_F10:
2193 case KeyboardEvent.DOM_VK_F11:
2195 case KeyboardEvent.DOM_VK_F12:
2197 case KeyboardEvent.DOM_VK_F13:
2199 case KeyboardEvent.DOM_VK_F14:
2201 case KeyboardEvent.DOM_VK_F15:
2203 case KeyboardEvent.DOM_VK_F16:
2205 case KeyboardEvent.DOM_VK_F17:
2207 case KeyboardEvent.DOM_VK_F18:
2209 case KeyboardEvent.DOM_VK_F19:
2211 case KeyboardEvent.DOM_VK_F20:
2213 case KeyboardEvent.DOM_VK_F21:
2215 case KeyboardEvent.DOM_VK_F22:
2217 case KeyboardEvent.DOM_VK_F23:
2219 case KeyboardEvent.DOM_VK_F24:
2221 case KeyboardEvent.DOM_VK_NUM_LOCK:
2223 case KeyboardEvent.DOM_VK_SCROLL_LOCK:
2224 return "ScrollLock";
2225 case KeyboardEvent.DOM_VK_VOLUME_MUTE:
2226 return "AudioVolumeMute";
2227 case KeyboardEvent.DOM_VK_VOLUME_DOWN:
2228 return "AudioVolumeDown";
2229 case KeyboardEvent.DOM_VK_VOLUME_UP:
2230 return "AudioVolumeUp";
2231 case KeyboardEvent.DOM_VK_META:
2233 case KeyboardEvent.DOM_VK_ALTGR:
2235 case KeyboardEvent.DOM_VK_PROCESSKEY:
2237 case KeyboardEvent.DOM_VK_ATTN:
2239 case KeyboardEvent.DOM_VK_CRSEL:
2241 case KeyboardEvent.DOM_VK_EXSEL:
2243 case KeyboardEvent.DOM_VK_EREOF:
2245 case KeyboardEvent.DOM_VK_PLAY:
2248 return "Unidentified";
2252 function _createKeyboardEventDictionary(
2258 var result = { dictionary: null, flags: 0 };
2259 var keyCodeIsDefined = "keyCode" in aKeyEvent;
2261 keyCodeIsDefined && aKeyEvent.keyCode >= 0 && aKeyEvent.keyCode <= 255
2264 var keyName = "Unidentified";
2265 var code = aKeyEvent.code;
2267 aTIP = _getTIP(aWindow);
2269 if (aKey.indexOf("KEY_") == 0) {
2270 keyName = aKey.substr("KEY_".length);
2271 result.flags |= _EU_Ci.nsITextInputProcessor.KEY_NON_PRINTABLE_KEY;
2272 if (code === undefined) {
2273 code = aTIP.computeCodeValueOfNonPrintableKey(
2278 } else if (aKey.indexOf("VK_") == 0) {
2279 keyCode = _getKeyboardEvent(aWindow)["DOM_" + aKey];
2281 throw new Error("Unknown key: " + aKey);
2283 keyName = _guessKeyNameFromKeyCode(keyCode, aWindow);
2284 result.flags |= _EU_Ci.nsITextInputProcessor.KEY_NON_PRINTABLE_KEY;
2285 if (code === undefined) {
2286 code = aTIP.computeCodeValueOfNonPrintableKey(
2291 } else if (aKey != "") {
2293 if (!keyCodeIsDefined) {
2294 keyCode = aTIP.guessKeyCodeValueOfPrintableKeyInUSEnglishKeyboardLayout(
2300 result.flags |= _EU_Ci.nsITextInputProcessor.KEY_KEEP_KEYCODE_ZERO;
2302 result.flags |= _EU_Ci.nsITextInputProcessor.KEY_FORCE_PRINTABLE_KEY;
2303 if (code === undefined) {
2304 code = aTIP.guessCodeValueOfPrintableKeyInUSEnglishKeyboardLayout(
2310 var locationIsDefined = "location" in aKeyEvent;
2311 if (locationIsDefined && aKeyEvent.location === 0) {
2312 result.flags |= _EU_Ci.nsITextInputProcessor.KEY_KEEP_KEY_LOCATION_STANDARD;
2314 if (aKeyEvent.doNotMarkKeydownAsProcessed) {
2316 _EU_Ci.nsITextInputProcessor.KEY_DONT_MARK_KEYDOWN_AS_PROCESSED;
2318 if (aKeyEvent.markKeyupAsProcessed) {
2319 result.flags |= _EU_Ci.nsITextInputProcessor.KEY_MARK_KEYUP_AS_PROCESSED;
2321 result.dictionary = {
2324 location: locationIsDefined ? aKeyEvent.location : 0,
2325 repeat: "repeat" in aKeyEvent ? aKeyEvent.repeat === true : false,
2331 function _emulateToActivateModifiers(aTIP, aKeyEvent, aWindow = window) {
2335 var KeyboardEvent = _getKeyboardEvent(aWindow);
2339 { key: "Alt", attr: "altKey" },
2340 { key: "AltGraph", attr: "altGraphKey" },
2341 { key: "Control", attr: "ctrlKey" },
2342 { key: "Fn", attr: "fnKey" },
2343 { key: "Meta", attr: "metaKey" },
2344 { key: "Shift", attr: "shiftKey" },
2345 { key: "Symbol", attr: "symbolKey" },
2346 { key: _EU_isMac(aWindow) ? "Meta" : "Control", attr: "accelKey" },
2349 { key: "CapsLock", attr: "capsLockKey" },
2350 { key: "FnLock", attr: "fnLockKey" },
2351 { key: "NumLock", attr: "numLockKey" },
2352 { key: "ScrollLock", attr: "scrollLockKey" },
2353 { key: "SymbolLock", attr: "symbolLockKey" },
2357 for (let i = 0; i < modifiers.normal.length; i++) {
2358 if (!aKeyEvent[modifiers.normal[i].attr]) {
2361 if (aTIP.getModifierState(modifiers.normal[i].key)) {
2362 continue; // already activated.
2364 let event = new KeyboardEvent("", { key: modifiers.normal[i].key });
2367 aTIP.KEY_NON_PRINTABLE_KEY | aTIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT
2369 modifiers.normal[i].activated = true;
2371 for (let i = 0; i < modifiers.lockable.length; i++) {
2372 if (!aKeyEvent[modifiers.lockable[i].attr]) {
2375 if (aTIP.getModifierState(modifiers.lockable[i].key)) {
2376 continue; // already activated.
2378 let event = new KeyboardEvent("", { key: modifiers.lockable[i].key });
2381 aTIP.KEY_NON_PRINTABLE_KEY | aTIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT
2385 aTIP.KEY_NON_PRINTABLE_KEY | aTIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT
2387 modifiers.lockable[i].activated = true;
2392 function _emulateToInactivateModifiers(aTIP, aModifiers, aWindow = window) {
2396 var KeyboardEvent = _getKeyboardEvent(aWindow);
2397 for (let i = 0; i < aModifiers.normal.length; i++) {
2398 if (!aModifiers.normal[i].activated) {
2401 let event = new KeyboardEvent("", { key: aModifiers.normal[i].key });
2404 aTIP.KEY_NON_PRINTABLE_KEY | aTIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT
2407 for (let i = 0; i < aModifiers.lockable.length; i++) {
2408 if (!aModifiers.lockable[i].activated) {
2411 if (!aTIP.getModifierState(aModifiers.lockable[i].key)) {
2412 continue; // who already inactivated this?
2414 let event = new KeyboardEvent("", { key: aModifiers.lockable[i].key });
2417 aTIP.KEY_NON_PRINTABLE_KEY | aTIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT
2421 aTIP.KEY_NON_PRINTABLE_KEY | aTIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT
2427 * Synthesize a composition event and keydown event and keyup events unless
2428 * you prevent to dispatch them explicitly (see aEvent.key's explanation).
2430 * Note that you shouldn't call this with "compositionstart" unless you need to
2431 * test compositionstart event which is NOT followed by compositionupdate
2432 * event immediately. Typically, native IME starts composition with
2433 * a pair of keydown and keyup event and dispatch compositionstart and
2434 * compositionupdate (and non-standard text event) between them. So, in most
2435 * cases, you should call synthesizeCompositionChange() directly.
2436 * If you call this with compositionstart, keyup event will be fired
2437 * immediately after compositionstart. In other words, you should use
2438 * "compositionstart" only when you need to emulate IME which just starts
2439 * composition with compositionstart event but does not send composing text to
2440 * us until committing the composition. This is behavior of some Chinese IMEs.
2442 * @param aEvent The composition event information. This must
2443 * have |type| member. The value must be
2444 * "compositionstart", "compositionend",
2445 * "compositioncommitasis" or "compositioncommit".
2447 * And also this may have |data| and |locale| which
2448 * would be used for the value of each property of
2449 * the composition event. Note that the |data| is
2450 * ignored if the event type is "compositionstart"
2451 * or "compositioncommitasis".
2453 * If |key| is undefined, "keydown" and "keyup"
2454 * events which are marked as "processed by IME"
2455 * are dispatched. If |key| is not null, "keydown"
2456 * and/or "keyup" events are dispatched (if the
2457 * |key.type| is specified as "keydown", only
2458 * "keydown" event is dispatched). Otherwise,
2459 * i.e., if |key| is null, neither "keydown" nor
2460 * "keyup" event is dispatched.
2462 * If |key.doNotMarkKeydownAsProcessed| is not true,
2463 * key value and keyCode value of "keydown" event
2464 * will be set to "Process" and DOM_VK_PROCESSKEY.
2465 * If |key.markKeyupAsProcessed| is true,
2466 * key value and keyCode value of "keyup" event
2467 * will be set to "Process" and DOM_VK_PROCESSKEY.
2468 * @param aWindow Optional (If null, current |window| will be used)
2469 * @param aCallback Optional (If non-null, use the callback for
2470 * receiving notifications to IME)
2472 function synthesizeComposition(aEvent, aWindow = window, aCallback) {
2473 var TIP = _getTIP(aWindow, aCallback);
2477 var KeyboardEvent = _getKeyboardEvent(aWindow);
2478 var modifiers = _emulateToActivateModifiers(TIP, aEvent.key, aWindow);
2479 var keyEventDict = { dictionary: null, flags: 0 };
2480 var keyEvent = null;
2481 if (aEvent.key && typeof aEvent.key.key === "string") {
2482 keyEventDict = _createKeyboardEventDictionary(
2488 keyEvent = new KeyboardEvent(
2489 // eslint-disable-next-line no-nested-ternary
2490 aEvent.key.type === "keydown"
2492 : aEvent.key.type === "keyup"
2495 keyEventDict.dictionary
2497 } else if (aEvent.key === undefined) {
2498 keyEventDict = _createKeyboardEventDictionary(
2504 keyEvent = new KeyboardEvent("", keyEventDict.dictionary);
2507 switch (aEvent.type) {
2508 case "compositionstart":
2509 TIP.startComposition(keyEvent, keyEventDict.flags);
2511 case "compositioncommitasis":
2512 TIP.commitComposition(keyEvent, keyEventDict.flags);
2514 case "compositioncommit":
2515 TIP.commitCompositionWith(aEvent.data, keyEvent, keyEventDict.flags);
2519 _emulateToInactivateModifiers(TIP, modifiers, aWindow);
2523 * Synthesize eCompositionChange event which causes a DOM text event, may
2524 * cause compositionupdate event, and causes keydown event and keyup event
2525 * unless you prevent to dispatch them explicitly (see aEvent.key's
2528 * Note that if you call this when there is no composition, compositionstart
2529 * event will be fired automatically. This is better than you use
2530 * synthesizeComposition("compositionstart") in most cases. See the
2531 * explanation of synthesizeComposition().
2533 * @param aEvent The compositionchange event's information, this has
2534 * |composition| and |caret| members. |composition| has
2535 * |string| and |clauses| members. |clauses| must be array
2536 * object. Each object has |length| and |attr|. And |caret|
2537 * has |start| and |length|. See the following tree image.
2550 * Set the composition string to |composition.string|. Set its
2551 * clauses information to the |clauses| array.
2553 * When it's composing, set the each clauses' length to the
2554 * |composition.clauses[n].length|. The sum of the all length
2555 * values must be same as the length of |composition.string|.
2556 * Set nsICompositionStringSynthesizer.ATTR_* to the
2557 * |composition.clauses[n].attr|.
2559 * When it's not composing, set 0 to the
2560 * |composition.clauses[0].length| and
2561 * |composition.clauses[0].attr|.
2563 * Set caret position to the |caret.start|. It's offset from
2564 * the start of the composition string. Set caret length to
2565 * |caret.length|. If it's larger than 0, it should be wide
2566 * caret. However, current nsEditor doesn't support wide
2567 * caret, therefore, you should always set 0 now.
2569 * If |key| is undefined, "keydown" and "keyup" events which
2570 * are marked as "processed by IME" are dispatched. If |key|
2571 * is not null, "keydown" and/or "keyup" events are dispatched
2572 * (if the |key.type| is specified as "keydown", only "keydown"
2573 * event is dispatched). Otherwise, i.e., if |key| is null,
2574 * neither "keydown" nor "keyup" event is dispatched.
2575 * If |key.doNotMarkKeydownAsProcessed| is not true, key value
2576 * and keyCode value of "keydown" event will be set to
2577 * "Process" and DOM_VK_PROCESSKEY.
2578 * If |key.markKeyupAsProcessed| is true key value and keyCode
2579 * value of "keyup" event will be set to "Process" and
2580 * DOM_VK_PROCESSKEY.
2582 * @param aWindow Optional (If null, current |window| will be used)
2583 * @param aCallback Optional (If non-null, use the callback for receiving
2584 * notifications to IME)
2586 function synthesizeCompositionChange(aEvent, aWindow = window, aCallback) {
2587 var TIP = _getTIP(aWindow, aCallback);
2591 var KeyboardEvent = _getKeyboardEvent(aWindow);
2594 !aEvent.composition ||
2595 !aEvent.composition.clauses ||
2596 !aEvent.composition.clauses[0]
2601 TIP.setPendingCompositionString(aEvent.composition.string);
2602 if (aEvent.composition.clauses[0].length) {
2603 for (var i = 0; i < aEvent.composition.clauses.length; i++) {
2604 switch (aEvent.composition.clauses[i].attr) {
2605 case TIP.ATTR_RAW_CLAUSE:
2606 case TIP.ATTR_SELECTED_RAW_CLAUSE:
2607 case TIP.ATTR_CONVERTED_CLAUSE:
2608 case TIP.ATTR_SELECTED_CLAUSE:
2609 TIP.appendClauseToPendingComposition(
2610 aEvent.composition.clauses[i].length,
2611 aEvent.composition.clauses[i].attr
2615 // Ignore dummy clause for the argument.
2618 throw new Error("invalid clause attribute specified");
2624 TIP.setCaretInPendingComposition(aEvent.caret.start);
2627 var modifiers = _emulateToActivateModifiers(TIP, aEvent.key, aWindow);
2629 var keyEventDict = { dictionary: null, flags: 0 };
2630 var keyEvent = null;
2631 if (aEvent.key && typeof aEvent.key.key === "string") {
2632 keyEventDict = _createKeyboardEventDictionary(
2638 keyEvent = new KeyboardEvent(
2639 // eslint-disable-next-line no-nested-ternary
2640 aEvent.key.type === "keydown"
2642 : aEvent.key.type === "keyup"
2645 keyEventDict.dictionary
2647 } else if (aEvent.key === undefined) {
2648 keyEventDict = _createKeyboardEventDictionary(
2654 keyEvent = new KeyboardEvent("", keyEventDict.dictionary);
2656 TIP.flushPendingComposition(keyEvent, keyEventDict.flags);
2658 _emulateToInactivateModifiers(TIP, modifiers, aWindow);
2662 // Must be synchronized with nsIDOMWindowUtils.
2663 const QUERY_CONTENT_FLAG_USE_NATIVE_LINE_BREAK = 0x0000;
2664 const QUERY_CONTENT_FLAG_USE_XP_LINE_BREAK = 0x0001;
2666 const QUERY_CONTENT_FLAG_SELECTION_NORMAL = 0x0000;
2667 const QUERY_CONTENT_FLAG_SELECTION_SPELLCHECK = 0x0002;
2668 const QUERY_CONTENT_FLAG_SELECTION_IME_RAWINPUT = 0x0004;
2669 const QUERY_CONTENT_FLAG_SELECTION_IME_SELECTEDRAWTEXT = 0x0008;
2670 const QUERY_CONTENT_FLAG_SELECTION_IME_CONVERTEDTEXT = 0x0010;
2671 const QUERY_CONTENT_FLAG_SELECTION_IME_SELECTEDCONVERTEDTEXT = 0x0020;
2672 const QUERY_CONTENT_FLAG_SELECTION_ACCESSIBILITY = 0x0040;
2673 const QUERY_CONTENT_FLAG_SELECTION_FIND = 0x0080;
2674 const QUERY_CONTENT_FLAG_SELECTION_URLSECONDARY = 0x0100;
2675 const QUERY_CONTENT_FLAG_SELECTION_URLSTRIKEOUT = 0x0200;
2677 const QUERY_CONTENT_FLAG_OFFSET_RELATIVE_TO_INSERTION_POINT = 0x0400;
2679 const SELECTION_SET_FLAG_USE_NATIVE_LINE_BREAK = 0x0000;
2680 const SELECTION_SET_FLAG_USE_XP_LINE_BREAK = 0x0001;
2681 const SELECTION_SET_FLAG_REVERSE = 0x0002;
2684 * Synthesize a query text content event.
2686 * @param aOffset The character offset. 0 means the first character in the
2688 * @param aLength The length of getting text. If the length is too long,
2689 * the extra length is ignored.
2690 * @param aIsRelative Optional (If true, aOffset is relative to start of
2691 * composition if there is, or start of selection.)
2692 * @param aWindow Optional (If null, current |window| will be used)
2693 * @return An nsIQueryContentEventResult object. If this failed,
2694 * the result might be null.
2696 function synthesizeQueryTextContent(aOffset, aLength, aIsRelative, aWindow) {
2697 var utils = _getDOMWindowUtils(aWindow);
2701 var flags = QUERY_CONTENT_FLAG_USE_NATIVE_LINE_BREAK;
2702 if (aIsRelative === true) {
2703 flags |= QUERY_CONTENT_FLAG_OFFSET_RELATIVE_TO_INSERTION_POINT;
2705 return utils.sendQueryContentEvent(
2706 utils.QUERY_TEXT_CONTENT,
2716 * Synthesize a query selected text event.
2718 * @param aSelectionType Optional, one of QUERY_CONTENT_FLAG_SELECTION_*.
2719 * If null, QUERY_CONTENT_FLAG_SELECTION_NORMAL will
2721 * @param aWindow Optional (If null, current |window| will be used)
2722 * @return An nsIQueryContentEventResult object. If this failed,
2723 * the result might be null.
2725 function synthesizeQuerySelectedText(aSelectionType, aWindow) {
2726 var utils = _getDOMWindowUtils(aWindow);
2727 var flags = QUERY_CONTENT_FLAG_USE_NATIVE_LINE_BREAK;
2728 if (aSelectionType) {
2729 flags |= aSelectionType;
2732 return utils.sendQueryContentEvent(
2733 utils.QUERY_SELECTED_TEXT,
2743 * Synthesize a query caret rect event.
2745 * @param aOffset The caret offset. 0 means left side of the first character
2746 * in the selection root.
2747 * @param aWindow Optional (If null, current |window| will be used)
2748 * @return An nsIQueryContentEventResult object. If this failed,
2749 * the result might be null.
2751 function synthesizeQueryCaretRect(aOffset, aWindow) {
2752 var utils = _getDOMWindowUtils(aWindow);
2756 return utils.sendQueryContentEvent(
2757 utils.QUERY_CARET_RECT,
2762 QUERY_CONTENT_FLAG_USE_NATIVE_LINE_BREAK
2767 * Synthesize a selection set event.
2769 * @param aOffset The character offset. 0 means the first character in the
2771 * @param aLength The length of the text. If the length is too long,
2772 * the extra length is ignored.
2773 * @param aReverse If true, the selection is from |aOffset + aLength| to
2774 * |aOffset|. Otherwise, from |aOffset| to |aOffset + aLength|.
2775 * @param aWindow Optional (If null, current |window| will be used)
2776 * @return True, if succeeded. Otherwise false.
2778 async function synthesizeSelectionSet(
2784 const utils = _getDOMWindowUtils(aWindow);
2788 // eSetSelection event will be compared with selection cache in
2789 // IMEContentObserver, but it may have not been updated yet. Therefore, we
2790 // need to flush pending things of IMEContentObserver.
2791 await new Promise(resolve =>
2792 aWindow.requestAnimationFrame(() => aWindow.requestAnimationFrame(resolve))
2794 const flags = aReverse ? SELECTION_SET_FLAG_REVERSE : 0;
2795 return utils.sendSelectionSetEvent(aOffset, aLength, flags);
2799 * Synthesize a query text rect event.
2801 * @param aOffset The character offset. 0 means the first character in the
2803 * @param aLength The length of the text. If the length is too long,
2804 * the extra length is ignored.
2805 * @param aIsRelative Optional (If true, aOffset is relative to start of
2806 * composition if there is, or start of selection.)
2807 * @param aWindow Optional (If null, current |window| will be used)
2808 * @return An nsIQueryContentEventResult object. If this failed,
2809 * the result might be null.
2811 function synthesizeQueryTextRect(aOffset, aLength, aIsRelative, aWindow) {
2812 if (aIsRelative !== undefined && typeof aIsRelative !== "boolean") {
2814 "Maybe, you set Window object to the 3rd argument, but it should be a boolean value"
2817 var utils = _getDOMWindowUtils(aWindow);
2818 let flags = QUERY_CONTENT_FLAG_USE_NATIVE_LINE_BREAK;
2819 if (aIsRelative === true) {
2820 flags |= QUERY_CONTENT_FLAG_OFFSET_RELATIVE_TO_INSERTION_POINT;
2822 return utils.sendQueryContentEvent(
2823 utils.QUERY_TEXT_RECT,
2833 * Synthesize a query text rect array event.
2835 * @param aOffset The character offset. 0 means the first character in the
2837 * @param aLength The length of the text. If the length is too long,
2838 * the extra length is ignored.
2839 * @param aWindow Optional (If null, current |window| will be used)
2840 * @return An nsIQueryContentEventResult object. If this failed,
2841 * the result might be null.
2843 function synthesizeQueryTextRectArray(aOffset, aLength, aWindow) {
2844 var utils = _getDOMWindowUtils(aWindow);
2845 return utils.sendQueryContentEvent(
2846 utils.QUERY_TEXT_RECT_ARRAY,
2851 QUERY_CONTENT_FLAG_USE_NATIVE_LINE_BREAK
2856 * Synthesize a query editor rect event.
2858 * @param aWindow Optional (If null, current |window| will be used)
2859 * @return An nsIQueryContentEventResult object. If this failed,
2860 * the result might be null.
2862 function synthesizeQueryEditorRect(aWindow) {
2863 var utils = _getDOMWindowUtils(aWindow);
2864 return utils.sendQueryContentEvent(
2865 utils.QUERY_EDITOR_RECT,
2870 QUERY_CONTENT_FLAG_USE_NATIVE_LINE_BREAK
2875 * Synthesize a character at point event.
2877 * @param aX, aY The offset in the client area of the DOM window.
2878 * @param aWindow Optional (If null, current |window| will be used)
2879 * @return An nsIQueryContentEventResult object. If this failed,
2880 * the result might be null.
2882 function synthesizeCharAtPoint(aX, aY, aWindow) {
2883 var utils = _getDOMWindowUtils(aWindow);
2884 return utils.sendQueryContentEvent(
2885 utils.QUERY_CHARACTER_AT_POINT,
2890 QUERY_CONTENT_FLAG_USE_NATIVE_LINE_BREAK
2896 * Create an event object to pass to sendDragEvent.
2898 * @param aType The string represents drag event type.
2899 * @param aDestElement The element to fire the drag event, used to calculate
2900 * screenX/Y and clientX/Y.
2901 * @param aDestWindow Optional; Defaults to the current window object.
2902 * @param aDataTransfer dataTransfer for current drag session.
2903 * @param aDragEvent The object contains properties to override the event
2905 * @return An object to pass to sendDragEvent.
2907 function createDragEventObject(
2914 var destRect = aDestElement.getBoundingClientRect();
2915 var destClientX = destRect.left + destRect.width / 2;
2916 var destClientY = destRect.top + destRect.height / 2;
2917 var destScreenX = aDestWindow.mozInnerScreenX + destClientX;
2918 var destScreenY = aDestWindow.mozInnerScreenY + destClientY;
2919 if ("clientX" in aDragEvent && !("screenX" in aDragEvent)) {
2920 aDragEvent.screenX = aDestWindow.mozInnerScreenX + aDragEvent.clientX;
2922 if ("clientY" in aDragEvent && !("screenY" in aDragEvent)) {
2923 aDragEvent.screenY = aDestWindow.mozInnerScreenY + aDragEvent.clientY;
2926 // Wrap only in plain mochitests
2928 if (aDataTransfer) {
2929 dataTransfer = _EU_maybeUnwrap(
2930 _EU_maybeWrap(aDataTransfer).mozCloneForEvent(aType)
2933 // Copy over the drop effect. This isn't copied over by Clone, as it uses
2934 // more complex logic in the actual implementation (see
2935 // nsContentUtils::SetDataTransferInEvent for actual impl).
2936 dataTransfer.dropEffect = aDataTransfer.dropEffect;
2939 return Object.assign(
2942 screenX: destScreenX,
2943 screenY: destScreenY,
2944 clientX: destClientX,
2945 clientY: destClientY,
2947 _domDispatchOnly: aDragEvent._domDispatchOnly,
2954 * Emulate a event sequence of dragstart, dragenter, and dragover.
2956 * @param {Element} aSrcElement
2957 * The element to use to start the drag.
2958 * @param {Element} aDestElement
2959 * The element to fire the dragover, dragenter events
2960 * @param {Array} aDragData
2961 * The data to supply for the data transfer.
2962 * This data is in the format:
2966 * {"type": value, "data": value },
2972 * Pass null to avoid modifying dataTransfer.
2973 * @param {String} [aDropEffect="move"]
2974 * The drop effect to set during the dragstart event, or 'move' if omitted.
2975 * @param {Window} [aWindow=window]
2976 * The window in which the drag happens. Defaults to the window in which
2977 * EventUtils.js is loaded.
2978 * @param {Window} [aDestWindow=aWindow]
2979 * Used when aDestElement is in a different window than aSrcElement.
2980 * Default is to match ``aWindow``.
2981 * @param {Object} [aDragEvent={}]
2982 * Defaults to empty object. Overwrites an object passed to sendDragEvent.
2984 * A two element array, where the first element is the value returned
2985 * from sendDragEvent for dragover event, and the second element is the
2986 * dataTransfer for the current drag session.
2988 function synthesizeDragOver(
3001 aDestWindow = aWindow;
3004 // eslint-disable-next-line mozilla/use-services
3005 const obs = _EU_Cc["@mozilla.org/observer-service;1"].getService(
3006 _EU_Ci.nsIObserverService
3008 const ds = _EU_Cc["@mozilla.org/widget/dragservice;1"].getService(
3009 _EU_Ci.nsIDragService
3011 var sess = ds.getCurrentSession();
3013 // This method runs before other callbacks, and acts as a way to inject the
3014 // initial drag data into the DataTransfer.
3015 function fillDrag(event) {
3017 for (var i = 0; i < aDragData.length; i++) {
3018 var item = aDragData[i];
3019 for (var j = 0; j < item.length; j++) {
3020 _EU_maybeWrap(event.dataTransfer).mozSetDataAt(
3028 event.dataTransfer.dropEffect = aDropEffect || "move";
3029 event.preventDefault();
3032 function trapDrag(subject, topic) {
3033 if (topic == "on-datatransfer-available") {
3034 sess.dataTransfer = _EU_maybeUnwrap(
3035 _EU_maybeWrap(subject).mozCloneForEvent("drop")
3037 sess.dataTransfer.dropEffect = subject.dropEffect;
3041 // need to use real mouse action
3042 aWindow.addEventListener("dragstart", fillDrag, true);
3043 obs.addObserver(trapDrag, "on-datatransfer-available");
3044 synthesizeMouseAtCenter(aSrcElement, { type: "mousedown" }, aWindow);
3046 var rect = aSrcElement.getBoundingClientRect();
3047 var x = rect.width / 2;
3048 var y = rect.height / 2;
3049 synthesizeMouse(aSrcElement, x, y, { type: "mousemove" }, aWindow);
3050 synthesizeMouse(aSrcElement, x + 10, y + 10, { type: "mousemove" }, aWindow);
3051 aWindow.removeEventListener("dragstart", fillDrag, true);
3052 obs.removeObserver(trapDrag, "on-datatransfer-available");
3054 var dataTransfer = sess.dataTransfer;
3055 if (!dataTransfer) {
3056 throw new Error("No data transfer object after synthesizing the mouse!");
3059 // The EventStateManager will fire our dragenter event if it needs to.
3060 var event = createDragEventObject(
3067 var result = sendDragEvent(event, aDestElement, aDestWindow);
3069 return [result, dataTransfer];
3073 * Emulate the drop event and mouseup event.
3074 * This should be called after synthesizeDragOver.
3076 * @param {*} aResult
3077 * The first element of the array returned from ``synthesizeDragOver``.
3078 * @param {DataTransfer} aDataTransfer
3079 * The second element of the array returned from ``synthesizeDragOver``.
3080 * @param {Element} aDestElement
3081 * The element on which to fire the drop event.
3082 * @param {Window} [aDestWindow=window]
3083 * The window in which the drop happens. Defaults to the window in which
3084 * EventUtils.js is loaded.
3085 * @param {Object} [aDragEvent={}]
3086 * Defaults to empty object. Overwrites an object passed to sendDragEvent.
3088 * "none" if aResult is true, ``aDataTransfer.dropEffect`` otherwise.
3090 function synthesizeDropAfterDragOver(
3098 aDestWindow = window;
3101 var effect = aDataTransfer.dropEffect;
3106 } else if (effect != "none") {
3107 event = createDragEventObject(
3114 sendDragEvent(event, aDestElement, aDestWindow);
3116 // Don't run accessibility checks for this click, since we're not actually
3117 // clicking. It's just generated as part of the drop.
3118 // this.AccessibilityUtils might not be set if this isn't a browser test or
3119 // if a browser test has loaded its own copy of EventUtils for some reason.
3120 // In the latter case, the test probably shouldn't do that.
3121 this.AccessibilityUtils?.suppressClickHandling(true);
3122 synthesizeMouse(aDestElement, 2, 2, { type: "mouseup" }, aDestWindow);
3123 this.AccessibilityUtils?.suppressClickHandling(false);
3129 * Emulate a drag and drop by emulating a dragstart and firing events dragenter,
3130 * dragover, and drop.
3132 * @param {Element} aSrcElement
3133 * The element to use to start the drag.
3134 * @param {Element} aDestElement
3135 * The element to fire the dragover, dragenter events
3136 * @param {Array} aDragData
3137 * The data to supply for the data transfer.
3138 * This data is in the format:
3142 * {"type": value, "data": value },
3148 * Pass null to avoid modifying dataTransfer.
3149 * @param {String} [aDropEffect="move"]
3150 * The drop effect to set during the dragstart event, or 'move' if omitted..
3151 * @param {Window} [aWindow=window]
3152 * The window in which the drag happens. Defaults to the window in which
3153 * EventUtils.js is loaded.
3154 * @param {Window} [aDestWindow=aWindow]
3155 * Used when aDestElement is in a different window than aSrcElement.
3156 * Default is to match ``aWindow``.
3157 * @param {Object} [aDragEvent={}]
3158 * Defaults to empty object. Overwrites an object passed to sendDragEvent.
3160 * The drop effect that was desired.
3162 function synthesizeDrop(
3175 aDestWindow = aWindow;
3178 var ds = _EU_Cc["@mozilla.org/widget/dragservice;1"].getService(
3179 _EU_Ci.nsIDragService
3183 switch (aDropEffect) {
3187 dropAction = _EU_Ci.nsIDragService.DRAGDROP_ACTION_MOVE;
3190 dropAction = _EU_Ci.nsIDragService.DRAGDROP_ACTION_COPY;
3193 dropAction = _EU_Ci.nsIDragService.DRAGDROP_ACTION_LINK;
3196 throw new Error(`${aDropEffect} is an invalid drop effect value`);
3199 ds.startDragSessionForTests(dropAction);
3202 var [result, dataTransfer] = synthesizeDragOver(
3211 return synthesizeDropAfterDragOver(
3219 ds.endDragSession(true, _parseModifiers(aDragEvent));
3223 function _getFlattenedTreeParentNode(aNode) {
3224 return _EU_maybeUnwrap(_EU_maybeWrap(aNode).flattenedTreeParentNode);
3227 function _getInclusiveFlattenedTreeParentElement(aNode) {
3229 let inclusiveAncestor = aNode;
3231 inclusiveAncestor = _getFlattenedTreeParentNode(inclusiveAncestor)
3233 if (inclusiveAncestor.nodeType == Node.ELEMENT_NODE) {
3234 return inclusiveAncestor;
3240 function _nodeIsFlattenedTreeDescendantOf(
3241 aPossibleDescendant,
3245 if (aPossibleDescendant == aPossibleAncestor) {
3248 aPossibleDescendant = _getFlattenedTreeParentNode(aPossibleDescendant);
3249 } while (aPossibleDescendant);
3253 function _computeSrcElementFromSrcSelection(aSrcSelection) {
3254 let srcElement = aSrcSelection.focusNode;
3255 while (_EU_maybeWrap(srcElement).isNativeAnonymous) {
3256 srcElement = _getFlattenedTreeParentNode(srcElement);
3258 if (srcElement.nodeType !== Node.ELEMENT_NODE) {
3259 srcElement = _getInclusiveFlattenedTreeParentElement(srcElement);
3265 * Emulate a drag and drop by emulating a dragstart by mousedown and mousemove,
3266 * and firing events dragenter, dragover, drop, and dragend.
3267 * This does not modify dataTransfer and tries to emulate the plain drag and
3268 * drop as much as possible, compared to synthesizeDrop.
3269 * Note that if synthesized dragstart is canceled, this throws an exception
3270 * because in such case, Gecko does not start drag session.
3272 * @param {Object} aParams
3273 * @param {Event} aParams.dragEvent
3274 * The DnD events will be generated with modifiers specified with this.
3275 * @param {Element} aParams.srcElement
3276 * The element to start dragging. If srcSelection is
3277 * set, this is computed for element at focus node.
3278 * @param {Selection|nil} aParams.srcSelection
3279 * The selection to start to drag, set null if srcElement is set.
3280 * @param {Element|nil} aParams.destElement
3281 * The element to drop on. Pass null to emulate a drop on an invalid target.
3282 * @param {Number} aParams.srcX
3283 * The initial x coordinate inside srcElement or ignored if srcSelection is set.
3284 * @param {Number} aParams.srcY
3285 * The initial y coordinate inside srcElement or ignored if srcSelection is set.
3286 * @param {Number} aParams.stepX
3287 * The x-axis step for mousemove inside srcElement
3288 * @param {Number} aParams.stepY
3289 * The y-axis step for mousemove inside srcElement
3290 * @param {Number} aParams.finalX
3291 * The final x coordinate inside srcElement
3292 * @param {Number} aParams.finalY
3293 * The final x coordinate inside srcElement
3294 * @param {Any} aParams.id
3295 * The pointer event id
3296 * @param {Window} aParams.srcWindow
3297 * The window for dispatching event on srcElement, defaults to the current window object.
3298 * @param {Window} aParams.destWindow
3299 * The window for dispatching event on destElement, defaults to the current window object.
3300 * @param {Boolean} aParams.expectCancelDragStart
3301 * Set to true if the test cancels "dragstart"
3302 * @param {Boolean} aParams.expectSrcElementDisconnected
3303 * Set to true if srcElement will be disconnected and
3304 * "dragend" event won't be fired.
3305 * @param {Function} aParams.logFunc
3306 * Set function which takes one argument if you need to log rect of target. E.g., `console.log`.
3308 // eslint-disable-next-line complexity
3309 async function synthesizePlainDragAndDrop(aParams) {
3319 finalX = srcX + stepX * 2,
3320 finalY = srcY + stepY * 2,
3321 id = _getDOMWindowUtils(window).DEFAULT_MOUSE_POINTER_ID,
3323 destWindow = window,
3324 expectCancelDragStart = false,
3325 expectSrcElementDisconnected = false,
3328 // Don't modify given dragEvent object because we modify dragEvent below and
3329 // callers may use the object multiple times so that callers must not assume
3330 // that it'll be modified.
3331 if (aParams.dragEvent !== undefined) {
3332 dragEvent = Object.assign({}, aParams.dragEvent);
3335 function rectToString(aRect) {
3336 return `left: ${aRect.left}, top: ${aRect.top}, right: ${aRect.right}, bottom: ${aRect.bottom}`;
3340 logFunc("synthesizePlainDragAndDrop() -- START");
3344 srcElement = _computeSrcElementFromSrcSelection(srcSelection);
3345 let srcElementRect = srcElement.getBoundingClientRect();
3348 `srcElement.getBoundingClientRect(): ${rectToString(srcElementRect)}`
3351 // Use last selection client rect because nsIDragSession.sourceNode is
3352 // initialized from focus node which is usually in last rect.
3353 let selectionRectList = srcSelection.getRangeAt(0).getClientRects();
3354 let lastSelectionRect = selectionRectList[selectionRectList.length - 1];
3357 `srcSelection.getRangeAt(0).getClientRects()[${
3358 selectionRectList.length - 1
3359 }]: ${rectToString(lastSelectionRect)}`
3362 // Click at center of last selection rect.
3363 srcX = Math.floor(lastSelectionRect.left + lastSelectionRect.width / 2);
3364 srcY = Math.floor(lastSelectionRect.top + lastSelectionRect.height / 2);
3365 // Then, adjust srcX and srcY for making them offset relative to
3366 // srcElementRect because they will be used when we call synthesizeMouse()
3368 srcX = Math.floor(srcX - srcElementRect.left);
3369 srcY = Math.floor(srcY - srcElementRect.top);
3370 // Finally, recalculate finalX and finalY with new srcX and srcY if they
3371 // are not specified by the caller.
3372 if (aParams.finalX === undefined) {
3373 finalX = srcX + stepX * 2;
3375 if (aParams.finalY === undefined) {
3376 finalY = srcY + stepY * 2;
3378 } else if (logFunc) {
3380 `srcElement.getBoundingClientRect(): ${rectToString(
3381 srcElement.getBoundingClientRect()
3386 const ds = _EU_Cc["@mozilla.org/widget/dragservice;1"].getService(
3387 _EU_Ci.nsIDragService
3390 const editingHost = (() => {
3391 if (!srcElement.matches(":read-write")) {
3394 let lastEditableElement = srcElement;
3396 let inclusiveAncestor =
3397 _getInclusiveFlattenedTreeParentElement(srcElement);
3399 inclusiveAncestor = _getInclusiveFlattenedTreeParentElement(
3400 _getFlattenedTreeParentNode(inclusiveAncestor)
3403 if (inclusiveAncestor.matches(":read-write")) {
3404 lastEditableElement = inclusiveAncestor;
3405 if (lastEditableElement == srcElement.ownerDocument.body) {
3410 return lastEditableElement;
3413 _getDOMWindowUtils(srcWindow).disableNonTestMouseEvents(true);
3415 await new Promise(r => setTimeout(r, 0));
3418 function onMouseDown(aEvent) {
3419 mouseDownEvent = aEvent;
3422 `"${aEvent.type}" event is fired on ${
3424 } (composedTarget: ${_EU_maybeUnwrap(
3425 _EU_maybeWrap(aEvent).composedTarget
3430 !_nodeIsFlattenedTreeDescendantOf(
3431 _EU_maybeUnwrap(_EU_maybeWrap(aEvent).composedTarget),
3435 // If srcX and srcY does not point in one of rects in srcElement,
3436 // "mousedown" target is not in srcElement. Such case must not
3437 // be expected by this API users so that we should throw an exception
3438 // for making debugging easier.
3440 'event target of "mousedown" is not srcElement nor its descendant'
3445 srcWindow.addEventListener("mousedown", onMouseDown, { capture: true });
3450 { type: "mousedown", id },
3454 logFunc(`mousedown at ${srcX}, ${srcY}`);
3456 if (!mouseDownEvent) {
3457 throw new Error('"mousedown" event is not fired');
3460 srcWindow.removeEventListener("mousedown", onMouseDown, {
3466 function onDragStart(aEvent) {
3467 dragStartEvent = aEvent;
3469 logFunc(`"${aEvent.type}" event is fired`);
3472 !_nodeIsFlattenedTreeDescendantOf(
3473 _EU_maybeUnwrap(_EU_maybeWrap(aEvent).composedTarget),
3477 // If srcX and srcY does not point in one of rects in srcElement,
3478 // "dragstart" target is not in srcElement. Such case must not
3479 // be expected by this API users so that we should throw an exception
3480 // for making debugging easier.
3482 'event target of "dragstart" is not srcElement nor its descendant'
3487 function onDragEnterGenerated(aEvent) {
3488 dragEnterEvent = aEvent;
3490 srcWindow.addEventListener("dragstart", onDragStart, { capture: true });
3491 srcWindow.addEventListener("dragenter", onDragEnterGenerated, {
3495 // Wait for the next event tick after each event dispatch, so that UI
3496 // elements (e.g. menu) work like the real user input.
3497 await new Promise(r => setTimeout(r, 0));
3505 { type: "mousemove", id },
3509 logFunc(`first mousemove at ${srcX}, ${srcY}`);
3512 await new Promise(r => setTimeout(r, 0));
3520 { type: "mousemove", id },
3524 logFunc(`second mousemove at ${srcX}, ${srcY}`);
3527 await new Promise(r => setTimeout(r, 0));
3529 if (!dragStartEvent) {
3530 throw new Error('"dragstart" event is not fired');
3533 srcWindow.removeEventListener("dragstart", onDragStart, {
3536 srcWindow.removeEventListener("dragenter", onDragEnterGenerated, {
3541 let session = ds.getCurrentSession();
3543 if (expectCancelDragStart) {
3548 { type: "mouseup", id },
3553 throw new Error("drag hasn't been started by the operation");
3554 } else if (expectCancelDragStart) {
3555 throw new Error("drag has been started by the operation");
3560 (srcElement != destElement && !dragEnterEvent) ||
3561 destElement != dragEnterEvent.target
3565 `destElement.getBoundingClientRect(): ${rectToString(
3566 destElement.getBoundingClientRect()
3571 function onDragEnter(aEvent) {
3572 dragEnterEvent = aEvent;
3574 logFunc(`"${aEvent.type}" event is fired`);
3576 if (aEvent.target != destElement) {
3577 throw new Error('event target of "dragenter" is not destElement');
3580 destWindow.addEventListener("dragenter", onDragEnter, {
3584 let event = createDragEventObject(
3591 sendDragEvent(event, destElement, destWindow);
3592 if (!dragEnterEvent && !destElement.disabled) {
3593 throw new Error('"dragenter" event is not fired');
3595 if (dragEnterEvent && destElement.disabled) {
3597 '"dragenter" event should not be fired on disable element'
3601 destWindow.removeEventListener("dragenter", onDragEnter, {
3608 function onDragOver(aEvent) {
3609 dragOverEvent = aEvent;
3611 logFunc(`"${aEvent.type}" event is fired`);
3613 if (aEvent.target != destElement) {
3614 throw new Error('event target of "dragover" is not destElement');
3617 destWindow.addEventListener("dragover", onDragOver, { capture: true });
3619 // dragover and drop are only fired to a valid drop target. If the
3620 // destElement parameter is null, this function is being used to
3621 // simulate a drag'n'drop over an invalid drop target.
3622 let event = createDragEventObject(
3629 sendDragEvent(event, destElement, destWindow);
3630 if (!dragOverEvent && !destElement.disabled) {
3631 throw new Error('"dragover" event is not fired');
3633 if (dragEnterEvent && destElement.disabled) {
3635 '"dragover" event should not be fired on disable element'
3639 destWindow.removeEventListener("dragover", onDragOver, {
3644 await new Promise(r => setTimeout(r, 0));
3646 // If there is not accept to drop the data, "drop" event shouldn't be
3648 // XXX nsIDragSession.canDrop is different only on Linux. It must be
3649 // a bug of gtk/nsDragService since it manages `mCanDrop` by itself.
3650 // Thus, we should use nsIDragSession.dragAction instead.
3651 if (session.dragAction != _EU_Ci.nsIDragService.DRAGDROP_ACTION_NONE) {
3653 function onDrop(aEvent) {
3656 logFunc(`"${aEvent.type}" event is fired`);
3659 !_nodeIsFlattenedTreeDescendantOf(
3660 _EU_maybeUnwrap(_EU_maybeWrap(aEvent).composedTarget),
3665 'event target of "drop" is not destElement nor its descendant'
3669 destWindow.addEventListener("drop", onDrop, { capture: true });
3671 let event = createDragEventObject(
3678 sendDragEvent(event, destElement, destWindow);
3679 if (!dropEvent && session.canDrop) {
3680 throw new Error('"drop" event is not fired');
3683 destWindow.removeEventListener("drop", onDrop, { capture: true });
3689 // Since we don't synthesize drop event, we need to set drag end point
3690 // explicitly for "dragEnd" event which will be fired by
3691 // endDragSession().
3692 dragEvent.clientX = finalX;
3693 dragEvent.clientY = finalY;
3694 let event = createDragEventObject(
3696 destElement || srcElement,
3697 destElement ? srcWindow : destWindow,
3701 session.setDragEndPointForTests(event.screenX, event.screenY);
3703 await new Promise(r => setTimeout(r, 0));
3705 if (ds.getCurrentSession()) {
3706 const sourceNode = ds.sourceNode;
3708 function onDragEnd(aEvent) {
3709 dragEndEvent = aEvent;
3711 logFunc(`"${aEvent.type}" event is fired`);
3714 !_nodeIsFlattenedTreeDescendantOf(
3715 _EU_maybeUnwrap(_EU_maybeWrap(aEvent).composedTarget),
3718 _EU_maybeUnwrap(_EU_maybeWrap(aEvent).composedTarget) != editingHost
3721 'event target of "dragend" is not srcElement nor its descendant'
3724 if (expectSrcElementDisconnected) {
3726 `"dragend" event shouldn't be fired when the source node is disconnected (the source node is ${
3727 sourceNode?.isConnected ? "connected" : "null or disconnected"
3732 srcWindow.addEventListener("dragend", onDragEnd, { capture: true });
3734 ds.endDragSession(true, _parseModifiers(dragEvent));
3735 if (!expectSrcElementDisconnected && !dragEndEvent) {
3736 // eslint-disable-next-line no-unsafe-finally
3738 `"dragend" event is not fired by nsIDragService.endDragSession()${
3739 ds.sourceNode && !ds.sourceNode.isConnected
3740 ? "(sourceNode was disconnected)"
3746 srcWindow.removeEventListener("dragend", onDragEnd, { capture: true });
3749 _getDOMWindowUtils(srcWindow).disableNonTestMouseEvents(false);
3751 logFunc("synthesizePlainDragAndDrop() -- END");
3756 function _checkDataTransferItems(aDataTransfer, aExpectedDragData) {
3758 // We must wrap only in plain mochitests, not chrome
3759 let dataTransfer = _EU_maybeWrap(aDataTransfer);
3760 if (!dataTransfer) {
3764 aExpectedDragData == null ||
3765 dataTransfer.mozItemCount != aExpectedDragData.length
3767 return dataTransfer;
3769 for (let i = 0; i < dataTransfer.mozItemCount; i++) {
3770 let dtTypes = dataTransfer.mozTypesAt(i);
3771 if (dtTypes.length != aExpectedDragData[i].length) {
3772 return dataTransfer;
3774 for (let j = 0; j < dtTypes.length; j++) {
3775 if (dtTypes[j] != aExpectedDragData[i][j].type) {
3776 return dataTransfer;
3778 let dtData = dataTransfer.mozGetDataAt(dtTypes[j], i);
3779 if (aExpectedDragData[i][j].eqTest) {
3781 !aExpectedDragData[i][j].eqTest(
3783 aExpectedDragData[i][j].data
3786 return dataTransfer;
3788 } else if (aExpectedDragData[i][j].data != dtData) {
3789 return dataTransfer;
3800 * This callback type is used with ``synthesizePlainDragAndCancel()``.
3801 * It should compare ``actualData`` and ``expectedData`` and return
3802 * true if the two should be considered equal, false otherwise.
3805 * @param {*} actualData
3806 * @param {*} expectedData
3811 * synthesizePlainDragAndCancel() synthesizes drag start with
3812 * synthesizePlainDragAndDrop(), but always cancel it with preventing default
3813 * of "dragstart". Additionally, this checks whether the dataTransfer of
3814 * "dragstart" event has only expected items.
3816 * @param {Object} aParams
3817 * The params which is set to the argument of ``synthesizePlainDragAndDrop()``.
3818 * @param {Array} aExpectedDataTransferItems
3819 * All expected dataTransfer items.
3820 * This data is in the format:
3824 * {"type": value, "data": value, eqTest: function}
3830 * This can also be null.
3831 * You can optionally provide ``eqTest`` {@type eqTest} if the
3832 * comparison to the expected data transfer items can't be done
3835 * true if aExpectedDataTransferItems matches with
3836 * DragEvent.dataTransfer of "dragstart" event.
3837 * Otherwise, the dataTransfer object (may be null) or
3838 * thrown exception, NOT false. Therefore, you shouldn't
3841 async function synthesizePlainDragAndCancel(
3843 aExpectedDataTransferItems
3845 let srcElement = aParams.srcSelection
3846 ? _computeSrcElementFromSrcSelection(aParams.srcSelection)
3847 : aParams.srcElement;
3849 function onDragStart(aEvent) {
3850 aEvent.preventDefault();
3851 result = _checkDataTransferItems(
3852 aEvent.dataTransfer,
3853 aExpectedDataTransferItems
3856 SpecialPowers.addSystemEventListener(
3857 srcElement.ownerDocument,
3863 aParams.expectCancelDragStart = true;
3864 await synthesizePlainDragAndDrop(aParams);
3866 SpecialPowers.removeSystemEventListener(
3867 srcElement.ownerDocument,
3876 class EventCounter {
3877 constructor(aTarget, aType, aOptions = {}) {
3878 this.target = aTarget;
3880 this.options = aOptions;
3882 this.eventCount = 0;
3884 // SpecialPowers is picky and needs to be passed an explicit reference to
3885 // the function to be called. To avoid having to bind "this", we therefore
3886 // define the method this way, via a property.
3887 this.handleEvent = aEvent => {
3891 if (aOptions.mozSystemGroup) {
3892 SpecialPowers.addSystemEventListener(
3899 aTarget.addEventListener(aType, this, aOptions);
3904 if (this.options.mozSystemGroup) {
3905 SpecialPowers.removeSystemEventListener(
3909 this.options.capture
3912 this.target.removeEventListener(this.type, this, this.options);
3917 return this.eventCount;