EventUtils.js (162319B)
1 /* eslint-disable no-nested-ternary */ 2 /** 3 * EventUtils provides some utility methods for creating and sending DOM events. 4 * 5 * When adding methods to this file, please add a performance test for it. 6 */ 7 8 // Certain functions assume this is loaded into browser window scope. 9 // This is modifiable because certain chrome tests create their own gBrowser. 10 /* global gBrowser:true */ 11 12 // This file is used both in privileged and unprivileged contexts, so we have to 13 // be careful about our access to Components.interfaces. We also want to avoid 14 // naming collisions with anything that might be defined in the scope that imports 15 // this script. 16 // 17 // Even if the real |Components| doesn't exist, we might shim in a simple JS 18 // placebo for compat. An easy way to differentiate this from the real thing 19 // is whether the property is read-only or not. The real |Components| property 20 // is read-only. 21 /* global _EU_Ci, _EU_Cc, _EU_Cu, _EU_ChromeUtils, _EU_OS */ 22 window.__defineGetter__("_EU_Ci", function () { 23 var c = Object.getOwnPropertyDescriptor(window, "Components"); 24 return c && c.value && !c.writable ? Ci : SpecialPowers.Ci; 25 }); 26 27 window.__defineGetter__("_EU_Cc", function () { 28 var c = Object.getOwnPropertyDescriptor(window, "Components"); 29 return c && c.value && !c.writable ? Cc : SpecialPowers.Cc; 30 }); 31 32 window.__defineGetter__("_EU_Cu", function () { 33 var c = Object.getOwnPropertyDescriptor(window, "Components"); 34 return c && c.value && !c.writable ? Cu : SpecialPowers.Cu; 35 }); 36 37 window.__defineGetter__("_EU_ChromeUtils", function () { 38 var c = Object.getOwnPropertyDescriptor(window, "ChromeUtils"); 39 return c && c.value && !c.writable ? ChromeUtils : SpecialPowers.ChromeUtils; 40 }); 41 42 window.__defineGetter__("_EU_OS", function () { 43 delete this._EU_OS; 44 try { 45 this._EU_OS = _EU_ChromeUtils.importESModule( 46 "resource://gre/modules/AppConstants.sys.mjs" 47 ).platform; 48 } catch (ex) { 49 this._EU_OS = null; 50 } 51 return this._EU_OS; 52 }); 53 54 function _EU_isMac(aWindow = window) { 55 if (window._EU_OS) { 56 return window._EU_OS == "macosx"; 57 } 58 if (aWindow) { 59 try { 60 return aWindow.navigator.platform.indexOf("Mac") > -1; 61 } catch (ex) {} 62 } 63 return navigator.platform.indexOf("Mac") > -1; 64 } 65 66 function _EU_isWin(aWindow = window) { 67 if (window._EU_OS) { 68 return window._EU_OS == "win"; 69 } 70 if (aWindow) { 71 try { 72 return aWindow.navigator.platform.indexOf("Win") > -1; 73 } catch (ex) {} 74 } 75 return navigator.platform.indexOf("Win") > -1; 76 } 77 78 function _EU_isLinux(aWindow = window) { 79 if (window._EU_OS) { 80 return window._EU_OS == "linux"; 81 } 82 if (aWindow) { 83 try { 84 return aWindow.navigator.platform.startsWith("Linux"); 85 } catch (ex) {} 86 } 87 return navigator.platform.startsWith("Linux"); 88 } 89 90 function _EU_isAndroid(aWindow = window) { 91 if (window._EU_OS) { 92 return window._EU_OS == "android"; 93 } 94 if (aWindow) { 95 try { 96 return aWindow.navigator.userAgent.includes("Android"); 97 } catch (ex) {} 98 } 99 return navigator.userAgent.includes("Android"); 100 } 101 102 function _EU_maybeWrap(o) { 103 // We're used in some contexts where there is no SpecialPowers and also in 104 // some where it exists but has no wrap() method. And this is somewhat 105 // independent of whether window.Components is a thing... 106 var haveWrap = false; 107 try { 108 haveWrap = SpecialPowers.wrap != undefined; 109 } catch (e) { 110 // Just leave it false. 111 } 112 if (!haveWrap) { 113 // Not much we can do here. 114 return o; 115 } 116 var c = Object.getOwnPropertyDescriptor(window, "Components"); 117 return c && c.value && !c.writable ? o : SpecialPowers.wrap(o); 118 } 119 120 function _EU_maybeUnwrap(o) { 121 var haveWrap = false; 122 try { 123 haveWrap = SpecialPowers.unwrap != undefined; 124 } catch (e) { 125 // Just leave it false. 126 } 127 if (!haveWrap) { 128 // Not much we can do here. 129 return o; 130 } 131 var c = Object.getOwnPropertyDescriptor(window, "Components"); 132 return c && c.value && !c.writable ? o : SpecialPowers.unwrap(o); 133 } 134 135 function _EU_getPlatform() { 136 if (_EU_isWin()) { 137 return "windows"; 138 } 139 if (_EU_isMac()) { 140 return "mac"; 141 } 142 if (_EU_isAndroid()) { 143 return "android"; 144 } 145 if (_EU_isLinux()) { 146 return "linux"; 147 } 148 return "unknown"; 149 } 150 151 function _EU_roundDevicePixels(aMaybeFractionalPixels) { 152 return Math.floor(aMaybeFractionalPixels + 0.5); 153 } 154 155 /** 156 * promiseElementReadyForUserInput() dispatches mousemove events to aElement 157 * and waits one of them for a while. Then, returns "resolved" state when it's 158 * successfully received. Otherwise, if it couldn't receive mousemove event on 159 * it, this throws an exception. So, aElement must be an element which is 160 * assumed non-collapsed visible element in the window. 161 * 162 * This is useful if you need to synthesize mouse events via the main process 163 * but your test cannot check whether the element is now in APZ to deliver 164 * a user input event. 165 */ 166 async function promiseElementReadyForUserInput( 167 aElement, 168 aWindow = window, 169 aLogFunc = null 170 ) { 171 if (typeof aElement == "string") { 172 aElement = aWindow.document.getElementById(aElement); 173 } 174 175 function waitForMouseMoveForHittest() { 176 return new Promise(resolve => { 177 let timeout; 178 const onHit = () => { 179 if (aLogFunc) { 180 aLogFunc("mousemove received"); 181 } 182 aWindow.clearInterval(timeout); 183 resolve(true); 184 }; 185 aElement.addEventListener("mousemove", onHit, { 186 capture: true, 187 once: true, 188 }); 189 timeout = aWindow.setInterval(() => { 190 if (aLogFunc) { 191 aLogFunc("mousemove not received in this 300ms"); 192 } 193 aElement.removeEventListener("mousemove", onHit, { 194 capture: true, 195 }); 196 resolve(false); 197 }, 300); 198 synthesizeMouseAtCenter(aElement, { type: "mousemove" }, aWindow); 199 }); 200 } 201 for (let i = 0; i < 20; i++) { 202 if (await waitForMouseMoveForHittest()) { 203 return Promise.resolve(); 204 } 205 } 206 throw new Error("The element or the window did not become interactive"); 207 } 208 209 function getElement(id) { 210 return typeof id == "string" ? document.getElementById(id) : id; 211 } 212 213 this.$ = this.getElement; 214 215 function computeButton(aEvent) { 216 if (typeof aEvent.button != "undefined") { 217 return aEvent.button; 218 } 219 return aEvent.type == "contextmenu" ? 2 : 0; 220 } 221 222 /** 223 * Send a mouse event to the node aTarget (aTarget can be an id, or an 224 * actual node) . The "event" passed in to aEvent is just a JavaScript 225 * object with the properties set that the real mouse event object should 226 * have. This includes the type of the mouse event. Pretty much all those 227 * properties are optional. 228 * E.g. to send an click event to the node with id 'node' you might do this: 229 * 230 * ``sendMouseEvent({type:'click'}, 'node');`` 231 */ 232 function sendMouseEvent(aEvent, aTarget, aWindow) { 233 if ( 234 ![ 235 "click", 236 "contextmenu", 237 "dblclick", 238 "mousedown", 239 "mouseup", 240 "mouseover", 241 "mouseout", 242 ].includes(aEvent.type) 243 ) { 244 throw new Error( 245 "sendMouseEvent doesn't know about event type '" + aEvent.type + "'" 246 ); 247 } 248 249 if (!aWindow) { 250 aWindow = window; 251 } 252 253 if (typeof aTarget == "string") { 254 aTarget = aWindow.document.getElementById(aTarget); 255 } 256 257 let dict = { 258 bubbles: true, 259 cancelable: true, 260 view: aWindow, 261 detail: 262 aEvent.detail || 263 // eslint-disable-next-line no-nested-ternary 264 (aEvent.type == "click" || 265 aEvent.type == "mousedown" || 266 aEvent.type == "mouseup" 267 ? 1 268 : aEvent.type == "dblclick" 269 ? 2 270 : 0), 271 screenX: aEvent.screenX || 0, 272 screenY: aEvent.screenY || 0, 273 clientX: aEvent.clientX || 0, 274 clientY: aEvent.clientY || 0, 275 ctrlKey: aEvent.ctrlKey || false, 276 altKey: aEvent.altKey || false, 277 shiftKey: aEvent.shiftKey || false, 278 metaKey: aEvent.metaKey || false, 279 button: computeButton(aEvent), 280 // FIXME: Set buttons 281 relatedTarget: aEvent.relatedTarget || null, 282 }; 283 284 let event = 285 aEvent.type == "click" || aEvent.type == "contextmenu" 286 ? new aWindow.PointerEvent(aEvent.type, dict) 287 : new aWindow.MouseEvent(aEvent.type, dict); 288 289 // If documentURIObject exists or `window` is a stub object, we're in 290 // a chrome scope, so don't bother trying to go through SpecialPowers. 291 if (!window.document || window.document.documentURIObject) { 292 return aTarget.dispatchEvent(event); 293 } 294 return SpecialPowers.dispatchEvent(aWindow, aTarget, event); 295 } 296 297 function isHidden(aElement) { 298 var box = aElement.getBoundingClientRect(); 299 return box.width == 0 && box.height == 0; 300 } 301 302 /** 303 * Send a drag event to the node aTarget (aTarget can be an id, or an 304 * actual node) . The "event" passed in to aEvent is just a JavaScript 305 * object with the properties set that the real drag event object should 306 * have. This includes the type of the drag event. 307 * 308 * @returns {boolean} 309 */ 310 function sendDragEvent(aEvent, aTarget, aWindow = window) { 311 if ( 312 ![ 313 "drag", 314 "dragstart", 315 "dragend", 316 "dragover", 317 "dragenter", 318 "dragleave", 319 "drop", 320 ].includes(aEvent.type) 321 ) { 322 throw new Error( 323 "sendDragEvent doesn't know about event type '" + aEvent.type + "'" 324 ); 325 } 326 327 if (typeof aTarget == "string") { 328 aTarget = aWindow.document.getElementById(aTarget); 329 } 330 331 /* 332 * Drag event cannot be performed if the element is hidden, except 'dragend' 333 * event where the element can becomes hidden after start dragging. 334 */ 335 if (aEvent.type != "dragend" && isHidden(aTarget)) { 336 var targetName = aTarget.nodeName; 337 if ("id" in aTarget && aTarget.id) { 338 targetName += "#" + aTarget.id; 339 } 340 throw new Error(`${aEvent.type} event target ${targetName} is hidden`); 341 } 342 343 var event = aWindow.document.createEvent("DragEvent"); 344 345 var typeArg = aEvent.type; 346 var canBubbleArg = true; 347 var cancelableArg = true; 348 var viewArg = aWindow; 349 var detailArg = aEvent.detail || 0; 350 var screenXArg = aEvent.screenX || 0; 351 var screenYArg = aEvent.screenY || 0; 352 var clientXArg = aEvent.clientX || 0; 353 var clientYArg = aEvent.clientY || 0; 354 var ctrlKeyArg = aEvent.ctrlKey || false; 355 var altKeyArg = aEvent.altKey || false; 356 var shiftKeyArg = aEvent.shiftKey || false; 357 var metaKeyArg = aEvent.metaKey || false; 358 var buttonArg = computeButton(aEvent); 359 var relatedTargetArg = aEvent.relatedTarget || null; 360 var dataTransfer = aEvent.dataTransfer || null; 361 362 event.initDragEvent( 363 typeArg, 364 canBubbleArg, 365 cancelableArg, 366 viewArg, 367 detailArg, 368 Math.round(screenXArg), 369 Math.round(screenYArg), 370 Math.round(clientXArg), 371 Math.round(clientYArg), 372 ctrlKeyArg, 373 altKeyArg, 374 shiftKeyArg, 375 metaKeyArg, 376 buttonArg, 377 relatedTargetArg, 378 dataTransfer 379 ); 380 381 if (aEvent._domDispatchOnly) { 382 return aTarget.dispatchEvent(event); 383 } 384 385 var utils = _getDOMWindowUtils(aWindow); 386 return utils.dispatchDOMEventViaPresShellForTesting(aTarget, event); 387 } 388 389 /** 390 * Send the char aChar to the focused element. This method handles casing of 391 * chars (sends the right charcode, and sends a shift key for uppercase chars). 392 * No other modifiers are handled at this point. 393 * 394 * For now this method only works for ASCII characters and emulates the shift 395 * key state on US keyboard layout. 396 */ 397 function sendChar(aChar, aWindow) { 398 var hasShift; 399 // Emulate US keyboard layout for the shiftKey state. 400 switch (aChar) { 401 case "!": 402 case "@": 403 case "#": 404 case "$": 405 case "%": 406 case "^": 407 case "&": 408 case "*": 409 case "(": 410 case ")": 411 case "_": 412 case "+": 413 case "{": 414 case "}": 415 case ":": 416 case '"': 417 case "|": 418 case "<": 419 case ">": 420 case "?": 421 hasShift = true; 422 break; 423 default: 424 hasShift = 425 aChar.toLowerCase() != aChar.toUpperCase() && 426 aChar == aChar.toUpperCase(); 427 break; 428 } 429 synthesizeKey(aChar, { shiftKey: hasShift }, aWindow); 430 } 431 432 /** 433 * Send the string aStr to the focused element. 434 * 435 * For now this method only works for ASCII characters and emulates the shift 436 * key state on US keyboard layout. 437 */ 438 function sendString(aStr, aWindow) { 439 for (let i = 0; i < aStr.length; ++i) { 440 // Do not split a surrogate pair to call synthesizeKey. Dispatching two 441 // sets of keydown and keyup caused by two calls of synthesizeKey is not 442 // good behavior. It could happen due to a bug, but a surrogate pair should 443 // be introduced with one key press operation. Therefore, calling it with 444 // a surrogate pair is the right thing. 445 // Note that TextEventDispatcher will consider whether a surrogate pair 446 // should cause one or two keypress events automatically. Therefore, we 447 // don't need to check the related prefs here. 448 if ( 449 (aStr.charCodeAt(i) & 0xfc00) == 0xd800 && 450 i + 1 < aStr.length && 451 (aStr.charCodeAt(i + 1) & 0xfc00) == 0xdc00 452 ) { 453 sendChar(aStr.substring(i, i + 2), aWindow); 454 i++; 455 } else { 456 sendChar(aStr.charAt(i), aWindow); 457 } 458 } 459 } 460 461 /** 462 * Send the non-character key aKey to the focused node. 463 * The name of the key should be the part that comes after ``DOM_VK_`` in the 464 * KeyEvent constant name for this key. 465 * No modifiers are handled at this point. 466 */ 467 function sendKey(aKey, aWindow) { 468 var keyName = "VK_" + aKey.toUpperCase(); 469 synthesizeKey(keyName, { shiftKey: false }, aWindow); 470 } 471 472 /** 473 * Parse the key modifier flags from aEvent. Used to share code between 474 * synthesizeMouse and synthesizeKey. 475 */ 476 function _parseModifiers(aEvent, aWindow = window) { 477 var nsIDOMWindowUtils = _EU_Ci.nsIDOMWindowUtils; 478 var mval = 0; 479 if (aEvent.shiftKey) { 480 mval |= nsIDOMWindowUtils.MODIFIER_SHIFT; 481 } 482 if (aEvent.ctrlKey) { 483 mval |= nsIDOMWindowUtils.MODIFIER_CONTROL; 484 } 485 if (aEvent.altKey) { 486 mval |= nsIDOMWindowUtils.MODIFIER_ALT; 487 } 488 if (aEvent.metaKey) { 489 mval |= nsIDOMWindowUtils.MODIFIER_META; 490 } 491 if (aEvent.accelKey) { 492 mval |= _EU_isMac(aWindow) 493 ? nsIDOMWindowUtils.MODIFIER_META 494 : nsIDOMWindowUtils.MODIFIER_CONTROL; 495 } 496 if (aEvent.altGrKey) { 497 mval |= nsIDOMWindowUtils.MODIFIER_ALTGRAPH; 498 } 499 if (aEvent.capsLockKey) { 500 mval |= nsIDOMWindowUtils.MODIFIER_CAPSLOCK; 501 } 502 if (aEvent.fnKey) { 503 mval |= nsIDOMWindowUtils.MODIFIER_FN; 504 } 505 if (aEvent.fnLockKey) { 506 mval |= nsIDOMWindowUtils.MODIFIER_FNLOCK; 507 } 508 if (aEvent.numLockKey) { 509 mval |= nsIDOMWindowUtils.MODIFIER_NUMLOCK; 510 } 511 if (aEvent.scrollLockKey) { 512 mval |= nsIDOMWindowUtils.MODIFIER_SCROLLLOCK; 513 } 514 if (aEvent.symbolKey) { 515 mval |= nsIDOMWindowUtils.MODIFIER_SYMBOL; 516 } 517 if (aEvent.symbolLockKey) { 518 mval |= nsIDOMWindowUtils.MODIFIER_SYMBOLLOCK; 519 } 520 521 return mval; 522 } 523 524 /** 525 * Return the drag service. Note that if we're in the headless mode, this 526 * may return null because the service may be never instantiated (e.g., on 527 * Linux). 528 */ 529 function getDragService() { 530 try { 531 return _EU_Cc["@mozilla.org/widget/dragservice;1"].getService( 532 _EU_Ci.nsIDragService 533 ); 534 } catch (e) { 535 // If we're in the headless mode, the drag service may be never 536 // instantiated. In this case, an exception is thrown. Let's ignore 537 // any exceptions since without the drag service, nobody can create a 538 // drag session. 539 return null; 540 } 541 } 542 543 /** 544 * End drag session if there is. 545 * 546 * TODO: This should synthesize "drop" if necessary. 547 * 548 * @param left X offset in the viewport 549 * @param top Y offset in the viewport 550 * @param aEvent The event data, the modifiers are applied to the 551 * "dragend" event. 552 * @param aWindow The window. 553 * @return true if handled. In this case, the caller should not 554 * synthesize DOM events basically. 555 */ 556 function _maybeEndDragSession(left, top, aEvent, aWindow) { 557 let utils = _getDOMWindowUtils(aWindow); 558 const dragSession = utils.dragSession; 559 if (!dragSession) { 560 return false; 561 } 562 // FIXME: If dragSession.dragAction is not 563 // nsIDragService.DRAGDROP_ACTION_NONE nor aEvent.type is not `keydown`, we 564 // need to synthesize a "drop" event or call setDragEndPointForTests here to 565 // set proper left/top to `dragend` event. 566 try { 567 dragSession.endDragSession(false, _parseModifiers(aEvent, aWindow)); 568 } catch (e) {} 569 return true; 570 } 571 572 function _maybeSynthesizeDragOver(left, top, aEvent, aWindow) { 573 let utils = _getDOMWindowUtils(aWindow); 574 const dragSession = utils.dragSession; 575 if (!dragSession) { 576 return false; 577 } 578 const target = aWindow.document.elementFromPoint(left, top); 579 if (target) { 580 sendDragEvent( 581 createDragEventObject( 582 "dragover", 583 target, 584 aWindow, 585 dragSession.dataTransfer, 586 { 587 accelKey: aEvent.accelKey, 588 altKey: aEvent.altKey, 589 altGrKey: aEvent.altGrKey, 590 ctrlKey: aEvent.ctrlKey, 591 metaKey: aEvent.metaKey, 592 shiftKey: aEvent.shiftKey, 593 capsLockKey: aEvent.capsLockKey, 594 fnKey: aEvent.fnKey, 595 fnLockKey: aEvent.fnLockKey, 596 numLockKey: aEvent.numLockKey, 597 scrollLockKey: aEvent.scrollLockKey, 598 symbolKey: aEvent.symbolKey, 599 symbolLockKey: aEvent.symbolLockKey, 600 } 601 ), 602 target, 603 aWindow 604 ); 605 } 606 return true; 607 } 608 609 /** 610 * @typedef {object} MouseEventData 611 * 612 * @property {string} [accessKey] - The character or key associated with 613 * the access key event. Typically a single character used to activate a UI 614 * element via keyboard shortcuts (e.g., Alt + accessKey). 615 * @property {boolean} [altKey] - If set to `true`, the Alt key will be 616 * considered pressed. 617 * @property {boolean} [asyncEnabled] - If `true`, the event is 618 * dispatched to the parent process through APZ, without being injected 619 * into the OS event queue. 620 * @property {number} [button=0] - Button to synthesize. 621 * @property {number} [buttons] - Indicates which mouse buttons are pressed 622 * when a mouse event is triggered. 623 * @property {number} [clickCount=1] - Number of clicks that have to be performed. 624 * @property {boolean} [ctrlKey] - If set to `true`, the Ctrl key will 625 * be considered pressed. 626 * @property {number} [id] - A unique identifier for the pointer causing the event. 627 * @property {number} [inputSource] - Input source, see MouseEvent for values. 628 * Defaults to MouseEvent.MOZ_SOURCE_MOUSE. 629 * @property {boolean} [isSynthesized] - Controls Event.isSynthesized value that 630 * helps identifying test related events 631 * @property {boolean} [isWidgetEventSynthesized] - Controls WidgetMouseEvent.mReason value. 632 * @property {boolean} [metaKey] - If set to `true`, the Meta key will 633 * be considered pressed. 634 * @property {number} [pressure=0] - Touch input pressure (0.0 -> 1.0). 635 * @property {boolean} [shiftKey] - If set to `true`, the Shift key will 636 * be considered pressed. 637 * @property {string} [type] - Event type to synthesize. If not specified 638 * a `mousedown` followed by a `mouseup` are performed. 639 * 640 * @see nsIDOMWindowUtils.sendMouseEvent 641 */ 642 643 /** 644 * Synthesize a mouse event on a target. 645 * 646 * The actual client point is determined by taking the aTarget's client box 647 * and offsetting it by aOffsetX and aOffsetY. 648 * 649 * Note that additional events may be fired as a result of this call. For 650 * instance, typically a click event will be fired as a result of a 651 * mousedown and mouseup in sequence. 652 * 653 * @param {Element} aTarget - DOM element to dispatch the event on. 654 * @param {number} aOffsetX - X offset in CSS pixels from the element’s left edge. 655 * @param {number} aOffsetY - Y offset in CSS pixels from the element’s top edge. 656 * @param {MouseEventData} aEvent - Details of the mouse event to dispatch. 657 * @param {DOMWindow} [aWindow=window] - DOM window used to dispatch the event. 658 * @param {Function} [aCallback] - A callback function that is invoked when the 659 * mouse event is dispatched. 660 * 661 * @returns {boolean} Whether the event had preventDefault() called on it. 662 */ 663 function synthesizeMouse( 664 aTarget, 665 aOffsetX, 666 aOffsetY, 667 aEvent, 668 aWindow, 669 aCallback 670 ) { 671 var rect = aTarget.getBoundingClientRect(); 672 return synthesizeMouseAtPoint( 673 rect.left + aOffsetX, 674 rect.top + aOffsetY, 675 aEvent, 676 aWindow, 677 aCallback 678 ); 679 } 680 681 /** 682 * Synthesize a mouse event in `aWindow` at a point. 683 * 684 * `nsIDOMWindowUtils.sendMouseEvent` takes floats for the coordinates. 685 * Therefore, don't round or truncate the values. 686 * 687 * Note that additional events may be fired as a result of this call. For 688 * instance, typically a click event will be fired as a result of a 689 * mousedown and mouseup in sequence. 690 * 691 * @param {number} aLeft - Floating-point value for the X offset in CSS pixels. 692 * @param {number} aTop - Floating-point value for the Y offset in CSS pixels. 693 * @param {MouseEventData} aEvent - Details of the mouse event to dispatch. 694 * @param {DOMWindow} [aWindow=window] - DOM window used to dispatch the event. 695 * @param {Function} [aCallback] - A callback function that is invoked when the 696 * mouse event is dispatched. 697 * 698 * @returns {boolean} Whether the event had preventDefault() called on it. 699 */ 700 function synthesizeMouseAtPoint( 701 aLeft, 702 aTop, 703 aEvent, 704 aWindow = window, 705 aCallback 706 ) { 707 if (aEvent.allowToHandleDragDrop) { 708 if (aEvent.type == "mouseup" || !aEvent.type) { 709 if (_maybeEndDragSession(aLeft, aTop, aEvent, aWindow)) { 710 return false; 711 } 712 } else if (aEvent.type == "mousemove") { 713 if (_maybeSynthesizeDragOver(aLeft, aTop, aEvent, aWindow)) { 714 return false; 715 } 716 } 717 } 718 719 var utils = _getDOMWindowUtils(aWindow); 720 var defaultPrevented = false; 721 722 if (utils) { 723 var button = computeButton(aEvent); 724 var clickCount = aEvent.clickCount || 1; 725 var modifiers = _parseModifiers(aEvent, aWindow); 726 var pressure = "pressure" in aEvent ? aEvent.pressure : 0; 727 728 // aWindow might be cross-origin from us. 729 var MouseEvent = _EU_maybeWrap(aWindow).MouseEvent; 730 731 // Default source to mouse. 732 var inputSource = 733 "inputSource" in aEvent 734 ? aEvent.inputSource 735 : MouseEvent.MOZ_SOURCE_MOUSE; 736 // Compute a pointerId if needed. 737 var id; 738 if ("id" in aEvent) { 739 id = aEvent.id; 740 } else { 741 var isFromPen = inputSource === MouseEvent.MOZ_SOURCE_PEN; 742 id = isFromPen 743 ? utils.DEFAULT_PEN_POINTER_ID 744 : utils.DEFAULT_MOUSE_POINTER_ID; 745 } 746 747 // FYI: Window.synthesizeMouseEvent takes floats for the coordinates. 748 // Therefore, don't round/truncate the fractional values. 749 const isDOMEventSynthesized = 750 "isSynthesized" in aEvent ? aEvent.isSynthesized : true; 751 const isWidgetEventSynthesized = 752 "isWidgetEventSynthesized" in aEvent 753 ? aEvent.isWidgetEventSynthesized 754 : false; 755 const isAsyncEnabled = 756 "asyncEnabled" in aEvent ? aEvent.asyncEnabled : false; 757 758 if ("type" in aEvent && aEvent.type) { 759 defaultPrevented = _EU_maybeWrap(aWindow).synthesizeMouseEvent( 760 aEvent.type, 761 aLeft, 762 aTop, 763 { 764 identifier: id, 765 button, 766 buttons: aEvent.buttons, 767 clickCount, 768 modifiers, 769 pressure, 770 inputSource, 771 }, 772 { 773 isDOMEventSynthesized, 774 isWidgetEventSynthesized, 775 isAsyncEnabled, 776 }, 777 aCallback 778 ); 779 } else { 780 _EU_maybeWrap(aWindow).synthesizeMouseEvent( 781 "mousedown", 782 aLeft, 783 aTop, 784 { 785 identifier: id, 786 button, 787 buttons: aEvent.buttons, 788 clickCount, 789 modifiers, 790 pressure, 791 inputSource, 792 }, 793 { 794 isDOMEventSynthesized, 795 isWidgetEventSynthesized, 796 isAsyncEnabled, 797 }, 798 aCallback 799 ); 800 _EU_maybeWrap(aWindow).synthesizeMouseEvent( 801 "mouseup", 802 aLeft, 803 aTop, 804 { 805 identifier: id, 806 button, 807 buttons: aEvent.buttons, 808 clickCount, 809 modifiers, 810 pressure, 811 inputSource, 812 }, 813 { 814 isDOMEventSynthesized, 815 isWidgetEventSynthesized, 816 isAsyncEnabled, 817 }, 818 aCallback 819 ); 820 } 821 } 822 823 return defaultPrevented; 824 } 825 826 /** 827 * Synthesize a mouse event at the center of `aTarget`. 828 * 829 * Note that additional events may be fired as a result of this call. For 830 * instance, typically a click event will be fired as a result of a 831 * mousedown and mouseup in sequence. 832 * 833 * @param {Element} aTarget - DOM element to dispatch the event on. 834 * @param {MouseEventData} aEvent - Details of the mouse event to dispatch. 835 * @param {DOMWindow} [aWindow=window] - DOM window used to dispatch the event. 836 * @param {Function} [aCallback] - A callback function that is invoked when the 837 * mouse event is dispatched. 838 * 839 * @returns {boolean} Whether the event had preventDefault() called on it. 840 */ 841 function synthesizeMouseAtCenter(aTarget, aEvent, aWindow, aCallback) { 842 var rect = aTarget.getBoundingClientRect(); 843 844 return synthesizeMouse( 845 aTarget, 846 rect.width / 2, 847 rect.height / 2, 848 aEvent, 849 aWindow, 850 aCallback 851 ); 852 } 853 854 /** 855 * @typedef {object} TouchEventData 856 * @property {boolean} [aEvent.asyncEnabled] - If `true`, the event is 857 * dispatched to the parent process through APZ, without being injected 858 * into the OS event queue. 859 * @property {string} [aEvent.type] - The touch event type. If undefined, 860 * "touchstart" and "touchend" will be synthesized at same point. 861 * @property {number | number[]} [aEvent.id] - The touch id. If you don't specify this, 862 * default touch id will be used for first touch and further touch ids 863 * are the values incremented from the first id. 864 * @property {number | number[]} [aEvent.ry] - The X radius in CSS pixels of the touch 865 * @property {number | number[]} [aEvent.ry] - The Y radius in CSS pixels of the touch 866 * @property {number | number[]} [aEvent.angle] - The angle in degrees 867 * @property {number | number[]} [aEvent.force] - The force of the touch 868 * @property {number | number[]} [aEvent.tiltX] - The X tilt of the touch 869 * @property {number | number[]} [aEvent.tiltY] - The Y tilt of the touch 870 * @property {number | number[]} [aEvent.twist] - The twist of the touch 871 */ 872 873 /** 874 * Synthesize one or more touches on aTarget. aTarget can be either Element 875 * or Array of Elements. aOffsetX, aOffsetY, aEvent.id, aEvent.rx, aEvent.ry, 876 * aEvent.angle, aEvent.force, aEvent.tiltX, aEvent.tiltY and aEvent.twist can 877 * be either number or array of numbers (can be mixed). If you specify array 878 * to synthesize a multi-touch, you need to specify same length arrays. If 879 * you don't specify array to them, same values (or computed default values for 880 * aEvent.id) are used for all touches. 881 * 882 * @param {Element | Element[]} aTarget - The target element which you specify 883 * relative offset from its top-left. 884 * @param {number | number[]} aOffsetX - The relative offset from left of aTarget. 885 * @param {number | number[]} aOffsetY - The relative offset from top of aTarget. 886 * @param {TouchEventData} aEvent - Details of the touch event to dispatch 887 * @param {DOMWindow} [aWindow=window] - DOM window used to dispatch the event. 888 * 889 * @returns true if and only if aEvent.type is specified and default of the 890 * event is prevented. 891 */ 892 function synthesizeTouch( 893 aTarget, 894 aOffsetX, 895 aOffsetY, 896 aEvent = {}, 897 aWindow = window 898 ) { 899 let rectX, rectY; 900 if (Array.isArray(aTarget)) { 901 let lastTarget, lastTargetRect; 902 aTarget.forEach(target => { 903 const rect = 904 target == lastTarget ? lastTargetRect : target.getBoundingClientRect(); 905 rectX.push(rect.left); 906 rectY.push(rect.top); 907 lastTarget = target; 908 lastTargetRect = rect; 909 }); 910 } else { 911 const rect = aTarget.getBoundingClientRect(); 912 rectX = [rect.left]; 913 rectY = [rect.top]; 914 } 915 const offsetX = (() => { 916 if (Array.isArray(aOffsetX)) { 917 let ret = []; 918 aOffsetX.forEach((value, index) => { 919 ret.push(value + rectX[Math.min(index, rectX.length - 1)]); 920 }); 921 return ret; 922 } 923 return aOffsetX + rectX[0]; 924 })(); 925 const offsetY = (() => { 926 if (Array.isArray(aOffsetY)) { 927 let ret = []; 928 aOffsetY.forEach((value, index) => { 929 ret.push(value + rectY[Math.min(index, rectY.length - 1)]); 930 }); 931 return ret; 932 } 933 return aOffsetY + rectY[0]; 934 })(); 935 return synthesizeTouchAtPoint(offsetX, offsetY, aEvent, aWindow); 936 } 937 938 /** 939 * Synthesize one or more touches at the points. aLeft, aTop, aEvent.id, 940 * aEvent.rx, aEvent.ry, aEvent.angle, aEvent.force, aEvent.tiltX, aEvent.tiltY 941 * and aEvent.twist can be either number or array of numbers (can be mixed). 942 * If you specify array to synthesize a multi-touch, you need to specify same 943 * length arrays. If you don't specify array to them, same values are used for 944 * all touches. 945 * 946 * @param {number | number[]} aLeft - The relative offset from left of aTarget. 947 * @param {number | number[]} aTop - The relative offset from top of aTarget. 948 * @param {TouchEventData} aEvent - Details of the touch event to dispatch 949 * @param {DOMWindow} [aWindow=window] - DOM window used to dispatch the event. 950 * 951 * @returns true if and only if aEvent.type is specified and default of the 952 * event is prevented. 953 */ 954 function synthesizeTouchAtPoint(aLeft, aTop, aEvent = {}, aWindow = window) { 955 let utils = _getDOMWindowUtils(aWindow); 956 if (!utils) { 957 return false; 958 } 959 960 if ( 961 Array.isArray(aLeft) && 962 Array.isArray(aTop) && 963 aLeft.length != aTop.length 964 ) { 965 throw new Error(`aLeft and aTop should be same length array`); 966 } 967 968 const arrayLength = Array.isArray(aLeft) 969 ? aLeft.length 970 : Array.isArray(aTop) 971 ? aTop.length 972 : 1; 973 974 function throwExceptionIfDifferentLengthArray(aArray, aName) { 975 if (Array.isArray(aArray) && arrayLength !== aArray.length) { 976 throw new Error(`${aName} is different length array`); 977 } 978 } 979 const leftArray = (() => { 980 if (Array.isArray(aLeft)) { 981 for (let i = 0; i < aLeft.length; i++) { 982 aLeft[i] = _EU_roundDevicePixels(aLeft[i]); 983 } 984 return aLeft; 985 } 986 return new Array(arrayLength).fill(_EU_roundDevicePixels(aLeft)); 987 })(); 988 const topArray = (() => { 989 if (Array.isArray(aTop)) { 990 throwExceptionIfDifferentLengthArray(aTop, "aTop"); 991 for (let i = 0; i < aTop.length; i++) { 992 aTop[i] = _EU_roundDevicePixels(aTop[i]); 993 } 994 return aTop; 995 } 996 return new Array(arrayLength).fill(_EU_roundDevicePixels(aTop)); 997 })(); 998 const idArray = (() => { 999 if ("id" in aEvent && Array.isArray(aEvent.id)) { 1000 throwExceptionIfDifferentLengthArray(aEvent.id, "aEvent.id"); 1001 return aEvent.id; 1002 } 1003 let id = aEvent.id || utils.DEFAULT_TOUCH_POINTER_ID; 1004 let ret = []; 1005 for (let i = 0; i < arrayLength; i++) { 1006 ret.push(id++); 1007 } 1008 return ret; 1009 })(); 1010 function getSameLengthArrayOfEventProperty(aProperty, aDefaultValue) { 1011 if (aProperty in aEvent && Array.isArray(aEvent[aProperty])) { 1012 throwExceptionIfDifferentLengthArray( 1013 aEvent.rx, 1014 arrayLength, 1015 `aEvent.${aProperty}` 1016 ); 1017 return aEvent[aProperty]; 1018 } 1019 return new Array(arrayLength).fill(aEvent[aProperty] || aDefaultValue); 1020 } 1021 const rxArray = getSameLengthArrayOfEventProperty("rx", 1); 1022 const ryArray = getSameLengthArrayOfEventProperty("ry", 1); 1023 const angleArray = getSameLengthArrayOfEventProperty("angle", 0); 1024 const forceArray = getSameLengthArrayOfEventProperty( 1025 "force", 1026 aEvent.type === "touchend" ? 0 : 1 1027 ); 1028 const tiltXArray = getSameLengthArrayOfEventProperty("tiltX", 0); 1029 const tiltYArray = getSameLengthArrayOfEventProperty("tiltY", 0); 1030 const twistArray = getSameLengthArrayOfEventProperty("twist", 0); 1031 1032 const modifiers = _parseModifiers(aEvent, aWindow); 1033 1034 const asyncOption = aEvent.asyncEnabled 1035 ? utils.ASYNC_ENABLED 1036 : utils.ASYNC_DISABLED; 1037 1038 const args = [ 1039 idArray, 1040 leftArray, 1041 topArray, 1042 rxArray, 1043 ryArray, 1044 angleArray, 1045 forceArray, 1046 tiltXArray, 1047 tiltYArray, 1048 twistArray, 1049 modifiers, 1050 asyncOption, 1051 ]; 1052 1053 const sender = 1054 aEvent.mozInputSource === "pen" ? "sendTouchEventAsPen" : "sendTouchEvent"; 1055 1056 if ("type" in aEvent && aEvent.type) { 1057 return utils[sender](aEvent.type, ...args); 1058 } 1059 1060 utils[sender]("touchstart", ...args); 1061 utils[sender]("touchend", ...args); 1062 return false; 1063 } 1064 1065 /** 1066 * Synthesize one or more touches at the center of your target 1067 * 1068 * @param {Element | Element[]} aTarget - The target element 1069 * @param {TouchEventData} aEvent - Details of the touch event to dispatch 1070 * @param {DOMWindow} [aWindow=window] - DOM window used to dispatch the event. 1071 */ 1072 function synthesizeTouchAtCenter(aTarget, aEvent = {}, aWindow = window) { 1073 var rect = aTarget.getBoundingClientRect(); 1074 synthesizeTouchAtPoint( 1075 rect.left + rect.width / 2, 1076 rect.top + rect.height / 2, 1077 aEvent, 1078 aWindow 1079 ); 1080 } 1081 1082 /** 1083 * @typedef {object} WheelEventData 1084 * @property {string} [aEvent.accessKey] - The character or key associated with 1085 * the access key event. Typically a single character used to activate a UI 1086 * element via keyboard shortcuts (e.g., Alt + accessKey). 1087 * @property {boolean} [aEvent.altKey] - If set to `true`, the Alt key will be 1088 * considered pressed. 1089 * @property {boolean} [aEvent.asyncEnabled] - If `true`, the event is 1090 * dispatched to the parent process through APZ, without being injected 1091 * into the OS event queue. 1092 * @property {boolean} [aEvent.ctrlKey] - If set to `true`, the Ctrl key will 1093 * be considered pressed. 1094 * @property {number} [aEvent.deltaMode=WheelEvent.DOM_DELTA_PIXEL] - Delta Mode 1095 * for scrolling (pixel, line, or page), which must be one of the 1096 * `WheelEvent.DOM_DELTA_*` constants. 1097 * @property {number} [aEvent.deltaX=0] - Floating-point value in CSS pixels to 1098 * scroll in the x direction. 1099 * @property {number} [aEvent.deltaY=0] - Floating-point value in CSS pixels to 1100 * scroll in the y direction. 1101 * @property {number} [aEvent.deltaZ=0] - Floating-point value in CSS pixels to 1102 * scroll in the z direction. 1103 * @property {number} [aEvent.expectedOverflowDeltaX] - Decimal value 1104 * indicating horizontal scroll overflow. Only the sign is checked: `0`, 1105 * positive, or negative. 1106 * @property {number} [aEvent.expectedOverflowDeltaY] - Decimal value 1107 * indicating vertical scroll overflow. Only the sign is checked: `0`, 1108 * positive, or negative. 1109 * @property {boolean} [aEvent.isCustomizedByPrefs] - If set to `true` the 1110 * delta values are computed from preferences. 1111 * @property {boolean} [aEvent.isMomentum] - If set to `true` the event will be 1112 * caused by momentum. 1113 * @property {boolean} [aEvent.isNoLineOrPageDelta] - If `true`, the creator 1114 * does not set `lineOrPageDeltaX/Y`. When a widget wheel event is 1115 * generated from this object, those fields will be automatically 1116 * calculated during dispatch by the `EventStateManager`. 1117 * @property {number} [aEvent.lineOrPageDeltaX] - If set to a non-zero value 1118 * for a `DOM_DELTA_PIXEL` event, the EventStateManager will dispatch a 1119 * `NS_MOUSE_SCROLL` event for a horizontal scroll. 1120 * @property {number} [aEvent.lineOrPageDeltaY] - If set to a non-zero value 1121 * for a `DOM_DELTA_PIXEL` event, the EventStateManager will dispatch a 1122 * `NS_MOUSE_SCROLL` event for a vertical scroll. 1123 * @property {boolean} [aEvent.metaKey] - If set to `true`, the Meta key will 1124 * be considered pressed. 1125 * @property {boolean} [aEvent.shiftKey] - If set to `true`, the Shift key will 1126 * be considered pressed. 1127 */ 1128 1129 /** 1130 * Synthesize a wheel event in `aWindow` at a point, without flushing layout. 1131 * 1132 * `nsIDOMWindowUtils.sendWheelEvent` takes floats for the coordinates. 1133 * Therefore, don't round or truncate the values. 1134 * 1135 * @param {number} aLeft - Floating-point value for the X offset in CSS pixels. 1136 * @param {number} aTop - Floating-point value for the Y offset in CSS pixels. 1137 * @param {WheelEventData} aEvent - Details of the wheel event to dispatch. 1138 * @param {DOMWindow} [aWindow=window] - DOM window used to dispatch the event. 1139 * @param {Function} [aCallback=null] - A callback function that is invoked when 1140 * the wheel event is dispatched. 1141 */ 1142 function synthesizeWheelAtPoint( 1143 aLeft, 1144 aTop, 1145 aEvent, 1146 aWindow = window, 1147 aCallback = null 1148 ) { 1149 var utils = _getDOMWindowUtils(aWindow); 1150 if (!utils) { 1151 return; 1152 } 1153 1154 var modifiers = _parseModifiers(aEvent, aWindow); 1155 var options = 0; 1156 1157 if (aEvent.isNoLineOrPageDelta) { 1158 options |= utils.WHEEL_EVENT_CAUSED_BY_NO_LINE_OR_PAGE_DELTA_DEVICE; 1159 } 1160 if (aEvent.isMomentum) { 1161 options |= utils.WHEEL_EVENT_CAUSED_BY_MOMENTUM; 1162 } 1163 if (aEvent.isCustomizedByPrefs) { 1164 options |= utils.WHEEL_EVENT_CUSTOMIZED_BY_USER_PREFS; 1165 } 1166 if (typeof aEvent.expectedOverflowDeltaX !== "undefined") { 1167 if (aEvent.expectedOverflowDeltaX === 0) { 1168 options |= utils.WHEEL_EVENT_EXPECTED_OVERFLOW_DELTA_X_ZERO; 1169 } else if (aEvent.expectedOverflowDeltaX > 0) { 1170 options |= utils.WHEEL_EVENT_EXPECTED_OVERFLOW_DELTA_X_POSITIVE; 1171 } else { 1172 options |= utils.WHEEL_EVENT_EXPECTED_OVERFLOW_DELTA_X_NEGATIVE; 1173 } 1174 } 1175 if (typeof aEvent.expectedOverflowDeltaY !== "undefined") { 1176 if (aEvent.expectedOverflowDeltaY === 0) { 1177 options |= utils.WHEEL_EVENT_EXPECTED_OVERFLOW_DELTA_Y_ZERO; 1178 } else if (aEvent.expectedOverflowDeltaY > 0) { 1179 options |= utils.WHEEL_EVENT_EXPECTED_OVERFLOW_DELTA_Y_POSITIVE; 1180 } else { 1181 options |= utils.WHEEL_EVENT_EXPECTED_OVERFLOW_DELTA_Y_NEGATIVE; 1182 } 1183 } 1184 if (aEvent.asyncEnabled) { 1185 options |= utils.WHEEL_EVENT_ASYNC_ENABLED; 1186 } 1187 1188 // Avoid the JS warnings "reference to undefined property" 1189 if (!aEvent.deltaMode) { 1190 aEvent.deltaMode = WheelEvent.DOM_DELTA_PIXEL; 1191 } 1192 if (!aEvent.deltaX) { 1193 aEvent.deltaX = 0; 1194 } 1195 if (!aEvent.deltaY) { 1196 aEvent.deltaY = 0; 1197 } 1198 if (!aEvent.deltaZ) { 1199 aEvent.deltaZ = 0; 1200 } 1201 1202 var lineOrPageDeltaX = 1203 // eslint-disable-next-line no-nested-ternary 1204 aEvent.lineOrPageDeltaX != null 1205 ? aEvent.lineOrPageDeltaX 1206 : aEvent.deltaX > 0 1207 ? Math.floor(aEvent.deltaX) 1208 : Math.ceil(aEvent.deltaX); 1209 var lineOrPageDeltaY = 1210 // eslint-disable-next-line no-nested-ternary 1211 aEvent.lineOrPageDeltaY != null 1212 ? aEvent.lineOrPageDeltaY 1213 : aEvent.deltaY > 0 1214 ? Math.floor(aEvent.deltaY) 1215 : Math.ceil(aEvent.deltaY); 1216 1217 utils.sendWheelEvent( 1218 aLeft, 1219 aTop, 1220 aEvent.deltaX, 1221 aEvent.deltaY, 1222 aEvent.deltaZ, 1223 aEvent.deltaMode, 1224 modifiers, 1225 lineOrPageDeltaX, 1226 lineOrPageDeltaY, 1227 options, 1228 aCallback 1229 ); 1230 } 1231 1232 /** 1233 * Synthesize a wheel event on a target. 1234 * 1235 * The actual client point is determined by taking the aTarget's client box 1236 * and offsetting it by aOffsetX and aOffsetY. 1237 * 1238 * @param {Element} aTarget - DOM element to dispatch the event on. 1239 * @param {number} aOffsetX - X offset in CSS pixels from the element’s left edge. 1240 * @param {number} aOffsetY - Y offset in CSS pixels from the element’s top edge. 1241 * @param {WheelEventData} aEvent - Details of the wheel event to dispatch. 1242 * @param {DOMWindow} [aWindow=window] - DOM window used to dispatch the event. 1243 * @param {Function} [aCallback=null] - A callback function that is invoked when 1244 * the wheel event is dispatched. 1245 */ 1246 function synthesizeWheel( 1247 aTarget, 1248 aOffsetX, 1249 aOffsetY, 1250 aEvent, 1251 aWindow = window, 1252 aCallback = null 1253 ) { 1254 var rect = aTarget.getBoundingClientRect(); 1255 synthesizeWheelAtPoint( 1256 rect.left + aOffsetX, 1257 rect.top + aOffsetY, 1258 aEvent, 1259 aWindow, 1260 aCallback 1261 ); 1262 } 1263 1264 const _FlushModes = { 1265 FLUSH: 0, 1266 NOFLUSH: 1, 1267 }; 1268 1269 function _sendWheelAndPaint( 1270 aTarget, 1271 aOffsetX, 1272 aOffsetY, 1273 aEvent, 1274 aCallback, 1275 aFlushMode = _FlushModes.FLUSH, 1276 aWindow = window 1277 ) { 1278 var utils = _getDOMWindowUtils(aWindow); 1279 if (!utils) { 1280 return; 1281 } 1282 1283 if (utils.isMozAfterPaintPending) { 1284 // If a paint is pending, then APZ may be waiting for a scroll acknowledgement 1285 // from the content thread. If we send a wheel event now, it could be ignored 1286 // by APZ (or its scroll offset could be overridden). To avoid problems we 1287 // just wait for the paint to complete. 1288 aWindow.waitForAllPaintsFlushed(function () { 1289 _sendWheelAndPaint( 1290 aTarget, 1291 aOffsetX, 1292 aOffsetY, 1293 aEvent, 1294 aCallback, 1295 aFlushMode, 1296 aWindow 1297 ); 1298 }); 1299 return; 1300 } 1301 1302 var onwheel = function () { 1303 SpecialPowers.wrap(window).removeEventListener("wheel", onwheel, { 1304 mozSystemGroup: true, 1305 }); 1306 1307 // Wait one frame since the wheel event has not caused a refresh observer 1308 // to be added yet. 1309 setTimeout(function () { 1310 utils.advanceTimeAndRefresh(1000); 1311 1312 if (!aCallback) { 1313 utils.advanceTimeAndRefresh(0); 1314 return; 1315 } 1316 1317 var waitForPaints = function () { 1318 SpecialPowers.Services.obs.removeObserver( 1319 waitForPaints, 1320 "apz-repaints-flushed" 1321 ); 1322 aWindow.waitForAllPaintsFlushed(function () { 1323 utils.restoreNormalRefresh(); 1324 aCallback(); 1325 }); 1326 }; 1327 1328 SpecialPowers.Services.obs.addObserver( 1329 waitForPaints, 1330 "apz-repaints-flushed" 1331 ); 1332 if (!utils.flushApzRepaints()) { 1333 waitForPaints(); 1334 } 1335 }, 0); 1336 }; 1337 1338 // Listen for the system wheel event, because it happens after all of 1339 // the other wheel events, including legacy events. 1340 SpecialPowers.wrap(aWindow).addEventListener("wheel", onwheel, { 1341 mozSystemGroup: true, 1342 }); 1343 if (aFlushMode === _FlushModes.FLUSH) { 1344 synthesizeWheel(aTarget, aOffsetX, aOffsetY, aEvent, aWindow); 1345 } else { 1346 synthesizeWheelAtPoint(aOffsetX, aOffsetY, aEvent, aWindow); 1347 } 1348 } 1349 1350 /** 1351 * Wrapper around synthesizeWheel that waits for the wheel event to be 1352 * dispatched and for any resulting layout and paint operations to flush. 1353 * 1354 * Requires including `paint_listener.js`. Tests using this function must call 1355 * `DOMWindowUtils.restoreNormalRefresh()` before finishing. 1356 * 1357 * @param {Element} aTarget - DOM element to dispatch the event on. 1358 * @param {number} aOffsetX - X offset in CSS pixels from the element’s left edge. 1359 * @param {number} aOffsetY - Y offset in CSS pixels from the element’s top edge. 1360 * @param {WheelEventData} aEvent - Details of the wheel event to dispatch. 1361 * @param {Function} [aCallback] - Called after paint flush, if provided. If not, 1362 * the caller is expected to handle scroll completion manually. In this case, 1363 * the refresh driver will not be restored automatically. 1364 * @param {DOMWindow} [aWindow=window] - DOM window used to dispatch the event. 1365 */ 1366 function sendWheelAndPaint( 1367 aTarget, 1368 aOffsetX, 1369 aOffsetY, 1370 aEvent, 1371 aCallback, 1372 aWindow = window 1373 ) { 1374 _sendWheelAndPaint( 1375 aTarget, 1376 aOffsetX, 1377 aOffsetY, 1378 aEvent, 1379 aCallback, 1380 _FlushModes.FLUSH, 1381 aWindow 1382 ); 1383 } 1384 1385 /** 1386 * Similar to `sendWheelAndPaint()`, but skips layout flush when resolving 1387 * `aTarget`'s position in `aWindow` before dispatching the wheel event. 1388 * 1389 * @param {Element} aTarget - DOM element to dispatch the event on. 1390 * @param {number} aOffsetX - X offset in CSS pixels from the `aWindow`’s left edge. 1391 * @param {number} aOffsetY - Y offset in CSS pixels from the `aWindow`’s top edge. 1392 * @param {WheelEventData} aEvent - Details of the wheel event to dispatch. 1393 * @param {Function} [aCallback] - Called after paint, if provided. If not, 1394 * the caller is expected to handle scroll completion manually. In this case, 1395 * the refresh driver will not be restored automatically. 1396 * @param {DOMWindow} [aWindow=window] - DOM window used to dispatch the event. 1397 */ 1398 function sendWheelAndPaintNoFlush( 1399 aTarget, 1400 aOffsetX, 1401 aOffsetY, 1402 aEvent, 1403 aCallback, 1404 aWindow = window 1405 ) { 1406 _sendWheelAndPaint( 1407 aTarget, 1408 aOffsetX, 1409 aOffsetY, 1410 aEvent, 1411 aCallback, 1412 _FlushModes.NOFLUSH, 1413 aWindow 1414 ); 1415 } 1416 1417 function synthesizeNativeTapAtCenter( 1418 aTarget, 1419 aLongTap = false, 1420 aCallback = null, 1421 aWindow = window 1422 ) { 1423 let rect = aTarget.getBoundingClientRect(); 1424 return synthesizeNativeTap( 1425 aTarget, 1426 rect.width / 2, 1427 rect.height / 2, 1428 aLongTap, 1429 aCallback, 1430 aWindow 1431 ); 1432 } 1433 1434 function synthesizeNativeTap( 1435 aTarget, 1436 aOffsetX, 1437 aOffsetY, 1438 aLongTap = false, 1439 aCallback = null, 1440 aWindow = window 1441 ) { 1442 let utils = _getDOMWindowUtils(aWindow); 1443 if (!utils) { 1444 return; 1445 } 1446 1447 let scale = aWindow.devicePixelRatio; 1448 let rect = aTarget.getBoundingClientRect(); 1449 let x = _EU_roundDevicePixels( 1450 (aWindow.mozInnerScreenX + rect.left + aOffsetX) * scale 1451 ); 1452 let y = _EU_roundDevicePixels( 1453 (aWindow.mozInnerScreenY + rect.top + aOffsetY) * scale 1454 ); 1455 1456 utils.sendNativeTouchTap(x, y, aLongTap, aCallback); 1457 } 1458 1459 /** 1460 * Similar to synthesizeMouse but generates a native widget level event 1461 * (so will actually move the "real" mouse cursor etc. Be careful because 1462 * this can impact later code as well! (e.g. with hover states etc.) 1463 * 1464 * @description There are 3 mutually exclusive ways of indicating the location of the 1465 * mouse event: set ``atCenter``, or pass ``offsetX`` and ``offsetY``, 1466 * or pass ``screenX`` and ``screenY``. Do not attempt to mix these. 1467 * 1468 * @param {object} aParams 1469 * @param {string} aParams.type "click", "mousedown", "mouseup" or "mousemove" 1470 * @param {Element} aParams.target Origin of offsetX and offsetY, must be an element 1471 * @param {boolean} [aParams.atCenter] 1472 * Instead of offsetX/Y, synthesize the event at center of `target`. 1473 * @param {number} [aParams.offsetX] 1474 * X offset in `target` (in CSS pixels if `scale` is "screenPixelsPerCSSPixel") 1475 * @param {number} [aParams.offsetY] 1476 * Y offset in `target` (in CSS pixels if `scale` is "screenPixelsPerCSSPixel") 1477 * @param {number} [aParams.screenX] 1478 * X offset in screen (in CSS pixels if `scale` is "screenPixelsPerCSSPixel"), 1479 * Neither offsetX/Y nor atCenter must be set if this is set. 1480 * @param {number} [aParams.screenY] 1481 * Y offset in screen (in CSS pixels if `scale` is "screenPixelsPerCSSPixel"), 1482 * Neither offsetX/Y nor atCenter must be set if this is set. 1483 * @param {string} [aParams.scale="screenPixelsPerCSSPixel"] 1484 * If scale is "screenPixelsPerCSSPixel", devicePixelRatio will be used. 1485 * If scale is "inScreenPixels", clientX/Y nor scaleX/Y are not adjusted with screenPixelsPerCSSPixel. 1486 * @param {number} [aParams.button=0] 1487 * Defaults to 0, if "click", "mousedown", "mouseup", set same value as DOM MouseEvent.button 1488 * @param {object} [aParams.modifiers={}] 1489 * Active modifiers, see `_parseNativeModifiers` 1490 * @param {DOMWindow} [aParams.win=window] 1491 * The window to use its utils. Defaults to the window in which EventUtils.js is running. 1492 * @param {Element} [aParams.elementOnWidget=target] 1493 * Defaults to target. If element under the point is in another widget from target's widget, 1494 * e.g., when it's in a XUL <panel>, specify this. 1495 */ 1496 function synthesizeNativeMouseEvent(aParams, aCallback = null) { 1497 const { 1498 type, 1499 target, 1500 offsetX, 1501 offsetY, 1502 atCenter, 1503 screenX, 1504 screenY, 1505 scale = "screenPixelsPerCSSPixel", 1506 button = 0, 1507 modifiers = {}, 1508 win = window, 1509 elementOnWidget = target, 1510 } = aParams; 1511 if (atCenter) { 1512 if (offsetX != undefined || offsetY != undefined) { 1513 throw Error( 1514 `atCenter is specified, but offsetX (${offsetX}) and/or offsetY (${offsetY}) are also specified` 1515 ); 1516 } 1517 if (screenX != undefined || screenY != undefined) { 1518 throw Error( 1519 `atCenter is specified, but screenX (${screenX}) and/or screenY (${screenY}) are also specified` 1520 ); 1521 } 1522 if (!target) { 1523 throw Error("atCenter is specified, but target is not specified"); 1524 } 1525 } else if (offsetX != undefined && offsetY != undefined) { 1526 if (screenX != undefined || screenY != undefined) { 1527 throw Error( 1528 `offsetX/Y are specified, but screenX (${screenX}) and/or screenY (${screenY}) are also specified` 1529 ); 1530 } 1531 if (!target) { 1532 throw Error( 1533 "offsetX and offsetY are specified, but target is not specified" 1534 ); 1535 } 1536 } else if (screenX != undefined && screenY != undefined) { 1537 if (offsetX != undefined || offsetY != undefined) { 1538 throw Error( 1539 `screenX/Y are specified, but offsetX (${offsetX}) and/or offsetY (${offsetY}) are also specified` 1540 ); 1541 } 1542 } 1543 const utils = _getDOMWindowUtils(win); 1544 if (!utils) { 1545 return; 1546 } 1547 1548 const rect = target?.getBoundingClientRect(); 1549 const resolution = _getTopWindowResolution(win); 1550 const scaleValue = (() => { 1551 if (scale === "inScreenPixels") { 1552 return 1.0; 1553 } 1554 if (scale === "screenPixelsPerCSSPixel") { 1555 return win.devicePixelRatio; 1556 } 1557 throw Error(`invalid scale value (${scale}) is specified`); 1558 })(); 1559 // XXX mozInnerScreen might be invalid value on mobile viewport (Bug 1701546), 1560 // so use window.top's mozInnerScreen. But this won't work fission+xorigin 1561 // with mobile viewport until mozInnerScreen returns valid value with 1562 // scale. 1563 const x = _EU_roundDevicePixels( 1564 (() => { 1565 if (screenX != undefined) { 1566 return screenX * scaleValue; 1567 } 1568 const winInnerOffsetX = _getScreenXInUnscaledCSSPixels(win); 1569 return ( 1570 (((atCenter ? rect.width / 2 : offsetX) + rect.left) * resolution + 1571 winInnerOffsetX) * 1572 scaleValue 1573 ); 1574 })() 1575 ); 1576 const y = _EU_roundDevicePixels( 1577 (() => { 1578 if (screenY != undefined) { 1579 return screenY * scaleValue; 1580 } 1581 const winInnerOffsetY = _getScreenYInUnscaledCSSPixels(win); 1582 return ( 1583 (((atCenter ? rect.height / 2 : offsetY) + rect.top) * resolution + 1584 winInnerOffsetY) * 1585 scaleValue 1586 ); 1587 })() 1588 ); 1589 const modifierFlags = _parseNativeModifiers(modifiers); 1590 1591 if (type === "click") { 1592 utils.sendNativeMouseEvent( 1593 x, 1594 y, 1595 utils.NATIVE_MOUSE_MESSAGE_BUTTON_DOWN, 1596 button, 1597 modifierFlags, 1598 elementOnWidget, 1599 function () { 1600 utils.sendNativeMouseEvent( 1601 x, 1602 y, 1603 utils.NATIVE_MOUSE_MESSAGE_BUTTON_UP, 1604 button, 1605 modifierFlags, 1606 elementOnWidget, 1607 aCallback 1608 ); 1609 } 1610 ); 1611 return; 1612 } 1613 utils.sendNativeMouseEvent( 1614 x, 1615 y, 1616 (() => { 1617 switch (type) { 1618 case "mousedown": 1619 return utils.NATIVE_MOUSE_MESSAGE_BUTTON_DOWN; 1620 case "mouseup": 1621 return utils.NATIVE_MOUSE_MESSAGE_BUTTON_UP; 1622 case "mousemove": 1623 return utils.NATIVE_MOUSE_MESSAGE_MOVE; 1624 default: 1625 throw Error(`Invalid type is specified: ${type}`); 1626 } 1627 })(), 1628 button, 1629 modifierFlags, 1630 elementOnWidget, 1631 aCallback 1632 ); 1633 } 1634 1635 function promiseNativeMouseEvent(aParams) { 1636 return new Promise(resolve => synthesizeNativeMouseEvent(aParams, resolve)); 1637 } 1638 1639 function synthesizeNativeMouseEventAndWaitForEvent(aParams, aCallback) { 1640 const listener = aParams.eventTargetToListen || aParams.target; 1641 const eventType = aParams.eventTypeToWait || aParams.type; 1642 listener.addEventListener(eventType, aCallback, { 1643 capture: true, 1644 once: true, 1645 }); 1646 synthesizeNativeMouseEvent(aParams); 1647 } 1648 1649 function promiseNativeMouseEventAndWaitForEvent(aParams) { 1650 return new Promise(resolve => 1651 synthesizeNativeMouseEventAndWaitForEvent(aParams, resolve) 1652 ); 1653 } 1654 1655 /** 1656 * This is a wrapper around synthesizeNativeMouseEvent that waits for the mouse 1657 * event to be dispatched to the target content. 1658 * 1659 * This API is supposed to be used in those test cases that synthesize some 1660 * input events to chrome process and have some checks in content. 1661 */ 1662 function synthesizeAndWaitNativeMouseMove( 1663 aTarget, 1664 aOffsetX, 1665 aOffsetY, 1666 aCallback, 1667 aWindow = window 1668 ) { 1669 let browser = gBrowser.selectedTab.linkedBrowser; 1670 let mm = browser.messageManager; 1671 let { ContentTask } = _EU_ChromeUtils.importESModule( 1672 "resource://testing-common/ContentTask.sys.mjs" 1673 ); 1674 1675 let eventRegisteredPromise = new Promise(resolve => { 1676 mm.addMessageListener("Test:MouseMoveRegistered", function processed() { 1677 mm.removeMessageListener("Test:MouseMoveRegistered", processed); 1678 resolve(); 1679 }); 1680 }); 1681 let eventReceivedPromise = ContentTask.spawn( 1682 browser, 1683 [aOffsetX, aOffsetY], 1684 ([clientX, clientY]) => { 1685 return new Promise(resolve => { 1686 addEventListener("mousemove", function onMouseMoveEvent(e) { 1687 if (e.clientX == clientX && e.clientY == clientY) { 1688 removeEventListener("mousemove", onMouseMoveEvent); 1689 resolve(); 1690 } 1691 }); 1692 sendAsyncMessage("Test:MouseMoveRegistered"); 1693 }); 1694 } 1695 ); 1696 eventRegisteredPromise.then(() => { 1697 synthesizeNativeMouseEvent({ 1698 type: "mousemove", 1699 target: aTarget, 1700 offsetX: aOffsetX, 1701 offsetY: aOffsetY, 1702 win: aWindow, 1703 }); 1704 }); 1705 return eventReceivedPromise; 1706 } 1707 1708 /** 1709 * Synthesize a key event. It is targeted at whatever would be targeted by an 1710 * actual keypress by the user, typically the focused element. 1711 * 1712 * @param {string} aKey 1713 * Should be either: 1714 * 1715 * - key value (recommended). If you specify a non-printable key name, 1716 * prepend the ``KEY_`` prefix. Otherwise, specifying a printable key, the 1717 * key value should be specified. 1718 * 1719 * - keyCode name starting with ``VK_`` (e.g., ``VK_RETURN``). This is available 1720 * only for compatibility with legacy API. Don't use this with new tests. 1721 * 1722 * @param {object} [aEvent] 1723 * Optional event object with more specifics about the key event to 1724 * synthesize. 1725 * @param {string} [aEvent.code] 1726 * If you don't specify this explicitly, it'll be guessed from aKey 1727 * of US keyboard layout. Note that this value may be different 1728 * between browsers. For example, "Insert" is never set only on 1729 * macOS since actual key operation won't cause this code value. 1730 * In such case, the value becomes empty string. 1731 * If you need to emulate non-US keyboard layout or virtual keyboard 1732 * which doesn't emulate hardware key input, you should set this value 1733 * to empty string explicitly. 1734 * @param {number} [aEvent.repeat] 1735 * If you emulate auto-repeat, you should set the count of repeat. 1736 * This method will automatically synthesize keydown (and keypress). 1737 * @param {*} aEvent.location 1738 * If you want to specify this, you can specify this explicitly. 1739 * However, if you don't specify this value, it will be computed 1740 * from code value. 1741 * @param {string} aEvent.type 1742 * Basically, you shouldn't specify this. Then, this function will 1743 * synthesize keydown (, keypress) and keyup. 1744 * If keydown is specified, this only fires keydown (and keypress if 1745 * it should be fired). 1746 * If keyup is specified, this only fires keyup. 1747 * @param {number} aEvent.keyCode 1748 * Must be 0 - 255 (0xFF). If this is specified explicitly, 1749 * .keyCode value is initialized with this value. 1750 * @param {DOMWindow} [aWindow=window] 1751 * DOM window used to dispatch the event. 1752 * @param {Function} aCallback 1753 * Is optional and can be used to receive notifications from TIP. 1754 * 1755 * @description 1756 * ``accelKey``, ``altKey``, ``altGraphKey``, ``ctrlKey``, ``capsLockKey``, 1757 * ``fnKey``, ``fnLockKey``, ``numLockKey``, ``metaKey``, ``scrollLockKey``, 1758 * ``shiftKey``, ``symbolKey``, ``symbolLockKey`` 1759 * Basically, you shouldn't use these attributes. nsITextInputProcessor 1760 * manages modifier key state when you synthesize modifier key events. 1761 * However, if some of these attributes are true, this function activates 1762 * the modifiers only during dispatching the key events. 1763 * Note that if some of these values are false, they are ignored (i.e., 1764 * not inactivated with this function). 1765 */ 1766 function synthesizeKey(aKey, aEvent = undefined, aWindow = window, aCallback) { 1767 const event = aEvent === undefined || aEvent === null ? {} : aEvent; 1768 let dispatchKeydown = 1769 !("type" in event) || event.type === "keydown" || !event.type; 1770 const dispatchKeyup = 1771 !("type" in event) || event.type === "keyup" || !event.type; 1772 1773 if (dispatchKeydown && aKey == "KEY_Escape") { 1774 let eventForKeydown = Object.assign({}, JSON.parse(JSON.stringify(event))); 1775 eventForKeydown.type = "keydown"; 1776 if ( 1777 _maybeEndDragSession( 1778 // TODO: We should set the last dragover point instead 1779 0, 1780 0, 1781 eventForKeydown, 1782 aWindow 1783 ) 1784 ) { 1785 if (!dispatchKeyup) { 1786 return; 1787 } 1788 // We don't need to dispatch only keydown event because it's consumed by 1789 // the drag session. 1790 dispatchKeydown = false; 1791 } 1792 } 1793 1794 var TIP = _getTIP(aWindow, aCallback); 1795 if (!TIP) { 1796 return; 1797 } 1798 var KeyboardEvent = _getKeyboardEvent(aWindow); 1799 var modifiers = _emulateToActivateModifiers(TIP, event, aWindow); 1800 var keyEventDict = _createKeyboardEventDictionary(aKey, event, TIP, aWindow); 1801 var keyEvent = new KeyboardEvent("", keyEventDict.dictionary); 1802 1803 try { 1804 if (dispatchKeydown) { 1805 TIP.keydown(keyEvent, keyEventDict.flags); 1806 if ("repeat" in event && event.repeat > 1) { 1807 keyEventDict.dictionary.repeat = true; 1808 var repeatedKeyEvent = new KeyboardEvent("", keyEventDict.dictionary); 1809 for (var i = 1; i < event.repeat; i++) { 1810 TIP.keydown(repeatedKeyEvent, keyEventDict.flags); 1811 } 1812 } 1813 } 1814 if (dispatchKeyup) { 1815 TIP.keyup(keyEvent, keyEventDict.flags); 1816 } 1817 } finally { 1818 _emulateToInactivateModifiers(TIP, modifiers, aWindow); 1819 } 1820 } 1821 1822 /** 1823 * This is a wrapper around synthesizeKey that waits for the key event to be 1824 * dispatched to the target content. It returns a promise which is resolved 1825 * when the content receives the key event. 1826 * 1827 * This API is supposed to be used in those test cases that synthesize some 1828 * input events to chrome process and have some checks in content. 1829 */ 1830 function synthesizeAndWaitKey( 1831 aKey, 1832 aEvent, 1833 aWindow = window, 1834 checkBeforeSynthesize, 1835 checkAfterSynthesize 1836 ) { 1837 let browser = gBrowser.selectedTab.linkedBrowser; 1838 let mm = browser.messageManager; 1839 let keyCode = _createKeyboardEventDictionary(aKey, aEvent, null, aWindow) 1840 .dictionary.keyCode; 1841 let { ContentTask } = _EU_ChromeUtils.importESModule( 1842 "resource://testing-common/ContentTask.sys.mjs" 1843 ); 1844 1845 let keyRegisteredPromise = new Promise(resolve => { 1846 mm.addMessageListener("Test:KeyRegistered", function processed() { 1847 mm.removeMessageListener("Test:KeyRegistered", processed); 1848 resolve(); 1849 }); 1850 }); 1851 // eslint-disable-next-line no-shadow 1852 let keyReceivedPromise = ContentTask.spawn(browser, keyCode, keyCode => { 1853 return new Promise(resolve => { 1854 addEventListener("keyup", function onKeyEvent(e) { 1855 if (e.keyCode == keyCode) { 1856 removeEventListener("keyup", onKeyEvent); 1857 resolve(); 1858 } 1859 }); 1860 sendAsyncMessage("Test:KeyRegistered"); 1861 }); 1862 }); 1863 keyRegisteredPromise.then(() => { 1864 if (checkBeforeSynthesize) { 1865 checkBeforeSynthesize(); 1866 } 1867 synthesizeKey(aKey, aEvent, aWindow); 1868 if (checkAfterSynthesize) { 1869 checkAfterSynthesize(); 1870 } 1871 }); 1872 return keyReceivedPromise; 1873 } 1874 1875 function _parseNativeModifiers(aModifiers, aWindow = window) { 1876 let modifiers = 0; 1877 if (aModifiers.capsLockKey) { 1878 modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CAPS_LOCK; 1879 } 1880 if (aModifiers.numLockKey) { 1881 modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_NUM_LOCK; 1882 } 1883 if (aModifiers.shiftKey) { 1884 modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_SHIFT_LEFT; 1885 } 1886 if (aModifiers.shiftRightKey) { 1887 modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_SHIFT_RIGHT; 1888 } 1889 if (aModifiers.ctrlKey) { 1890 modifiers |= 1891 SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CONTROL_LEFT; 1892 } 1893 if (aModifiers.ctrlRightKey) { 1894 modifiers |= 1895 SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CONTROL_RIGHT; 1896 } 1897 if (aModifiers.altKey) { 1898 modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_ALT_LEFT; 1899 } 1900 if (aModifiers.altRightKey) { 1901 modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_ALT_RIGHT; 1902 } 1903 if (aModifiers.metaKey) { 1904 modifiers |= 1905 SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_COMMAND_LEFT; 1906 } 1907 if (aModifiers.metaRightKey) { 1908 modifiers |= 1909 SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_COMMAND_RIGHT; 1910 } 1911 if (aModifiers.helpKey) { 1912 modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_HELP; 1913 } 1914 if (aModifiers.fnKey) { 1915 modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_FUNCTION; 1916 } 1917 if (aModifiers.numericKeyPadKey) { 1918 modifiers |= 1919 SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_NUMERIC_KEY_PAD; 1920 } 1921 1922 if (aModifiers.accelKey) { 1923 modifiers |= _EU_isMac(aWindow) 1924 ? SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_COMMAND_LEFT 1925 : SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CONTROL_LEFT; 1926 } 1927 if (aModifiers.accelRightKey) { 1928 modifiers |= _EU_isMac(aWindow) 1929 ? SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_COMMAND_RIGHT 1930 : SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CONTROL_RIGHT; 1931 } 1932 if (aModifiers.altGrKey) { 1933 modifiers |= _EU_isMac(aWindow) 1934 ? SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_ALT_LEFT 1935 : SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_ALT_GRAPH; 1936 } 1937 return modifiers; 1938 } 1939 1940 // Mac: Any unused number is okay for adding new keyboard layout. 1941 // When you add new keyboard layout here, you need to modify 1942 // TISInputSourceWrapper::InitByLayoutID(). 1943 // Win: These constants can be found by inspecting registry keys under 1944 // HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Keyboard Layouts 1945 1946 const KEYBOARD_LAYOUT_ARABIC = { 1947 name: "Arabic", 1948 Mac: 6, 1949 Win: 0x00000401, 1950 hasAltGrOnWin: false, 1951 }; 1952 _defineConstant("KEYBOARD_LAYOUT_ARABIC", KEYBOARD_LAYOUT_ARABIC); 1953 const KEYBOARD_LAYOUT_ARABIC_PC = { 1954 name: "Arabic - PC", 1955 Mac: 7, 1956 Win: null, 1957 hasAltGrOnWin: false, 1958 }; 1959 _defineConstant("KEYBOARD_LAYOUT_ARABIC_PC", KEYBOARD_LAYOUT_ARABIC_PC); 1960 const KEYBOARD_LAYOUT_BRAZILIAN_ABNT = { 1961 name: "Brazilian ABNT", 1962 Mac: null, 1963 Win: 0x00000416, 1964 hasAltGrOnWin: true, 1965 }; 1966 _defineConstant( 1967 "KEYBOARD_LAYOUT_BRAZILIAN_ABNT", 1968 KEYBOARD_LAYOUT_BRAZILIAN_ABNT 1969 ); 1970 const KEYBOARD_LAYOUT_DVORAK_QWERTY = { 1971 name: "Dvorak-QWERTY", 1972 Mac: 4, 1973 Win: null, 1974 hasAltGrOnWin: false, 1975 }; 1976 _defineConstant("KEYBOARD_LAYOUT_DVORAK_QWERTY", KEYBOARD_LAYOUT_DVORAK_QWERTY); 1977 const KEYBOARD_LAYOUT_EN_US = { 1978 name: "US", 1979 Mac: 0, 1980 Win: 0x00000409, 1981 hasAltGrOnWin: false, 1982 }; 1983 _defineConstant("KEYBOARD_LAYOUT_EN_US", KEYBOARD_LAYOUT_EN_US); 1984 const KEYBOARD_LAYOUT_FRENCH = { 1985 name: "French", 1986 Mac: 8, // Some keys mapped different from PC, e.g., Digit6, Digit8, Equal, Slash and Backslash 1987 Win: 0x0000040c, 1988 hasAltGrOnWin: true, 1989 }; 1990 _defineConstant("KEYBOARD_LAYOUT_FRENCH", KEYBOARD_LAYOUT_FRENCH); 1991 const KEYBOARD_LAYOUT_FRENCH_PC = { 1992 name: "French-PC", 1993 Mac: 13, // Compatible with Windows 1994 Win: 0x0000040c, 1995 hasAltGrOnWin: true, 1996 }; 1997 _defineConstant("KEYBOARD_LAYOUT_FRENCH_PC", KEYBOARD_LAYOUT_FRENCH_PC); 1998 const KEYBOARD_LAYOUT_GREEK = { 1999 name: "Greek", 2000 Mac: 1, 2001 Win: 0x00000408, 2002 hasAltGrOnWin: true, 2003 }; 2004 _defineConstant("KEYBOARD_LAYOUT_GREEK", KEYBOARD_LAYOUT_GREEK); 2005 const KEYBOARD_LAYOUT_GERMAN = { 2006 name: "German", 2007 Mac: 2, 2008 Win: 0x00000407, 2009 hasAltGrOnWin: true, 2010 }; 2011 _defineConstant("KEYBOARD_LAYOUT_GERMAN", KEYBOARD_LAYOUT_GERMAN); 2012 const KEYBOARD_LAYOUT_HEBREW = { 2013 name: "Hebrew", 2014 Mac: 9, 2015 Win: 0x0000040d, 2016 hasAltGrOnWin: true, 2017 }; 2018 _defineConstant("KEYBOARD_LAYOUT_HEBREW", KEYBOARD_LAYOUT_HEBREW); 2019 const KEYBOARD_LAYOUT_JAPANESE = { 2020 name: "Japanese", 2021 Mac: null, 2022 Win: 0x00000411, 2023 hasAltGrOnWin: false, 2024 }; 2025 _defineConstant("KEYBOARD_LAYOUT_JAPANESE", KEYBOARD_LAYOUT_JAPANESE); 2026 const KEYBOARD_LAYOUT_KHMER = { 2027 name: "Khmer", 2028 Mac: null, 2029 Win: 0x00000453, 2030 hasAltGrOnWin: true, 2031 }; // available on Win7 or later. 2032 _defineConstant("KEYBOARD_LAYOUT_KHMER", KEYBOARD_LAYOUT_KHMER); 2033 const KEYBOARD_LAYOUT_LITHUANIAN = { 2034 name: "Lithuanian", 2035 Mac: 10, 2036 Win: 0x00010427, 2037 hasAltGrOnWin: true, 2038 }; 2039 _defineConstant("KEYBOARD_LAYOUT_LITHUANIAN", KEYBOARD_LAYOUT_LITHUANIAN); 2040 const KEYBOARD_LAYOUT_NORWEGIAN = { 2041 name: "Norwegian", 2042 Mac: 11, 2043 Win: 0x00000414, 2044 hasAltGrOnWin: true, 2045 }; 2046 _defineConstant("KEYBOARD_LAYOUT_NORWEGIAN", KEYBOARD_LAYOUT_NORWEGIAN); 2047 const KEYBOARD_LAYOUT_RUSSIAN = { 2048 name: "Russian", 2049 Mac: null, 2050 Win: 0x00000419, 2051 hasAltGrOnWin: true, // No AltGr, but Ctrl + Alt + Digit8 introduces a char 2052 }; 2053 _defineConstant("KEYBOARD_LAYOUT_RUSSIAN", KEYBOARD_LAYOUT_RUSSIAN); 2054 const KEYBOARD_LAYOUT_RUSSIAN_MNEMONIC = { 2055 name: "Russian - Mnemonic", 2056 Mac: null, 2057 Win: 0x00020419, 2058 hasAltGrOnWin: true, 2059 }; // available on Win8 or later. 2060 _defineConstant( 2061 "KEYBOARD_LAYOUT_RUSSIAN_MNEMONIC", 2062 KEYBOARD_LAYOUT_RUSSIAN_MNEMONIC 2063 ); 2064 const KEYBOARD_LAYOUT_SPANISH = { 2065 name: "Spanish", 2066 Mac: 12, 2067 Win: 0x0000040a, 2068 hasAltGrOnWin: true, 2069 }; 2070 _defineConstant("KEYBOARD_LAYOUT_SPANISH", KEYBOARD_LAYOUT_SPANISH); 2071 const KEYBOARD_LAYOUT_SWEDISH = { 2072 name: "Swedish", 2073 Mac: 3, 2074 Win: 0x0000041d, 2075 hasAltGrOnWin: true, 2076 }; 2077 _defineConstant("KEYBOARD_LAYOUT_SWEDISH", KEYBOARD_LAYOUT_SWEDISH); 2078 const KEYBOARD_LAYOUT_THAI = { 2079 name: "Thai", 2080 Mac: 5, 2081 Win: 0x0002041e, 2082 hasAltGrOnWin: false, 2083 }; 2084 _defineConstant("KEYBOARD_LAYOUT_THAI", KEYBOARD_LAYOUT_THAI); 2085 2086 /** 2087 * synthesizeNativeKey() dispatches native key event on active window. 2088 * This is implemented only on Windows and Mac. Note that this function 2089 * dispatches the key event asynchronously and returns immediately. If a 2090 * callback function is provided, the callback will be called upon 2091 * completion of the key dispatch. 2092 * 2093 * @param aKeyboardLayout One of KEYBOARD_LAYOUT_* defined above. 2094 * @param aNativeKeyCode A native keycode value defined in 2095 * NativeKeyCodes.js. 2096 * @param aModifiers Modifier keys. If no modifire key is pressed, 2097 * this must be {}. Otherwise, one or more items 2098 * referred in _parseNativeModifiers() must be 2099 * true. 2100 * @param aChars Specify characters which should be generated 2101 * by the key event. 2102 * @param aUnmodifiedChars Specify characters of unmodified (except Shift) 2103 * aChar value. 2104 * @param aCallback If provided, this callback will be invoked 2105 * once the native keys have been processed 2106 * by Gecko. Will never be called if this 2107 * function returns false. 2108 * @return True if this function succeed dispatching 2109 * native key event. Otherwise, false. 2110 */ 2111 2112 function synthesizeNativeKey( 2113 aKeyboardLayout, 2114 aNativeKeyCode, 2115 aModifiers, 2116 aChars, 2117 aUnmodifiedChars, 2118 aCallback, 2119 aWindow = window 2120 ) { 2121 var utils = _getDOMWindowUtils(aWindow); 2122 if (!utils) { 2123 return false; 2124 } 2125 var nativeKeyboardLayout = null; 2126 if (_EU_isMac(aWindow)) { 2127 nativeKeyboardLayout = aKeyboardLayout.Mac; 2128 } else if (_EU_isWin(aWindow)) { 2129 nativeKeyboardLayout = aKeyboardLayout.Win; 2130 } 2131 if (nativeKeyboardLayout === null) { 2132 return false; 2133 } 2134 2135 utils.sendNativeKeyEvent( 2136 nativeKeyboardLayout, 2137 aNativeKeyCode, 2138 _parseNativeModifiers(aModifiers, aWindow), 2139 aChars, 2140 aUnmodifiedChars, 2141 aCallback 2142 ); 2143 return true; 2144 } 2145 2146 var _gSeenEvent = false; 2147 2148 /** 2149 * Indicate that an event with an original target of aExpectedTarget and 2150 * a type of aExpectedEvent is expected to be fired, or not expected to 2151 * be fired. 2152 */ 2153 function _expectEvent(aExpectedTarget, aExpectedEvent, aTestName) { 2154 if (!aExpectedTarget || !aExpectedEvent) { 2155 return null; 2156 } 2157 2158 _gSeenEvent = false; 2159 2160 var type = 2161 aExpectedEvent.charAt(0) == "!" 2162 ? aExpectedEvent.substring(1) 2163 : aExpectedEvent; 2164 var eventHandler = function (event) { 2165 var epassed = 2166 !_gSeenEvent && 2167 event.originalTarget == aExpectedTarget && 2168 event.type == type; 2169 is( 2170 epassed, 2171 true, 2172 aTestName + " " + type + " event target " + (_gSeenEvent ? "twice" : "") 2173 ); 2174 _gSeenEvent = true; 2175 }; 2176 2177 aExpectedTarget.addEventListener(type, eventHandler); 2178 return eventHandler; 2179 } 2180 2181 /** 2182 * Check if the event was fired or not. The event handler aEventHandler 2183 * will be removed. 2184 */ 2185 function _checkExpectedEvent( 2186 aExpectedTarget, 2187 aExpectedEvent, 2188 aEventHandler, 2189 aTestName 2190 ) { 2191 if (aEventHandler) { 2192 var expectEvent = aExpectedEvent.charAt(0) != "!"; 2193 var type = expectEvent ? aExpectedEvent : aExpectedEvent.substring(1); 2194 aExpectedTarget.removeEventListener(type, aEventHandler); 2195 var desc = type + " event"; 2196 if (!expectEvent) { 2197 desc += " not"; 2198 } 2199 is(_gSeenEvent, expectEvent, aTestName + " " + desc + " fired"); 2200 } 2201 2202 _gSeenEvent = false; 2203 } 2204 2205 /** 2206 * Similar to synthesizeMouse except that a test is performed to see if an 2207 * event is fired at the right target as a result. 2208 * 2209 * aExpectedTarget - the expected originalTarget of the event. 2210 * aExpectedEvent - the expected type of the event, such as 'select'. 2211 * aTestName - the test name when outputing results 2212 * 2213 * To test that an event is not fired, use an expected type preceded by an 2214 * exclamation mark, such as '!select'. This might be used to test that a 2215 * click on a disabled element doesn't fire certain events for instance. 2216 * 2217 * aWindow is optional, and defaults to the current window object. 2218 */ 2219 function synthesizeMouseExpectEvent( 2220 aTarget, 2221 aOffsetX, 2222 aOffsetY, 2223 aEvent, 2224 aExpectedTarget, 2225 aExpectedEvent, 2226 aTestName, 2227 aWindow 2228 ) { 2229 var eventHandler = _expectEvent(aExpectedTarget, aExpectedEvent, aTestName); 2230 synthesizeMouse(aTarget, aOffsetX, aOffsetY, aEvent, aWindow); 2231 _checkExpectedEvent(aExpectedTarget, aExpectedEvent, eventHandler, aTestName); 2232 } 2233 2234 /** 2235 * Similar to synthesizeKey except that a test is performed to see if an 2236 * event is fired at the right target as a result. 2237 * 2238 * aExpectedTarget - the expected originalTarget of the event. 2239 * aExpectedEvent - the expected type of the event, such as 'select'. 2240 * aTestName - the test name when outputing results 2241 * 2242 * To test that an event is not fired, use an expected type preceded by an 2243 * exclamation mark, such as '!select'. 2244 * 2245 * aWindow is optional, and defaults to the current window object. 2246 */ 2247 function synthesizeKeyExpectEvent( 2248 key, 2249 aEvent, 2250 aExpectedTarget, 2251 aExpectedEvent, 2252 aTestName, 2253 aWindow 2254 ) { 2255 var eventHandler = _expectEvent(aExpectedTarget, aExpectedEvent, aTestName); 2256 synthesizeKey(key, aEvent, aWindow); 2257 _checkExpectedEvent(aExpectedTarget, aExpectedEvent, eventHandler, aTestName); 2258 } 2259 2260 function disableNonTestMouseEvents(aDisable) { 2261 var domutils = _getDOMWindowUtils(); 2262 domutils.disableNonTestMouseEvents(aDisable); 2263 } 2264 2265 function _getDOMWindowUtils(aWindow = window) { 2266 // Leave this here as something, somewhere, passes a falsy argument 2267 // to this, causing the |window| default argument not to get picked up. 2268 if (!aWindow) { 2269 aWindow = window; 2270 } 2271 2272 // If documentURIObject exists or `window` is a stub object, we're in 2273 // a chrome scope, so don't bother trying to go through SpecialPowers. 2274 if (!aWindow.document || aWindow.document.documentURIObject) { 2275 return aWindow.windowUtils; 2276 } 2277 2278 // we need parent.SpecialPowers for: 2279 // layout/base/tests/test_reftests_with_caret.html 2280 // chrome: toolkit/content/tests/chrome/test_findbar.xul 2281 // chrome: toolkit/content/tests/chrome/test_popup_anchor.xul 2282 if ("SpecialPowers" in aWindow && aWindow.SpecialPowers != undefined) { 2283 return aWindow.SpecialPowers.getDOMWindowUtils(aWindow); 2284 } 2285 if ( 2286 "SpecialPowers" in aWindow.parent && 2287 aWindow.parent.SpecialPowers != undefined 2288 ) { 2289 return aWindow.parent.SpecialPowers.getDOMWindowUtils(aWindow); 2290 } 2291 2292 // TODO: this is assuming we are in chrome space 2293 return aWindow.windowUtils; 2294 } 2295 2296 /** 2297 * @param {DOMWindow} [aWindow] - DOM window 2298 * @returns The scaling value applied to the top window. 2299 */ 2300 function _getTopWindowResolution(aWindow) { 2301 let resolution = 1.0; 2302 try { 2303 resolution = _getDOMWindowUtils(aWindow.top).getResolution(); 2304 } catch (e) { 2305 // XXX How to get mobile viewport scale on Fission+xorigin since 2306 // window.top access isn't allowed due to cross-origin? 2307 } 2308 return resolution; 2309 } 2310 2311 /** 2312 * @param {DOMWindow} [aWindow] - The DOM window which you want 2313 * to get its x-offset in the screen. 2314 * @returns The screenX of aWindow in the unscaled CSS pixels. 2315 */ 2316 function _getScreenXInUnscaledCSSPixels(aWindow) { 2317 // XXX mozInnerScreen might be invalid value on mobile viewport (Bug 1701546), 2318 // so use window.top's mozInnerScreen. But this won't work fission+xorigin 2319 // with mobile viewport until mozInnerScreen returns valid value with 2320 // scale. 2321 let winInnerOffsetX = aWindow.mozInnerScreenX; 2322 try { 2323 winInnerOffsetX = 2324 aWindow.top.mozInnerScreenX + 2325 (aWindow.mozInnerScreenX - aWindow.top.mozInnerScreenX) * 2326 _getTopWindowResolution(aWindow); 2327 } catch (e) { 2328 // XXX fission+xorigin test throws permission denied since win.top is 2329 // cross-origin. 2330 } 2331 return winInnerOffsetX; 2332 } 2333 2334 /** 2335 * @param {DOMWindow} [aWindow] - The DOM window which you want 2336 * to get its y-offset in the screen. 2337 * @returns The screenY of aWindow in the unscaled CSS pixels. 2338 */ 2339 function _getScreenYInUnscaledCSSPixels(aWindow) { 2340 // XXX mozInnerScreen might be invalid value on mobile viewport (Bug 1701546), 2341 // so use window.top's mozInnerScreen. But this won't work fission+xorigin 2342 // with mobile viewport until mozInnerScreen returns valid value with 2343 // scale. 2344 let winInnerOffsetY = aWindow.mozInnerScreenY; 2345 try { 2346 winInnerOffsetY = 2347 aWindow.top.mozInnerScreenY + 2348 (aWindow.mozInnerScreenY - aWindow.top.mozInnerScreenY) * 2349 _getTopWindowResolution(aWindow); 2350 } catch (e) { 2351 // XXX fission+xorigin test throws permission denied since win.top is 2352 // cross-origin. 2353 } 2354 return winInnerOffsetY; 2355 } 2356 2357 function _defineConstant(name, value) { 2358 Object.defineProperty(this, name, { 2359 value, 2360 enumerable: true, 2361 writable: false, 2362 }); 2363 } 2364 2365 const COMPOSITION_ATTR_RAW_CLAUSE = 2366 _EU_Ci.nsITextInputProcessor.ATTR_RAW_CLAUSE; 2367 _defineConstant("COMPOSITION_ATTR_RAW_CLAUSE", COMPOSITION_ATTR_RAW_CLAUSE); 2368 const COMPOSITION_ATTR_SELECTED_RAW_CLAUSE = 2369 _EU_Ci.nsITextInputProcessor.ATTR_SELECTED_RAW_CLAUSE; 2370 _defineConstant( 2371 "COMPOSITION_ATTR_SELECTED_RAW_CLAUSE", 2372 COMPOSITION_ATTR_SELECTED_RAW_CLAUSE 2373 ); 2374 const COMPOSITION_ATTR_CONVERTED_CLAUSE = 2375 _EU_Ci.nsITextInputProcessor.ATTR_CONVERTED_CLAUSE; 2376 _defineConstant( 2377 "COMPOSITION_ATTR_CONVERTED_CLAUSE", 2378 COMPOSITION_ATTR_CONVERTED_CLAUSE 2379 ); 2380 const COMPOSITION_ATTR_SELECTED_CLAUSE = 2381 _EU_Ci.nsITextInputProcessor.ATTR_SELECTED_CLAUSE; 2382 _defineConstant( 2383 "COMPOSITION_ATTR_SELECTED_CLAUSE", 2384 COMPOSITION_ATTR_SELECTED_CLAUSE 2385 ); 2386 2387 var TIPMap = new WeakMap(); 2388 2389 function _getTIP(aWindow, aCallback) { 2390 if (!aWindow) { 2391 aWindow = window; 2392 } 2393 var tip; 2394 if (TIPMap.has(aWindow)) { 2395 tip = TIPMap.get(aWindow); 2396 } else { 2397 tip = _EU_Cc["@mozilla.org/text-input-processor;1"].createInstance( 2398 _EU_Ci.nsITextInputProcessor 2399 ); 2400 TIPMap.set(aWindow, tip); 2401 } 2402 if (!tip.beginInputTransactionForTests(aWindow, aCallback)) { 2403 tip = null; 2404 TIPMap.delete(aWindow); 2405 } 2406 return tip; 2407 } 2408 2409 function _getKeyboardEvent(aWindow = window) { 2410 if (typeof KeyboardEvent != "undefined") { 2411 try { 2412 // See if the object can be instantiated; sometimes this yields 2413 // 'TypeError: can't access dead object' or 'KeyboardEvent is not a constructor'. 2414 new KeyboardEvent("", {}); 2415 return KeyboardEvent; 2416 } catch (ex) {} 2417 } 2418 if (typeof content != "undefined" && "KeyboardEvent" in content) { 2419 return content.KeyboardEvent; 2420 } 2421 return aWindow.KeyboardEvent; 2422 } 2423 2424 // eslint-disable-next-line complexity 2425 function _guessKeyNameFromKeyCode(aKeyCode, aWindow = window) { 2426 var KeyboardEvent = _getKeyboardEvent(aWindow); 2427 switch (aKeyCode) { 2428 case KeyboardEvent.DOM_VK_CANCEL: 2429 return "Cancel"; 2430 case KeyboardEvent.DOM_VK_HELP: 2431 return "Help"; 2432 case KeyboardEvent.DOM_VK_BACK_SPACE: 2433 return "Backspace"; 2434 case KeyboardEvent.DOM_VK_TAB: 2435 return "Tab"; 2436 case KeyboardEvent.DOM_VK_CLEAR: 2437 return "Clear"; 2438 case KeyboardEvent.DOM_VK_RETURN: 2439 return "Enter"; 2440 case KeyboardEvent.DOM_VK_SHIFT: 2441 return "Shift"; 2442 case KeyboardEvent.DOM_VK_CONTROL: 2443 return "Control"; 2444 case KeyboardEvent.DOM_VK_ALT: 2445 return "Alt"; 2446 case KeyboardEvent.DOM_VK_PAUSE: 2447 return "Pause"; 2448 case KeyboardEvent.DOM_VK_EISU: 2449 return "Eisu"; 2450 case KeyboardEvent.DOM_VK_ESCAPE: 2451 return "Escape"; 2452 case KeyboardEvent.DOM_VK_CONVERT: 2453 return "Convert"; 2454 case KeyboardEvent.DOM_VK_NONCONVERT: 2455 return "NonConvert"; 2456 case KeyboardEvent.DOM_VK_ACCEPT: 2457 return "Accept"; 2458 case KeyboardEvent.DOM_VK_MODECHANGE: 2459 return "ModeChange"; 2460 case KeyboardEvent.DOM_VK_PAGE_UP: 2461 return "PageUp"; 2462 case KeyboardEvent.DOM_VK_PAGE_DOWN: 2463 return "PageDown"; 2464 case KeyboardEvent.DOM_VK_END: 2465 return "End"; 2466 case KeyboardEvent.DOM_VK_HOME: 2467 return "Home"; 2468 case KeyboardEvent.DOM_VK_LEFT: 2469 return "ArrowLeft"; 2470 case KeyboardEvent.DOM_VK_UP: 2471 return "ArrowUp"; 2472 case KeyboardEvent.DOM_VK_RIGHT: 2473 return "ArrowRight"; 2474 case KeyboardEvent.DOM_VK_DOWN: 2475 return "ArrowDown"; 2476 case KeyboardEvent.DOM_VK_SELECT: 2477 return "Select"; 2478 case KeyboardEvent.DOM_VK_PRINT: 2479 return "Print"; 2480 case KeyboardEvent.DOM_VK_EXECUTE: 2481 return "Execute"; 2482 case KeyboardEvent.DOM_VK_PRINTSCREEN: 2483 return "PrintScreen"; 2484 case KeyboardEvent.DOM_VK_INSERT: 2485 return "Insert"; 2486 case KeyboardEvent.DOM_VK_DELETE: 2487 return "Delete"; 2488 case KeyboardEvent.DOM_VK_WIN: 2489 return "OS"; 2490 case KeyboardEvent.DOM_VK_CONTEXT_MENU: 2491 return "ContextMenu"; 2492 case KeyboardEvent.DOM_VK_SLEEP: 2493 return "Standby"; 2494 case KeyboardEvent.DOM_VK_F1: 2495 return "F1"; 2496 case KeyboardEvent.DOM_VK_F2: 2497 return "F2"; 2498 case KeyboardEvent.DOM_VK_F3: 2499 return "F3"; 2500 case KeyboardEvent.DOM_VK_F4: 2501 return "F4"; 2502 case KeyboardEvent.DOM_VK_F5: 2503 return "F5"; 2504 case KeyboardEvent.DOM_VK_F6: 2505 return "F6"; 2506 case KeyboardEvent.DOM_VK_F7: 2507 return "F7"; 2508 case KeyboardEvent.DOM_VK_F8: 2509 return "F8"; 2510 case KeyboardEvent.DOM_VK_F9: 2511 return "F9"; 2512 case KeyboardEvent.DOM_VK_F10: 2513 return "F10"; 2514 case KeyboardEvent.DOM_VK_F11: 2515 return "F11"; 2516 case KeyboardEvent.DOM_VK_F12: 2517 return "F12"; 2518 case KeyboardEvent.DOM_VK_F13: 2519 return "F13"; 2520 case KeyboardEvent.DOM_VK_F14: 2521 return "F14"; 2522 case KeyboardEvent.DOM_VK_F15: 2523 return "F15"; 2524 case KeyboardEvent.DOM_VK_F16: 2525 return "F16"; 2526 case KeyboardEvent.DOM_VK_F17: 2527 return "F17"; 2528 case KeyboardEvent.DOM_VK_F18: 2529 return "F18"; 2530 case KeyboardEvent.DOM_VK_F19: 2531 return "F19"; 2532 case KeyboardEvent.DOM_VK_F20: 2533 return "F20"; 2534 case KeyboardEvent.DOM_VK_F21: 2535 return "F21"; 2536 case KeyboardEvent.DOM_VK_F22: 2537 return "F22"; 2538 case KeyboardEvent.DOM_VK_F23: 2539 return "F23"; 2540 case KeyboardEvent.DOM_VK_F24: 2541 return "F24"; 2542 case KeyboardEvent.DOM_VK_NUM_LOCK: 2543 return "NumLock"; 2544 case KeyboardEvent.DOM_VK_SCROLL_LOCK: 2545 return "ScrollLock"; 2546 case KeyboardEvent.DOM_VK_VOLUME_MUTE: 2547 return "AudioVolumeMute"; 2548 case KeyboardEvent.DOM_VK_VOLUME_DOWN: 2549 return "AudioVolumeDown"; 2550 case KeyboardEvent.DOM_VK_VOLUME_UP: 2551 return "AudioVolumeUp"; 2552 case KeyboardEvent.DOM_VK_META: 2553 return "Meta"; 2554 case KeyboardEvent.DOM_VK_ALTGR: 2555 return "AltGraph"; 2556 case KeyboardEvent.DOM_VK_PROCESSKEY: 2557 return "Process"; 2558 case KeyboardEvent.DOM_VK_ATTN: 2559 return "Attn"; 2560 case KeyboardEvent.DOM_VK_CRSEL: 2561 return "CrSel"; 2562 case KeyboardEvent.DOM_VK_EXSEL: 2563 return "ExSel"; 2564 case KeyboardEvent.DOM_VK_EREOF: 2565 return "EraseEof"; 2566 case KeyboardEvent.DOM_VK_PLAY: 2567 return "Play"; 2568 default: 2569 return "Unidentified"; 2570 } 2571 } 2572 2573 function _createKeyboardEventDictionary( 2574 aKey, 2575 aKeyEvent, 2576 aTIP = null, 2577 aWindow = window 2578 ) { 2579 var result = { dictionary: null, flags: 0 }; 2580 var keyCodeIsDefined = "keyCode" in aKeyEvent; 2581 var keyCode = 2582 keyCodeIsDefined && aKeyEvent.keyCode >= 0 && aKeyEvent.keyCode <= 255 2583 ? aKeyEvent.keyCode 2584 : 0; 2585 var keyName = "Unidentified"; 2586 var code = aKeyEvent.code; 2587 if (!aTIP) { 2588 aTIP = _getTIP(aWindow); 2589 } 2590 if (aKey.indexOf("KEY_") == 0) { 2591 keyName = aKey.substr("KEY_".length); 2592 result.flags |= _EU_Ci.nsITextInputProcessor.KEY_NON_PRINTABLE_KEY; 2593 if (code === undefined) { 2594 code = aTIP.computeCodeValueOfNonPrintableKey( 2595 keyName, 2596 aKeyEvent.location 2597 ); 2598 } 2599 } else if (aKey.indexOf("VK_") == 0) { 2600 keyCode = _getKeyboardEvent(aWindow)["DOM_" + aKey]; 2601 if (!keyCode) { 2602 throw new Error("Unknown key: " + aKey); 2603 } 2604 keyName = _guessKeyNameFromKeyCode(keyCode, aWindow); 2605 result.flags |= _EU_Ci.nsITextInputProcessor.KEY_NON_PRINTABLE_KEY; 2606 if (code === undefined) { 2607 code = aTIP.computeCodeValueOfNonPrintableKey( 2608 keyName, 2609 aKeyEvent.location 2610 ); 2611 } 2612 } else if (aKey != "") { 2613 keyName = aKey; 2614 if (!keyCodeIsDefined) { 2615 keyCode = aTIP.guessKeyCodeValueOfPrintableKeyInUSEnglishKeyboardLayout( 2616 aKey, 2617 aKeyEvent.location 2618 ); 2619 } 2620 if (!keyCode) { 2621 result.flags |= _EU_Ci.nsITextInputProcessor.KEY_KEEP_KEYCODE_ZERO; 2622 } 2623 result.flags |= _EU_Ci.nsITextInputProcessor.KEY_FORCE_PRINTABLE_KEY; 2624 if (code === undefined) { 2625 code = aTIP.guessCodeValueOfPrintableKeyInUSEnglishKeyboardLayout( 2626 keyName, 2627 aKeyEvent.location 2628 ); 2629 } 2630 } 2631 var locationIsDefined = "location" in aKeyEvent; 2632 if (locationIsDefined && aKeyEvent.location === 0) { 2633 result.flags |= _EU_Ci.nsITextInputProcessor.KEY_KEEP_KEY_LOCATION_STANDARD; 2634 } 2635 if (aKeyEvent.doNotMarkKeydownAsProcessed) { 2636 result.flags |= 2637 _EU_Ci.nsITextInputProcessor.KEY_DONT_MARK_KEYDOWN_AS_PROCESSED; 2638 } 2639 if (aKeyEvent.markKeyupAsProcessed) { 2640 result.flags |= _EU_Ci.nsITextInputProcessor.KEY_MARK_KEYUP_AS_PROCESSED; 2641 } 2642 result.dictionary = { 2643 key: keyName, 2644 code, 2645 location: locationIsDefined ? aKeyEvent.location : 0, 2646 repeat: "repeat" in aKeyEvent ? aKeyEvent.repeat === true : false, 2647 keyCode, 2648 }; 2649 return result; 2650 } 2651 2652 function _emulateToActivateModifiers(aTIP, aKeyEvent, aWindow = window) { 2653 if (!aKeyEvent) { 2654 return null; 2655 } 2656 var KeyboardEvent = _getKeyboardEvent(aWindow); 2657 2658 var modifiers = { 2659 normal: [ 2660 { key: "Alt", attr: "altKey" }, 2661 { key: "AltGraph", attr: "altGraphKey" }, 2662 { key: "Control", attr: "ctrlKey" }, 2663 { key: "Fn", attr: "fnKey" }, 2664 { key: "Meta", attr: "metaKey" }, 2665 { key: "Shift", attr: "shiftKey" }, 2666 { key: "Symbol", attr: "symbolKey" }, 2667 { key: _EU_isMac(aWindow) ? "Meta" : "Control", attr: "accelKey" }, 2668 ], 2669 lockable: [ 2670 { key: "CapsLock", attr: "capsLockKey" }, 2671 { key: "FnLock", attr: "fnLockKey" }, 2672 { key: "NumLock", attr: "numLockKey" }, 2673 { key: "ScrollLock", attr: "scrollLockKey" }, 2674 { key: "SymbolLock", attr: "symbolLockKey" }, 2675 ], 2676 }; 2677 2678 for (let i = 0; i < modifiers.normal.length; i++) { 2679 if (!aKeyEvent[modifiers.normal[i].attr]) { 2680 continue; 2681 } 2682 if (aTIP.getModifierState(modifiers.normal[i].key)) { 2683 continue; // already activated. 2684 } 2685 let event = new KeyboardEvent("", { key: modifiers.normal[i].key }); 2686 aTIP.keydown( 2687 event, 2688 aTIP.KEY_NON_PRINTABLE_KEY | aTIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT 2689 ); 2690 modifiers.normal[i].activated = true; 2691 } 2692 for (let i = 0; i < modifiers.lockable.length; i++) { 2693 if (!aKeyEvent[modifiers.lockable[i].attr]) { 2694 continue; 2695 } 2696 if (aTIP.getModifierState(modifiers.lockable[i].key)) { 2697 continue; // already activated. 2698 } 2699 let event = new KeyboardEvent("", { key: modifiers.lockable[i].key }); 2700 aTIP.keydown( 2701 event, 2702 aTIP.KEY_NON_PRINTABLE_KEY | aTIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT 2703 ); 2704 aTIP.keyup( 2705 event, 2706 aTIP.KEY_NON_PRINTABLE_KEY | aTIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT 2707 ); 2708 modifiers.lockable[i].activated = true; 2709 } 2710 return modifiers; 2711 } 2712 2713 function _emulateToInactivateModifiers(aTIP, aModifiers, aWindow = window) { 2714 if (!aModifiers) { 2715 return; 2716 } 2717 var KeyboardEvent = _getKeyboardEvent(aWindow); 2718 for (let i = 0; i < aModifiers.normal.length; i++) { 2719 if (!aModifiers.normal[i].activated) { 2720 continue; 2721 } 2722 let event = new KeyboardEvent("", { key: aModifiers.normal[i].key }); 2723 aTIP.keyup( 2724 event, 2725 aTIP.KEY_NON_PRINTABLE_KEY | aTIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT 2726 ); 2727 } 2728 for (let i = 0; i < aModifiers.lockable.length; i++) { 2729 if (!aModifiers.lockable[i].activated) { 2730 continue; 2731 } 2732 if (!aTIP.getModifierState(aModifiers.lockable[i].key)) { 2733 continue; // who already inactivated this? 2734 } 2735 let event = new KeyboardEvent("", { key: aModifiers.lockable[i].key }); 2736 aTIP.keydown( 2737 event, 2738 aTIP.KEY_NON_PRINTABLE_KEY | aTIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT 2739 ); 2740 aTIP.keyup( 2741 event, 2742 aTIP.KEY_NON_PRINTABLE_KEY | aTIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT 2743 ); 2744 } 2745 } 2746 2747 /** 2748 * Synthesize a composition event and keydown event and keyup events unless 2749 * you prevent to dispatch them explicitly (see aEvent.key's explanation). 2750 * 2751 * Note that you shouldn't call this with "compositionstart" unless you need to 2752 * test compositionstart event which is NOT followed by compositionupdate 2753 * event immediately. Typically, native IME starts composition with 2754 * a pair of keydown and keyup event and dispatch compositionstart and 2755 * compositionupdate (and non-standard text event) between them. So, in most 2756 * cases, you should call synthesizeCompositionChange() directly. 2757 * If you call this with compositionstart, keyup event will be fired 2758 * immediately after compositionstart. In other words, you should use 2759 * "compositionstart" only when you need to emulate IME which just starts 2760 * composition with compositionstart event but does not send composing text to 2761 * us until committing the composition. This is behavior of some Chinese IMEs. 2762 * 2763 * @param aEvent The composition event information. This must 2764 * have |type| member. The value must be 2765 * "compositionstart", "compositionend", 2766 * "compositioncommitasis" or "compositioncommit". 2767 * 2768 * And also this may have |data| and |locale| which 2769 * would be used for the value of each property of 2770 * the composition event. Note that the |data| is 2771 * ignored if the event type is "compositionstart" 2772 * or "compositioncommitasis". 2773 * 2774 * If |key| is undefined, "keydown" and "keyup" 2775 * events which are marked as "processed by IME" 2776 * are dispatched. If |key| is not null, "keydown" 2777 * and/or "keyup" events are dispatched (if the 2778 * |key.type| is specified as "keydown", only 2779 * "keydown" event is dispatched). Otherwise, 2780 * i.e., if |key| is null, neither "keydown" nor 2781 * "keyup" event is dispatched. 2782 * 2783 * If |key.doNotMarkKeydownAsProcessed| is not true, 2784 * key value and keyCode value of "keydown" event 2785 * will be set to "Process" and DOM_VK_PROCESSKEY. 2786 * If |key.markKeyupAsProcessed| is true, 2787 * key value and keyCode value of "keyup" event 2788 * will be set to "Process" and DOM_VK_PROCESSKEY. 2789 * @param aWindow Optional (If null, current |window| will be used) 2790 * @param aCallback Optional (If non-null, use the callback for 2791 * receiving notifications to IME) 2792 */ 2793 function synthesizeComposition(aEvent, aWindow = window, aCallback) { 2794 var TIP = _getTIP(aWindow, aCallback); 2795 if (!TIP) { 2796 return; 2797 } 2798 var KeyboardEvent = _getKeyboardEvent(aWindow); 2799 var modifiers = _emulateToActivateModifiers(TIP, aEvent.key, aWindow); 2800 var keyEventDict = { dictionary: null, flags: 0 }; 2801 var keyEvent = null; 2802 if (aEvent.key && typeof aEvent.key.key === "string") { 2803 keyEventDict = _createKeyboardEventDictionary( 2804 aEvent.key.key, 2805 aEvent.key, 2806 TIP, 2807 aWindow 2808 ); 2809 keyEvent = new KeyboardEvent( 2810 // eslint-disable-next-line no-nested-ternary 2811 aEvent.key.type === "keydown" 2812 ? "keydown" 2813 : aEvent.key.type === "keyup" 2814 ? "keyup" 2815 : "", 2816 keyEventDict.dictionary 2817 ); 2818 } else if (aEvent.key === undefined) { 2819 keyEventDict = _createKeyboardEventDictionary( 2820 "KEY_Process", 2821 {}, 2822 TIP, 2823 aWindow 2824 ); 2825 keyEvent = new KeyboardEvent("", keyEventDict.dictionary); 2826 } 2827 try { 2828 switch (aEvent.type) { 2829 case "compositionstart": 2830 TIP.startComposition(keyEvent, keyEventDict.flags); 2831 break; 2832 case "compositioncommitasis": 2833 TIP.commitComposition(keyEvent, keyEventDict.flags); 2834 break; 2835 case "compositioncommit": 2836 TIP.commitCompositionWith(aEvent.data, keyEvent, keyEventDict.flags); 2837 break; 2838 } 2839 } finally { 2840 _emulateToInactivateModifiers(TIP, modifiers, aWindow); 2841 } 2842 } 2843 /** 2844 * Synthesize eCompositionChange event which causes a DOM text event, may 2845 * cause compositionupdate event, and causes keydown event and keyup event 2846 * unless you prevent to dispatch them explicitly (see aEvent.key's 2847 * explanation). 2848 * 2849 * Note that if you call this when there is no composition, compositionstart 2850 * event will be fired automatically. This is better than you use 2851 * synthesizeComposition("compositionstart") in most cases. See the 2852 * explanation of synthesizeComposition(). 2853 * 2854 * @param aEvent The compositionchange event's information, this has 2855 * |composition| and |caret| members. |composition| has 2856 * |string| and |clauses| members. |clauses| must be array 2857 * object. Each object has |length| and |attr|. And |caret| 2858 * has |start| and |length|. See the following tree image. 2859 * 2860 * aEvent 2861 * +-- composition 2862 * | +-- string 2863 * | +-- clauses[] 2864 * | +-- length 2865 * | +-- attr 2866 * +-- caret 2867 * | +-- start 2868 * | +-- length 2869 * +-- key 2870 * 2871 * Set the composition string to |composition.string|. Set its 2872 * clauses information to the |clauses| array. 2873 * 2874 * When it's composing, set the each clauses' length to the 2875 * |composition.clauses[n].length|. The sum of the all length 2876 * values must be same as the length of |composition.string|. 2877 * Set nsICompositionStringSynthesizer.ATTR_* to the 2878 * |composition.clauses[n].attr|. 2879 * 2880 * When it's not composing, set 0 to the 2881 * |composition.clauses[0].length| and 2882 * |composition.clauses[0].attr|. 2883 * 2884 * Set caret position to the |caret.start|. It's offset from 2885 * the start of the composition string. Set caret length to 2886 * |caret.length|. If it's larger than 0, it should be wide 2887 * caret. However, current nsEditor doesn't support wide 2888 * caret, therefore, you should always set 0 now. 2889 * 2890 * If |key| is undefined, "keydown" and "keyup" events which 2891 * are marked as "processed by IME" are dispatched. If |key| 2892 * is not null, "keydown" and/or "keyup" events are dispatched 2893 * (if the |key.type| is specified as "keydown", only "keydown" 2894 * event is dispatched). Otherwise, i.e., if |key| is null, 2895 * neither "keydown" nor "keyup" event is dispatched. 2896 * If |key.doNotMarkKeydownAsProcessed| is not true, key value 2897 * and keyCode value of "keydown" event will be set to 2898 * "Process" and DOM_VK_PROCESSKEY. 2899 * If |key.markKeyupAsProcessed| is true key value and keyCode 2900 * value of "keyup" event will be set to "Process" and 2901 * DOM_VK_PROCESSKEY. 2902 * 2903 * @param aWindow Optional (If null, current |window| will be used) 2904 * @param aCallback Optional (If non-null, use the callback for receiving 2905 * notifications to IME) 2906 */ 2907 function synthesizeCompositionChange(aEvent, aWindow = window, aCallback) { 2908 var TIP = _getTIP(aWindow, aCallback); 2909 if (!TIP) { 2910 return; 2911 } 2912 var KeyboardEvent = _getKeyboardEvent(aWindow); 2913 2914 if ( 2915 !aEvent.composition || 2916 !aEvent.composition.clauses || 2917 !aEvent.composition.clauses[0] 2918 ) { 2919 return; 2920 } 2921 2922 TIP.setPendingCompositionString(aEvent.composition.string); 2923 if (aEvent.composition.clauses[0].length) { 2924 for (var i = 0; i < aEvent.composition.clauses.length; i++) { 2925 switch (aEvent.composition.clauses[i].attr) { 2926 case TIP.ATTR_RAW_CLAUSE: 2927 case TIP.ATTR_SELECTED_RAW_CLAUSE: 2928 case TIP.ATTR_CONVERTED_CLAUSE: 2929 case TIP.ATTR_SELECTED_CLAUSE: 2930 TIP.appendClauseToPendingComposition( 2931 aEvent.composition.clauses[i].length, 2932 aEvent.composition.clauses[i].attr 2933 ); 2934 break; 2935 case 0: 2936 // Ignore dummy clause for the argument. 2937 break; 2938 default: 2939 throw new Error("invalid clause attribute specified"); 2940 } 2941 } 2942 } 2943 2944 if (aEvent.caret) { 2945 TIP.setCaretInPendingComposition(aEvent.caret.start); 2946 } 2947 2948 var modifiers = _emulateToActivateModifiers(TIP, aEvent.key, aWindow); 2949 try { 2950 var keyEventDict = { dictionary: null, flags: 0 }; 2951 var keyEvent = null; 2952 if (aEvent.key && typeof aEvent.key.key === "string") { 2953 keyEventDict = _createKeyboardEventDictionary( 2954 aEvent.key.key, 2955 aEvent.key, 2956 TIP, 2957 aWindow 2958 ); 2959 keyEvent = new KeyboardEvent( 2960 // eslint-disable-next-line no-nested-ternary 2961 aEvent.key.type === "keydown" 2962 ? "keydown" 2963 : aEvent.key.type === "keyup" 2964 ? "keyup" 2965 : "", 2966 keyEventDict.dictionary 2967 ); 2968 } else if (aEvent.key === undefined) { 2969 keyEventDict = _createKeyboardEventDictionary( 2970 "KEY_Process", 2971 {}, 2972 TIP, 2973 aWindow 2974 ); 2975 keyEvent = new KeyboardEvent("", keyEventDict.dictionary); 2976 } 2977 TIP.flushPendingComposition(keyEvent, keyEventDict.flags); 2978 } finally { 2979 _emulateToInactivateModifiers(TIP, modifiers, aWindow); 2980 } 2981 } 2982 2983 // Must be synchronized with nsIDOMWindowUtils. 2984 const QUERY_CONTENT_FLAG_USE_NATIVE_LINE_BREAK = 0x0000; 2985 const QUERY_CONTENT_FLAG_USE_XP_LINE_BREAK = 0x0001; 2986 2987 const QUERY_CONTENT_FLAG_SELECTION_NORMAL = 0x0000; 2988 const QUERY_CONTENT_FLAG_SELECTION_SPELLCHECK = 0x0002; 2989 const QUERY_CONTENT_FLAG_SELECTION_IME_RAWINPUT = 0x0004; 2990 const QUERY_CONTENT_FLAG_SELECTION_IME_SELECTEDRAWTEXT = 0x0008; 2991 const QUERY_CONTENT_FLAG_SELECTION_IME_CONVERTEDTEXT = 0x0010; 2992 const QUERY_CONTENT_FLAG_SELECTION_IME_SELECTEDCONVERTEDTEXT = 0x0020; 2993 const QUERY_CONTENT_FLAG_SELECTION_ACCESSIBILITY = 0x0040; 2994 const QUERY_CONTENT_FLAG_SELECTION_FIND = 0x0080; 2995 const QUERY_CONTENT_FLAG_SELECTION_URLSECONDARY = 0x0100; 2996 const QUERY_CONTENT_FLAG_SELECTION_URLSTRIKEOUT = 0x0200; 2997 2998 const QUERY_CONTENT_FLAG_OFFSET_RELATIVE_TO_INSERTION_POINT = 0x0400; 2999 3000 const SELECTION_SET_FLAG_USE_NATIVE_LINE_BREAK = 0x0000; 3001 const SELECTION_SET_FLAG_USE_XP_LINE_BREAK = 0x0001; 3002 const SELECTION_SET_FLAG_REVERSE = 0x0002; 3003 3004 /** 3005 * Synthesize a query text content event. 3006 * 3007 * @param aOffset The character offset. 0 means the first character in the 3008 * selection root. 3009 * @param aLength The length of getting text. If the length is too long, 3010 * the extra length is ignored. 3011 * @param aIsRelative Optional (If true, aOffset is relative to start of 3012 * composition if there is, or start of selection.) 3013 * @param aWindow Optional (If null, current |window| will be used) 3014 * @return An nsIQueryContentEventResult object. If this failed, 3015 * the result might be null. 3016 */ 3017 function synthesizeQueryTextContent(aOffset, aLength, aIsRelative, aWindow) { 3018 var utils = _getDOMWindowUtils(aWindow); 3019 if (!utils) { 3020 return null; 3021 } 3022 var flags = QUERY_CONTENT_FLAG_USE_NATIVE_LINE_BREAK; 3023 if (aIsRelative === true) { 3024 flags |= QUERY_CONTENT_FLAG_OFFSET_RELATIVE_TO_INSERTION_POINT; 3025 } 3026 return utils.sendQueryContentEvent( 3027 utils.QUERY_TEXT_CONTENT, 3028 aOffset, 3029 aLength, 3030 0, 3031 0, 3032 flags 3033 ); 3034 } 3035 3036 /** 3037 * Synthesize a query selected text event. 3038 * 3039 * @param aSelectionType Optional, one of QUERY_CONTENT_FLAG_SELECTION_*. 3040 * If null, QUERY_CONTENT_FLAG_SELECTION_NORMAL will 3041 * be used. 3042 * @param aWindow Optional (If null, current |window| will be used) 3043 * @return An nsIQueryContentEventResult object. If this failed, 3044 * the result might be null. 3045 */ 3046 function synthesizeQuerySelectedText(aSelectionType, aWindow) { 3047 var utils = _getDOMWindowUtils(aWindow); 3048 var flags = QUERY_CONTENT_FLAG_USE_NATIVE_LINE_BREAK; 3049 if (aSelectionType) { 3050 flags |= aSelectionType; 3051 } 3052 3053 return utils.sendQueryContentEvent( 3054 utils.QUERY_SELECTED_TEXT, 3055 0, 3056 0, 3057 0, 3058 0, 3059 flags 3060 ); 3061 } 3062 3063 /** 3064 * Synthesize a query caret rect event. 3065 * 3066 * @param aOffset The caret offset. 0 means left side of the first character 3067 * in the selection root. 3068 * @param aWindow Optional (If null, current |window| will be used) 3069 * @return An nsIQueryContentEventResult object. If this failed, 3070 * the result might be null. 3071 */ 3072 function synthesizeQueryCaretRect(aOffset, aWindow) { 3073 var utils = _getDOMWindowUtils(aWindow); 3074 if (!utils) { 3075 return null; 3076 } 3077 return utils.sendQueryContentEvent( 3078 utils.QUERY_CARET_RECT, 3079 aOffset, 3080 0, 3081 0, 3082 0, 3083 QUERY_CONTENT_FLAG_USE_NATIVE_LINE_BREAK 3084 ); 3085 } 3086 3087 /** 3088 * Synthesize a selection set event. 3089 * 3090 * @param aOffset The character offset. 0 means the first character in the 3091 * selection root. 3092 * @param aLength The length of the text. If the length is too long, 3093 * the extra length is ignored. 3094 * @param aReverse If true, the selection is from |aOffset + aLength| to 3095 * |aOffset|. Otherwise, from |aOffset| to |aOffset + aLength|. 3096 * @param aWindow Optional (If null, current |window| will be used) 3097 * @return True, if succeeded. Otherwise false. 3098 */ 3099 async function synthesizeSelectionSet( 3100 aOffset, 3101 aLength, 3102 aReverse, 3103 aWindow = window 3104 ) { 3105 const utils = _getDOMWindowUtils(aWindow); 3106 if (!utils) { 3107 return false; 3108 } 3109 // eSetSelection event will be compared with selection cache in 3110 // IMEContentObserver, but it may have not been updated yet. Therefore, we 3111 // need to flush pending things of IMEContentObserver. 3112 await new Promise(resolve => 3113 aWindow.requestAnimationFrame(() => aWindow.requestAnimationFrame(resolve)) 3114 ); 3115 const flags = aReverse ? SELECTION_SET_FLAG_REVERSE : 0; 3116 return utils.sendSelectionSetEvent(aOffset, aLength, flags); 3117 } 3118 3119 /** 3120 * Synthesize a query text rect event. 3121 * 3122 * @param aOffset The character offset. 0 means the first character in the 3123 * selection root. 3124 * @param aLength The length of the text. If the length is too long, 3125 * the extra length is ignored. 3126 * @param aIsRelative Optional (If true, aOffset is relative to start of 3127 * composition if there is, or start of selection.) 3128 * @param aWindow Optional (If null, current |window| will be used) 3129 * @return An nsIQueryContentEventResult object. If this failed, 3130 * the result might be null. 3131 */ 3132 function synthesizeQueryTextRect(aOffset, aLength, aIsRelative, aWindow) { 3133 if (aIsRelative !== undefined && typeof aIsRelative !== "boolean") { 3134 throw new Error( 3135 "Maybe, you set Window object to the 3rd argument, but it should be a boolean value" 3136 ); 3137 } 3138 var utils = _getDOMWindowUtils(aWindow); 3139 let flags = QUERY_CONTENT_FLAG_USE_NATIVE_LINE_BREAK; 3140 if (aIsRelative === true) { 3141 flags |= QUERY_CONTENT_FLAG_OFFSET_RELATIVE_TO_INSERTION_POINT; 3142 } 3143 return utils.sendQueryContentEvent( 3144 utils.QUERY_TEXT_RECT, 3145 aOffset, 3146 aLength, 3147 0, 3148 0, 3149 flags 3150 ); 3151 } 3152 3153 /** 3154 * Synthesize a query text rect array event. 3155 * 3156 * @param aOffset The character offset. 0 means the first character in the 3157 * selection root. 3158 * @param aLength The length of the text. If the length is too long, 3159 * the extra length is ignored. 3160 * @param aWindow Optional (If null, current |window| will be used) 3161 * @return An nsIQueryContentEventResult object. If this failed, 3162 * the result might be null. 3163 */ 3164 function synthesizeQueryTextRectArray(aOffset, aLength, aWindow) { 3165 var utils = _getDOMWindowUtils(aWindow); 3166 return utils.sendQueryContentEvent( 3167 utils.QUERY_TEXT_RECT_ARRAY, 3168 aOffset, 3169 aLength, 3170 0, 3171 0, 3172 QUERY_CONTENT_FLAG_USE_NATIVE_LINE_BREAK 3173 ); 3174 } 3175 3176 /** 3177 * Synthesize a query editor rect event. 3178 * 3179 * @param aWindow Optional (If null, current |window| will be used) 3180 * @return An nsIQueryContentEventResult object. If this failed, 3181 * the result might be null. 3182 */ 3183 function synthesizeQueryEditorRect(aWindow) { 3184 var utils = _getDOMWindowUtils(aWindow); 3185 return utils.sendQueryContentEvent( 3186 utils.QUERY_EDITOR_RECT, 3187 0, 3188 0, 3189 0, 3190 0, 3191 QUERY_CONTENT_FLAG_USE_NATIVE_LINE_BREAK 3192 ); 3193 } 3194 3195 /** 3196 * Synthesize a character at point event. 3197 * 3198 * @param aX, aY The offset in the client area of the DOM window. 3199 * @param aWindow Optional (If null, current |window| will be used) 3200 * @return An nsIQueryContentEventResult object. If this failed, 3201 * the result might be null. 3202 */ 3203 function synthesizeCharAtPoint(aX, aY, aWindow) { 3204 var utils = _getDOMWindowUtils(aWindow); 3205 return utils.sendQueryContentEvent( 3206 utils.QUERY_CHARACTER_AT_POINT, 3207 0, 3208 0, 3209 aX, 3210 aY, 3211 QUERY_CONTENT_FLAG_USE_NATIVE_LINE_BREAK 3212 ); 3213 } 3214 3215 /** 3216 * INTERNAL USE ONLY 3217 * Create an event object to pass to sendDragEvent. 3218 * 3219 * @param aType The string represents drag event type. 3220 * @param aDestElement The element to fire the drag event, used to calculate 3221 * screenX/Y and clientX/Y. 3222 * @param aDestWindow Optional; Defaults to the current window object. 3223 * @param aDataTransfer dataTransfer for current drag session. 3224 * @param aDragEvent The object contains properties to override the event 3225 * object 3226 * @return An object to pass to sendDragEvent. 3227 */ 3228 function createDragEventObject( 3229 aType, 3230 aDestElement, 3231 aDestWindow, 3232 aDataTransfer, 3233 aDragEvent 3234 ) { 3235 const resolution = _getTopWindowResolution(aDestWindow.top); 3236 const destRect = aDestElement.getBoundingClientRect(); 3237 // If clientX and/or clientY are specified, we should use them. Otherwise, 3238 // use the center of the dest element. 3239 const destClientXInCSSPixels = 3240 "clientX" in aDragEvent && !("screenX" in aDragEvent) 3241 ? aDragEvent.clientX 3242 : destRect.left + destRect.width / 2; 3243 const destClientYInCSSPixels = 3244 "clientY" in aDragEvent && !("screenY" in aDragEvent) 3245 ? aDragEvent.clientY 3246 : destRect.top + destRect.height / 2; 3247 3248 const devicePixelRatio = aDestWindow.devicePixelRatio; 3249 const destScreenXInDevicePixels = 3250 (_getScreenXInUnscaledCSSPixels(aDestWindow) + 3251 destClientXInCSSPixels * resolution) * 3252 devicePixelRatio; 3253 const destScreenYInDevicePixels = 3254 (_getScreenYInUnscaledCSSPixels(aDestWindow) + 3255 destClientYInCSSPixels * resolution) * 3256 devicePixelRatio; 3257 3258 // Wrap only in plain mochitests 3259 let dataTransfer; 3260 if (aDataTransfer) { 3261 dataTransfer = _EU_maybeUnwrap( 3262 _EU_maybeWrap(aDataTransfer).mozCloneForEvent(aType) 3263 ); 3264 3265 // Copy over the drop effect. This isn't copied over by Clone, as it uses 3266 // more complex logic in the actual implementation (see 3267 // nsContentUtils::SetDataTransferInEvent for actual impl). 3268 dataTransfer.dropEffect = aDataTransfer.dropEffect; 3269 } 3270 return Object.assign( 3271 { 3272 type: aType, 3273 screenX: _EU_roundDevicePixels(destScreenXInDevicePixels), 3274 screenY: _EU_roundDevicePixels(destScreenYInDevicePixels), 3275 clientX: _EU_roundDevicePixels(destClientXInCSSPixels), 3276 clientY: _EU_roundDevicePixels(destClientYInCSSPixels), 3277 dataTransfer, 3278 _domDispatchOnly: aDragEvent._domDispatchOnly, 3279 }, 3280 aDragEvent 3281 ); 3282 } 3283 3284 /** 3285 * Emulate a event sequence of dragstart, dragenter, and dragover. 3286 * 3287 * @param {Element} aSrcElement 3288 * The element to use to start the drag. 3289 * @param {Element} aDestElement 3290 * The element to fire the dragover, dragenter events 3291 * @param {Array} aDragData 3292 * The data to supply for the data transfer. 3293 * This data is in the format: 3294 * 3295 * [ 3296 * [ 3297 * {"type": value, "data": value }, 3298 * ..., 3299 * ], 3300 * ... 3301 * ] 3302 * 3303 * Pass null to avoid modifying dataTransfer. 3304 * @param {string} [aDropEffect="move"] 3305 * The drop effect to set during the dragstart event, or 'move' if omitted. 3306 * @param {DOMWindow} [aWindow=window] 3307 * The DOM window in which the drag happens. Defaults to the window in which 3308 * EventUtils.js is loaded. 3309 * @param {DOMWindow} [aDestWindow=aWindow] 3310 * Used when aDestElement is in a different DOM window than aSrcElement. 3311 * Default is to match ``aWindow``. 3312 * @param {object} [aDragEvent={}] 3313 * Defaults to empty object. Overwrites an object passed to sendDragEvent. 3314 * @return {[boolean, DataTransfer]} 3315 * A two element array, where the first element is the value returned 3316 * from sendDragEvent for dragover event, and the second element is the 3317 * dataTransfer for the current drag session. 3318 */ 3319 function synthesizeDragOver( 3320 aSrcElement, 3321 aDestElement, 3322 aDragData, 3323 aDropEffect, 3324 aWindow, 3325 aDestWindow, 3326 aDragEvent = {} 3327 ) { 3328 if (!aWindow) { 3329 aWindow = window; 3330 } 3331 if (!aDestWindow) { 3332 aDestWindow = aWindow; 3333 } 3334 3335 // eslint-disable-next-line mozilla/use-services 3336 const obs = _EU_Cc["@mozilla.org/observer-service;1"].getService( 3337 _EU_Ci.nsIObserverService 3338 ); 3339 let utils = _getDOMWindowUtils(aWindow); 3340 var sess = utils.dragSession; 3341 3342 // This method runs before other callbacks, and acts as a way to inject the 3343 // initial drag data into the DataTransfer. 3344 function fillDrag(event) { 3345 if (aDragData) { 3346 for (var i = 0; i < aDragData.length; i++) { 3347 var item = aDragData[i]; 3348 for (var j = 0; j < item.length; j++) { 3349 _EU_maybeWrap(event.dataTransfer).mozSetDataAt( 3350 item[j].type, 3351 item[j].data, 3352 i 3353 ); 3354 } 3355 } 3356 } 3357 event.dataTransfer.dropEffect = aDropEffect || "move"; 3358 event.preventDefault(); 3359 } 3360 3361 function trapDrag(subject, topic) { 3362 if (topic == "on-datatransfer-available") { 3363 sess.dataTransfer = _EU_maybeUnwrap( 3364 _EU_maybeWrap(subject).mozCloneForEvent("drop") 3365 ); 3366 sess.dataTransfer.dropEffect = subject.dropEffect; 3367 } 3368 } 3369 3370 // need to use real mouse action 3371 aWindow.addEventListener("dragstart", fillDrag, true); 3372 obs.addObserver(trapDrag, "on-datatransfer-available"); 3373 synthesizeMouseAtCenter(aSrcElement, { type: "mousedown" }, aWindow); 3374 3375 var rect = aSrcElement.getBoundingClientRect(); 3376 var x = rect.width / 2; 3377 var y = rect.height / 2; 3378 synthesizeMouse(aSrcElement, x, y, { type: "mousemove" }, aWindow); 3379 synthesizeMouse(aSrcElement, x + 10, y + 10, { type: "mousemove" }, aWindow); 3380 aWindow.removeEventListener("dragstart", fillDrag, true); 3381 obs.removeObserver(trapDrag, "on-datatransfer-available"); 3382 3383 var dataTransfer = sess.dataTransfer; 3384 if (!dataTransfer) { 3385 throw new Error("No data transfer object after synthesizing the mouse!"); 3386 } 3387 3388 // The EventStateManager will fire our dragenter event if it needs to. 3389 var event = createDragEventObject( 3390 "dragover", 3391 aDestElement, 3392 aDestWindow, 3393 dataTransfer, 3394 aDragEvent 3395 ); 3396 var result = sendDragEvent(event, aDestElement, aDestWindow); 3397 3398 return [result, dataTransfer]; 3399 } 3400 3401 /** 3402 * Emulate the drop event and mouseup event. 3403 * This should be called after synthesizeDragOver. 3404 * 3405 * @param {*} aResult 3406 * The first element of the array returned from ``synthesizeDragOver``. 3407 * @param {DataTransfer} aDataTransfer 3408 * The second element of the array returned from ``synthesizeDragOver``. 3409 * @param {Element} aDestElement 3410 * The element on which to fire the drop event. 3411 * @param {DOMWindow} [aDestWindow=window] 3412 * The DOM window in which the drop happens. Defaults to the window in which 3413 * EventUtils.js is loaded. 3414 * @param {object} [aDragEvent={}] 3415 * Defaults to empty object. Overwrites an object passed to sendDragEvent. 3416 * @return {string} 3417 * "none" if aResult is true, ``aDataTransfer.dropEffect`` otherwise. 3418 */ 3419 function synthesizeDropAfterDragOver( 3420 aResult, 3421 aDataTransfer, 3422 aDestElement, 3423 aDestWindow, 3424 aDragEvent = {} 3425 ) { 3426 if (!aDestWindow) { 3427 aDestWindow = window; 3428 } 3429 3430 var effect = aDataTransfer.dropEffect; 3431 var event; 3432 3433 if (aResult) { 3434 effect = "none"; 3435 } else if (effect != "none") { 3436 event = createDragEventObject( 3437 "drop", 3438 aDestElement, 3439 aDestWindow, 3440 aDataTransfer, 3441 aDragEvent 3442 ); 3443 sendDragEvent(event, aDestElement, aDestWindow); 3444 } 3445 // Don't run accessibility checks for this click, since we're not actually 3446 // clicking. It's just generated as part of the drop. 3447 // this.AccessibilityUtils might not be set if this isn't a browser test or 3448 // if a browser test has loaded its own copy of EventUtils for some reason. 3449 // In the latter case, the test probably shouldn't do that. 3450 this.AccessibilityUtils?.suppressClickHandling(true); 3451 synthesizeMouse(aDestElement, 2, 2, { type: "mouseup" }, aDestWindow); 3452 this.AccessibilityUtils?.suppressClickHandling(false); 3453 3454 return effect; 3455 } 3456 3457 /** 3458 * Calls `nsIDragService.startDragSessionForTests`, which is required before 3459 * any other code can use `nsIDOMWindowUtils.dragSession`. Most notably, 3460 * a drag session is required before populating a drag-drop event's 3461 * `dataTransfer` property. 3462 * 3463 * @param {Window} aWindow 3464 * @param {typeof DataTransfer.prototype.dropEffect} aDropEffect 3465 */ 3466 function startDragSession(aWindow, aDropEffect) { 3467 const ds = _EU_Cc["@mozilla.org/widget/dragservice;1"].getService( 3468 _EU_Ci.nsIDragService 3469 ); 3470 3471 let dropAction; 3472 switch (aDropEffect) { 3473 case null: 3474 case undefined: 3475 case "move": 3476 dropAction = _EU_Ci.nsIDragService.DRAGDROP_ACTION_MOVE; 3477 break; 3478 case "copy": 3479 dropAction = _EU_Ci.nsIDragService.DRAGDROP_ACTION_COPY; 3480 break; 3481 case "link": 3482 dropAction = _EU_Ci.nsIDragService.DRAGDROP_ACTION_LINK; 3483 break; 3484 default: 3485 throw new Error(`${aDropEffect} is an invalid drop effect value`); 3486 } 3487 3488 ds.startDragSessionForTests(aWindow, dropAction); 3489 } 3490 3491 /** 3492 * Emulate a drag and drop by emulating a dragstart and firing events dragenter, 3493 * dragover, and drop. 3494 * 3495 * @param {Element} aSrcElement 3496 * The element to use to start the drag. 3497 * @param {Element} aDestElement 3498 * The element to fire the dragover, dragenter events 3499 * @param {Array} aDragData 3500 * The data to supply for the data transfer. 3501 * This data is in the format: 3502 * 3503 * [ 3504 * [ 3505 * {"type": value, "data": value }, 3506 * ..., 3507 * ], 3508 * ... 3509 * ] 3510 * 3511 * Pass null to avoid modifying dataTransfer. 3512 * @param {string} [aDropEffect="move"] 3513 * The drop effect to set during the dragstart event, or 'move' if omitted.. 3514 * @param {DOMWindow} [aWindow=window] 3515 * The DOM window in which the drag happens. Defaults to the window in which 3516 * EventUtils.js is loaded. 3517 * @param {DOMWindow} [aDestWindow=aWindow] 3518 * Used when aDestElement is in a different DOM window than aSrcElement. 3519 * Default is to match ``aWindow``. 3520 * @param {object} [aDragEvent={}] 3521 * Defaults to empty object. Overwrites an object passed to sendDragEvent. 3522 * @return {string} 3523 * The drop effect that was desired. 3524 */ 3525 function synthesizeDrop( 3526 aSrcElement, 3527 aDestElement, 3528 aDragData, 3529 aDropEffect, 3530 aWindow, 3531 aDestWindow, 3532 aDragEvent = {} 3533 ) { 3534 if (!aWindow) { 3535 aWindow = window; 3536 } 3537 if (!aDestWindow) { 3538 aDestWindow = aWindow; 3539 } 3540 3541 startDragSession(aWindow, aDropEffect); 3542 3543 try { 3544 var [result, dataTransfer] = synthesizeDragOver( 3545 aSrcElement, 3546 aDestElement, 3547 aDragData, 3548 aDropEffect, 3549 aWindow, 3550 aDestWindow, 3551 aDragEvent 3552 ); 3553 return synthesizeDropAfterDragOver( 3554 result, 3555 dataTransfer, 3556 aDestElement, 3557 aDestWindow, 3558 aDragEvent 3559 ); 3560 } finally { 3561 let srcWindowUtils = _getDOMWindowUtils(aWindow); 3562 const srcDragSession = srcWindowUtils.dragSession; 3563 if (srcDragSession) { 3564 // After each event handler, there is a microtask checkpoint. 3565 // Event handlers or microtasks might've already ended our drag session. 3566 // E.g. in SubDialog.open during browser_toolbar_drop_bookmarklet.js 3567 srcDragSession.endDragSession(true, _parseModifiers(aDragEvent)); 3568 } 3569 } 3570 } 3571 3572 function _getFlattenedTreeParentNode(aNode) { 3573 return _EU_maybeUnwrap(_EU_maybeWrap(aNode).flattenedTreeParentNode); 3574 } 3575 3576 function _getInclusiveFlattenedTreeParentElement(aNode) { 3577 for ( 3578 let inclusiveAncestor = aNode; 3579 inclusiveAncestor; 3580 inclusiveAncestor = _getFlattenedTreeParentNode(inclusiveAncestor) 3581 ) { 3582 if (inclusiveAncestor.nodeType == Node.ELEMENT_NODE) { 3583 return inclusiveAncestor; 3584 } 3585 } 3586 return null; 3587 } 3588 3589 function _nodeIsFlattenedTreeDescendantOf( 3590 aPossibleDescendant, 3591 aPossibleAncestor 3592 ) { 3593 do { 3594 if (aPossibleDescendant == aPossibleAncestor) { 3595 return true; 3596 } 3597 aPossibleDescendant = _getFlattenedTreeParentNode(aPossibleDescendant); 3598 } while (aPossibleDescendant); 3599 return false; 3600 } 3601 3602 function _computeSrcElementFromSrcSelection(aSrcSelection) { 3603 let srcElement = _EU_maybeUnwrap( 3604 _EU_maybeWrap(aSrcSelection).mayCrossShadowBoundaryFocusNode 3605 ); 3606 while (_EU_maybeWrap(srcElement).isNativeAnonymous) { 3607 srcElement = _getFlattenedTreeParentNode(srcElement); 3608 } 3609 if (srcElement.nodeType !== Node.ELEMENT_NODE) { 3610 srcElement = _getInclusiveFlattenedTreeParentElement(srcElement); 3611 } 3612 return srcElement; 3613 } 3614 3615 /** 3616 * Emulate a drag and drop by emulating a dragstart by mousedown and mousemove, 3617 * and firing events dragenter, dragover, drop, and dragend. 3618 * This does not modify dataTransfer and tries to emulate the plain drag and 3619 * drop as much as possible, compared to synthesizeDrop. 3620 * Note that if synthesized dragstart is canceled, this throws an exception 3621 * because in such case, Gecko does not start drag session. 3622 * 3623 * @param {object} aParams 3624 * @param {Event} aParams.dragEvent 3625 * The DnD events will be generated with modifiers specified with this. 3626 * @param {Element} aParams.srcElement 3627 * The element to start dragging. If srcSelection is 3628 * set, this is computed for element at focus node. 3629 * @param {Selection|null} aParams.srcSelection 3630 * The selection to start to drag, set null if srcElement is set. 3631 * @param {Element|null} aParams.destElement 3632 * The element to drop on. Pass null to emulate a drop on an invalid target. 3633 * @param {number} aParams.srcX 3634 * The initial x coordinate inside srcElement or ignored if srcSelection is set. 3635 * @param {number} aParams.srcY 3636 * The initial y coordinate inside srcElement or ignored if srcSelection is set. 3637 * @param {number} aParams.stepX 3638 * The x-axis step for mousemove inside srcElement 3639 * @param {number} aParams.stepY 3640 * The y-axis step for mousemove inside srcElement 3641 * @param {number} aParams.finalX 3642 * The final x coordinate inside srcElement 3643 * @param {number} aParams.finalY 3644 * The final x coordinate inside srcElement 3645 * @param {Any} aParams.id 3646 * The pointer event id 3647 * @param {DOMWindow} aParams.srcWindow 3648 * The DOM window for dispatching event on srcElement, defaults to the current window object. 3649 * @param {DOMWindow} aParams.destWindow 3650 * The DOM window for dispatching event on destElement, defaults to the current window object. 3651 * @param {boolean} aParams.expectCancelDragStart 3652 * Set to true if the test cancels "dragstart" 3653 * @param {boolean} aParams.expectSrcElementDisconnected 3654 * Set to true if srcElement will be disconnected and 3655 * "dragend" event won't be fired. 3656 * @param {Function} aParams.logFunc 3657 * Set function which takes one argument if you need to log rect of target. E.g., `console.log`. 3658 */ 3659 // eslint-disable-next-line complexity 3660 async function synthesizePlainDragAndDrop(aParams) { 3661 let { 3662 dragEvent = {}, 3663 srcElement, 3664 srcSelection, 3665 destElement, 3666 srcX = 2, 3667 srcY = 2, 3668 stepX = 9, 3669 stepY = 9, 3670 finalX = srcX + stepX * 2, 3671 finalY = srcY + stepY * 2, 3672 id = _getDOMWindowUtils(window).DEFAULT_MOUSE_POINTER_ID, 3673 srcWindow = window, 3674 destWindow = window, 3675 expectCancelDragStart = false, 3676 expectSrcElementDisconnected = false, 3677 logFunc, 3678 } = aParams; 3679 // Don't modify given dragEvent object because we modify dragEvent below and 3680 // callers may use the object multiple times so that callers must not assume 3681 // that it'll be modified. 3682 if (aParams.dragEvent !== undefined) { 3683 dragEvent = Object.assign({}, aParams.dragEvent); 3684 } 3685 3686 function rectToString(aRect) { 3687 return `left: ${aRect.left}, top: ${aRect.top}, right: ${aRect.right}, bottom: ${aRect.bottom}`; 3688 } 3689 3690 let srcWindowUtils = _getDOMWindowUtils(srcWindow); 3691 let destWindowUtils = _getDOMWindowUtils(destWindow); 3692 3693 if (logFunc) { 3694 logFunc("synthesizePlainDragAndDrop() -- START"); 3695 } 3696 3697 if (srcSelection) { 3698 srcElement = _computeSrcElementFromSrcSelection(srcSelection); 3699 let srcElementRect = srcElement.getBoundingClientRect(); 3700 if (logFunc) { 3701 logFunc( 3702 `srcElement.getBoundingClientRect(): ${rectToString(srcElementRect)}` 3703 ); 3704 } 3705 // Use last selection client rect because nsIDragSession.sourceNode is 3706 // initialized from focus node which is usually in last rect. 3707 let selectionRectList = SpecialPowers.wrap( 3708 srcSelection.getRangeAt(0) 3709 ).getAllowCrossShadowBoundaryClientRects(); 3710 let lastSelectionRect = selectionRectList[selectionRectList.length - 1]; 3711 if (logFunc) { 3712 logFunc( 3713 `srcSelection.getRangeAt(0).getClientRects()[${ 3714 selectionRectList.length - 1 3715 }]: ${rectToString(lastSelectionRect)}` 3716 ); 3717 } 3718 // Click at center of last selection rect. 3719 srcX = Math.floor(lastSelectionRect.left + lastSelectionRect.width / 2); 3720 srcY = Math.floor(lastSelectionRect.top + lastSelectionRect.height / 2); 3721 // Then, adjust srcX and srcY for making them offset relative to 3722 // srcElementRect because they will be used when we call synthesizeMouse() 3723 // with srcElement. 3724 srcX = Math.floor(srcX - srcElementRect.left); 3725 srcY = Math.floor(srcY - srcElementRect.top); 3726 // Finally, recalculate finalX and finalY with new srcX and srcY if they 3727 // are not specified by the caller. 3728 if (aParams.finalX === undefined) { 3729 finalX = srcX + stepX * 2; 3730 } 3731 if (aParams.finalY === undefined) { 3732 finalY = srcY + stepY * 2; 3733 } 3734 } else if (logFunc) { 3735 logFunc( 3736 `srcElement.getBoundingClientRect(): ${rectToString( 3737 srcElement.getBoundingClientRect() 3738 )}` 3739 ); 3740 } 3741 3742 const editingHost = (() => { 3743 if (!srcElement.matches(":read-write")) { 3744 return null; 3745 } 3746 let lastEditableElement = srcElement; 3747 for ( 3748 let inclusiveAncestor = 3749 _getInclusiveFlattenedTreeParentElement(srcElement); 3750 inclusiveAncestor; 3751 inclusiveAncestor = _getInclusiveFlattenedTreeParentElement( 3752 _getFlattenedTreeParentNode(inclusiveAncestor) 3753 ) 3754 ) { 3755 if (inclusiveAncestor.matches(":read-write")) { 3756 lastEditableElement = inclusiveAncestor; 3757 if (lastEditableElement == srcElement.ownerDocument.body) { 3758 break; 3759 } 3760 } 3761 } 3762 return lastEditableElement; 3763 })(); 3764 try { 3765 srcWindowUtils.disableNonTestMouseEvents(true); 3766 3767 await new Promise(r => setTimeout(r, 0)); 3768 3769 let mouseDownEvent; 3770 function onMouseDown(aEvent) { 3771 mouseDownEvent = aEvent; 3772 if (logFunc) { 3773 logFunc( 3774 `"${aEvent.type}" event is fired on ${ 3775 aEvent.target 3776 } (composedTarget: ${_EU_maybeUnwrap( 3777 _EU_maybeWrap(aEvent).composedTarget 3778 )}` 3779 ); 3780 } 3781 if ( 3782 !_nodeIsFlattenedTreeDescendantOf( 3783 _EU_maybeUnwrap(_EU_maybeWrap(aEvent).composedTarget), 3784 srcElement 3785 ) 3786 ) { 3787 // If srcX and srcY does not point in one of rects in srcElement, 3788 // "mousedown" target is not in srcElement. Such case must not 3789 // be expected by this API users so that we should throw an exception 3790 // for making debugging easier. 3791 throw new Error( 3792 'event target of "mousedown" is not srcElement nor its descendant' 3793 ); 3794 } 3795 } 3796 try { 3797 srcWindow.addEventListener("mousedown", onMouseDown, { capture: true }); 3798 synthesizeMouse( 3799 srcElement, 3800 srcX, 3801 srcY, 3802 { type: "mousedown", id }, 3803 srcWindow 3804 ); 3805 if (logFunc) { 3806 logFunc(`mousedown at ${srcX}, ${srcY}`); 3807 } 3808 if (!mouseDownEvent) { 3809 throw new Error('"mousedown" event is not fired'); 3810 } 3811 } finally { 3812 srcWindow.removeEventListener("mousedown", onMouseDown, { 3813 capture: true, 3814 }); 3815 } 3816 3817 let dragStartEvent; 3818 function onDragStart(aEvent) { 3819 dragStartEvent = aEvent; 3820 if (logFunc) { 3821 logFunc(`"${aEvent.type}" event is fired`); 3822 } 3823 if ( 3824 !_nodeIsFlattenedTreeDescendantOf( 3825 _EU_maybeUnwrap(_EU_maybeWrap(aEvent).composedTarget), 3826 srcElement 3827 ) 3828 ) { 3829 // If srcX and srcY does not point in one of rects in srcElement, 3830 // "dragstart" target is not in srcElement. Such case must not 3831 // be expected by this API users so that we should throw an exception 3832 // for making debugging easier. 3833 throw new Error( 3834 'event target of "dragstart" is not srcElement nor its descendant' 3835 ); 3836 } 3837 } 3838 let dragEnterEvent; 3839 function onDragEnterGenerated(aEvent) { 3840 dragEnterEvent = aEvent; 3841 } 3842 srcWindow.addEventListener("dragstart", onDragStart, { capture: true }); 3843 srcWindow.addEventListener("dragenter", onDragEnterGenerated, { 3844 capture: true, 3845 }); 3846 try { 3847 // Wait for the next event tick after each event dispatch, so that UI 3848 // elements (e.g. menu) work like the real user input. 3849 await new Promise(r => setTimeout(r, 0)); 3850 3851 srcX += stepX; 3852 srcY += stepY; 3853 synthesizeMouse( 3854 srcElement, 3855 srcX, 3856 srcY, 3857 { type: "mousemove", id }, 3858 srcWindow 3859 ); 3860 if (logFunc) { 3861 logFunc(`first mousemove at ${srcX}, ${srcY}`); 3862 } 3863 3864 await new Promise(r => setTimeout(r, 0)); 3865 3866 srcX += stepX; 3867 srcY += stepY; 3868 synthesizeMouse( 3869 srcElement, 3870 srcX, 3871 srcY, 3872 { type: "mousemove", id }, 3873 srcWindow 3874 ); 3875 if (logFunc) { 3876 logFunc(`second mousemove at ${srcX}, ${srcY}`); 3877 } 3878 3879 await new Promise(r => setTimeout(r, 0)); 3880 3881 if (!dragStartEvent) { 3882 throw new Error('"dragstart" event is not fired'); 3883 } 3884 } finally { 3885 srcWindow.removeEventListener("dragstart", onDragStart, { 3886 capture: true, 3887 }); 3888 srcWindow.removeEventListener("dragenter", onDragEnterGenerated, { 3889 capture: true, 3890 }); 3891 } 3892 3893 let srcSession = srcWindowUtils.dragSession; 3894 if (!srcSession) { 3895 if (expectCancelDragStart) { 3896 synthesizeMouse( 3897 srcElement, 3898 finalX, 3899 finalY, 3900 { type: "mouseup", id }, 3901 srcWindow 3902 ); 3903 return; 3904 } 3905 throw new Error("drag hasn't been started by the operation"); 3906 } else if (expectCancelDragStart) { 3907 throw new Error("drag has been started by the operation"); 3908 } 3909 3910 if (destElement) { 3911 if ( 3912 (srcElement != destElement && !dragEnterEvent) || 3913 destElement != dragEnterEvent.target 3914 ) { 3915 if (logFunc) { 3916 logFunc( 3917 `destElement.getBoundingClientRect(): ${rectToString( 3918 destElement.getBoundingClientRect() 3919 )}` 3920 ); 3921 } 3922 3923 function onDragEnter(aEvent) { 3924 dragEnterEvent = aEvent; 3925 if (logFunc) { 3926 logFunc(`"${aEvent.type}" event is fired`); 3927 } 3928 if (aEvent.target != destElement) { 3929 throw new Error('event target of "dragenter" is not destElement'); 3930 } 3931 } 3932 destWindow.addEventListener("dragenter", onDragEnter, { 3933 capture: true, 3934 }); 3935 try { 3936 let event = createDragEventObject( 3937 "dragenter", 3938 destElement, 3939 destWindow, 3940 null, 3941 dragEvent 3942 ); 3943 sendDragEvent(event, destElement, destWindow); 3944 if (!dragEnterEvent && !destElement.disabled) { 3945 throw new Error('"dragenter" event is not fired'); 3946 } 3947 if (dragEnterEvent && destElement.disabled) { 3948 throw new Error( 3949 '"dragenter" event should not be fired on disable element' 3950 ); 3951 } 3952 } finally { 3953 destWindow.removeEventListener("dragenter", onDragEnter, { 3954 capture: true, 3955 }); 3956 } 3957 } 3958 3959 let dragOverEvent; 3960 function onDragOver(aEvent) { 3961 dragOverEvent = aEvent; 3962 if (logFunc) { 3963 logFunc(`"${aEvent.type}" event is fired`); 3964 } 3965 if (aEvent.target != destElement) { 3966 throw new Error('event target of "dragover" is not destElement'); 3967 } 3968 } 3969 destWindow.addEventListener("dragover", onDragOver, { capture: true }); 3970 try { 3971 // dragover and drop are only fired to a valid drop target. If the 3972 // destElement parameter is null, this function is being used to 3973 // simulate a drag'n'drop over an invalid drop target. 3974 let event = createDragEventObject( 3975 "dragover", 3976 destElement, 3977 destWindow, 3978 null, 3979 dragEvent 3980 ); 3981 sendDragEvent(event, destElement, destWindow); 3982 if (!dragOverEvent && !destElement.disabled) { 3983 throw new Error('"dragover" event is not fired'); 3984 } 3985 if (dragEnterEvent && destElement.disabled) { 3986 throw new Error( 3987 '"dragover" event should not be fired on disable element' 3988 ); 3989 } 3990 } finally { 3991 destWindow.removeEventListener("dragover", onDragOver, { 3992 capture: true, 3993 }); 3994 } 3995 3996 await new Promise(r => setTimeout(r, 0)); 3997 3998 // If there is not accept to drop the data, "drop" event shouldn't be 3999 // fired. 4000 // XXX nsIDragSession.canDrop is different only on Linux. It must be 4001 // a bug of gtk/nsDragService since it manages `mCanDrop` by itself. 4002 // Thus, we should use nsIDragSession.dragAction instead. 4003 let destSession = destWindowUtils.dragSession; 4004 if ( 4005 destSession.dragAction != _EU_Ci.nsIDragService.DRAGDROP_ACTION_NONE 4006 ) { 4007 let dropEvent; 4008 function onDrop(aEvent) { 4009 dropEvent = aEvent; 4010 if (logFunc) { 4011 logFunc(`"${aEvent.type}" event is fired`); 4012 } 4013 if ( 4014 !_nodeIsFlattenedTreeDescendantOf( 4015 _EU_maybeUnwrap(_EU_maybeWrap(aEvent).composedTarget), 4016 destElement 4017 ) 4018 ) { 4019 throw new Error( 4020 'event target of "drop" is not destElement nor its descendant' 4021 ); 4022 } 4023 } 4024 destWindow.addEventListener("drop", onDrop, { capture: true }); 4025 try { 4026 let event = createDragEventObject( 4027 "drop", 4028 destElement, 4029 destWindow, 4030 null, 4031 dragEvent 4032 ); 4033 sendDragEvent(event, destElement, destWindow); 4034 if (!dropEvent && destSession.canDrop) { 4035 throw new Error('"drop" event is not fired'); 4036 } 4037 } finally { 4038 destWindow.removeEventListener("drop", onDrop, { capture: true }); 4039 } 4040 return; 4041 } 4042 } 4043 4044 // Since we don't synthesize drop event, we need to set drag end point 4045 // explicitly for "dragEnd" event which will be fired by 4046 // endDragSession(). 4047 dragEvent.clientX = srcElement.getBoundingClientRect().x + finalX; 4048 dragEvent.clientY = srcElement.getBoundingClientRect().y + finalY; 4049 let event = createDragEventObject( 4050 "dragend", 4051 srcElement, 4052 srcWindow, 4053 null, 4054 dragEvent 4055 ); 4056 srcSession.setDragEndPointForTests(event.screenX, event.screenY); 4057 if (logFunc) { 4058 logFunc( 4059 `dragend event client (X,Y) = (${event.clientX}, ${event.clientY})` 4060 ); 4061 logFunc( 4062 `dragend event screen (X,Y) = (${event.screenX}, ${event.screenY})` 4063 ); 4064 } 4065 } finally { 4066 await new Promise(r => setTimeout(r, 0)); 4067 4068 if (srcWindowUtils.dragSession) { 4069 const sourceNode = srcWindowUtils.dragSession.sourceNode; 4070 let dragEndEvent; 4071 function onDragEnd(aEvent) { 4072 dragEndEvent = aEvent; 4073 if (logFunc) { 4074 logFunc(`"${aEvent.type}" event is fired`); 4075 } 4076 if ( 4077 !_nodeIsFlattenedTreeDescendantOf( 4078 _EU_maybeUnwrap(_EU_maybeWrap(aEvent).composedTarget), 4079 srcElement 4080 ) && 4081 _EU_maybeUnwrap(_EU_maybeWrap(aEvent).composedTarget) != editingHost 4082 ) { 4083 throw new Error( 4084 'event target of "dragend" is not srcElement nor its descendant' 4085 ); 4086 } 4087 if (expectSrcElementDisconnected) { 4088 throw new Error( 4089 `"dragend" event shouldn't be fired when the source node is disconnected (the source node is ${ 4090 sourceNode?.isConnected ? "connected" : "null or disconnected" 4091 })` 4092 ); 4093 } 4094 } 4095 srcWindow.addEventListener("dragend", onDragEnd, { capture: true }); 4096 try { 4097 srcWindowUtils.dragSession.endDragSession( 4098 true, 4099 _parseModifiers(dragEvent) 4100 ); 4101 if (!expectSrcElementDisconnected && !dragEndEvent) { 4102 // eslint-disable-next-line no-unsafe-finally 4103 throw new Error( 4104 `"dragend" event is not fired by nsIDragSession.endDragSession()${ 4105 srcWindowUtils.dragSession.sourceNode && 4106 !srcWindowUtils.dragSession.sourceNode.isConnected 4107 ? "(sourceNode was disconnected)" 4108 : "" 4109 }` 4110 ); 4111 } 4112 } finally { 4113 srcWindow.removeEventListener("dragend", onDragEnd, { capture: true }); 4114 } 4115 } 4116 srcWindowUtils.disableNonTestMouseEvents(false); 4117 if (logFunc) { 4118 logFunc("synthesizePlainDragAndDrop() -- END"); 4119 } 4120 } 4121 } 4122 4123 function _checkDataTransferItems(aDataTransfer, aExpectedDragData) { 4124 try { 4125 // We must wrap only in plain mochitests, not chrome 4126 let dataTransfer = _EU_maybeWrap(aDataTransfer); 4127 if (!dataTransfer) { 4128 return null; 4129 } 4130 if ( 4131 aExpectedDragData == null || 4132 dataTransfer.mozItemCount != aExpectedDragData.length 4133 ) { 4134 return dataTransfer; 4135 } 4136 for (let i = 0; i < dataTransfer.mozItemCount; i++) { 4137 let dtTypes = dataTransfer.mozTypesAt(i); 4138 if (dtTypes.length != aExpectedDragData[i].length) { 4139 return dataTransfer; 4140 } 4141 for (let j = 0; j < dtTypes.length; j++) { 4142 if (dtTypes[j] != aExpectedDragData[i][j].type) { 4143 return dataTransfer; 4144 } 4145 let dtData = dataTransfer.mozGetDataAt(dtTypes[j], i); 4146 if (aExpectedDragData[i][j].eqTest) { 4147 if ( 4148 !aExpectedDragData[i][j].eqTest( 4149 dtData, 4150 aExpectedDragData[i][j].data 4151 ) 4152 ) { 4153 return dataTransfer; 4154 } 4155 } else if (aExpectedDragData[i][j].data != dtData) { 4156 return dataTransfer; 4157 } 4158 } 4159 } 4160 } catch (ex) { 4161 return ex; 4162 } 4163 return true; 4164 } 4165 4166 /** 4167 * @typedef {(actualData: any, expectedData: any) -> boolean} eqTest 4168 * This callback type is used with ``synthesizePlainDragAndCancel()``. 4169 * It should compare ``actualData`` and ``expectedData`` and return 4170 * true if the two should be considered equal, false otherwise. 4171 */ 4172 4173 /** 4174 * synthesizePlainDragAndCancel() synthesizes drag start with 4175 * synthesizePlainDragAndDrop(), but always cancel it with preventing default 4176 * of "dragstart". Additionally, this checks whether the dataTransfer of 4177 * "dragstart" event has only expected items. 4178 * 4179 * @param {object} aParams 4180 * The params which is set to the argument of ``synthesizePlainDragAndDrop()``. 4181 * @param {Array} aExpectedDataTransferItems 4182 * All expected dataTransfer items. 4183 * This data is in the format: 4184 * 4185 * [ 4186 * [ 4187 * {"type": value, "data": value, "eqTest": eqTest} 4188 * ..., 4189 * ], 4190 * ... 4191 * ] 4192 * 4193 * This can also be null. 4194 * You can optionally provide ``eqTest`` if the 4195 * comparison to the expected data transfer items can't be done 4196 * with x == y; 4197 * @return {boolean} 4198 * true if aExpectedDataTransferItems matches with 4199 * DragEvent.dataTransfer of "dragstart" event. 4200 * Otherwise, the dataTransfer object (may be null) or 4201 * thrown exception, NOT false. Therefore, you shouldn't 4202 * use. 4203 */ 4204 async function synthesizePlainDragAndCancel( 4205 aParams, 4206 aExpectedDataTransferItems 4207 ) { 4208 let srcElement = aParams.srcSelection 4209 ? _computeSrcElementFromSrcSelection(aParams.srcSelection) 4210 : aParams.srcElement; 4211 let result; 4212 function onDragStart(aEvent) { 4213 aEvent.preventDefault(); 4214 result = _checkDataTransferItems( 4215 aEvent.dataTransfer, 4216 aExpectedDataTransferItems 4217 ); 4218 } 4219 SpecialPowers.wrap(srcElement.ownerDocument).addEventListener( 4220 "dragstart", 4221 onDragStart, 4222 { capture: true, mozSystemGroup: true } 4223 ); 4224 try { 4225 aParams.expectCancelDragStart = true; 4226 await synthesizePlainDragAndDrop(aParams); 4227 } finally { 4228 SpecialPowers.wrap(srcElement.ownerDocument).removeEventListener( 4229 "dragstart", 4230 onDragStart, 4231 { capture: true, mozSystemGroup: true } 4232 ); 4233 } 4234 return result; 4235 } 4236 4237 async function _synthesizeMockDndFromChild(aParams) { 4238 // Since we know that this is the (only) content process that will be involved 4239 // in the drag, we can set this for the caller. 4240 const ds = SpecialPowers.Cc["@mozilla.org/widget/dragservice;1"].getService( 4241 SpecialPowers.Ci.nsIDragService 4242 ); 4243 ds.neverAllowSessionIsSynthesizedForTests = true; 4244 4245 let sourceElt = document.getElementById(aParams.srcElement); 4246 let targetElt = document.getElementById(aParams.targetElement); 4247 4248 // The spawnChrome call below may return before the DND is complete since 4249 // the parent process will not synchronize with the child for child-initiated 4250 // drags. So we need to wait for the dragend here. If the drag motion is 4251 // expected to not result in a drag session then we wait for the mouseup 4252 // instead. 4253 let resolveEndPromise; 4254 let endPromise = new Promise(res => { 4255 resolveEndPromise = res; 4256 }); 4257 let endEvent = aParams.expectNoDragEvents ? "mouseup" : "dragend"; 4258 sourceElt.addEventListener( 4259 endEvent, 4260 () => { 4261 resolveEndPromise(); 4262 }, 4263 { once: true } 4264 ); 4265 4266 // The parent call will not get the element positions, so set 'sourceOffset' to 4267 // the screen coordinates for the drag start and 'targetOffset' for the screen 4268 // position to drag to. 4269 const scale = window.devicePixelRatio; 4270 let sourceOffset = [ 4271 (window.mozInnerScreenX + sourceElt.offsetLeft) * scale + 4272 aParams.sourceOffset[0], 4273 (window.mozInnerScreenY + sourceElt.offsetTop) * scale + 4274 aParams.sourceOffset[1], 4275 ]; 4276 let targetOffset = [ 4277 (window.mozInnerScreenX + targetElt.offsetLeft) * scale + 4278 aParams.targetOffset[0], 4279 (window.mozInnerScreenY + targetElt.offsetTop) * scale + 4280 aParams.targetOffset[1], 4281 ]; 4282 let params = { 4283 srcElement: aParams.srcElement, 4284 targetElement: aParams.targetElement, 4285 sourceOffset, 4286 targetOffset, 4287 step: aParams.step, 4288 expectCancelDragStart: aParams.expectCancelDragStart, 4289 cancel: aParams.cancel, 4290 expectSrcElementDisconnected: aParams.expectSrcElementDisconnected, 4291 expectDragLeave: aParams.expectDragLeave, 4292 expectNoDragEvents: aParams.expectNoDragEvents, 4293 expectNoDragTargetEvents: aParams.expectNoDragTargetEvents, 4294 contextLabel: aParams.contextLabel, 4295 throwOnExtraMessage: aParams.throwOnExtraMessage, 4296 }; 4297 4298 let record = 4299 aParams.record || 4300 ((cond, msg, _, stack) => { 4301 if (cond) { 4302 console.error(msg + "\n" + stack); 4303 } 4304 }); 4305 let info = aParams.info || console.log; 4306 4307 try { 4308 await SpecialPowers.spawnChrome([params], async _params => { 4309 let params = { 4310 sourceBrowsingCxt: browsingContext, 4311 targetBrowsingCxt: browsingContext, 4312 record: () => {}, 4313 info: () => {}, 4314 ..._params, 4315 }; 4316 await EventUtils.synthesizeMockDragAndDrop(params); 4317 }); 4318 await endPromise; 4319 } catch (ex) { 4320 // Display any exceptions since EventUtils does not propagate them. 4321 record( 4322 true, 4323 `Parent synthesizeMockDragAndDrop threw exception: ${ex}`, 4324 null, 4325 ex.stack 4326 ); 4327 } finally { 4328 info("Remote synthesizeMockDragAndDrop has completed."); 4329 } 4330 } 4331 4332 /** 4333 * Emulate a drag and drop by generating a dragstart from mousedown and mousemove, 4334 * then firing events dragover and drop (or dragleave if expectDragLeave is set). 4335 * This does not modify dataTransfer and tries to emulate the plain drag and 4336 * drop as much as possible, compared to synthesizeDrop and 4337 * synthesizePlainDragAndDrop. MockDragService is used in place of the native 4338 * nsIDragService implementation. All coordinates are in client space. 4339 * 4340 * This method can be called from the parent process, in which case it will 4341 * perform checks of DND internals (if 'record' is set). It can also be 4342 * called from content processes, in which case the drag is over the window 4343 * that is in context, and no checks of DND internals will occur. 4344 * 4345 * @param {object} aParams 4346 * @param {Window} aParams.sourceBrowsingCxt 4347 * The BrowsingContext (possibly remote) that contains 4348 * srcElement. Only set in parent process. 4349 * @param {Window} aParams.targetBrowsingCxt 4350 * The BrowsingContext (possibly remote) that contains 4351 * targetElement. Only set in parent process. 4352 * Default is sourceBrowsingCxt. 4353 * @param {Element} aParams.srcElement 4354 * ID of the element to drag. 4355 * @param {Element|null} aParams.targetElement 4356 * ID of the element to drop on. 4357 * @param {number} aParams.sourceOffset 4358 * The 2D offset from the source element at which the drag 4359 * starts. Default is [0,0]. 4360 * @param {number} aParams.targetOffset 4361 * The 2D offset from the target element at which the drag ends. 4362 * Default is [0,0]. 4363 * @param {number} aParams.step 4364 * The 2D step for intermediate dragging mousemoves. 4365 * Default is [5,5]. 4366 * @param {boolean} aParams.expectCancelDragStart 4367 * Set to true if srcElement is set up to cancel "dragstart" 4368 * @param {number} aParams.cancel 4369 * The 2D coord the mouse is moved to as the last step if 4370 * expectCancelDragStart is set 4371 * @param {boolean} aParams.expectSrcElementDisconnected 4372 * Set to true if srcElement will be disconnected and 4373 * "dragend" event won't be fired. 4374 * @param {boolean} aParams.expectDragLeave 4375 * Set to true if the drop event will be converted to a 4376 * dragleave before it is sent (e.g. it was rejected by a 4377 * content analysis check). 4378 * @param {boolean} aParams.expectNoDragEvents 4379 * Set to true if no mouse or drag events should be received 4380 * on the source or target. 4381 * @param {boolean} aParams.expectNoDragTargetEvents 4382 * Set to true if the drag should be blocked from sending 4383 * events to the target. 4384 * @param {boolean} aParams.dropPromise 4385 * A promise that the caller will resolve before we check 4386 * that the drop has happened. Default is a pre-resolved 4387 * promise. 4388 * @param {string} aParms.contextLabel 4389 * Label that will appear in each output message. Useful to 4390 * distinguish between concurrent calls. Default is none. 4391 * @param {boolean} aParams.throwOnExtraMessage 4392 * Throw an exception in child process when an unexpected 4393 * event is received. Used for debugging. Default is false. 4394 * @param {Function} aParams.record 4395 * Four-parameter function that logs the results of a remote 4396 * assertion. The parameters are (condition, message, ignored, 4397 * stack). This is the type of the mochitest report function. 4398 * Pass the empty function, or call this from content, to skip 4399 * testing of DND internals. 4400 * This parameter is required in the parent process and is 4401 * optional in content processes. 4402 * @param {Function} aParams.info 4403 * One-parameter info logging function. This is the type of 4404 * the mochitest info function. Pass the empty function, or 4405 * call this from content, to skip testing of DND internals. 4406 * This parameter is required in the parent process and is 4407 * optional in content processes. 4408 * @param {object} aParams.dragController 4409 * MockDragController that the function should use. This 4410 * function will automatically generate one if none is given. 4411 * This can only be set in the parent process. 4412 */ 4413 // eslint-disable-next-line complexity 4414 async function synthesizeMockDragAndDrop(aParams) { 4415 // eslint-disable-next-line mozilla/use-services 4416 let appinfo = _EU_Cc["@mozilla.org/xre/app-info;1"].getService( 4417 _EU_Ci.nsIXULRuntime 4418 ); 4419 if (appinfo.processType !== appinfo.PROCESS_TYPE_DEFAULT) { 4420 await _synthesizeMockDndFromChild(aParams); 4421 return; 4422 } 4423 4424 const { 4425 srcElement, 4426 targetElement, 4427 sourceOffset = [0, 0], 4428 targetOffset = [0, 0], 4429 step = [5, 5], 4430 cancel = [0, 0], 4431 sourceBrowsingCxt, 4432 targetBrowsingCxt = sourceBrowsingCxt, 4433 expectCancelDragStart = false, 4434 expectSrcElementDisconnected = false, 4435 expectDragLeave = false, 4436 expectNoDragEvents = false, 4437 dropPromise = Promise.resolve(undefined), 4438 contextLabel = "", 4439 throwOnExtraMessage = false, 4440 } = aParams; 4441 4442 let { dragController = null, expectNoDragTargetEvents = false } = aParams; 4443 4444 // Configure test reporting functions 4445 const prefix = contextLabel ? `[${contextLabel}]| ` : ""; 4446 const info = msg => { 4447 aParams.info(`${prefix}${msg}`); 4448 }; 4449 const record = (cond, msg, _, stack) => { 4450 aParams.record(cond, `${prefix}${msg}`, null, stack); 4451 }; 4452 const ok = (cond, msg) => { 4453 record(cond, msg, null, Components.stack.caller); 4454 }; 4455 4456 info("synthesizeMockDragAndDrop() -- START"); 4457 4458 // Validate parameters 4459 ok(sourceBrowsingCxt, "sourceBrowsingCxt was given"); 4460 ok( 4461 sourceBrowsingCxt != targetBrowsingCxt || srcElement != targetElement, 4462 "sourceBrowsingCxt+Element cannot be the same as targetBrowsingCxt+Element" 4463 ); 4464 4465 // no drag implies no drag target 4466 expectNoDragTargetEvents |= expectNoDragEvents; 4467 4468 // Returns true if one browsing context is an ancestor of the other. 4469 let browsingContextsAreRelated = function (cxt1, cxt2) { 4470 return cxt1.top == cxt2.top; 4471 }; 4472 4473 // The rules for accessing the dataTransfer from internal drags in Gecko 4474 // during drag event handlers are as follows: 4475 // 4476 // dragstart: 4477 // Always grants read-write access 4478 // dragenter/dragover/dragleave: 4479 // If dom.events.dataTransfer.protected.enabled is set: 4480 // Read-only permission is granted if any of these holds: 4481 // * The drag target's browsing context is the same as the drag 4482 // source's (e.g. dragging inside of one frame on a web page). 4483 // * The drag source and target are the same domain/principal and 4484 // one has a browsing context that is an ancestor of the other 4485 // (e.g. one is an iframe nested inside of the other). 4486 // * The principal of the drag target element is privileged (not 4487 // a content principal). 4488 // Otherwise: 4489 // Permission is never granted 4490 // drop: 4491 // Always grants read-only permission 4492 // dragend: 4493 // Read-only permission is granted if 4494 // dom.events.dataTransfer.protected.enabled is set. 4495 // 4496 // dragstart and dragend are special because they target the drag-source, 4497 // not the drag-target. 4498 // eslint-disable-next-line mozilla/use-services 4499 let prefs = _EU_Cc["@mozilla.org/preferences-service;1"].getService( 4500 Ci.nsIPrefBranch 4501 ); 4502 let expectProtectedDataTransferAccessSource = !prefs.getBoolPref( 4503 "dom.events.dataTransfer.protected.enabled" 4504 ); 4505 let expectProtectedDataTransferAccessTarget = 4506 expectProtectedDataTransferAccessSource && 4507 browsingContextsAreRelated(targetBrowsingCxt, sourceBrowsingCxt); 4508 4509 info( 4510 `expectProtectedDataTransferAccessSource: ${expectProtectedDataTransferAccessSource}` 4511 ); 4512 info( 4513 `expectProtectedDataTransferAccessTarget: ${expectProtectedDataTransferAccessTarget}` 4514 ); 4515 4516 // Essentially the entire function is in a try block so that we can make sure 4517 // that the mock drag service is removed and non-test mouse events are 4518 // restored. 4519 const { MockRegistrar } = ChromeUtils.importESModule( 4520 "resource://testing-common/MockRegistrar.sys.mjs" 4521 ); 4522 let dragServiceCid; 4523 let sourceCxt; 4524 let targetCxt; 4525 let srcWindowUtils = _getDOMWindowUtils(sourceBrowsingCxt.ownerGlobal); 4526 let targetWindowUtils = _getDOMWindowUtils(targetBrowsingCxt.ownerGlobal); 4527 4528 try { 4529 // Disable native mouse events to avoid external interference while the test 4530 // runs. One call disables for all windows. 4531 if (srcWindowUtils) { 4532 srcWindowUtils.disableNonTestMouseEvents(true); 4533 } 4534 if (targetWindowUtils) { 4535 targetWindowUtils.disableNonTestMouseEvents(true); 4536 } 4537 4538 // Install mock drag service. 4539 if (!dragController) { 4540 info("No dragController was given so creating mock drag service"); 4541 const oldDragService = _EU_Cc[ 4542 "@mozilla.org/widget/dragservice;1" 4543 ].getService(_EU_Ci.nsIDragService); 4544 dragController = oldDragService.getMockDragController(); 4545 dragServiceCid = MockRegistrar.register( 4546 "@mozilla.org/widget/dragservice;1", 4547 dragController.mockDragService 4548 ); 4549 ok(dragServiceCid, "MockDragService was registered"); 4550 // If the mock failed then don't continue or else we will trigger native 4551 // DND behavior. 4552 if (!dragServiceCid) { 4553 throw new Error("MockDragService failed to register"); 4554 } 4555 } 4556 4557 const mockDragService = _EU_Cc[ 4558 "@mozilla.org/widget/dragservice;1" 4559 ].getService(_EU_Ci.nsIDragService); 4560 4561 let runChecks = globalThis.hasOwnProperty("SpecialPowers"); 4562 if (runChecks) { 4563 // Variables that are added to the child actor objects. 4564 const srcVars = { 4565 expectCancelDragStart, 4566 expectSrcElementDisconnected, 4567 expectNoDragEvents, 4568 expectProtectedDataTransferAccess: 4569 expectProtectedDataTransferAccessSource, 4570 dragElementId: srcElement, 4571 }; 4572 const targetVars = { 4573 expectDragLeave, 4574 expectNoDragTargetEvents, 4575 expectProtectedDataTransferAccess: 4576 expectProtectedDataTransferAccessTarget, 4577 dragElementId: targetElement, 4578 }; 4579 const bothVars = { 4580 contextLabel, 4581 throwOnExtraMessage, 4582 relevantEvents: [ 4583 "mousedown", 4584 "mouseup", 4585 "dragstart", 4586 "dragenter", 4587 "dragover", 4588 "drop", 4589 "dragleave", 4590 "dragend", 4591 ], 4592 }; 4593 4594 const makeDragSourceContext = async (aBC, aRemoteVars) => { 4595 let { DragSourceParentContext } = ChromeUtils.importESModule( 4596 "chrome://mochikit/content/tests/SimpleTest/DragSourceParentContext.sys.mjs" 4597 ); 4598 4599 let ret = new DragSourceParentContext(aBC, aRemoteVars, SpecialPowers); 4600 await ret.initialize(); 4601 return ret; 4602 }; 4603 4604 const makeDragTargetContext = async (aBC, aRemoteVars) => { 4605 let { DragTargetParentContext } = ChromeUtils.importESModule( 4606 "chrome://mochikit/content/tests/SimpleTest/DragTargetParentContext.sys.mjs" 4607 ); 4608 4609 let ret = new DragTargetParentContext(aBC, aRemoteVars, SpecialPowers); 4610 await ret.initialize(); 4611 return ret; 4612 }; 4613 4614 [sourceCxt, targetCxt] = await Promise.all([ 4615 makeDragSourceContext(sourceBrowsingCxt, { ...srcVars, ...bothVars }), 4616 makeDragTargetContext(targetBrowsingCxt, { 4617 ...targetVars, 4618 ...bothVars, 4619 }), 4620 ]); 4621 } else { 4622 // We don't have SpecialPowers so we cannot perform DND checks. Make 4623 // them empty functions. 4624 info("synthesizeMockDragAndDrop will skip DND checks"); 4625 let dragParentBaseCxt = { 4626 expect: () => {}, 4627 checkExpected: () => {}, 4628 checkHasDrag: () => {}, 4629 checkSessionHasAction: () => {}, 4630 synchronize: () => {}, 4631 cleanup: () => {}, 4632 }; 4633 sourceCxt = { 4634 getElementPositions: () => { 4635 return { screenPos: [0, 0] }; 4636 }, 4637 checkMouseDown: () => {}, 4638 checkDragStart: () => {}, 4639 checkDragEnd: () => {}, 4640 ...dragParentBaseCxt, 4641 }; 4642 targetCxt = { 4643 getElementPositions: () => { 4644 return { screenPos: [0, 0] }; 4645 }, 4646 checkDropOrDragLeave: () => {}, 4647 ...dragParentBaseCxt, 4648 }; 4649 } 4650 4651 // Get element positions in screen coords 4652 let add2d = (a, b) => { 4653 return [a[0] + b[0], a[1] + b[1]]; 4654 }; 4655 let srcPos = add2d( 4656 (await sourceCxt.getElementPositions()).screenPos, 4657 sourceOffset 4658 ); 4659 let targetPos = add2d( 4660 (await targetCxt.getElementPositions()).screenPos, 4661 targetOffset 4662 ); 4663 info(`screenSrcPos: ${srcPos} | screenTargetPos: ${targetPos}`); 4664 4665 // Send and verify the mousedown on src. 4666 if (!expectNoDragEvents) { 4667 sourceCxt.expect("mousedown"); 4668 } 4669 4670 // Take ceiling of ccoordinates to make sure that the integer coordinates 4671 // are over the element. 4672 let currentSrcScreenPos = [Math.ceil(srcPos[0]), Math.ceil(srcPos[1])]; 4673 info( 4674 `sending mousedown at ${currentSrcScreenPos[0]}, ${currentSrcScreenPos[1]}` 4675 ); 4676 dragController.sendEvent( 4677 sourceBrowsingCxt, 4678 Ci.nsIMockDragServiceController.eMouseDown, 4679 currentSrcScreenPos[0], 4680 currentSrcScreenPos[1] 4681 ); 4682 info(`mousedown sent`); 4683 4684 await sourceCxt.synchronize(); 4685 4686 await sourceCxt.checkMouseDown(); 4687 4688 let contentInvokedDragPromise; 4689 4690 info("setting up content-invoked-drag observer and expecting dragstart"); 4691 if (!expectNoDragEvents) { 4692 sourceCxt.expect("dragstart"); 4693 // Set up observable for content-invoked-drag, which is sent when the 4694 // parent learns that content has begun a drag session. 4695 contentInvokedDragPromise = new Promise(cb => { 4696 Services.obs.addObserver(function observe() { 4697 info("content-invoked-drag observer received message"); 4698 Services.obs.removeObserver(observe, "content-invoked-drag"); 4699 cb(); 4700 }, "content-invoked-drag"); 4701 }); 4702 } 4703 4704 // It takes two mouse-moves to initiate a drag session. 4705 currentSrcScreenPos = [ 4706 currentSrcScreenPos[0] + step[0], 4707 currentSrcScreenPos[1] + step[1], 4708 ]; 4709 info( 4710 `first mousemove at ${currentSrcScreenPos[0]}, ${currentSrcScreenPos[1]}` 4711 ); 4712 dragController.sendEvent( 4713 sourceBrowsingCxt, 4714 Ci.nsIMockDragServiceController.eMouseMove, 4715 currentSrcScreenPos[0], 4716 currentSrcScreenPos[1] 4717 ); 4718 info(`first mousemove sent`); 4719 4720 currentSrcScreenPos = [ 4721 currentSrcScreenPos[0] + step[0], 4722 currentSrcScreenPos[1] + step[1], 4723 ]; 4724 info( 4725 `second mousemove at ${currentSrcScreenPos[0]}, ${currentSrcScreenPos[1]}` 4726 ); 4727 dragController.sendEvent( 4728 sourceBrowsingCxt, 4729 Ci.nsIMockDragServiceController.eMouseMove, 4730 currentSrcScreenPos[0], 4731 currentSrcScreenPos[1] 4732 ); 4733 info(`second mousemove sent`); 4734 4735 if (!expectNoDragEvents) { 4736 info("waiting for content-invoked-drag observable"); 4737 await contentInvokedDragPromise; 4738 ok(true, "content-invoked-drag was received"); 4739 } 4740 4741 info("checking dragstart"); 4742 await sourceCxt.checkDragStart(); 4743 4744 if (expectNoDragEvents) { 4745 ok( 4746 !mockDragService.getCurrentSession(), 4747 "Drag was properly blocked from starting." 4748 ); 4749 dragController.sendEvent( 4750 sourceBrowsingCxt, 4751 Ci.nsIMockDragServiceController.eMouseUp, 4752 cancel[0], 4753 cancel[1] 4754 ); 4755 return; 4756 } 4757 4758 // Another move creates the drag session in the parent process (but we need 4759 // to wait for the src process to get there). 4760 currentSrcScreenPos = [ 4761 currentSrcScreenPos[0] + step[0], 4762 currentSrcScreenPos[1] + step[1], 4763 ]; 4764 info( 4765 `third mousemove at ${currentSrcScreenPos[0]}, ${currentSrcScreenPos[1]}` 4766 ); 4767 dragController.sendEvent( 4768 sourceBrowsingCxt, 4769 Ci.nsIMockDragServiceController.eMouseMove, 4770 currentSrcScreenPos[0], 4771 currentSrcScreenPos[1] 4772 ); 4773 info(`third mousemove sent`); 4774 4775 ok(mockDragService.getCurrentSession(), `Parent process has drag session.`); 4776 4777 if (expectCancelDragStart) { 4778 dragController.sendEvent( 4779 sourceBrowsingCxt, 4780 Ci.nsIMockDragServiceController.eMouseUp, 4781 cancel[0], 4782 cancel[1] 4783 ); 4784 return; 4785 } 4786 4787 await sourceCxt.checkExpected(); 4788 4789 // Implementation detail: EventStateManager::GenerateDragDropEnterExit 4790 // expects the source to get at least one dragover before leaving the 4791 // widget or else it fails to send dragenter/dragleave events to the 4792 // browsers. 4793 info("synthesizing dragover inside source"); 4794 sourceCxt.expect("dragenter"); 4795 sourceCxt.expect("dragover"); 4796 currentSrcScreenPos = [ 4797 currentSrcScreenPos[0] + step[0], 4798 currentSrcScreenPos[1] + step[1], 4799 ]; 4800 info(`dragover at ${currentSrcScreenPos[0]}, ${currentSrcScreenPos[1]}`); 4801 dragController.sendEvent( 4802 sourceBrowsingCxt, 4803 Ci.nsIMockDragServiceController.eDragOver, 4804 currentSrcScreenPos[0], 4805 currentSrcScreenPos[1] 4806 ); 4807 4808 info(`dragover sent`); 4809 await sourceCxt.checkExpected(); 4810 4811 let currentTargetScreenPos = [ 4812 Math.ceil(targetPos[0]), 4813 Math.ceil(targetPos[1]), 4814 ]; 4815 4816 // The next step is to drag to the target element. 4817 if (!expectNoDragTargetEvents) { 4818 sourceCxt.expect("dragleave"); 4819 } 4820 4821 if ( 4822 sourceBrowsingCxt.top.embedderElement !== 4823 targetBrowsingCxt.top.embedderElement 4824 ) { 4825 // Send dragexit and dragenter only if we are dragging to another widget. 4826 // If we are dragging in the same widget then dragenter does not involve 4827 // the parent process. This mirrors the native behavior. In the 4828 // widget-to-widget case, the source gets the dragexit immediately but 4829 // the target won't get a dragenter in content until we send a dragover -- 4830 // this is because dragenters are generated by the EventStateManager and 4831 // are not forwarded remotely. 4832 // NB: dragleaves are synthesized by Gecko from dragexits. 4833 info("synthesizing dragexit and dragenter to enter new widget"); 4834 if (!expectNoDragTargetEvents) { 4835 info("This will generate dragleave on the source"); 4836 } 4837 4838 dragController.sendEvent( 4839 sourceBrowsingCxt, 4840 Ci.nsIMockDragServiceController.eDragExit, 4841 currentTargetScreenPos[0], 4842 currentTargetScreenPos[1] 4843 ); 4844 4845 dragController.sendEvent( 4846 targetBrowsingCxt, 4847 Ci.nsIMockDragServiceController.eDragEnter, 4848 currentTargetScreenPos[0], 4849 currentTargetScreenPos[1] 4850 ); 4851 4852 await sourceCxt.synchronize(); 4853 4854 await sourceCxt.checkExpected(); 4855 await targetCxt.checkExpected(); 4856 } 4857 4858 info( 4859 "Synthesizing dragover over target. This will first generate a dragenter." 4860 ); 4861 if (!expectNoDragTargetEvents) { 4862 targetCxt.expect("dragenter"); 4863 targetCxt.expect("dragover"); 4864 } 4865 4866 dragController.sendEvent( 4867 targetBrowsingCxt, 4868 Ci.nsIMockDragServiceController.eDragOver, 4869 currentTargetScreenPos[0], 4870 currentTargetScreenPos[1] 4871 ); 4872 4873 await targetCxt.checkExpected(); 4874 4875 let expectedMessage = expectDragLeave ? "dragleave" : "drop"; 4876 4877 if (expectNoDragTargetEvents) { 4878 await targetCxt.checkHasDrag(false); 4879 } else { 4880 await targetCxt.checkSessionHasAction(); 4881 targetCxt.expect(expectedMessage); 4882 } 4883 4884 if (!expectSrcElementDisconnected) { 4885 await sourceCxt.checkHasDrag(true); 4886 sourceCxt.expect("dragend"); 4887 } 4888 4889 info( 4890 `issuing drop event that should be ` + 4891 `${ 4892 !expectNoDragTargetEvents 4893 ? `received as a ${expectedMessage} event` 4894 : "ignored" 4895 }, followed by a dragend event` 4896 ); 4897 4898 currentTargetScreenPos = [ 4899 currentTargetScreenPos[0] + step[0], 4900 currentTargetScreenPos[1] + step[1], 4901 ]; 4902 dragController.sendEvent( 4903 targetBrowsingCxt, 4904 Ci.nsIMockDragServiceController.eDrop, 4905 currentTargetScreenPos[0], 4906 currentTargetScreenPos[1] 4907 ); 4908 4909 // Wait for any caller-supplied dropPromise before continuing. 4910 await dropPromise; 4911 4912 if (!expectNoDragTargetEvents) { 4913 await targetCxt.checkDropOrDragLeave(); 4914 } else { 4915 await targetCxt.checkExpected(); 4916 } 4917 4918 if (!expectSrcElementDisconnected) { 4919 await sourceCxt.checkDragEnd(); 4920 } else { 4921 await sourceCxt.checkExpected(); 4922 } 4923 4924 ok( 4925 !mockDragService.getCurrentSession(), 4926 `Parent process does not have a drag session.` 4927 ); 4928 } catch (e) { 4929 // Any exception is a test failure. 4930 record(false, e.toString(), null, e.stack); 4931 throw e; 4932 } finally { 4933 if (sourceCxt) { 4934 await sourceCxt.cleanup(); 4935 } 4936 if (targetCxt) { 4937 await targetCxt.cleanup(); 4938 } 4939 4940 if (dragServiceCid) { 4941 MockRegistrar.unregister(dragServiceCid); 4942 } 4943 4944 if (srcWindowUtils) { 4945 srcWindowUtils.disableNonTestMouseEvents(false); 4946 } 4947 if (targetWindowUtils) { 4948 targetWindowUtils.disableNonTestMouseEvents(false); 4949 } 4950 4951 info("synthesizeMockDragAndDrop() -- END"); 4952 } 4953 } 4954 4955 class EventCounter { 4956 constructor(aTarget, aType, aOptions = {}) { 4957 this.target = aTarget; 4958 this.type = aType; 4959 this.options = aOptions; 4960 4961 this.eventCount = 0; 4962 // Bug 1512817: 4963 // SpecialPowers is picky and needs to be passed an explicit reference to 4964 // the function to be called. To avoid having to bind "this", we therefore 4965 // define the method this way, via a property. 4966 this.handleEvent = () => { 4967 this.eventCount++; 4968 }; 4969 4970 SpecialPowers.wrap(aTarget).addEventListener( 4971 aType, 4972 this.handleEvent, 4973 aOptions 4974 ); 4975 } 4976 4977 unregister() { 4978 SpecialPowers.wrap(this.target).removeEventListener( 4979 this.type, 4980 this.handleEvent, 4981 this.options 4982 ); 4983 } 4984 4985 get count() { 4986 return this.eventCount; 4987 } 4988 }