Bug 1844723 - Synthesizing mouseup during a drag session should end the session r=edgar,dom-core,webdriver-reviewers,whimboo

The WPT which was added by the previous patch (D187644) fails if it runs
after `mousemove_prevent_default_action.tentative.html` because it synthesize
`dragstart` with synthesizing multiple mouse events, however, `mouseup`
does not ends the drag session and the following test starts with the session.

The TestDriver finally runs `EventUtils`.  Therefore, we can make it manage
the drag session with XPCOM API.

Note that we should synthesize `dragover` for `mousemove`, and `drop` if
the drop is accepted.  However, it requires more work, so we should do it
in a separate bug.

Differential Revision: https://phabricator.services.mozilla.com/D188934
This commit is contained in:
Masayuki Nakano 2023-10-10 07:33:06 +00:00
parent b8f3f296ed
commit b9f2cc10a7
5 changed files with 159 additions and 11 deletions

View File

@ -16,6 +16,9 @@ export const event = {};
ChromeUtils.defineLazyGetter(lazy, "dblclickTimer", () => {
return Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
});
ChromeUtils.defineLazyGetter(event, "dragService", () => {
return Cc["@mozilla.org/widget/dragservice;1"].getService(Ci.nsIDragService);
});
const _eventUtils = new WeakMap();

View File

