mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-12-02 18:08:58 +00:00
Bug 1864654 - Make PresShell
flush pending synthetic mousemove
before dispatching mousedown
or mouseup
r=smaug,dom-core,extension-reviewers,edgar,robwu
`inert-iframe-hittest.html` expects that `:hover` state should be updated immediately after a pointer down. The state is updated by `EventStateManager::NotifyMouseOver` [1] when we received a real or synthetic `eMouseMove`. Therefore, the hover state may have not set yet if no refresh occurs between the pointer down and checking the result. Additionally, some WPT for UI Events and Pointer Events expect that `mouseover` or `mouseout` should be fired before `mouseup`/`pointerup` if a preceding `mousedown` or `pointerdown` event listener removes the target. So, this patch may fix intermittent failures of them if there are. Therefore, we should flush pending synthetic `mousemove` event for keeping the event order as same as they happen. However, simply flushing it causes unexpected pointer capture state change because `PointerEventHandler::ProcessPointerCaptureForMouse` handles synthetic `mousemove` but it should not cause pointer boundary events at the moment (the pointer boundary events should be fired when the pointer is actually moved, note that this is different rule from the rules of mouse boundary events). Therefore, this patch changes `PointerEventHandler::ShouldGeneratePointerEventFromMouse`. However, this blocks `lostpointercatpure` event when the capturing content has already been removed from the DOM tree. The path is, `EventHandler::HandleEventWithPointerCapturingContentWithoutItsFrame` stops dispatching `pointerup` nor `pointercancel` in this case, but `EventStateManager::PostHandleEvent` is the only handler of implicit pointer capture release. Therefore, we need to make it dispatch lostpointercatpure` event if the canceling event caused `ePointerUp` or `ePointerCancel`. Finally, this patch fixes a bug of `browser_ext_browserAction_popup_preload.js`. It tests the pre-loading which starts when `mouseover` before `mousedown` on a widget. However, it does not correctly emulate the user input. * Synthesizing only `mouseover` does not update the internal pointer info and does not cause dispatching related events. * Synthesizing `mousedown` without `mousemove` cause `mouseover` before `mouseup` because at dispatching `mousedown`, `PresShell` stores the cursor [2] and the mouse boundary events will be dispatched before `mouseup`. `ext-browserAction.js` assumes the events are fired as this order, `mouseover` -> `mousedown` -> `mouseup`, but this would not work if the preceding `mousemove` of `mousedown` is not correctly fired. I'm not sure whether `mousemove` is always fired before `mousedown` on any environments, but it should be rare and it affects only this kind of tricky code. For now, fixing this in the test side must be enough. 1. https://searchfox.org/mozilla-central/rev/9a5bf21ea2dd04946734658f67f83f62ca76b0fa/dom/events/EventStateManager.cpp#4874,4913-4914 2. https://searchfox.org/mozilla-central/rev/9a5bf21ea2dd04946734658f67f83f62ca76b0fa/layout/base/PresShell.cpp#6756 Differential Revision: https://phabricator.services.mozilla.com/D193870
This commit is contained in:
parent
566db78cc5
commit
d3b4a4d6ac
@ -15,7 +15,7 @@ add_task(async function testBrowserActionClickCanceled() {
|
||||
// Make sure the mouse isn't hovering over the browserAction widget.
|
||||
EventUtils.synthesizeMouseAtCenter(
|
||||
gURLBar.textbox,
|
||||
{ type: "mouseover" },
|
||||
{ type: "mousemove" },
|
||||
window
|
||||
);
|
||||
|
||||
@ -48,6 +48,11 @@ add_task(async function testBrowserActionClickCanceled() {
|
||||
let widget = getBrowserActionWidget(extension).forWindow(window);
|
||||
|
||||
// Test canceled click.
|
||||
EventUtils.synthesizeMouseAtCenter(
|
||||
widget.node,
|
||||
{ type: "mousemove" },
|
||||
window
|
||||
);
|
||||
EventUtils.synthesizeMouseAtCenter(
|
||||
widget.node,
|
||||
{ type: "mousedown", button: 0 },
|
||||
@ -77,6 +82,11 @@ add_task(async function testBrowserActionClickCanceled() {
|
||||
AccessibilityUtils.setEnv({
|
||||
mustHaveAccessibleRule: false,
|
||||
});
|
||||
EventUtils.synthesizeMouseAtCenter(
|
||||
document.documentElement,
|
||||
{ type: "mousemove" },
|
||||
window
|
||||
);
|
||||
EventUtils.synthesizeMouseAtCenter(
|
||||
document.documentElement,
|
||||
{ type: "mouseup", button: 0 },
|
||||
@ -99,6 +109,11 @@ add_task(async function testBrowserActionClickCanceled() {
|
||||
);
|
||||
|
||||
// Test completed click.
|
||||
EventUtils.synthesizeMouseAtCenter(
|
||||
widget.node,
|
||||
{ type: "mousemove" },
|
||||
window
|
||||
);
|
||||
EventUtils.synthesizeMouseAtCenter(
|
||||
widget.node,
|
||||
{ type: "mousedown", button: 0 },
|
||||
@ -159,7 +174,7 @@ add_task(async function testBrowserActionDisabled() {
|
||||
// Make sure the mouse isn't hovering over the browserAction widget.
|
||||
EventUtils.synthesizeMouseAtCenter(
|
||||
gURLBar.textbox,
|
||||
{ type: "mouseover" },
|
||||
{ type: "mousemove" },
|
||||
window
|
||||
);
|
||||
|
||||
@ -206,6 +221,11 @@ add_task(async function testBrowserActionDisabled() {
|
||||
is(browserAction.pendingPopup, null, "Have no pending popup prior to click");
|
||||
|
||||
// Test canceled click.
|
||||
EventUtils.synthesizeMouseAtCenter(
|
||||
widget.node,
|
||||
{ type: "mousemove" },
|
||||
window
|
||||
);
|
||||
EventUtils.synthesizeMouseAtCenter(
|
||||
widget.node,
|
||||
{ type: "mousedown", button: 0 },
|
||||
@ -222,6 +242,11 @@ add_task(async function testBrowserActionDisabled() {
|
||||
AccessibilityUtils.setEnv({
|
||||
mustHaveAccessibleRule: false,
|
||||
});
|
||||
EventUtils.synthesizeMouseAtCenter(
|
||||
document.documentElement,
|
||||
{ type: "mousemove" },
|
||||
window
|
||||
);
|
||||
EventUtils.synthesizeMouseAtCenter(
|
||||
document.documentElement,
|
||||
{ type: "mouseup", button: 0 },
|
||||
@ -233,6 +258,11 @@ add_task(async function testBrowserActionDisabled() {
|
||||
is(browserAction.pendingPopupTimeout, null, "Have no pending popup timeout");
|
||||
|
||||
// Test completed click.
|
||||
EventUtils.synthesizeMouseAtCenter(
|
||||
widget.node,
|
||||
{ type: "mousemove" },
|
||||
window
|
||||
);
|
||||
EventUtils.synthesizeMouseAtCenter(
|
||||
widget.node,
|
||||
{ type: "mousedown", button: 0 },
|
||||
@ -322,13 +352,14 @@ add_task(async function testBrowserActionTabPopulation() {
|
||||
// Make sure the mouse isn't hovering over the browserAction widget.
|
||||
EventUtils.synthesizeMouseAtCenter(
|
||||
win.gURLBar.textbox,
|
||||
{ type: "mouseover" },
|
||||
{ type: "mousemove" },
|
||||
win
|
||||
);
|
||||
|
||||
await extension.startup();
|
||||
|
||||
let widget = getBrowserActionWidget(extension).forWindow(win);
|
||||
EventUtils.synthesizeMouseAtCenter(widget.node, { type: "mousemove" }, win);
|
||||
EventUtils.synthesizeMouseAtCenter(
|
||||
widget.node,
|
||||
{ type: "mousedown", button: 0 },
|
||||
@ -371,7 +402,7 @@ add_task(async function testClosePopupDuringPreload() {
|
||||
// Make sure the mouse isn't hovering over the browserAction widget.
|
||||
EventUtils.synthesizeMouseAtCenter(
|
||||
gURLBar.textbox,
|
||||
{ type: "mouseover" },
|
||||
{ type: "mousemove" },
|
||||
window
|
||||
);
|
||||
|
||||
@ -388,6 +419,11 @@ add_task(async function testClosePopupDuringPreload() {
|
||||
|
||||
let widget = getBrowserActionWidget(extension).forWindow(window);
|
||||
|
||||
EventUtils.synthesizeMouseAtCenter(
|
||||
widget.node,
|
||||
{ type: "mousemove" },
|
||||
window
|
||||
);
|
||||
EventUtils.synthesizeMouseAtCenter(
|
||||
widget.node,
|
||||
{ type: "mousedown", button: 0 },
|
||||
|
@ -417,6 +417,19 @@ void PointerEventHandler::ImplicitlyReleasePointerCapture(WidgetEvent* aEvent) {
|
||||
CheckPointerCaptureState(pointerEvent);
|
||||
}
|
||||
|
||||
/* static */
|
||||
void PointerEventHandler::MaybeImplicitlyReleasePointerCapture(
|
||||
WidgetGUIEvent* aEvent) {
|
||||
MOZ_ASSERT(aEvent);
|
||||
const EventMessage pointerEventMessage =
|
||||
PointerEventHandler::ToPointerEventMessage(aEvent);
|
||||
if (pointerEventMessage != ePointerUp &&
|
||||
pointerEventMessage != ePointerCancel) {
|
||||
return;
|
||||
}
|
||||
PointerEventHandler::MaybeProcessPointerCapture(aEvent);
|
||||
}
|
||||
|
||||
/* static */
|
||||
Element* PointerEventHandler::GetPointerCapturingElement(uint32_t aPointerId) {
|
||||
PointerCaptureInfo* pointerCaptureInfo = GetPointerCaptureInfo(aPointerId);
|
||||
@ -563,6 +576,38 @@ void PointerEventHandler::InitPointerEventFromTouch(
|
||||
aPointerEvent.mPressure = aTouch.mForce;
|
||||
}
|
||||
|
||||
/* static */
|
||||
EventMessage PointerEventHandler::ToPointerEventMessage(
|
||||
const WidgetGUIEvent* aMouseOrTouchEvent) {
|
||||
MOZ_ASSERT(aMouseOrTouchEvent);
|
||||
|
||||
switch (aMouseOrTouchEvent->mMessage) {
|
||||
case eMouseMove:
|
||||
return ePointerMove;
|
||||
case eMouseUp:
|
||||
return aMouseOrTouchEvent->AsMouseEvent()->mButtons ? ePointerMove
|
||||
: ePointerUp;
|
||||
case eMouseDown: {
|
||||
const WidgetMouseEvent* mouseEvent = aMouseOrTouchEvent->AsMouseEvent();
|
||||
return mouseEvent->mButtons & ~nsContentUtils::GetButtonsFlagForButton(
|
||||
mouseEvent->mButton)
|
||||
? ePointerMove
|
||||
: ePointerDown;
|
||||
}
|
||||
case eTouchMove:
|
||||
return ePointerMove;
|
||||
case eTouchEnd:
|
||||
return ePointerUp;
|
||||
case eTouchStart:
|
||||
return ePointerDown;
|
||||
case eTouchCancel:
|
||||
case eTouchPointerCancel:
|
||||
return ePointerCancel;
|
||||
default:
|
||||
return eVoidEvent;
|
||||
}
|
||||
}
|
||||
|
||||
/* static */
|
||||
void PointerEventHandler::DispatchPointerFromMouseOrTouch(
|
||||
PresShell* aShell, nsIFrame* aFrame, nsIContent* aContent,
|
||||
@ -595,24 +640,10 @@ void PointerEventHandler::DispatchPointerFromMouseOrTouch(
|
||||
return;
|
||||
}
|
||||
|
||||
switch (mouseEvent->mMessage) {
|
||||
case eMouseMove:
|
||||
pointerMessage = ePointerMove;
|
||||
break;
|
||||
case eMouseUp:
|
||||
pointerMessage = mouseEvent->mButtons ? ePointerMove : ePointerUp;
|
||||
break;
|
||||
case eMouseDown:
|
||||
pointerMessage =
|
||||
mouseEvent->mButtons & ~nsContentUtils::GetButtonsFlagForButton(
|
||||
mouseEvent->mButton)
|
||||
? ePointerMove
|
||||
: ePointerDown;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
pointerMessage = PointerEventHandler::ToPointerEventMessage(mouseEvent);
|
||||
if (pointerMessage == eVoidEvent) {
|
||||
return;
|
||||
}
|
||||
|
||||
WidgetPointerEvent event(*mouseEvent);
|
||||
InitPointerEventFromMouse(&event, mouseEvent, pointerMessage);
|
||||
event.convertToPointer = mouseEvent->convertToPointer = false;
|
||||
@ -633,24 +664,10 @@ void PointerEventHandler::DispatchPointerFromMouseOrTouch(
|
||||
WidgetTouchEvent* touchEvent = aEvent->AsTouchEvent();
|
||||
// loop over all touches and dispatch pointer events on each touch
|
||||
// copy the event
|
||||
switch (touchEvent->mMessage) {
|
||||
case eTouchMove:
|
||||
pointerMessage = ePointerMove;
|
||||
break;
|
||||
case eTouchEnd:
|
||||
pointerMessage = ePointerUp;
|
||||
break;
|
||||
case eTouchStart:
|
||||
pointerMessage = ePointerDown;
|
||||
break;
|
||||
case eTouchCancel:
|
||||
case eTouchPointerCancel:
|
||||
pointerMessage = ePointerCancel;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
pointerMessage = PointerEventHandler::ToPointerEventMessage(touchEvent);
|
||||
if (pointerMessage == eVoidEvent) {
|
||||
return;
|
||||
}
|
||||
|
||||
RefPtr<PresShell> shell(aShell);
|
||||
for (uint32_t i = 0; i < touchEvent->mTouches.Length(); ++i) {
|
||||
Touch* touch = touchEvent->mTouches[i];
|
||||
|
@ -117,6 +117,8 @@ class PointerEventHandler final {
|
||||
static void ImplicitlyCapturePointer(nsIFrame* aFrame, WidgetEvent* aEvent);
|
||||
MOZ_CAN_RUN_SCRIPT
|
||||
static void ImplicitlyReleasePointerCapture(WidgetEvent* aEvent);
|
||||
MOZ_CAN_RUN_SCRIPT static void MaybeImplicitlyReleasePointerCapture(
|
||||
WidgetGUIEvent* aEvent);
|
||||
|
||||
/**
|
||||
* GetPointerCapturingContent returns a target element which captures the
|
||||
@ -183,7 +185,8 @@ class PointerEventHandler final {
|
||||
|
||||
static bool ShouldGeneratePointerEventFromMouse(WidgetGUIEvent* aEvent) {
|
||||
return aEvent->mMessage == eMouseDown || aEvent->mMessage == eMouseUp ||
|
||||
aEvent->mMessage == eMouseMove ||
|
||||
(aEvent->mMessage == eMouseMove &&
|
||||
aEvent->AsMouseEvent()->IsReal()) ||
|
||||
aEvent->mMessage == eMouseExitFromWidget;
|
||||
}
|
||||
|
||||
@ -202,6 +205,10 @@ class PointerEventHandler final {
|
||||
static bool IsDragAndDropEnabled(WidgetMouseEvent& aEvent);
|
||||
|
||||
private:
|
||||
// Get proper pointer event message for a mouse or touch event.
|
||||
static EventMessage ToPointerEventMessage(
|
||||
const WidgetGUIEvent* aMouseOrTouchEvent);
|
||||
|
||||
// Set pointer capture of the specified pointer by the element.
|
||||
static void SetPointerCaptureById(uint32_t aPointerId,
|
||||
dom::Element* aElement);
|
||||
|
@ -474,6 +474,8 @@ skip-if = [
|
||||
"http2",
|
||||
]
|
||||
|
||||
["test_mouse_over_at_removing_down_target.html"]
|
||||
|
||||
["test_moving_and_expanding_selection_per_page.html"]
|
||||
support-files = ["window_empty_document.html"]
|
||||
|
||||
|
75
dom/events/test/test_mouse_over_at_removing_down_target.html
Normal file
75
dom/events/test/test_mouse_over_at_removing_down_target.html
Normal file
@ -0,0 +1,75 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Check whether `mouseup` events are fired after pending boundary events</title>
|
||||
<script src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script src="/tests/SimpleTest/EventUtils.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
|
||||
<style>
|
||||
div#parent {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
background-color: gray;
|
||||
}
|
||||
div#child {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
background-color: lime;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
"use strict";
|
||||
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
SimpleTest.waitForFocus(async () => {
|
||||
await SpecialPowers.pushPrefEnv({ set: [["layout.reflow.synthMouseMove", true]] });
|
||||
const winUtils = SpecialPowers.wrap(window).windowUtils;
|
||||
try {
|
||||
winUtils.disableNonTestMouseEvents(true);
|
||||
const parent = document.querySelector("div");
|
||||
const child = parent.querySelector("div");
|
||||
synthesizeMouseAtCenter(child, { type: "mousemove" });
|
||||
await new Promise(resolve => requestAnimationFrame(
|
||||
() => requestAnimationFrame(resolve)
|
||||
));
|
||||
|
||||
const mouseEvents = [];
|
||||
child.addEventListener("mousedown", event => {
|
||||
event.target.remove();
|
||||
mouseEvents.push("mousedown@div#child");
|
||||
});
|
||||
document.addEventListener("mouseover", event => {
|
||||
mouseEvents.push(`mouseover@${event.target.localName}${event.target.id ? `#${event.target.id}` : ""}`);
|
||||
}, {capture: true});
|
||||
document.addEventListener("mouseup", event => {
|
||||
mouseEvents.push(`mouseup@${event.target.localName}${event.target.id ? `#${event.target.id}` : ""}`);
|
||||
}, {capture: true});
|
||||
winUtils.advanceTimeAndRefresh(100);
|
||||
// Click in the child, then, the child will be removed by the "mousedown"
|
||||
// event listener and that should cause "mouseover" on the parent and that
|
||||
// must be fired before "mouseup".
|
||||
synthesizeMouseAtCenter(child, {});
|
||||
winUtils.restoreNormalRefresh();
|
||||
await new Promise(resolve => requestAnimationFrame(
|
||||
() => requestAnimationFrame(resolve)
|
||||
));
|
||||
is(
|
||||
mouseEvents.toString(),
|
||||
"mousedown@div#child,mouseover@div#parent,mouseup@div#parent",
|
||||
"mouseover should be fired before mouseup on the ex-parent of the removed child"
|
||||
);
|
||||
} finally {
|
||||
winUtils.disableNonTestMouseEvents(false);
|
||||
document.querySelector("style").remove();
|
||||
}
|
||||
SimpleTest.finish();
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="parent">
|
||||
<div id="child"></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -6944,6 +6944,38 @@ nsresult PresShell::HandleEvent(nsIFrame* aFrameForPresShell,
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
if (mPresContext) {
|
||||
switch (aGUIEvent->mMessage) {
|
||||
case eMouseDown:
|
||||
case eMouseUp: {
|
||||
// We should flush pending mousemove event before that because the event
|
||||
// may occur without proper boundary event. E.g., if a mousedown event
|
||||
// listener removed or appended an element under the cursor and mouseup
|
||||
// event comes immediately after that, mouseover or mouseout may have
|
||||
// not been dispatched on the new element yet.
|
||||
// XXX If eMouseMove is not propery dispatched before eMouseDown and
|
||||
// a `mousedown` event listener removes the event target or its
|
||||
// ancestor, eMouseOver will be dispatched between eMouseDown and
|
||||
// eMouseUp. That could cause unexpected behavior if a `mouseover`
|
||||
// event listener assumes it's always disptached before `mousedown`.
|
||||
// However, we're not sure whether it could happen with users' input.
|
||||
RefPtr<PresShell> rootPresShell =
|
||||
mPresContext->IsRoot() ? this : GetRootPresShell();
|
||||
if (rootPresShell && rootPresShell->mSynthMouseMoveEvent.IsPending()) {
|
||||
RefPtr<nsSynthMouseMoveEvent> synthMouseMoveEvent =
|
||||
rootPresShell->mSynthMouseMoveEvent.get();
|
||||
synthMouseMoveEvent->Run();
|
||||
}
|
||||
if (IsDestroying()) {
|
||||
return NS_OK;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
EventHandler eventHandler(*this);
|
||||
return eventHandler.HandleEvent(aFrameForPresShell, aGUIEvent,
|
||||
aDontRetargetEvents, aEventStatus);
|
||||
@ -7976,6 +8008,10 @@ PresShell::EventHandler::HandleEventWithPointerCapturingContentWithoutItsFrame(
|
||||
// If we can't process event for the capturing content, release
|
||||
// the capture.
|
||||
PointerEventHandler::ReleaseIfCaptureByDescendant(aPointerCapturingContent);
|
||||
// Since we don't dispatch ePointeUp nor ePointerCancel in this case,
|
||||
// EventStateManager::PostHandleEvent does not have a chance to dispatch
|
||||
// ePointerLostCapture event. Therefore, we need to dispatch it here.
|
||||
PointerEventHandler::MaybeImplicitlyReleasePointerCapture(aGUIEvent);
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
|
@ -2033,7 +2033,9 @@ class PresShell final : public nsStubDocumentObserver,
|
||||
void Revoke();
|
||||
|
||||
MOZ_CAN_RUN_SCRIPT
|
||||
void WillRefresh(TimeStamp aTime) override {
|
||||
void WillRefresh(TimeStamp aTime) override { Run(); }
|
||||
|
||||
MOZ_CAN_RUN_SCRIPT void Run() {
|
||||
if (mPresShell) {
|
||||
RefPtr<PresShell> shell = mPresShell;
|
||||
shell->ProcessSynthMouseMoveEvent(mFromScroll);
|
||||
|
@ -0,0 +1,5 @@
|
||||
[mouseover-at-removing-mousedown-target.html?duration=16]
|
||||
prefs: [layout.reflow.synthMouseMove:true]
|
||||
|
||||
[mouseover-at-removing-mousedown-target.html?duration=42]
|
||||
prefs: [layout.reflow.synthMouseMove:true]
|
@ -0,0 +1,81 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="timeout" content="long">
|
||||
<meta name="variant" content="?duration=16"> <!-- 60fps -->
|
||||
<meta name="variant" content="?duration=42"> <!-- 24fps -->
|
||||
<title>Check whether `mouseup` events are fired after pending boundary events</title>
|
||||
<script src=/resources/testharness.js></script>
|
||||
<script src=/resources/testharnessreport.js></script>
|
||||
<script src=/resources/testdriver.js></script>
|
||||
<script src=/resources/testdriver-actions.js></script>
|
||||
<script src=/resources/testdriver-vendor.js></script>
|
||||
<style>
|
||||
div#parent {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
background-color: gray;
|
||||
}
|
||||
div#child {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
background-color: lime;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="parent"><div id="child"></div></div>
|
||||
<script>
|
||||
"use strict";
|
||||
|
||||
const searchParams = new URLSearchParams(document.location.search);
|
||||
const duration = parseInt(searchParams.get("duration"));
|
||||
|
||||
async function runTest(t) {
|
||||
const parent = document.getElementById("parent");
|
||||
const child = document.getElementById("child");
|
||||
const mouseEvents = [];
|
||||
function onMouseOverOrUp(event) {
|
||||
// Ignore events before `mousedown` to make this test simpler.
|
||||
if (mouseEvents[0]?.startsWith("mousedown")) {
|
||||
mouseEvents.push(`${event.type}@${event.target.localName}${event.target.id ? `#${event.target.id}` : ""}`);
|
||||
}
|
||||
}
|
||||
try {
|
||||
child.getBoundingClientRect(); // flush layout
|
||||
child.addEventListener("mousedown", event => {
|
||||
event.target.remove();
|
||||
mouseEvents.push("mousedown@div#child");
|
||||
}, {once: true});
|
||||
document.addEventListener("mouseover", onMouseOverOrUp, {capture: true});
|
||||
document.addEventListener("mouseup", onMouseOverOrUp, {once: true, capture: true});
|
||||
const actions = new test_driver.Actions(duration);
|
||||
await actions.pointerMove(10, 10, {origin: child})
|
||||
.pointerDown({button: actions.ButtonType.LEFT})
|
||||
.pointerUp({button: actions.ButtonType.LEFT})
|
||||
.send();
|
||||
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
||||
assert_equals(
|
||||
mouseEvents.toString(),
|
||||
"mousedown@div#child,mouseover@div#parent,mouseup@div#parent",
|
||||
t.name
|
||||
);
|
||||
} finally {
|
||||
document.removeEventListener("mouseover", onMouseOverOrUp, {capture: true});
|
||||
parent.appendChild(child);
|
||||
}
|
||||
}
|
||||
|
||||
// This test tries to detect intermittent case that mouseout might be fired
|
||||
// after a while from a DOM tree change. Therefore, trying same test 30 times.
|
||||
for (let i = 0; i < 30; i++) {
|
||||
promise_test(async t => {
|
||||
await runTest(t);
|
||||
// Make things stabler to start next test.
|
||||
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
||||
}, `mouseover should be fired before mouseup if mousedown target is removed (${i})`);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in New Issue
Block a user