// Utilities for synthesizing of native events. function getPlatform() { if (navigator.platform.indexOf("Win") == 0) { return "windows"; } if (navigator.platform.indexOf("Mac") == 0) { return "mac"; } // Check for Android before Linux if (navigator.appVersion.indexOf("Android") >= 0) { return "android" } if (navigator.platform.indexOf("Linux") == 0) { return "linux"; } return "unknown"; } function nativeVerticalWheelEventMsg() { switch (getPlatform()) { case "windows": return 0x020A; // WM_MOUSEWHEEL case "mac": return 0; // value is unused, can be anything case "linux": return 4; // value is unused, pass GDK_SCROLL_SMOOTH anyway } throw "Native wheel events not supported on platform " + getPlatform(); } function nativeHorizontalWheelEventMsg() { switch (getPlatform()) { case "windows": return 0x020E; // WM_MOUSEHWHEEL case "mac": return 0; // value is unused, can be anything case "linux": return 4; // value is unused, pass GDK_SCROLL_SMOOTH anyway } throw "Native wheel events not supported on platform " + getPlatform(); } // Given a pixel scrolling delta, converts it to the platform's native units. function nativeScrollUnits(aElement, aDimen) { switch (getPlatform()) { case "linux": { // GTK deltas are treated as line height divided by 3 by gecko. var targetWindow = aElement.ownerDocument.defaultView; var lineHeight = targetWindow.getComputedStyle(aElement)["font-size"]; return aDimen / (parseInt(lineHeight) * 3); } } return aDimen; } function nativeMouseDownEventMsg() { switch (getPlatform()) { case "windows": return 2; // MOUSEEVENTF_LEFTDOWN case "mac": return 1; // NSLeftMouseDown case "linux": return 4; // GDK_BUTTON_PRESS case "android": return 5; // ACTION_POINTER_DOWN } throw "Native mouse-down events not supported on platform " + getPlatform(); } function nativeMouseMoveEventMsg() { switch (getPlatform()) { case "windows": return 1; // MOUSEEVENTF_MOVE case "mac": return 5; // NSMouseMoved case "linux": return 3; // GDK_MOTION_NOTIFY case "android": return 7; // ACTION_HOVER_MOVE } throw "Native mouse-move events not supported on platform " + getPlatform(); } function nativeMouseUpEventMsg() { switch (getPlatform()) { case "windows": return 4; // MOUSEEVENTF_LEFTUP case "mac": return 2; // NSLeftMouseUp case "linux": return 7; // GDK_BUTTON_RELEASE case "android": return 6; // ACTION_POINTER_UP } throw "Native mouse-up events not supported on platform " + getPlatform(); } // Convert (aX, aY), in CSS pixels relative to aElement's bounding rect, // to device pixels relative to the screen. function coordinatesRelativeToScreen(aX, aY, aElement) { var targetWindow = aElement.ownerDocument.defaultView; var scale = targetWindow.devicePixelRatio; var rect = aElement.getBoundingClientRect(); return { x: (targetWindow.mozInnerScreenX + rect.left + aX) * scale, y: (targetWindow.mozInnerScreenY + rect.top + aY) * scale }; } // Get the bounding box of aElement, and return it in device pixels // relative to the screen. function rectRelativeToScreen(aElement) { var targetWindow = aElement.ownerDocument.defaultView; var scale = targetWindow.devicePixelRatio; var rect = aElement.getBoundingClientRect(); return { x: (targetWindow.mozInnerScreenX + rect.left) * scale, y: (targetWindow.mozInnerScreenY + rect.top) * scale, w: (rect.width * scale), h: (rect.height * scale) }; } // Synthesizes a native mousewheel event and returns immediately. This does not // guarantee anything; you probably want to use one of the other functions below // which actually wait for results. // aX and aY are relative to the top-left of |aElement|'s containing window. // aDeltaX and aDeltaY are pixel deltas, and aObserver can be left undefined // if not needed. function synthesizeNativeWheel(aElement, aX, aY, aDeltaX, aDeltaY, aObserver) { var pt = coordinatesRelativeToScreen(aX, aY, aElement); if (aDeltaX && aDeltaY) { throw "Simultaneous wheeling of horizontal and vertical is not supported on all platforms."; } aDeltaX = nativeScrollUnits(aElement, aDeltaX); aDeltaY = nativeScrollUnits(aElement, aDeltaY); var msg = aDeltaX ? nativeHorizontalWheelEventMsg() : nativeVerticalWheelEventMsg(); var utils = SpecialPowers.getDOMWindowUtils(aElement.ownerDocument.defaultView); utils.sendNativeMouseScrollEvent(pt.x, pt.y, msg, aDeltaX, aDeltaY, 0, 0, 0, aElement, aObserver); return true; } // Synthesizes a native mousewheel event and invokes the callback once the // request has been successfully made to the OS. This does not necessarily // guarantee that the OS generates the event we requested. See // synthesizeNativeWheel for details on the parameters. function synthesizeNativeWheelAndWaitForObserver(aElement, aX, aY, aDeltaX, aDeltaY, aCallback) { var observer = { observe: function(aSubject, aTopic, aData) { if (aCallback && aTopic == "mousescrollevent") { setTimeout(aCallback, 0); } } }; return synthesizeNativeWheel(aElement, aX, aY, aDeltaX, aDeltaY, observer); } // Synthesizes a native mousewheel event and invokes the callback once the // wheel event is dispatched to |aElement|'s containing window. If the event // targets content in a subdocument, |aElement| should be inside the // subdocument. See synthesizeNativeWheel for details on the other parameters. function synthesizeNativeWheelAndWaitForWheelEvent(aElement, aX, aY, aDeltaX, aDeltaY, aCallback) { var targetWindow = aElement.ownerDocument.defaultView; targetWindow.addEventListener("wheel", function wheelWaiter(e) { targetWindow.removeEventListener("wheel", wheelWaiter); setTimeout(aCallback, 0); }); return synthesizeNativeWheel(aElement, aX, aY, aDeltaX, aDeltaY); } // Synthesizes a native mousewheel event and invokes the callback once the // first resulting scroll event is dispatched to |aElement|'s containing window. // If the event targets content in a subdocument, |aElement| should be inside // the subdocument. See synthesizeNativeWheel for details on the other // parameters. function synthesizeNativeWheelAndWaitForScrollEvent(aElement, aX, aY, aDeltaX, aDeltaY, aCallback) { var targetWindow = aElement.ownerDocument.defaultView; var useCapture = true; // scroll events don't always bubble targetWindow.addEventListener("scroll", function scrollWaiter(e) { targetWindow.removeEventListener("scroll", scrollWaiter, useCapture); setTimeout(aCallback, 0); }, useCapture); return synthesizeNativeWheel(aElement, aX, aY, aDeltaX, aDeltaY); } // Synthesizes a native mouse move event and returns immediately. // aX and aY are relative to the top-left of |aElement|'s containing window. function synthesizeNativeMouseMove(aElement, aX, aY) { var pt = coordinatesRelativeToScreen(aX, aY, aElement); var utils = SpecialPowers.getDOMWindowUtils(aElement.ownerDocument.defaultView); utils.sendNativeMouseEvent(pt.x, pt.y, nativeMouseMoveEventMsg(), 0, aElement); return true; } // Synthesizes a native mouse move event and invokes the callback once the // mouse move event is dispatched to |aElement|'s containing window. If the event // targets content in a subdocument, |aElement| should be inside the // subdocument. See synthesizeNativeMouseMove for details on the other // parameters. function synthesizeNativeMouseMoveAndWaitForMoveEvent(aElement, aX, aY, aCallback) { var targetWindow = aElement.ownerDocument.defaultView; targetWindow.addEventListener("mousemove", function mousemoveWaiter(e) { targetWindow.removeEventListener("mousemove", mousemoveWaiter); setTimeout(aCallback, 0); }); return synthesizeNativeMouseMove(aElement, aX, aY); } // Synthesizes a native touch event and dispatches it. aX and aY in CSS pixels // relative to the top-left of |aElement|'s bounding rect. function synthesizeNativeTouch(aElement, aX, aY, aType, aObserver = null, aTouchId = 0) { var pt = coordinatesRelativeToScreen(aX, aY, aElement); var utils = SpecialPowers.getDOMWindowUtils(aElement.ownerDocument.defaultView); utils.sendNativeTouchPoint(aTouchId, aType, pt.x, pt.y, 1, 90, aObserver); return true; } // A handy constant when synthesizing native touch drag events with the pref // "apz.touch_start_tolerance" set to 0. In this case, the first touchmove with // a nonzero pixel movement is consumed by the APZ to transition from the // "touching" state to the "panning" state, so calls to synthesizeNativeTouchDrag // should add an extra pixel pixel for this purpose. The TOUCH_SLOP provides // a constant that can be used for this purpose. Note that if the touch start // tolerance is set to something higher, the touch slop amount used must be // correspondingly increased so as to be higher than the tolerance. const TOUCH_SLOP = 1; function synthesizeNativeTouchDrag(aElement, aX, aY, aDeltaX, aDeltaY, aObserver = null, aTouchId = 0) { synthesizeNativeTouch(aElement, aX, aY, SpecialPowers.DOMWindowUtils.TOUCH_CONTACT, null, aTouchId); var steps = Math.max(Math.abs(aDeltaX), Math.abs(aDeltaY)); for (var i = 1; i < steps; i++) { var dx = i * (aDeltaX / steps); var dy = i * (aDeltaY / steps); synthesizeNativeTouch(aElement, aX + dx, aY + dy, SpecialPowers.DOMWindowUtils.TOUCH_CONTACT, null, aTouchId); } synthesizeNativeTouch(aElement, aX + aDeltaX, aY + aDeltaY, SpecialPowers.DOMWindowUtils.TOUCH_CONTACT, null, aTouchId); return synthesizeNativeTouch(aElement, aX + aDeltaX, aY + aDeltaY, SpecialPowers.DOMWindowUtils.TOUCH_REMOVE, aObserver, aTouchId); } function synthesizeNativeTap(aElement, aX, aY, aObserver = null) { var pt = coordinatesRelativeToScreen(aX, aY, aElement); var utils = SpecialPowers.getDOMWindowUtils(aElement.ownerDocument.defaultView); utils.sendNativeTouchTap(pt.x, pt.y, false, aObserver); return true; } function synthesizeNativeMouseEvent(aElement, aX, aY, aType, aObserver = null) { var pt = coordinatesRelativeToScreen(aX, aY, aElement); var utils = SpecialPowers.getDOMWindowUtils(aElement.ownerDocument.defaultView); utils.sendNativeMouseEvent(pt.x, pt.y, aType, 0, aElement, aObserver); return true; } function synthesizeNativeClick(aElement, aX, aY, aObserver = null) { var pt = coordinatesRelativeToScreen(aX, aY, aElement); var utils = SpecialPowers.getDOMWindowUtils(aElement.ownerDocument.defaultView); utils.sendNativeMouseEvent(pt.x, pt.y, nativeMouseDownEventMsg(), 0, aElement, function() { utils.sendNativeMouseEvent(pt.x, pt.y, nativeMouseUpEventMsg(), 0, aElement, aObserver); }); return true; } // Move the mouse to (dx, dy) relative to |element|, and scroll the wheel // at that location. // Moving the mouse is necessary to avoid wheel events from two consecutive // moveMouseAndScrollWheelOver() calls on different elements being incorrectly // considered as part of the same wheel transaction. // We also wait for the mouse move event to be processed before sending the // wheel event, otherwise there is a chance they might get reordered, and // we have the transaction problem again. function moveMouseAndScrollWheelOver(element, dx, dy, testDriver, waitForScroll = true) { return synthesizeNativeMouseMoveAndWaitForMoveEvent(element, dx, dy, function() { if (waitForScroll) { synthesizeNativeWheelAndWaitForScrollEvent(element, dx, dy, 0, -10, testDriver); } else { synthesizeNativeWheelAndWaitForWheelEvent(element, dx, dy, 0, -10, testDriver); } }); }