@ -174,23 +174,44 @@ async function webdriverClickElement(el, a11y) {
if (el.localName == "option") {
interaction.selectOption(el);
} else {
// step 9
let clicked = interaction.flushEventLoop(containerEl);
// Synthesize a pointerMove action.
lazy.event.synthesizeMouseAtPoint(
clickPoint.x,
clickPoint.y,
{
type: "mousemove",
allowToHandleDragDrop: true,
},
win
);
// Synthesize a pointerDown + pointerUp action.
lazy.event.synthesizeMouseAtPoint(clickPoint.x, clickPoint.y, {}, win);
if (lazy.event.dragService.getCurrentSession()) {
// Special handling is required if the mousemove started a drag session.
// In this case, mousedown event shouldn't be fired, and the mouseup should
// end the session. Therefore, we should synthesize only mouseup.
lazy.event.synthesizeMouseAtPoint(
clickPoint.x,
clickPoint.y,
{
type: "mouseup",
allowToHandleDragDrop: true,
},
win
);
} else {
// step 9
let clicked = interaction.flushEventLoop(containerEl);
await clicked;
// Synthesize a pointerDown + pointerUp action.
lazy.event.synthesizeMouseAtPoint(
clickPoint.x,
clickPoint.y,
{ allowToHandleDragDrop: true },
win
);
await clicked;
}
}
// step 10

View File

@ -2172,6 +2172,12 @@ class MouseEventData extends PointerEventData {
this.button = button;
this.buttons = 0;
// Some WPTs try to synthesize DnD only with mouse events. However,
// Gecko waits DnD events directly and non-WPT-tests use Gecko specific
// test API to synthesize DnD. Therefore, we want new path only for
// synthesized events coming from the webdriver.
this.allowToHandleDragDrop = true;
}
update(state, inputSource) {

View File

@ -117,6 +117,16 @@ function _EU_maybeWrap(o) {
}
function _EU_maybeUnwrap(o) {
var haveWrap = false;
try {
haveWrap = SpecialPowers.unwrap != undefined;
} catch (e) {
// Just leave it false.
}
if (!haveWrap) {
// Not much we can do here.
return o;
}
var c = Object.getOwnPropertyDescriptor(window, "Components");
return c && c.value && !c.writable ? o : SpecialPowers.unwrap(o);
}
@ -569,6 +579,77 @@ function synthesizeTouch(aTarget, aOffsetX, aOffsetY, aEvent, aWindow) {
);
}
function getDragService() {
return _EU_Cc["@mozilla.org/widget/dragservice;1"].getService(
_EU_Ci.nsIDragService
);
}
/**
* End drag session if there is.
*
* TODO: This should synthesize "drop" if necessary.
*
* @param left X offset in the viewport
* @param top Y offset in the viewport
* @param aEvent The event data, the modifiers are applied to the
* "dragend" event.
* @param aWindow The window.
* @return true if handled. In this case, the caller should not
* synthesize DOM events basically.
*/
function _maybeEndDragSession(left, top, aEvent, aWindow) {
const dragService = getDragService();
const dragSession = dragService?.getCurrentSession();
if (!dragSession) {
return false;
}
// FIXME: If dragSession.dragAction is not
// nsIDragService.DRAGDROP_ACTION_NONE nor aEvent.type is not `keydown`, we
// need to synthesize a "drop" event or call setDragEndPointForTests here to
// set proper left/top to `dragend` event.
try {
dragService.endDragSession(false, _parseModifiers(aEvent, aWindow));
} catch (e) {}
return true;
}
function _maybeSynthesizeDragOver(left, top, aEvent, aWindow) {
const dragSession = getDragService()?.getCurrentSession();
if (!dragSession) {
return false;
}
const target = aWindow.document.elementFromPoint(left, top);
if (target) {
sendDragEvent(
createDragEventObject(
"dragover",
target,
aWindow,
dragSession.dataTransfer,
{
accelKey: aEvent.accelKey,
altKey: aEvent.altKey,
altGrKey: aEvent.altGrKey,
ctrlKey: aEvent.ctrlKey,
metaKey: aEvent.metaKey,
shiftKey: aEvent.shiftKey,
capsLockKey: aEvent.capsLockKey,
fnKey: aEvent.fnKey,
fnLockKey: aEvent.fnLockKey,
numLockKey: aEvent.numLockKey,
scrollLockKey: aEvent.scrollLockKey,
symbolKey: aEvent.symbolKey,
symbolLockKey: aEvent.symbolLockKey,
}
),
target,
aWindow
);
}
return true;
}
/*
* Synthesize a mouse event at a particular point in aWindow.
*
@ -583,6 +664,18 @@ function synthesizeTouch(aTarget, aOffsetX, aOffsetY, aEvent, aWindow) {
* aWindow is optional, and defaults to the current window object.
*/
function synthesizeMouseAtPoint(left, top, aEvent, aWindow = window) {
if (aEvent.allowToHandleDragDrop) {
if (aEvent.type == "mouseup" || !aEvent.type) {
if (_maybeEndDragSession(left, top, aEvent, aWindow)) {
return false;
}
} else if (aEvent.type == "mousemove") {
if (_maybeSynthesizeDragOver(left, top, aEvent, aWindow)) {
return false;
}
}
}
var utils = _getDOMWindowUtils(aWindow);
var defaultPrevented = false;
@ -1394,7 +1487,32 @@ function synthesizeAndWaitNativeMouseMove(
*
*/
function synthesizeKey(aKey, aEvent = undefined, aWindow = window, aCallback) {
var event = aEvent === undefined || aEvent === null ? {} : aEvent;
const event = aEvent === undefined || aEvent === null ? {} : aEvent;
let dispatchKeydown =
!("type" in event) || event.type === "keydown" || !event.type;
const dispatchKeyup =
!("type" in event) || event.type === "keyup" || !event.type;
if (dispatchKeydown && aKey == "KEY_Escape") {
let eventForKeydown = Object.assign({}, JSON.parse(JSON.stringify(event)));
eventForKeydown.type = "keydown";
if (
_maybeEndDragSession(
// TODO: We should set the last dragover point instead
0,
0,
eventForKeydown,
aWindow
)
) {
if (!dispatchKeyup) {
return;
}
// We don't need to dispatch only keydown event because it's consumed by
// the drag session.
dispatchKeydown = false;
}
}
var TIP = _getTIP(aWindow, aCallback);
if (!TIP) {
@ -1404,10 +1522,6 @@ function synthesizeKey(aKey, aEvent = undefined, aWindow = window, aCallback) {
var modifiers = _emulateToActivateModifiers(TIP, event, aWindow);
var keyEventDict = _createKeyboardEventDictionary(aKey, event, TIP, aWindow);
var keyEvent = new KeyboardEvent("", keyEventDict.dictionary);
var dispatchKeydown =
!("type" in event) || event.type === "keydown" || !event.type;
var dispatchKeyup =
!("type" in event) || event.type === "keyup" || !event.type;
try {
if (dispatchKeydown) {

View File

@ -1,4 +1,6 @@
[synthetic-mouse-enter-leave-over-out-button-state-after-target-removed.tentative.html?buttonType=MIDDLE&button=1&buttons=4]
expected:
if os == "android": [OK, ERROR]
[Removing an element at mousedown: mouseout and mouseleave should've been fired on the removed child]
expected: FAIL
@ -13,6 +15,8 @@
[synthetic-mouse-enter-leave-over-out-button-state-after-target-removed.tentative.html?buttonType=LEFT&button=0&buttons=1]
expected:
if os == "android": [OK, ERROR]
[Removing an element at mousedown: mouseout and mouseleave should've been fired on the removed child]
expected: FAIL