apz_test_native_event_utils.js (62405B)
1 // ownerGlobal isn't defined in content privileged windows. 2 /* eslint-disable mozilla/use-ownerGlobal */ 3 4 // Utilities for synthesizing of native events. 5 6 async function getResolution() { 7 let resolution = -1; // bogus value in case DWU fails us 8 // Use window.top to get the root content window which is what has 9 // the resolution. 10 resolution = await SpecialPowers.spawn(window.top, [], () => { 11 return SpecialPowers.getDOMWindowUtils(content.window).getResolution(); 12 }); 13 return resolution; 14 } 15 16 function getPlatform() { 17 if (navigator.platform.indexOf("Win") == 0) { 18 return "windows"; 19 } 20 if (navigator.platform.indexOf("Mac") == 0) { 21 return "mac"; 22 } 23 // Check for Android before Linux 24 if (navigator.appVersion.includes("Android")) { 25 return "android"; 26 } 27 if (navigator.platform.indexOf("Linux") == 0) { 28 return "linux"; 29 } 30 return "unknown"; 31 } 32 33 function nativeVerticalWheelEventMsg() { 34 switch (getPlatform()) { 35 case "windows": 36 return 0x020a; // WM_MOUSEWHEEL 37 case "mac": 38 var useWheelCodepath = SpecialPowers.getBoolPref( 39 "apz.test.mac.synth_wheel_input", 40 false 41 ); 42 // Default to 1 (kCGScrollPhaseBegan) to trigger PanGestureInput events 43 // from widget code. Allow setting a pref to override this behaviour and 44 // trigger ScrollWheelInput events instead. 45 return useWheelCodepath ? 0 : 1; 46 case "linux": 47 return 4; // value is unused, pass GDK_SCROLL_SMOOTH anyway 48 } 49 throw new Error( 50 "Native wheel events not supported on platform " + getPlatform() 51 ); 52 } 53 54 function nativeHorizontalWheelEventMsg() { 55 switch (getPlatform()) { 56 case "windows": 57 return 0x020e; // WM_MOUSEHWHEEL 58 case "mac": 59 return 0; // value is unused, can be anything 60 case "linux": 61 return 4; // value is unused, pass GDK_SCROLL_SMOOTH anyway 62 } 63 throw new Error( 64 "Native wheel events not supported on platform " + getPlatform() 65 ); 66 } 67 68 function nativeArrowDownKey() { 69 switch (getPlatform()) { 70 case "windows": 71 return WIN_VK_DOWN; 72 case "mac": 73 return MAC_VK_DownArrow; 74 } 75 throw new Error( 76 "Native key events not supported on platform " + getPlatform() 77 ); 78 } 79 80 function nativeArrowUpKey() { 81 switch (getPlatform()) { 82 case "windows": 83 return WIN_VK_UP; 84 case "mac": 85 return MAC_VK_UpArrow; 86 } 87 throw new Error( 88 "Native key events not supported on platform " + getPlatform() 89 ); 90 } 91 92 function targetIsWindow(aTarget) { 93 return aTarget.Window && aTarget instanceof aTarget.Window; 94 } 95 96 function targetIsTopWindow(aTarget) { 97 if (!targetIsWindow(aTarget)) { 98 return false; 99 } 100 return aTarget == aTarget.top; 101 } 102 103 // Given an event target which may be a window or an element, get the associated window. 104 function windowForTarget(aTarget) { 105 if (targetIsWindow(aTarget)) { 106 return aTarget; 107 } 108 return aTarget.ownerDocument.defaultView; 109 } 110 111 // Given an event target which may be a window or an element, get the associated element. 112 function elementForTarget(aTarget) { 113 if (targetIsWindow(aTarget)) { 114 return aTarget.document.documentElement; 115 } 116 return aTarget; 117 } 118 119 // Given an event target which may be a window or an element, get the associatd nsIDOMWindowUtils. 120 function utilsForTarget(aTarget) { 121 return SpecialPowers.getDOMWindowUtils(windowForTarget(aTarget)); 122 } 123 124 // Given a pixel scrolling delta, converts it to the platform's native units. 125 function nativeScrollUnits(aTarget, aDimen) { 126 switch (getPlatform()) { 127 case "linux": { 128 // GTK deltas are treated as line height divided by 3 by gecko. 129 var targetWindow = windowForTarget(aTarget); 130 var targetElement = elementForTarget(aTarget); 131 var lineHeight = 132 targetWindow.getComputedStyle(targetElement)["font-size"]; 133 return aDimen / (parseInt(lineHeight) * 3); 134 } 135 } 136 return aDimen; 137 } 138 139 function parseNativeModifiers(aModifiers, aWindow = window) { 140 let modifiers = 0; 141 if (aModifiers.capsLockKey) { 142 modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CAPS_LOCK; 143 } 144 if (aModifiers.numLockKey) { 145 modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_NUM_LOCK; 146 } 147 if (aModifiers.shiftKey) { 148 modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_SHIFT_LEFT; 149 } 150 if (aModifiers.shiftRightKey) { 151 modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_SHIFT_RIGHT; 152 } 153 if (aModifiers.ctrlKey) { 154 modifiers |= 155 SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CONTROL_LEFT; 156 } 157 if (aModifiers.ctrlRightKey) { 158 modifiers |= 159 SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CONTROL_RIGHT; 160 } 161 if (aModifiers.altKey) { 162 modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_ALT_LEFT; 163 } 164 if (aModifiers.altRightKey) { 165 modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_ALT_RIGHT; 166 } 167 if (aModifiers.metaKey) { 168 modifiers |= 169 SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_COMMAND_LEFT; 170 } 171 if (aModifiers.metaRightKey) { 172 modifiers |= 173 SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_COMMAND_RIGHT; 174 } 175 if (aModifiers.helpKey) { 176 modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_HELP; 177 } 178 if (aModifiers.fnKey) { 179 modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_FUNCTION; 180 } 181 if (aModifiers.numericKeyPadKey) { 182 modifiers |= 183 SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_NUMERIC_KEY_PAD; 184 } 185 186 if (aModifiers.accelKey) { 187 modifiers |= _EU_isMac(aWindow) 188 ? SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_COMMAND_LEFT 189 : SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CONTROL_LEFT; 190 } 191 if (aModifiers.accelRightKey) { 192 modifiers |= _EU_isMac(aWindow) 193 ? SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_COMMAND_RIGHT 194 : SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CONTROL_RIGHT; 195 } 196 if (aModifiers.altGrKey) { 197 modifiers |= _EU_isMac(aWindow) 198 ? SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_ALT_LEFT 199 : SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_ALT_GRAPH; 200 } 201 return modifiers; 202 } 203 204 // Several event sythesization functions below (and their helpers) take a "target" 205 // parameter which may be either an element or a window. For such functions, 206 // the target's "bounding rect" refers to the bounding client rect for an element, 207 // and the window's origin for a window. 208 // Not all functions have been "upgraded" to allow a window argument yet; feel 209 // free to upgrade others as necessary. 210 211 // Get the origin of |aTarget| relative to the root content document's 212 // visual viewport in CSS coordinates. 213 // |aTarget| may be an element (contained in the root content document or 214 // a subdocument) or, as a special case, the root content window. 215 // FIXME: Support iframe windows as targets. 216 function _getTargetRect(aTarget, atCenter) { 217 let rect = { left: 0, top: 0, width: 0, height: 0 }; 218 219 aTarget = SpecialPowers.wrap(aTarget); 220 let containingWindow = null; 221 if ( 222 aTarget instanceof Window || 223 (aTarget.Window && aTarget instanceof aTarget.Window) 224 ) { 225 // If the target is the root content window, its origin relative 226 // to the visual viewport is (0, 0). 227 228 // FIXME: Compute proper rect against the root content window 229 230 // leave rect as all 0's. The top/left is correct, but the width/height is 231 // not necessarily correct, just assert that we are not sending event to the 232 // center of the target so that we are not using the width/height. 233 ok(!atCenter, "atCenter not supported with window targets, todo"); 234 containingWindow = aTarget; 235 } else { 236 // Otherwise, we have an element. Start with the origin of 237 // its bounding client rect which is relative to the enclosing 238 // document's layout viewport. Note that for iframes, the 239 // layout viewport is also the visual viewport. 240 241 const boundingClientRect = aTarget.getBoundingClientRect(); 242 rect.left = boundingClientRect.left; 243 rect.top = boundingClientRect.top; 244 rect.width = boundingClientRect.width; 245 rect.height = boundingClientRect.height; 246 containingWindow = aTarget.ownerDocument.defaultView; 247 } 248 249 // Iterate up the window hierarchy until we reach the root 250 // content window, adding the offsets of any iframe windows 251 // relative to their parent window. 252 while (containingWindow.browsingContext.embedderElement) { 253 const iframe = containingWindow.browsingContext.embedderElement; 254 // The offset of the iframe window relative to the parent window 255 // includes the iframe's border, and the iframe's origin in its 256 // containing document. 257 const style = iframe.ownerDocument.defaultView.getComputedStyle(iframe); 258 const borderLeft = parseFloat(style.borderLeftWidth) || 0; 259 const borderTop = parseFloat(style.borderTopWidth) || 0; 260 const borderRight = parseFloat(style.borderRightWidth) || 0; 261 const borderBottom = parseFloat(style.borderBottomWidth) || 0; 262 const paddingLeft = parseFloat(style.paddingLeft) || 0; 263 const paddingTop = parseFloat(style.paddingTop) || 0; 264 const paddingRight = parseFloat(style.paddingRight) || 0; 265 const paddingBottom = parseFloat(style.paddingBottom) || 0; 266 const iframeRect = iframe.getBoundingClientRect(); 267 rect.left += iframeRect.left + borderLeft + paddingLeft; 268 rect.top += iframeRect.top + borderTop + paddingTop; 269 if ( 270 rect.left + rect.width > 271 iframeRect.right - borderRight - paddingRight 272 ) { 273 rect.width = Math.max( 274 iframeRect.right - borderRight - paddingRight - rect.left, 275 0 276 ); 277 } 278 if ( 279 rect.top + rect.height > 280 iframeRect.bottom - borderBottom - paddingBottom 281 ) { 282 rect.height = Math.max( 283 iframeRect.bottom - borderBottom - paddingBottom - rect.top, 284 0 285 ); 286 } 287 aTarget = iframe; 288 containingWindow = aTarget.ownerDocument.defaultView; 289 } 290 291 return { rect, window: containingWindow }; 292 } 293 294 // Returns the in-process root window for the given |aWindow|. 295 function getInProcessRootWindow(aWindow) { 296 let window = SpecialPowers.wrap(aWindow); 297 while (window.browsingContext.embedderElement) { 298 window = window.browsingContext.embedderElement.ownerDocument.defaultView; 299 } 300 return window; 301 } 302 303 // Convert (offsetX, offsetY) of target or center of it, in CSS pixels to device 304 // pixels relative to the screen. 305 // TODO: this function currently does not incorporate some CSS transforms on 306 // elements enclosing target, e.g. scale transforms. 307 async function coordinatesRelativeToScreen(aParams) { 308 const { 309 target, // The target element or window 310 offsetX, // X offset relative to `target` 311 offsetY, // Y offset relative to `target` 312 atCenter, // Instead of offsetX/offsetY, return center of `target` 313 } = aParams; 314 // Note that |window| might not be the root content window, for two 315 // possible reasons: 316 // 1. The mochitest that's calling into this function is not using a mechanism 317 // like runSubtestsSeriallyInFreshWindows() to load the test page in 318 // a top-level context, so it's loaded into an iframe by the mochitest 319 // harness. 320 // 2. The mochitest itself creates an iframe and calls this function from 321 // script running in the context of the iframe. 322 // Since the resolution applies to the top level content document, below we 323 // use the mozInnerScreen{X,Y} of the top level content window (window.top) 324 // only for the case where this function gets called in the top level content 325 // document. In other cases we use nsIDOMWindowUtils.toScreenRect(). 326 327 // We do often specify `window` as the target, if it's the top level window, 328 // `nsIDOMWindowUtils.toScreenRect` isn't suitable because the function is 329 // supposed to be called with values in the document coords, so for example 330 // if desktop zoom is being applied, (0, 0) in the document coords might be 331 // outside of the visual viewport, i.e. it's going to be negative with the 332 // `toScreenRect` conversion, whereas the call sites with `window` of this 333 // function expect (0, 0) position should be the visual viport's offset. So 334 // in such cases we simply use mozInnerScreen{X,Y} to convert the given value 335 // to the screen coords. 336 if (target instanceof Window && window.parent == window) { 337 const resolution = await getResolution(); 338 const deviceScale = window.devicePixelRatio; 339 return { 340 x: 341 window.mozInnerScreenX * deviceScale + 342 (atCenter ? 0 : offsetX) * resolution * deviceScale, 343 y: 344 window.mozInnerScreenY * deviceScale + 345 (atCenter ? 0 : offsetY) * resolution * deviceScale, 346 }; 347 } 348 349 const rectAndWindow = _getTargetRect(target, atCenter); 350 351 const inProcessRootWindow = getInProcessRootWindow(window); 352 353 if ( 354 !(inProcessRootWindow.location.href === rectAndWindow.window.location.href) 355 ) { 356 info( 357 "warning: coordinatesRelativeToScreen using coords based on one window in another, this will likely produce incorrect results" 358 ); 359 info( 360 "inProcessRootWindow.location.href " + inProcessRootWindow.location.href 361 ); 362 info( 363 "rectAndWindow.window.location.href " + rectAndWindow.window.location.href 364 ); 365 } 366 // This doesn't hold yet. 367 //ok(inProcessRootWindow.location.href === rectAndWindow.window.location.href, "same root window"); 368 369 const utils = SpecialPowers.wrap( 370 SpecialPowers.getDOMWindowUtils(inProcessRootWindow) 371 ); 372 const positionInScreenCoords = utils.toScreenRect( 373 rectAndWindow.rect.left + 374 (atCenter ? rectAndWindow.rect.width / 2 : offsetX), 375 rectAndWindow.rect.top + 376 (atCenter ? rectAndWindow.rect.height / 2 : offsetY), 377 0, 378 0 379 ); 380 381 return { 382 x: positionInScreenCoords.x, 383 y: positionInScreenCoords.y, 384 }; 385 } 386 387 // Get the bounding box of aElement, and return it in device pixels 388 // relative to the screen. 389 // TODO: This function should probably take into account the resolution and 390 // the relative viewport rect like coordinatesRelativeToScreen() does. 391 function rectRelativeToScreen(aElement) { 392 var targetWindow = aElement.ownerDocument.defaultView; 393 var scale = targetWindow.devicePixelRatio; 394 var rect = aElement.getBoundingClientRect(); 395 return { 396 x: (targetWindow.mozInnerScreenX + rect.left) * scale, 397 y: (targetWindow.mozInnerScreenY + rect.top) * scale, 398 width: rect.width * scale, 399 height: rect.height * scale, 400 }; 401 } 402 403 // Synthesizes a native mousewheel event and returns immediately. This does not 404 // guarantee anything; you probably want to use one of the other functions below 405 // which actually wait for results. 406 // aX and aY are relative to the top-left of |aTarget|'s bounding rect. 407 // aDeltaX and aDeltaY are pixel deltas, and aObserver can be left undefined 408 // if not needed. 409 async function synthesizeNativeWheel( 410 aTarget, 411 aX, 412 aY, 413 aDeltaX, 414 aDeltaY, 415 aObserver 416 ) { 417 var pt = await coordinatesRelativeToScreen({ 418 offsetX: aX, 419 offsetY: aY, 420 target: aTarget, 421 }); 422 if (aDeltaX && aDeltaY) { 423 throw new Error( 424 "Simultaneous wheeling of horizontal and vertical is not supported on all platforms." 425 ); 426 } 427 aDeltaX = nativeScrollUnits(aTarget, aDeltaX); 428 aDeltaY = nativeScrollUnits(aTarget, aDeltaY); 429 var msg = aDeltaX 430 ? nativeHorizontalWheelEventMsg() 431 : nativeVerticalWheelEventMsg(); 432 var utils = utilsForTarget(aTarget); 433 var element = elementForTarget(aTarget); 434 utils.sendNativeMouseScrollEvent( 435 pt.x, 436 pt.y, 437 msg, 438 aDeltaX, 439 aDeltaY, 440 0, 441 0, 442 // Specify MOUSESCROLL_SCROLL_LINES if the test wants to run through wheel 443 // input code path on Mac since it's normal mouse wheel inputs. 444 SpecialPowers.getBoolPref("apz.test.mac.synth_wheel_input", false) 445 ? SpecialPowers.DOMWindowUtils.MOUSESCROLL_SCROLL_LINES 446 : 0, 447 element, 448 aObserver 449 ); 450 return true; 451 } 452 453 // Synthesizes a native pan gesture event and returns immediately. 454 // NOTE: This works only on Mac. 455 // You can specify kCGScrollPhaseBegan = 1, kCGScrollPhaseChanged = 2 and 456 // kCGScrollPhaseEnded = 4 for |aPhase|. 457 async function synthesizeNativePanGestureEvent( 458 aTarget, 459 aX, 460 aY, 461 aDeltaX, 462 aDeltaY, 463 aPhase, 464 aObserver 465 ) { 466 if (getPlatform() != "mac") { 467 throw new Error( 468 `synthesizeNativePanGestureEvent doesn't work on ${getPlatform()}` 469 ); 470 } 471 472 var pt = await coordinatesRelativeToScreen({ 473 offsetX: aX, 474 offsetY: aY, 475 target: aTarget, 476 }); 477 if (aDeltaX && aDeltaY) { 478 throw new Error( 479 "Simultaneous panning of horizontal and vertical is not supported." 480 ); 481 } 482 483 aDeltaX = nativeScrollUnits(aTarget, aDeltaX); 484 aDeltaY = nativeScrollUnits(aTarget, aDeltaY); 485 486 var element = elementForTarget(aTarget); 487 var utils = utilsForTarget(aTarget); 488 utils.sendNativeMouseScrollEvent( 489 pt.x, 490 pt.y, 491 aPhase, 492 aDeltaX, 493 aDeltaY, 494 0 /* deltaZ */, 495 0 /* modifiers */, 496 0 /* scroll event unit pixel */, 497 element, 498 aObserver 499 ); 500 501 return true; 502 } 503 504 // Sends a native touchpad pan event and resolve the returned promise once the 505 // request has been successfully made to the OS. 506 // NOTE: This works only on Windows and Linux. 507 // You can specify nsIDOMWindowUtils.PHASE_BEGIN, PHASE_UPDATE and PHASE_END 508 // for |aPhase|. 509 async function promiseNativeTouchpadPanEventAndWaitForObserver( 510 aTarget, 511 aX, 512 aY, 513 aDeltaX, 514 aDeltaY, 515 aPhase 516 ) { 517 if (getPlatform() != "windows" && getPlatform() != "linux") { 518 throw new Error( 519 `promiseNativeTouchpadPanEventAndWaitForObserver doesn't work on ${getPlatform()}` 520 ); 521 } 522 523 let pt = await coordinatesRelativeToScreen({ 524 offsetX: aX, 525 offsetY: aY, 526 target: aTarget, 527 }); 528 529 const utils = utilsForTarget(aTarget); 530 531 return new Promise(resolve => { 532 utils.sendNativeTouchpadPan( 533 aPhase, 534 pt.x, 535 pt.y, 536 aDeltaX, 537 aDeltaY, 538 0, 539 resolve 540 ); 541 }); 542 } 543 544 async function synthesizeSimpleGestureEvent( 545 aElement, 546 aType, 547 aX, 548 aY, 549 aDirection, 550 aDelta, 551 aModifiers, 552 aClickCount 553 ) { 554 let pt = await coordinatesRelativeToScreen({ 555 offsetX: aX, 556 offsetY: aY, 557 target: aElement, 558 }); 559 560 let utils = utilsForTarget(aElement); 561 utils.sendSimpleGestureEvent( 562 aType, 563 pt.x, 564 pt.y, 565 aDirection, 566 aDelta, 567 aModifiers, 568 aClickCount 569 ); 570 } 571 572 // Synthesizes a native pan gesture event and resolve the returned promise once the 573 // request has been successfully made to the OS. 574 function promiseNativePanGestureEventAndWaitForObserver( 575 aElement, 576 aX, 577 aY, 578 aDeltaX, 579 aDeltaY, 580 aPhase 581 ) { 582 return new Promise(resolve => { 583 synthesizeNativePanGestureEvent( 584 aElement, 585 aX, 586 aY, 587 aDeltaX, 588 aDeltaY, 589 aPhase, 590 resolve 591 ); 592 }); 593 } 594 595 // Synthesizes a native mousewheel event and resolve the returned promise once the 596 // request has been successfully made to the OS. This does not necessarily 597 // guarantee that the OS generates the event we requested. See 598 // synthesizeNativeWheel for details on the parameters. 599 function promiseNativeWheelAndWaitForObserver( 600 aElement, 601 aX, 602 aY, 603 aDeltaX, 604 aDeltaY 605 ) { 606 return new Promise(resolve => { 607 synthesizeNativeWheel(aElement, aX, aY, aDeltaX, aDeltaY, resolve); 608 }); 609 } 610 611 // Synthesizes a native mousewheel event and resolve the returned promise once the 612 // wheel event is dispatched to |aTarget|'s containing window. If the event 613 // targets content in a subdocument, |aTarget| should be inside the 614 // subdocument (or the subdocument's window). See synthesizeNativeWheel for 615 // details on the other parameters. 616 function promiseNativeWheelAndWaitForWheelEvent( 617 aTarget, 618 aX, 619 aY, 620 aDeltaX, 621 aDeltaY 622 ) { 623 return new Promise((resolve, reject) => { 624 var targetWindow = windowForTarget(aTarget); 625 targetWindow.addEventListener( 626 "wheel", 627 function () { 628 setTimeout(resolve, 0); 629 }, 630 { once: true } 631 ); 632 try { 633 synthesizeNativeWheel(aTarget, aX, aY, aDeltaX, aDeltaY); 634 } catch (e) { 635 reject(e); 636 } 637 }); 638 } 639 640 // Synthesizes a native mousewheel event and resolves the returned promise once the 641 // first resulting scroll event is dispatched to |aTarget|'s containing window. 642 // If the event targets content in a subdocument, |aTarget| should be inside 643 // the subdocument (or the subdocument's window). See synthesizeNativeWheel 644 // for details on the other parameters. 645 function promiseNativeWheelAndWaitForScrollEvent( 646 aTarget, 647 aX, 648 aY, 649 aDeltaX, 650 aDeltaY 651 ) { 652 return new Promise((resolve, reject) => { 653 var targetWindow = windowForTarget(aTarget); 654 targetWindow.addEventListener( 655 "scroll", 656 function () { 657 setTimeout(resolve, 0); 658 }, 659 { capture: true, once: true } 660 ); // scroll events don't always bubble 661 try { 662 synthesizeNativeWheel(aTarget, aX, aY, aDeltaX, aDeltaY); 663 } catch (e) { 664 reject(e); 665 } 666 }); 667 } 668 669 async function synthesizeTouchpadPinch(scales, focusX, focusY, options) { 670 var scalesAndFoci = []; 671 672 for (let i = 0; i < scales.length; i++) { 673 scalesAndFoci.push([scales[i], focusX, focusY]); 674 } 675 676 await synthesizeTouchpadGesture(scalesAndFoci, options); 677 } 678 679 // scalesAndFoci is an array of [scale, focusX, focuxY] tuples. 680 async function synthesizeTouchpadGesture(scalesAndFoci, options) { 681 // Check for options, fill in defaults if appropriate. 682 let waitForTransformEnd = 683 options.waitForTransformEnd !== undefined 684 ? options.waitForTransformEnd 685 : true; 686 let waitForFrames = 687 options.waitForFrames !== undefined ? options.waitForFrames : false; 688 689 // Register the listener for the TransformEnd observer topic 690 let transformEndPromise = promiseTransformEnd(); 691 692 var modifierFlags = 0; 693 var utils = utilsForTarget(document.body); 694 for (let i = 0; i < scalesAndFoci.length; i++) { 695 var pt = await coordinatesRelativeToScreen({ 696 offsetX: scalesAndFoci[i][1], 697 offsetY: scalesAndFoci[i][2], 698 target: document.body, 699 }); 700 var phase; 701 if (i === 0) { 702 phase = SpecialPowers.DOMWindowUtils.PHASE_BEGIN; 703 } else if (i === scalesAndFoci.length - 1) { 704 phase = SpecialPowers.DOMWindowUtils.PHASE_END; 705 } else { 706 phase = SpecialPowers.DOMWindowUtils.PHASE_UPDATE; 707 } 708 utils.sendNativeTouchpadPinch( 709 phase, 710 scalesAndFoci[i][0], 711 pt.x, 712 pt.y, 713 modifierFlags 714 ); 715 if (waitForFrames) { 716 await promiseFrame(); 717 } 718 } 719 720 // Wait for TransformEnd to fire. 721 if (waitForTransformEnd) { 722 await transformEndPromise; 723 } 724 } 725 726 async function synthesizeTouchpadPan( 727 focusX, 728 focusY, 729 deltaXs, 730 deltaYs, 731 options 732 ) { 733 // Check for options, fill in defaults if appropriate. 734 let waitForTransformEnd = 735 options.waitForTransformEnd !== undefined 736 ? options.waitForTransformEnd 737 : true; 738 let waitForFrames = 739 options.waitForFrames !== undefined ? options.waitForFrames : false; 740 741 // Register the listener for the TransformEnd observer topic 742 let transformEndPromise = promiseTransformEnd(); 743 744 var modifierFlags = 0; 745 var pt = await coordinatesRelativeToScreen({ 746 offsetX: focusX, 747 offsetY: focusY, 748 target: document.body, 749 }); 750 var utils = utilsForTarget(document.body); 751 for (let i = 0; i < deltaXs.length; i++) { 752 var phase; 753 if (i === 0) { 754 phase = SpecialPowers.DOMWindowUtils.PHASE_BEGIN; 755 } else if (i === deltaXs.length - 1) { 756 phase = SpecialPowers.DOMWindowUtils.PHASE_END; 757 } else { 758 phase = SpecialPowers.DOMWindowUtils.PHASE_UPDATE; 759 } 760 utils.sendNativeTouchpadPan( 761 phase, 762 pt.x, 763 pt.y, 764 deltaXs[i], 765 deltaYs[i], 766 modifierFlags 767 ); 768 if (waitForFrames) { 769 await promiseFrame(); 770 } 771 } 772 773 // Wait for TransformEnd to fire. 774 if (waitForTransformEnd) { 775 await transformEndPromise; 776 } 777 } 778 779 // Synthesizes a native touch event and dispatches it. aX and aY in CSS pixels 780 // relative to the top-left of |aTarget|'s bounding rect. 781 async function synthesizeNativeTouch( 782 aTarget, 783 aX, 784 aY, 785 aType, 786 aObserver = null, 787 aTouchId = 0 788 ) { 789 var pt = await coordinatesRelativeToScreen({ 790 offsetX: aX, 791 offsetY: aY, 792 target: aTarget, 793 }); 794 var utils = utilsForTarget(aTarget); 795 utils.sendNativeTouchPoint( 796 aTouchId, 797 aType, 798 pt.x, 799 pt.y, 800 1, 801 90, 802 aObserver, 803 aTarget instanceof Element ? aTarget : null 804 ); 805 return true; 806 } 807 808 function sendBasicNativePointerInput( 809 utils, 810 aId, 811 aPointerType, 812 aState, 813 aX, 814 aY, 815 aObserver, 816 aElement, 817 { pressure = 1, twist = 0, tiltX = 0, tiltY = 0, button = 0 } = {} 818 ) { 819 switch (aPointerType) { 820 case "touch": 821 utils.sendNativeTouchPoint( 822 aId, 823 aState, 824 aX, 825 aY, 826 pressure, 827 90, 828 aObserver, 829 aElement 830 ); 831 break; 832 case "pen": 833 utils.sendNativePenInput( 834 aId, 835 aState, 836 aX, 837 aY, 838 pressure, 839 twist, 840 tiltX, 841 tiltY, 842 button, 843 aObserver, 844 aElement 845 ); 846 break; 847 default: 848 throw new Error(`Not supported: ${aPointerType}`); 849 } 850 } 851 852 async function promiseNativePointerInput( 853 aTarget, 854 aPointerType, 855 aState, 856 aX, 857 aY, 858 options 859 ) { 860 const pt = await coordinatesRelativeToScreen({ 861 offsetX: aX, 862 offsetY: aY, 863 target: aTarget, 864 }); 865 const utils = utilsForTarget(aTarget); 866 return new Promise(resolve => { 867 sendBasicNativePointerInput( 868 utils, 869 options?.pointerId ?? 0, 870 aPointerType, 871 aState, 872 pt.x, 873 pt.y, 874 resolve, 875 aTarget instanceof Element ? aTarget : null, 876 options 877 ); 878 }); 879 } 880 881 /** 882 * Function to generate native pointer events as a sequence. 883 * 884 * @param aTarget is the element or window whose bounding rect the coordinates are 885 * relative to. 886 * @param aPointerType "touch" or "pen". 887 * @param aPositions is a 2D array of position data. It is indexed as [row][column], 888 * where advancing the row counter moves forward in time, and each column 889 * represents a single pointer. Each row must have exactly 890 * the same number of columns, and the number of columns must match the length 891 * of the aPointerIds parameter. 892 * For each row, each entry is either an object with x and y fields, 893 * or a null. A null value indicates that the pointer should be "lifted" 894 * (i.e. send a touchend for that touch input). A non-null value therefore 895 * indicates the position of the pointer input. 896 * This function takes care of the state tracking necessary to send 897 * pointerup/pointerdown inputs as necessary as the pointers go up and down. 898 * @param aObserver is the observer that will get registered on the very last 899 * native pointer synthesis call this function makes. 900 * @param aPointerIds is an array holding the pointer ID values. 901 */ 902 async function synthesizeNativePointerSequences( 903 aTarget, 904 aPointerType, 905 aPositions, 906 aObserver = null, 907 aPointerIds = [0], 908 options 909 ) { 910 // We use lastNonNullValue to figure out which synthesizeNativeTouch call 911 // will be the last one we make, so that we can register aObserver on it. 912 var lastNonNullValue = -1; 913 for (let i = 0; i < aPositions.length; i++) { 914 if (aPositions[i] == null) { 915 throw new Error(`aPositions[${i}] was unexpectedly null`); 916 } 917 if (aPositions[i].length != aPointerIds.length) { 918 throw new Error( 919 `aPositions[${i}] did not have the expected number of positions; ` + 920 `expected ${aPointerIds.length} pointers but found ${aPositions[i].length}` 921 ); 922 } 923 for (let j = 0; j < aPointerIds.length; j++) { 924 if (aPositions[i][j] != null) { 925 lastNonNullValue = i * aPointerIds.length + j; 926 // Do the conversion to screen space before actually synthesizing 927 // the events, otherwise the screen space may change as a result of 928 // the touch inputs and the conversion may not work as intended. 929 aPositions[i][j] = await coordinatesRelativeToScreen({ 930 offsetX: aPositions[i][j].x, 931 offsetY: aPositions[i][j].y, 932 target: aTarget, 933 }); 934 } 935 } 936 } 937 if (lastNonNullValue < 0) { 938 throw new Error("All values in positions array were null!"); 939 } 940 941 // Insert a row of nulls at the end of aPositions, to ensure that all 942 // touches get removed. If the touches have already been removed this will 943 // just add an extra no-op iteration in the aPositions loop below. 944 var allNullRow = new Array(aPointerIds.length); 945 allNullRow.fill(null); 946 aPositions.push(allNullRow); 947 948 // The last sendNativeTouchPoint call will be the TOUCH_REMOVE which happens 949 // one iteration of aPosition after the last non-null value. 950 var lastSynthesizeCall = lastNonNullValue + aPointerIds.length; 951 952 // track which touches are down and which are up. start with all up 953 var currentPositions = new Array(aPointerIds.length); 954 currentPositions.fill(null); 955 956 var utils = utilsForTarget(aTarget); 957 // Iterate over the position data now, and generate the touches requested 958 for (let i = 0; i < aPositions.length; i++) { 959 for (let j = 0; j < aPointerIds.length; j++) { 960 if (aPositions[i][j] == null) { 961 // null means lift the finger 962 if (currentPositions[j] == null) { 963 // it's already lifted, do nothing 964 } else { 965 // synthesize the touch-up. If this is the last call we're going to 966 // make, pass the observer as well 967 var thisIndex = i * aPointerIds.length + j; 968 var observer = lastSynthesizeCall == thisIndex ? aObserver : null; 969 sendBasicNativePointerInput( 970 utils, 971 aPointerIds[j], 972 aPointerType, 973 SpecialPowers.DOMWindowUtils.TOUCH_REMOVE, 974 currentPositions[j].x, 975 currentPositions[j].y, 976 observer, 977 aTarget instanceof Element ? aTarget : null, 978 options 979 ); 980 currentPositions[j] = null; 981 } 982 } else { 983 sendBasicNativePointerInput( 984 utils, 985 aPointerIds[j], 986 aPointerType, 987 SpecialPowers.DOMWindowUtils.TOUCH_CONTACT, 988 aPositions[i][j].x, 989 aPositions[i][j].y, 990 null, 991 aTarget instanceof Element ? aTarget : null, 992 options 993 ); 994 currentPositions[j] = aPositions[i][j]; 995 } 996 } 997 } 998 return true; 999 } 1000 1001 async function synthesizeNativeTouchSequences( 1002 aTarget, 1003 aPositions, 1004 aObserver = null, 1005 aTouchIds = [0] 1006 ) { 1007 await synthesizeNativePointerSequences( 1008 aTarget, 1009 "touch", 1010 aPositions, 1011 aObserver, 1012 aTouchIds 1013 ); 1014 } 1015 1016 async function synthesizeNativePointerDrag( 1017 aTarget, 1018 aPointerType, 1019 aX, 1020 aY, 1021 aDeltaX, 1022 aDeltaY, 1023 aObserver = null, 1024 aPointerId = 0, 1025 options 1026 ) { 1027 var steps = Math.max(Math.abs(aDeltaX), Math.abs(aDeltaY)); 1028 var positions = [[{ x: aX, y: aY }]]; 1029 for (var i = 1; i < steps; i++) { 1030 var dx = i * (aDeltaX / steps); 1031 var dy = i * (aDeltaY / steps); 1032 var pos = { x: aX + dx, y: aY + dy }; 1033 positions.push([pos]); 1034 } 1035 positions.push([{ x: aX + aDeltaX, y: aY + aDeltaY }]); 1036 return synthesizeNativePointerSequences( 1037 aTarget, 1038 aPointerType, 1039 positions, 1040 aObserver, 1041 [aPointerId], 1042 options 1043 ); 1044 } 1045 1046 // Note that when calling this function you'll want to make sure that the pref 1047 // "apz.touch_start_tolerance" is set to 0, or some of the touchmove will get 1048 // consumed to overcome the panning threshold. 1049 async function synthesizeNativeTouchDrag( 1050 aTarget, 1051 aX, 1052 aY, 1053 aDeltaX, 1054 aDeltaY, 1055 aObserver = null, 1056 aTouchId = 0 1057 ) { 1058 return synthesizeNativePointerDrag( 1059 aTarget, 1060 "touch", 1061 aX, 1062 aY, 1063 aDeltaX, 1064 aDeltaY, 1065 aObserver, 1066 aTouchId 1067 ); 1068 } 1069 1070 function promiseNativePointerDrag( 1071 aTarget, 1072 aPointerType, 1073 aX, 1074 aY, 1075 aDeltaX, 1076 aDeltaY, 1077 aPointerId = 0, 1078 options 1079 ) { 1080 return new Promise(resolve => { 1081 synthesizeNativePointerDrag( 1082 aTarget, 1083 aPointerType, 1084 aX, 1085 aY, 1086 aDeltaX, 1087 aDeltaY, 1088 resolve, 1089 aPointerId, 1090 options 1091 ); 1092 }); 1093 } 1094 1095 // Promise-returning variant of synthesizeNativeTouchDrag 1096 function promiseNativeTouchDrag( 1097 aTarget, 1098 aX, 1099 aY, 1100 aDeltaX, 1101 aDeltaY, 1102 aTouchId = 0 1103 ) { 1104 return new Promise(resolve => { 1105 synthesizeNativeTouchDrag( 1106 aTarget, 1107 aX, 1108 aY, 1109 aDeltaX, 1110 aDeltaY, 1111 resolve, 1112 aTouchId 1113 ); 1114 }); 1115 } 1116 1117 // Tapping is essentially a dragging with no move 1118 function promiseNativePointerTap(aTarget, aPointerType, aX, aY, options) { 1119 return promiseNativePointerDrag( 1120 aTarget, 1121 aPointerType, 1122 aX, 1123 aY, 1124 0, 1125 0, 1126 options?.pointerId ?? 0, 1127 options 1128 ); 1129 } 1130 1131 async function synthesizeNativeTap(aTarget, aX, aY, aObserver = null) { 1132 var pt = await coordinatesRelativeToScreen({ 1133 offsetX: aX, 1134 offsetY: aY, 1135 target: aTarget, 1136 }); 1137 let utils = utilsForTarget(aTarget); 1138 utils.sendNativeTouchTap(pt.x, pt.y, false, aObserver); 1139 return true; 1140 } 1141 1142 // only currently implemented on macOS 1143 async function synthesizeNativeTouchpadDoubleTap(aTarget, aX, aY) { 1144 ok( 1145 getPlatform() == "mac", 1146 "only implemented on mac. implement sendNativeTouchpadDoubleTap for this platform," + 1147 " see bug 1696802 for how it was done on macOS" 1148 ); 1149 let pt = await coordinatesRelativeToScreen({ 1150 offsetX: aX, 1151 offsetY: aY, 1152 target: aTarget, 1153 }); 1154 let utils = utilsForTarget(aTarget); 1155 utils.sendNativeTouchpadDoubleTap(pt.x, pt.y, 0); 1156 return true; 1157 } 1158 1159 // If the event targets content in a subdocument, |aTarget| should be inside the 1160 // subdocument (or the subdocument window). 1161 async function synthesizeNativeMouseEventWithAPZ(aParams, aObserver = null) { 1162 if (aParams.win !== undefined) { 1163 throw Error( 1164 "Are you trying to use EventUtils' API? `win` won't be used with synthesizeNativeMouseClickWithAPZ." 1165 ); 1166 } 1167 if (aParams.scale !== undefined) { 1168 throw Error( 1169 "Are you trying to use EventUtils' API? `scale` won't be used with synthesizeNativeMouseClickWithAPZ." 1170 ); 1171 } 1172 if (aParams.elementOnWidget !== undefined) { 1173 throw Error( 1174 "Are you trying to use EventUtils' API? `elementOnWidget` won't be used with synthesizeNativeMouseClickWithAPZ." 1175 ); 1176 } 1177 const { 1178 type, // "click", "mousedown", "mouseup" or "mousemove" 1179 target, // Origin of offsetX and offsetY, must be an element 1180 offsetX, // X offset in `target` in CSS Pixels 1181 offsetY, // Y offset in `target` in CSS pixels 1182 atCenter, // Instead of offsetX/Y, synthesize the event at center of `target` 1183 screenX, // X offset in screen in device pixels, offsetX/Y nor atCenter must not be set if this is set 1184 screenY, // Y offset in screen in device pixels, offsetX/Y nor atCenter must not be set if this is set 1185 button = 0, // if "click", "mousedown", "mouseup", set same value as DOM MouseEvent.button 1186 modifiers = {}, // Active modifiers, see `parseNativeModifiers` 1187 } = aParams; 1188 if (atCenter) { 1189 if (offsetX != undefined || offsetY != undefined) { 1190 throw Error( 1191 `atCenter is specified, but offsetX (${offsetX}) and/or offsetY (${offsetY}) are also specified` 1192 ); 1193 } 1194 if (screenX != undefined || screenY != undefined) { 1195 throw Error( 1196 `atCenter is specified, but screenX (${screenX}) and/or screenY (${screenY}) are also specified` 1197 ); 1198 } 1199 } else if (offsetX != undefined && offsetY != undefined) { 1200 if (screenX != undefined || screenY != undefined) { 1201 throw Error( 1202 `offsetX/Y are specified, but screenX (${screenX}) and/or screenY (${screenY}) are also specified` 1203 ); 1204 } 1205 } else if (screenX != undefined && screenY != undefined) { 1206 if (offsetX != undefined || offsetY != undefined) { 1207 throw Error( 1208 `screenX/Y are specified, but offsetX (${offsetX}) and/or offsetY (${offsetY}) are also specified` 1209 ); 1210 } 1211 } 1212 const pt = await (async () => { 1213 if (screenX != undefined) { 1214 return { x: screenX, y: screenY }; 1215 } 1216 return coordinatesRelativeToScreen({ 1217 offsetX, 1218 offsetY, 1219 atCenter, 1220 target, 1221 }); 1222 })(); 1223 const utils = utilsForTarget(target); 1224 const element = elementForTarget(target); 1225 const modifierFlags = parseNativeModifiers(modifiers); 1226 if (type === "click") { 1227 utils.sendNativeMouseEvent( 1228 pt.x, 1229 pt.y, 1230 utils.NATIVE_MOUSE_MESSAGE_BUTTON_DOWN, 1231 button, 1232 modifierFlags, 1233 element, 1234 function () { 1235 utils.sendNativeMouseEvent( 1236 pt.x, 1237 pt.y, 1238 utils.NATIVE_MOUSE_MESSAGE_BUTTON_UP, 1239 button, 1240 modifierFlags, 1241 element, 1242 aObserver 1243 ); 1244 } 1245 ); 1246 return; 1247 } 1248 1249 utils.sendNativeMouseEvent( 1250 pt.x, 1251 pt.y, 1252 (() => { 1253 switch (type) { 1254 case "mousedown": 1255 return utils.NATIVE_MOUSE_MESSAGE_BUTTON_DOWN; 1256 case "mouseup": 1257 return utils.NATIVE_MOUSE_MESSAGE_BUTTON_UP; 1258 case "mousemove": 1259 return utils.NATIVE_MOUSE_MESSAGE_MOVE; 1260 default: 1261 throw Error(`Invalid type is specified: ${type}`); 1262 } 1263 })(), 1264 button, 1265 modifierFlags, 1266 element, 1267 aObserver 1268 ); 1269 } 1270 1271 function promiseNativeMouseEventWithAPZ(aParams) { 1272 return new Promise(resolve => 1273 synthesizeNativeMouseEventWithAPZ(aParams, resolve) 1274 ); 1275 } 1276 1277 // See synthesizeNativeMouseEventWithAPZ for the detail of aParams. 1278 function promiseNativeMouseEventWithAPZAndWaitForEvent(aParams) { 1279 return new Promise(resolve => { 1280 const targetWindow = windowForTarget(aParams.target); 1281 const eventType = aParams.eventTypeToWait || aParams.type; 1282 targetWindow.addEventListener(eventType, resolve, { 1283 once: true, 1284 }); 1285 synthesizeNativeMouseEventWithAPZ(aParams); 1286 }); 1287 } 1288 1289 // Move the mouse to (dx, dy) relative to |target|, and scroll the wheel 1290 // at that location. 1291 // Moving the mouse is necessary to avoid wheel events from two consecutive 1292 // promiseMoveMouseAndScrollWheelOver() calls on different elements being incorrectly 1293 // considered as part of the same wheel transaction. 1294 // We also wait for the mouse move event to be processed before sending the 1295 // wheel event, otherwise there is a chance they might get reordered, and 1296 // we have the transaction problem again. 1297 // This function returns a promise that is resolved when the resulting wheel 1298 // (if waitForScroll = false) or scroll (if waitForScroll = true) event is 1299 // received. 1300 function promiseMoveMouseAndScrollWheelOver( 1301 target, 1302 dx, 1303 dy, 1304 waitForScroll = true, 1305 scrollDelta = 10 1306 ) { 1307 let p = promiseNativeMouseEventWithAPZAndWaitForEvent({ 1308 type: "mousemove", 1309 target, 1310 offsetX: dx, 1311 offsetY: dy, 1312 }); 1313 if (waitForScroll) { 1314 p = p.then(() => { 1315 info( 1316 "Printing something here to avoid failure; see https://bugzilla.mozilla.org/show_bug.cgi?id=1776963" 1317 ); 1318 return promiseNativeWheelAndWaitForScrollEvent( 1319 target, 1320 dx, 1321 dy, 1322 0, 1323 -scrollDelta 1324 ); 1325 }); 1326 } else { 1327 p = p.then(() => { 1328 return promiseNativeWheelAndWaitForWheelEvent( 1329 target, 1330 dx, 1331 dy, 1332 0, 1333 -scrollDelta 1334 ); 1335 }); 1336 } 1337 return p; 1338 } 1339 1340 async function scrollbarDragStart(aTarget, aScaleFactor) { 1341 var targetElement = elementForTarget(aTarget); 1342 var w = {}, 1343 h = {}; 1344 utilsForTarget(aTarget).getScrollbarSizes(targetElement, w, h); 1345 var verticalScrollbarWidth = w.value; 1346 if (verticalScrollbarWidth == 0) { 1347 return null; 1348 } 1349 1350 var upArrowHeight = verticalScrollbarWidth; // assume square scrollbar buttons 1351 var startX = targetElement.clientWidth + verticalScrollbarWidth / 2; 1352 var startY = upArrowHeight + 5; // start dragging somewhere in the thumb 1353 startX *= aScaleFactor; 1354 startY *= aScaleFactor; 1355 1356 // targetElement.clientWidth is unaffected by the zoom, but if the target 1357 // is the root content window, the distance from the window origin to the 1358 // scrollbar in CSS pixels does decrease proportionally to the zoom, 1359 // so the CSS coordinates we return need to be scaled accordingly. 1360 if (targetIsTopWindow(aTarget)) { 1361 var resolution = await getResolution(); 1362 startX /= resolution; 1363 startY /= resolution; 1364 } 1365 1366 return { x: startX, y: startY }; 1367 } 1368 1369 // Synthesizes events to drag |target|'s vertical scrollbar by the distance 1370 // specified, synthesizing a mousemove for each increment as specified. 1371 // Returns null if the element doesn't have a vertical scrollbar. Otherwise, 1372 // returns an async function that should be invoked after the mousemoves have been 1373 // processed by the widget code, to end the scrollbar drag. Mousemoves being 1374 // processed by the widget code can be detected by listening for the mousemove 1375 // events in the caller, or for some other event that is triggered by the 1376 // mousemove, such as the scroll event resulting from the scrollbar drag. 1377 // The aScaleFactor argument should be provided if the scrollframe has been 1378 // scaled by an enclosing CSS transform. (TODO: this is a workaround for the 1379 // fact that coordinatesRelativeToScreen is supposed to do this automatically 1380 // but it currently does not). 1381 // Note: helper_scrollbar_snap_bug1501062.html contains a copy of this code 1382 // with modifications. Fixes here should be copied there if appropriate. 1383 // |target| can be an element (for subframes) or a window (for root frames). 1384 async function promiseVerticalScrollbarDrag( 1385 aTarget, 1386 aDistance = 20, 1387 aIncrement = 5, 1388 aScaleFactor = 1 1389 ) { 1390 var startPoint = await scrollbarDragStart(aTarget, aScaleFactor); 1391 var targetElement = elementForTarget(aTarget); 1392 if (startPoint == null) { 1393 return null; 1394 } 1395 1396 dump( 1397 "Starting drag at " + 1398 startPoint.x + 1399 ", " + 1400 startPoint.y + 1401 " from top-left of #" + 1402 targetElement.id + 1403 "\n" 1404 ); 1405 1406 // Move the mouse to the scrollbar thumb and drag it down 1407 await promiseNativeMouseEventWithAPZ({ 1408 target: aTarget, 1409 offsetX: startPoint.x, 1410 offsetY: startPoint.y, 1411 type: "mousemove", 1412 }); 1413 // mouse down 1414 await promiseNativeMouseEventWithAPZ({ 1415 target: aTarget, 1416 offsetX: startPoint.x, 1417 offsetY: startPoint.y, 1418 type: "mousedown", 1419 }); 1420 // drag vertically by |aIncrement| until we reach the specified distance 1421 for (var y = aIncrement; y < aDistance; y += aIncrement) { 1422 await promiseNativeMouseEventWithAPZ({ 1423 target: aTarget, 1424 offsetX: startPoint.x, 1425 offsetY: startPoint.y + y, 1426 type: "mousemove", 1427 }); 1428 } 1429 await promiseNativeMouseEventWithAPZ({ 1430 target: aTarget, 1431 offsetX: startPoint.x, 1432 offsetY: startPoint.y + aDistance, 1433 type: "mousemove", 1434 }); 1435 1436 // and return an async function to call afterwards to finish up the drag 1437 return async function () { 1438 dump("Finishing drag of #" + targetElement.id + "\n"); 1439 await promiseNativeMouseEventWithAPZ({ 1440 target: aTarget, 1441 offsetX: startPoint.x, 1442 offsetY: startPoint.y + aDistance, 1443 type: "mouseup", 1444 }); 1445 }; 1446 } 1447 1448 // This is similar to promiseVerticalScrollbarDrag except this triggers 1449 // the vertical scrollbar drag with a touch drag input. This function 1450 // returns true if a scrollbar was present and false if no scrollbar 1451 // was found for the given element. 1452 async function promiseVerticalScrollbarTouchDrag( 1453 aTarget, 1454 aDistance = 20, 1455 aScaleFactor = 1 1456 ) { 1457 var startPoint = await scrollbarDragStart(aTarget, aScaleFactor); 1458 var targetElement = elementForTarget(aTarget); 1459 if (startPoint == null) { 1460 return false; 1461 } 1462 1463 dump( 1464 "Starting touch drag at " + 1465 startPoint.x + 1466 ", " + 1467 startPoint.y + 1468 " from top-left of #" + 1469 targetElement.id + 1470 "\n" 1471 ); 1472 1473 await promiseNativeTouchDrag( 1474 aTarget, 1475 startPoint.x, 1476 startPoint.y, 1477 0, 1478 aDistance 1479 ); 1480 1481 return true; 1482 } 1483 1484 // Synthesizes a native mouse drag, starting at offset (mouseX, mouseY) from 1485 // the given target. The drag occurs in the given number of steps, to a final 1486 // destination of (mouseX + distanceX, mouseY + distanceY) from the target. 1487 // Returns a promise (wrapped in a function, so it doesn't execute immediately) 1488 // that should be awaited after the mousemoves have been processed by the widget 1489 // code, to end the drag. This is important otherwise the OS can sometimes 1490 // reorder the events and the drag doesn't have the intended effect (see 1491 // bug 1368603). 1492 // Example usage: 1493 // let dragFinisher = await promiseNativeMouseDrag(myElement, 0, 0); 1494 // await myIndicationThatDragHadAnEffect; 1495 // await dragFinisher(); 1496 async function promiseNativeMouseDrag( 1497 target, 1498 mouseX, 1499 mouseY, 1500 distanceX = 20, 1501 distanceY = 20, 1502 steps = 20 1503 ) { 1504 var targetElement = elementForTarget(target); 1505 dump( 1506 "Starting drag at " + 1507 mouseX + 1508 ", " + 1509 mouseY + 1510 " from top-left of #" + 1511 targetElement.id + 1512 "\n" 1513 ); 1514 1515 // Move the mouse to the target position 1516 await promiseNativeMouseEventWithAPZ({ 1517 target, 1518 offsetX: mouseX, 1519 offsetY: mouseY, 1520 type: "mousemove", 1521 }); 1522 // mouse down 1523 await promiseNativeMouseEventWithAPZ({ 1524 target, 1525 offsetX: mouseX, 1526 offsetY: mouseY, 1527 type: "mousedown", 1528 }); 1529 // drag vertically by |increment| until we reach the specified distance 1530 for (var s = 1; s <= steps; s++) { 1531 let dx = distanceX * (s / steps); 1532 let dy = distanceY * (s / steps); 1533 dump(`Dragging to ${mouseX + dx}, ${mouseY + dy} from target\n`); 1534 await promiseNativeMouseEventWithAPZ({ 1535 target, 1536 offsetX: mouseX + dx, 1537 offsetY: mouseY + dy, 1538 type: "mousemove", 1539 }); 1540 } 1541 1542 // and return a function-wrapped promise to call afterwards to finish the drag 1543 return function () { 1544 return promiseNativeMouseEventWithAPZ({ 1545 target, 1546 offsetX: mouseX + distanceX, 1547 offsetY: mouseY + distanceY, 1548 type: "mouseup", 1549 }); 1550 }; 1551 } 1552 1553 // Synthesizes a native touch sequence of events corresponding to a pinch-zoom-in 1554 // at the given focus point. The focus point must be specified in CSS coordinates 1555 // relative to the document body. 1556 async function pinchZoomInTouchSequence(focusX, focusY) { 1557 // prettier-ignore 1558 var zoom_in = [ 1559 [ { x: focusX - 25, y: focusY - 50 }, { x: focusX + 25, y: focusY + 50 } ], 1560 [ { x: focusX - 30, y: focusY - 80 }, { x: focusX + 30, y: focusY + 80 } ], 1561 [ { x: focusX - 35, y: focusY - 110 }, { x: focusX + 40, y: focusY + 110 } ], 1562 [ { x: focusX - 40, y: focusY - 140 }, { x: focusX + 45, y: focusY + 140 } ], 1563 [ { x: focusX - 45, y: focusY - 170 }, { x: focusX + 50, y: focusY + 170 } ], 1564 [ { x: focusX - 50, y: focusY - 200 }, { x: focusX + 55, y: focusY + 200 } ], 1565 ]; 1566 1567 var touchIds = [0, 1]; 1568 return synthesizeNativeTouchSequences(document.body, zoom_in, null, touchIds); 1569 } 1570 1571 // Returns a promise that is resolved when the observer service dispatches a 1572 // message with the given topic. 1573 function promiseTopic(aTopic) { 1574 return new Promise((resolve, reject) => { 1575 SpecialPowers.Services.obs.addObserver(function observer( 1576 subject, 1577 topic, 1578 data 1579 ) { 1580 try { 1581 SpecialPowers.Services.obs.removeObserver(observer, topic); 1582 resolve([subject, data]); 1583 } catch (ex) { 1584 SpecialPowers.Services.obs.removeObserver(observer, topic); 1585 reject(ex); 1586 } 1587 }, aTopic); 1588 }); 1589 } 1590 1591 // Returns a promise that is resolved when a APZ transform ends. 1592 function promiseTransformEnd() { 1593 return promiseTopic("APZ:TransformEnd"); 1594 } 1595 1596 function promiseScrollend(aTarget = window) { 1597 return promiseOneEvent(aTarget, "scrollend"); 1598 } 1599 1600 // Returns a promise that resolves after the indicated number 1601 // of touchend events have fired on the given target element. 1602 function promiseTouchEnd(element, count = 1) { 1603 return new Promise(resolve => { 1604 var eventCount = 0; 1605 var counterFunction = function () { 1606 eventCount++; 1607 if (eventCount == count) { 1608 element.removeEventListener("touchend", counterFunction, { 1609 passive: true, 1610 }); 1611 resolve(); 1612 } 1613 }; 1614 element.addEventListener("touchend", counterFunction, { passive: true }); 1615 }); 1616 } 1617 1618 // This generates a touch-based pinch zoom-in gesture that is expected 1619 // to succeed. It returns after APZ has completed the zoom and reaches the end 1620 // of the transform. The focus point is expected to be in CSS coordinates 1621 // relative to the document body. 1622 async function pinchZoomInWithTouch(focusX, focusY) { 1623 // Register the listener for the TransformEnd observer topic 1624 let transformEndPromise = promiseTopic("APZ:TransformEnd"); 1625 1626 // Dispatch all the touch events 1627 await pinchZoomInTouchSequence(focusX, focusY); 1628 1629 // Wait for TransformEnd to fire. 1630 await transformEndPromise; 1631 } 1632 // This generates a touchpad pinch zoom-in gesture that is expected 1633 // to succeed. It returns after APZ has completed the zoom and reaches the end 1634 // of the transform. The focus point is expected to be in CSS coordinates 1635 // relative to the document body. 1636 async function pinchZoomInWithTouchpad(focusX, focusY, options = {}) { 1637 var zoomIn = [ 1638 1.0, 1.019531, 1.035156, 1.037156, 1.039156, 1.054688, 1.056688, 1.070312, 1639 1.072312, 1.089844, 1.091844, 1.109375, 1.128906, 1.144531, 1.160156, 1640 1.175781, 1.191406, 1.207031, 1.222656, 1.234375, 1.246094, 1.261719, 1641 1.273438, 1.285156, 1.296875, 1.3125, 1.328125, 1.347656, 1.363281, 1642 1.382812, 1.402344, 1.421875, 1.0, 1643 ]; 1644 await synthesizeTouchpadPinch(zoomIn, focusX, focusY, options); 1645 } 1646 1647 async function pinchZoomInAndPanWithTouchpad(options = {}) { 1648 var x = 584; 1649 var y = 347; 1650 var scalesAndFoci = []; 1651 // Zoom 1652 for (var scale = 1.0; scale <= 2.0; scale += 0.2) { 1653 scalesAndFoci.push([scale, x, y]); 1654 } 1655 // Pan (due to a limitation of the current implementation, events 1656 // for which the scale doesn't change are dropped, so vary the 1657 // scale slightly as well). 1658 for (var i = 1; i <= 20; i++) { 1659 x -= 4; 1660 y -= 5; 1661 scalesAndFoci.push([scale + 0.01 * i, x, y]); 1662 } 1663 await synthesizeTouchpadGesture(scalesAndFoci, options); 1664 } 1665 1666 async function pinchZoomOutWithTouchpad(focusX, focusY, options = {}) { 1667 // The last item equal one to indicate scale end 1668 var zoomOut = [ 1669 1.0, 1.375, 1.359375, 1.339844, 1.316406, 1.296875, 1.277344, 1.257812, 1670 1.238281, 1.21875, 1.199219, 1.175781, 1.15625, 1.132812, 1.101562, 1671 1.078125, 1.054688, 1.03125, 1.011719, 0.992188, 0.972656, 0.953125, 1672 0.933594, 1.0, 1673 ]; 1674 await synthesizeTouchpadPinch(zoomOut, focusX, focusY, options); 1675 } 1676 1677 async function pinchZoomInOutWithTouchpad(focusX, focusY, options = {}) { 1678 // Use the same scale for two events in a row to make sure the code handles this properly. 1679 var zoomInOut = [ 1680 1.0, 1.082031, 1.089844, 1.097656, 1.101562, 1.109375, 1.121094, 1.128906, 1681 1.128906, 1.125, 1.097656, 1.074219, 1.054688, 1.035156, 1.015625, 1.0, 1.0, 1682 ]; 1683 await synthesizeTouchpadPinch(zoomInOut, focusX, focusY, options); 1684 } 1685 // This generates a touch-based pinch gesture that is expected to succeed 1686 // and trigger an APZ:TransformEnd observer notification. 1687 // It returns after that notification has been dispatched. 1688 // The coordinates of touch events in `touchSequence` are expected to be 1689 // in CSS coordinates relative to the document body. 1690 async function synthesizeNativeTouchAndWaitForTransformEnd( 1691 touchSequence, 1692 touchIds 1693 ) { 1694 // Register the listener for the TransformEnd observer topic 1695 let transformEndPromise = promiseTopic("APZ:TransformEnd"); 1696 1697 // Dispatch all the touch events 1698 await synthesizeNativeTouchSequences( 1699 document.body, 1700 touchSequence, 1701 null, 1702 touchIds 1703 ); 1704 1705 // Wait for TransformEnd to fire. 1706 await transformEndPromise; 1707 } 1708 1709 // Returns a touch sequence for a pinch-zoom-out operation in the center 1710 // of the visual viewport. The touch sequence returned is in CSS coordinates 1711 // relative to the document body. 1712 function pinchZoomOutTouchSequenceAtCenter() { 1713 // Divide the half of visual viewport size by 8, then cause touch events 1714 // starting from the 7th furthest away from the center towards the center. 1715 const deltaX = window.visualViewport.width / 16; 1716 const deltaY = window.visualViewport.height / 16; 1717 const centerX = 1718 window.visualViewport.pageLeft + window.visualViewport.width / 2; 1719 const centerY = 1720 window.visualViewport.pageTop + window.visualViewport.height / 2; 1721 // prettier-ignore 1722 var zoom_out = [ 1723 [ { x: centerX - (deltaX * 6), y: centerY - (deltaY * 6) }, 1724 { x: centerX + (deltaX * 6), y: centerY + (deltaY * 6) } ], 1725 [ { x: centerX - (deltaX * 5), y: centerY - (deltaY * 5) }, 1726 { x: centerX + (deltaX * 5), y: centerY + (deltaY * 5) } ], 1727 [ { x: centerX - (deltaX * 4), y: centerY - (deltaY * 4) }, 1728 { x: centerX + (deltaX * 4), y: centerY + (deltaY * 4) } ], 1729 [ { x: centerX - (deltaX * 3), y: centerY - (deltaY * 3) }, 1730 { x: centerX + (deltaX * 3), y: centerY + (deltaY * 3) } ], 1731 [ { x: centerX - (deltaX * 2), y: centerY - (deltaY * 2) }, 1732 { x: centerX + (deltaX * 2), y: centerY + (deltaY * 2) } ], 1733 [ { x: centerX - (deltaX * 1), y: centerY - (deltaY * 1) }, 1734 { x: centerX + (deltaX * 1), y: centerY + (deltaY * 1) } ], 1735 ]; 1736 return zoom_out; 1737 } 1738 1739 // This generates a touch-based pinch zoom-out gesture that is expected 1740 // to succeed. It returns after APZ has completed the zoom and reaches the end 1741 // of the transform. The touch inputs are directed to the center of the 1742 // current visual viewport. 1743 async function pinchZoomOutWithTouchAtCenter() { 1744 var zoom_out = pinchZoomOutTouchSequenceAtCenter(); 1745 var touchIds = [0, 1]; 1746 await synthesizeNativeTouchAndWaitForTransformEnd(zoom_out, touchIds); 1747 } 1748 1749 // useTouchpad is only currently implemented on macOS 1750 async function synthesizeDoubleTap(element, x, y, useTouchpad) { 1751 if (useTouchpad) { 1752 await synthesizeNativeTouchpadDoubleTap(element, x, y); 1753 } else { 1754 await synthesizeNativeTap(element, x, y); 1755 await synthesizeNativeTap(element, x, y); 1756 } 1757 } 1758 // useTouchpad is only currently implemented on macOS 1759 async function doubleTapOn(element, x, y, useTouchpad) { 1760 let transformEndPromise = promiseTransformEnd(); 1761 1762 await synthesizeDoubleTap(element, x, y, useTouchpad); 1763 1764 // Wait for the APZ:TransformEnd to fire 1765 await transformEndPromise; 1766 1767 // Flush state so we can query an accurate resolution 1768 await promiseApzFlushedRepaints(); 1769 } 1770 1771 const NativePanHandlerForLinux = { 1772 beginPhase: SpecialPowers.DOMWindowUtils.PHASE_BEGIN, 1773 updatePhase: SpecialPowers.DOMWindowUtils.PHASE_UPDATE, 1774 endPhase: SpecialPowers.DOMWindowUtils.PHASE_END, 1775 promiseNativePanEvent: promiseNativeTouchpadPanEventAndWaitForObserver, 1776 delta: -50, 1777 }; 1778 1779 const NativePanHandlerForWindows = { 1780 beginPhase: SpecialPowers.DOMWindowUtils.PHASE_BEGIN, 1781 updatePhase: SpecialPowers.DOMWindowUtils.PHASE_UPDATE, 1782 endPhase: SpecialPowers.DOMWindowUtils.PHASE_END, 1783 promiseNativePanEvent: promiseNativeTouchpadPanEventAndWaitForObserver, 1784 delta: 50, 1785 }; 1786 1787 const NativePanHandlerForMac = { 1788 // From https://developer.apple.com/documentation/coregraphics/cgscrollphase/kcgscrollphasebegan?language=occ , etc. 1789 beginPhase: 1, // kCGScrollPhaseBegan 1790 updatePhase: 2, // kCGScrollPhaseChanged 1791 endPhase: 4, // kCGScrollPhaseEnded 1792 promiseNativePanEvent: promiseNativePanGestureEventAndWaitForObserver, 1793 delta: -50, 1794 }; 1795 1796 const NativePanHandlerForHeadless = { 1797 beginPhase: SpecialPowers.DOMWindowUtils.PHASE_BEGIN, 1798 updatePhase: SpecialPowers.DOMWindowUtils.PHASE_UPDATE, 1799 endPhase: SpecialPowers.DOMWindowUtils.PHASE_END, 1800 promiseNativePanEvent: promiseNativeTouchpadPanEventAndWaitForObserver, 1801 delta: 50, 1802 }; 1803 1804 function getPanHandler() { 1805 if (SpecialPowers.isHeadless) { 1806 return NativePanHandlerForHeadless; 1807 } 1808 1809 switch (getPlatform()) { 1810 case "linux": 1811 return NativePanHandlerForLinux; 1812 case "windows": 1813 return NativePanHandlerForWindows; 1814 case "mac": 1815 return NativePanHandlerForMac; 1816 default: 1817 throw new Error( 1818 "There's no native pan handler on platform " + getPlatform() 1819 ); 1820 } 1821 } 1822 1823 // Lazily get `NativePanHandler` to avoid an exception where we don't support 1824 // native pan events (e.g. Android). 1825 if (!window.hasOwnProperty("NativePanHandler")) { 1826 Object.defineProperty(window, "NativePanHandler", { 1827 get() { 1828 return getPanHandler(); 1829 }, 1830 }); 1831 } 1832 1833 async function panRightToLeftBegin(aElement, aX, aY, aMultiplier) { 1834 await NativePanHandler.promiseNativePanEvent( 1835 aElement, 1836 aX, 1837 aY, 1838 NativePanHandler.delta * aMultiplier, 1839 0, 1840 NativePanHandler.beginPhase 1841 ); 1842 } 1843 1844 async function panRightToLeftUpdate(aElement, aX, aY, aMultiplier) { 1845 await NativePanHandler.promiseNativePanEvent( 1846 aElement, 1847 aX, 1848 aY, 1849 NativePanHandler.delta * aMultiplier, 1850 0, 1851 NativePanHandler.updatePhase 1852 ); 1853 } 1854 1855 async function panRightToLeftEnd(aElement, aX, aY) { 1856 await NativePanHandler.promiseNativePanEvent( 1857 aElement, 1858 aX, 1859 aY, 1860 0, 1861 0, 1862 NativePanHandler.endPhase 1863 ); 1864 } 1865 1866 async function panRightToLeft(aElement, aX, aY, aMultiplier) { 1867 await panRightToLeftBegin(aElement, aX, aY, aMultiplier); 1868 await panRightToLeftUpdate(aElement, aX, aY, aMultiplier); 1869 await panRightToLeftEnd(aElement, aX, aY, aMultiplier); 1870 } 1871 1872 async function panLeftToRight(aElement, aX, aY, aMultiplier) { 1873 await panLeftToRightBegin(aElement, aX, aY, aMultiplier); 1874 await panLeftToRightUpdate(aElement, aX, aY, aMultiplier); 1875 await panLeftToRightEnd(aElement, aX, aY, aMultiplier); 1876 } 1877 1878 async function panLeftToRightBegin(aElement, aX, aY, aMultiplier) { 1879 await NativePanHandler.promiseNativePanEvent( 1880 aElement, 1881 aX, 1882 aY, 1883 -NativePanHandler.delta * aMultiplier, 1884 0, 1885 NativePanHandler.beginPhase 1886 ); 1887 } 1888 1889 async function panLeftToRightUpdate(aElement, aX, aY, aMultiplier) { 1890 await NativePanHandler.promiseNativePanEvent( 1891 aElement, 1892 aX, 1893 aY, 1894 -NativePanHandler.delta * aMultiplier, 1895 0, 1896 NativePanHandler.updatePhase 1897 ); 1898 await NativePanHandler.promiseNativePanEvent( 1899 aElement, 1900 aX, 1901 aY, 1902 -NativePanHandler.delta * aMultiplier, 1903 0, 1904 NativePanHandler.updatePhase 1905 ); 1906 } 1907 1908 async function panLeftToRightEnd(aElement, aX, aY) { 1909 await NativePanHandler.promiseNativePanEvent( 1910 aElement, 1911 aX, 1912 aY, 1913 0, 1914 0, 1915 NativePanHandler.endPhase 1916 ); 1917 } 1918 1919 // Close the context menu on desktop platforms. 1920 // NOTE: This function doesn't work if the context menu isn't open. 1921 async function closeContextMenu() { 1922 if (getPlatform() == "android") { 1923 return; 1924 } 1925 1926 const contextmenuClosedPromise = SpecialPowers.spawnChrome([], async () => { 1927 const menu = this.browsingContext.topChromeWindow.document.getElementById( 1928 "contentAreaContextMenu" 1929 ); 1930 ok( 1931 menu.state == "open" || menu.state == "showing", 1932 "This function is supposed to work only if the context menu is open or showing" 1933 ); 1934 1935 return new Promise(resolve => { 1936 menu.addEventListener( 1937 "popuphidden", 1938 () => { 1939 resolve(); 1940 }, 1941 { once: true } 1942 ); 1943 menu.hidePopup(); 1944 }); 1945 }); 1946 1947 await contextmenuClosedPromise; 1948 } 1949 1950 // Get a list of prefs which should be used for a subtest which wants to 1951 // generate a smooth scroll animation using an input event. The smooth 1952 // scroll animation is slowed down so the test can perform other actions 1953 // while it's still in progress. 1954 function getSmoothScrollPrefs(aInputType, aMsdPhysics) { 1955 let result = []; 1956 // Some callers just want the default and don't pass in aMsdPhysics. 1957 if (aMsdPhysics !== undefined) { 1958 result.push(["general.smoothScroll.msdPhysics.enabled", aMsdPhysics]); 1959 } else { 1960 aMsdPhysics = SpecialPowers.getBoolPref( 1961 "general.smoothScroll.msdPhysics.enabled" 1962 ); 1963 } 1964 if (aInputType == "wheel") { 1965 // We want to test real wheel events rather than pan events. 1966 result.push(["apz.test.mac.synth_wheel_input", true]); 1967 } /* keyboard input */ else { 1968 // The default verticalScrollDistance (which is 3) is too small for native 1969 // keyboard scrolling, it sometimes produces same scroll offsets in the early 1970 // stages of the smooth animation. 1971 result.push(["toolkit.scrollbox.verticalScrollDistance", 5]); 1972 } 1973 // Use a longer animation duration to avoid the situation that the 1974 // animation stops accidentally in between each arrow input event. 1975 // If the situation happens, scroll offsets will not change at the moment. 1976 if (aMsdPhysics) { 1977 // Prefs for MSD physics (applicable to any input type). 1978 result.push( 1979 ...[ 1980 ["general.smoothScroll.msdPhysics.motionBeginSpringConstant", 20], 1981 ["general.smoothScroll.msdPhysics.regularSpringConstant", 20], 1982 ["general.smoothScroll.msdPhysics.slowdownMinDeltaRatio", 0.1], 1983 ["general.smoothScroll.msdPhysics.slowdownSpringConstant", 20], 1984 ] 1985 ); 1986 } else if (aInputType == "wheel") { 1987 // Prefs for Bezier physics with wheel input. 1988 result.push( 1989 ...[ 1990 ["general.smoothScroll.mouseWheel.durationMaxMS", 1500], 1991 ["general.smoothScroll.mouseWheel.durationMinMS", 1500], 1992 ] 1993 ); 1994 } else { 1995 // Prefs for Bezier physics with keyboard input. 1996 result.push( 1997 ...[ 1998 ["general.smoothScroll.lines.durationMaxMS", 1500], 1999 ["general.smoothScroll.lines.durationMinMS", 1500], 2000 ] 2001 ); 2002 } 2003 return result; 2004 } 2005 2006 function buildRelativeScrollSmoothnessVariants(aInputType, aScrollMethods) { 2007 let subtests = []; 2008 for (let scrollMethod of aScrollMethods) { 2009 subtests.push({ 2010 file: `helper_relative_scroll_smoothness.html?input-type=${aInputType}&scroll-method=${scrollMethod}&strict=true`, 2011 prefs: [ 2012 ["apz.test.logging_enabled", true], 2013 ...getSmoothScrollPrefs(aInputType, /* Bezier physics */ false), 2014 ], 2015 }); 2016 // For MSD physics, run the test with strict=false. The shape of the 2017 // animation curve is highly timing dependent, and we can't guarantee 2018 // that an animation will run long enough until the next input event 2019 // arrives. 2020 subtests.push({ 2021 file: `helper_relative_scroll_smoothness.html?input-type=${aInputType}&scroll-method=${scrollMethod}&strict=false`, 2022 prefs: [ 2023 ["apz.test.logging_enabled", true], 2024 ...getSmoothScrollPrefs(aInputType, /* MSD physics */ true), 2025 ], 2026 }); 2027 } 2028 return subtests; 2029 } 2030 2031 // Right now this is only meaningful on Linux. 2032 async function getWindowProtocol() { 2033 if (getPlatform() != "linux") { 2034 return ""; 2035 } 2036 2037 return await SpecialPowers.spawnChrome([], () => { 2038 try { 2039 return Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo) 2040 .windowProtocol; 2041 } catch { 2042 return ""; 2043 } 2044 }); 2045 }