summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/uievents/mouse
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/uievents/mouse')
-rw-r--r--testing/web-platform/tests/uievents/mouse/attributes.html61
-rw-r--r--testing/web-platform/tests/uievents/mouse/cancel-mousedown-in-subframe.html64
-rw-r--r--testing/web-platform/tests/uievents/mouse/layout_change_should_fire_mouseover.html97
-rw-r--r--testing/web-platform/tests/uievents/mouse/mouse_boundary_events_after_removing_last_over_element.html153
-rw-r--r--testing/web-platform/tests/uievents/mouse/mouse_buttons_back_forward.html56
-rw-r--r--testing/web-platform/tests/uievents/mouse/mouseenter-mouseleave-on-drag.html187
-rw-r--r--testing/web-platform/tests/uievents/mouse/mouseevent_move_button.html102
-rw-r--r--testing/web-platform/tests/uievents/mouse/mousemove_prevent_default_action.tentative.html98
-rw-r--r--testing/web-platform/tests/uievents/mouse/mouseover-at-removing-mousedown-target.html81
-rw-r--r--testing/web-platform/tests/uievents/mouse/resources/mouse-event-reporter-subframe.html37
-rw-r--r--testing/web-platform/tests/uievents/mouse/resources/utils.js35
-rw-r--r--testing/web-platform/tests/uievents/mouse/synthetic-mouse-enter-leave-over-out-button-state-after-target-removed.tentative.html240
12 files changed, 1211 insertions, 0 deletions
diff --git a/testing/web-platform/tests/uievents/mouse/attributes.html b/testing/web-platform/tests/uievents/mouse/attributes.html
new file mode 100644
index 0000000000..bbc388445c
--- /dev/null
+++ b/testing/web-platform/tests/uievents/mouse/attributes.html
@@ -0,0 +1,61 @@
+<!DOCTYPE HTML>
+<title>MouseEvent attributes</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>
+<script src="resources/utils.js"></script>
+
+<span id="left">Left</span> <span id="right">Right</span>
+
+<script>
+ 'use strict';
+
+ const event_list = ["mouseover", "mouseenter", "mousemove",
+ "mousedown", "mouseup", "mouseout", "mouseleave"];
+
+ promise_test(async () => {
+ let left = document.getElementById("left");
+ let right = document.getElementById("right");
+
+ let event_promises = [];
+ event_list.forEach(ename => event_promises.push(getEvent(ename, right)));
+
+ let actions = new test_driver.Actions().addPointer("TestPointer", "mouse")
+ .pointerMove(0, 0, {origin: left})
+ .pointerDown()
+ .pointerUp()
+ .pointerMove(0, 0, {origin: right})
+ .pointerDown()
+ .pointerUp()
+ .pointerMove(0, 0, {origin: left})
+ .pointerDown()
+ .pointerUp()
+ await actions.send();
+
+ for (let i = 0; i < event_promises.length; i++) {
+ let e = await event_promises[i];
+
+ assert_equals(e.constructor, window.MouseEvent,
+ e.type + " should use a MouseEvent constructor");
+ assert_true(e instanceof MouseEvent,
+ e.type + " should be a MouseEvent");
+
+ assert_true(e.isTrusted,
+ e.type + ".isTrusted attribute");
+
+ assert_equals(e.composed,
+ e.type != 'mouseenter' && e.type != 'mouseleave',
+ e.type + ".composed attribute");
+
+ assert_equals(e.bubbles,
+ e.type != 'mouseenter' && e.type != 'mouseleave',
+ e.type + ".bubbles attribute");
+
+ assert_equals(e.cancelable,
+ e.type != 'mouseenter' && e.type != 'mouseleave',
+ e.type + ".cancelable attribute");
+ }
+ }, "MouseEvent attributes");
+</script>
diff --git a/testing/web-platform/tests/uievents/mouse/cancel-mousedown-in-subframe.html b/testing/web-platform/tests/uievents/mouse/cancel-mousedown-in-subframe.html
new file mode 100644
index 0000000000..0735037735
--- /dev/null
+++ b/testing/web-platform/tests/uievents/mouse/cancel-mousedown-in-subframe.html
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="resources/utils.js"></script>
+<body>
+ <div>
+ Dragging the mouse from child frame to parent frame causes both <code>mousedown</code> and
+ <code>mouseup</code> events being dispatched to child frame, regardless of whether the
+ <code>mousedown</code> event is canceled or not.
+ </div>
+ <iframe id="child_frame" width="300px" height="40px"
+ src="resources/mouse-event-reporter-subframe.html">
+ </iframe>
+</body>
+<script>
+ "use strict"
+ let topframe_loaded = getEvent("load", window);
+ let subframe_loaded = getMessageData("load", frames[0]);
+
+ let top_frame_mousedown;
+ let top_frame_mouseup;
+
+ promise_setup(async () => {
+ await topframe_loaded;
+ await subframe_loaded;
+
+ window.addEventListener("mousedown", e => top_frame_mousedown = true);
+ window.addEventListener("mouseup", e => top_frame_mouseup = true);
+ });
+
+ [false, true].forEach(cancel_mousedown => {
+ let mousedown_msg = cancel_mousedown ? "canceled" : "not-canceled";
+
+ promise_test(async () => {
+ top_frame_mousedown = false;
+ top_frame_mouseup = false;
+
+ sendMessage(frames[0], "cancel-mousedown", cancel_mousedown);
+
+ const mousedown_promise = getMessageData("mousedown", frames[0]);
+ const mouseup_promise = getMessageData("mouseup", frames[0]);
+
+ const child_frame = document.getElementById("child_frame");
+ const actions_promise = new test_driver.Actions()
+ .pointerMove(5, 5, {origin: child_frame})
+ .pointerDown()
+ .pointerMove(5, 5, {origin: document.body})
+ .pointerUp()
+ .send();
+
+ await actions_promise;
+
+ let mousedown_message = await mousedown_promise;
+ let mouseup_message = await mouseup_promise;
+
+ assert_equals(mousedown_message.param, mousedown_msg, "Child frame canceled mousedown?");
+ assert_false(top_frame_mousedown, "Top frame received mousedown?");
+ assert_false(top_frame_mouseup, "Top frame received mouseup?");
+ }, "Child frame receives mousedown/mouseup when mousedown is " + mousedown_msg);
+ });
+</script>
diff --git a/testing/web-platform/tests/uievents/mouse/layout_change_should_fire_mouseover.html b/testing/web-platform/tests/uievents/mouse/layout_change_should_fire_mouseover.html
new file mode 100644
index 0000000000..49257ae60d
--- /dev/null
+++ b/testing/web-platform/tests/uievents/mouse/layout_change_should_fire_mouseover.html
@@ -0,0 +1,97 @@
+<!doctype html>
+<html>
+ <head>
+ <title>Mouseover/enter is sent on layout change</title>
+ <meta name="viewport" content="width=device-width">
+ <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>
+ #spacer {
+ height: 100px;
+ width: 100px;
+ }
+ #red {
+ background-color: rgb(255, 0, 0);
+ position: absolute;
+ z-index: 0;
+ left: 0px;
+ top: 0px;
+ height: 100px;
+ width: 100px;
+ }
+ #blue {
+ background-color: rgb(0, 0, 255);
+ position: absolute;
+ z-index: 1;
+ left: 0px;
+ top: 0px;
+ height: 100px;
+ width: 100px;
+ }
+ #blue:hover {
+ background-color: rgb(255, 255, 0);
+ }
+ </style>
+ </head>
+ <body onload="run();">
+ <div id="spacer"></div>
+ <div id="red"></div>
+ <h4>Test Description: Tests that the mouseover event is fired and the element has a hover effect when the element underneath the mouse cursor is changed.
+ <ol>
+ <li>Put your mouse over the red rectangle</li>
+ <li>Click the primary mouse button</li>
+ </ol>
+ </h4>
+ <script type="text/javascript">
+ var testMouseOver = async_test('Tests that the mouseover event is fired and the element has a hover effect when the element underneath the mouse cursor is changed.');
+ var actions_promise;
+
+ var eventList = [];
+ function addBlue() {
+ document.body.innerHTML += '<div id="blue"></div>';
+ var blue = document.getElementById("blue");
+ var events = ['mouseover', 'mousemove', 'mouseout', 'mouseenter', 'mouseleave'];
+ events.forEach(function (event) {
+ blue.addEventListener(event, checkHoverEffect);
+ });
+ testMouseOver.step_timeout(function () {
+ checkEventSequence();
+ }, 2500);
+ }
+
+ function checkEventSequence() {
+ var result = eventList.join();
+ assert_equals(result, 'mouseover,mouseenter');
+ // Make sure the test finishes after all the input actions are completed.
+ actions_promise.then( () => {
+ testMouseOver.done();
+ });
+ }
+
+ function run() {
+ document.addEventListener('click', addBlue);
+ }
+
+ function checkHoverEffect(event) {
+ eventList.push(event.type);
+ testMouseOver.step(function () {
+ assert_equals(event.target.id, "blue");
+ assert_equals(getComputedStyle(event.target).backgroundColor, "rgb(255, 255, 0)");
+ if (event.type == "mouseenter") {
+ checkEventSequence();
+ }
+ });
+ }
+
+ // Inject mouse inputs.
+ actions_promise = new test_driver.Actions()
+ .pointerMove(0, 0, {origin: red})
+ .pointerDown()
+ .pointerUp()
+ .send();
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/uievents/mouse/mouse_boundary_events_after_removing_last_over_element.html b/testing/web-platform/tests/uievents/mouse/mouse_boundary_events_after_removing_last_over_element.html
new file mode 100644
index 0000000000..817c5d9ecc
--- /dev/null
+++ b/testing/web-platform/tests/uievents/mouse/mouse_boundary_events_after_removing_last_over_element.html
@@ -0,0 +1,153 @@
+<!doctype html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>Redundant "mouseenter" shouldn't be fired without "mouseleave"s</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>
+<script>
+"use strict";
+
+function stringifyEvents(eventArray) {
+ if (!eventArray.length) {
+ return "[]";
+ }
+ let result = "";
+ eventArray.forEach(event => {
+ if (result != "") {
+ result += ", ";
+ }
+ result += `${event.type}@${
+ event.target?.nodeType == Node.ELEMENT_NODE
+ ? `${event.target.localName}${
+ event.target.id ? `#${event.target.id}` : ""
+ }`
+ : event.target?.localName
+ }`;
+ });
+ return result;
+}
+
+function eventsAfterClick(eventArray) {
+ const indexAtClick = eventArray.findIndex(e => e.type == "click");
+ if (indexAtClick >= 0) {
+ return eventArray.slice(indexAtClick + 1);
+ }
+ return [];
+}
+
+addEventListener("load", () => {
+ promise_test(async () => {
+ const div1 = document.createElement("div");
+ div1.setAttribute("id", "grandparent");
+ div1.setAttribute("style", "width: 32px; height: 32px");
+ const div2 = document.createElement("div");
+ div2.setAttribute("id", "parent");
+ div2.setAttribute("style", "width: 32px; height: 32px");
+ const div3 = document.createElement("div");
+ div3.setAttribute("id", "child");
+ div3.setAttribute("style", "width: 32px; height: 32px");
+ div1.appendChild(div2);
+ div2.appendChild(div3);
+ document.body.appendChild(div1);
+ const bodyRect = document.body.getBoundingClientRect();
+ const div3Rect = div3.getBoundingClientRect();
+ let events = [];
+ for (const type of ["mouseenter", "mouseleave", "mouseover", "mouseout", "mousemove"]) {
+ for (const node of [document.body, div1, div2, div3]) {
+ node.addEventListener(type, event => {
+ if (event.target == node) {
+ events.push({type: event.type, target: event.target});
+ }
+ }, {capture: true});
+ }
+ }
+ div3.addEventListener("click", event => {
+ div3.remove();
+ events.push({type: event.type, target: event.target});
+ }, {once: true});
+ await new test_driver.Actions()
+ .pointerMove(div3Rect.x + 10, div3Rect.y + 10, {})
+ .pointerDown()
+ .pointerUp() // The clicked in the child, then it's removed from the DOM tree
+ .pointerMove(bodyRect.x + 10, bodyRect.y + 10, {}) // Then, move onto the <body>
+ .send();
+ // FYI: Comparing `mouseenter`s before `click` requires additional
+ // initialization, but it's out of scope of this bug. Therefore, we
+ // compare only events after `click`.
+ const expectedEvents = [ // no events should be fired on the child due to disconnected
+ { type: "mouseover", target: div2 }, // mouseover should be fired because of the mutation
+ { type: "mouseout", target: div2}, // mouseout should be fired because of the mutation
+ { type: "mouseleave", target: div2},
+ { type: "mouseleave", target: div1},
+ { type: "mouseover", target: document.body},
+ { type: "mousemove", target: document.body},
+ ];
+ assert_equals(
+ stringifyEvents(eventsAfterClick(events)),
+ stringifyEvents(expectedEvents),
+ );
+ div1.remove();
+ }, "After removing the last over element, redundant mouseenter events should not be fired on the ancestors");
+
+ promise_test(async () => {
+ const hostContainer = document.createElement("div");
+ hostContainer.setAttribute("id", "containerOfShadowHost");
+ hostContainer.setAttribute("style", "margin-top: 32px; height: 32px");
+ const host = document.createElement("div");
+ host.setAttribute("id", "shadowHost");
+ host.setAttribute("style", "width: 32px; height: 32px");
+ const root = host.attachShadow({mode: "open"});
+ const rootElementInShadow = document.createElement("div");
+ root.appendChild(rootElementInShadow);
+ rootElementInShadow.setAttribute("id", "divInShadow");
+ rootElementInShadow.setAttribute("style", "width: 32px; height: 32px");
+ hostContainer.appendChild(host);
+ document.body.appendChild(hostContainer);
+ const bodyRect = document.body.getBoundingClientRect();
+ const rootElementInShadowRect = rootElementInShadow.getBoundingClientRect();
+ let events = [];
+ for (const type of ["mouseenter", "mouseleave", "mouseover", "mouseout", "mousemove"]) {
+ for (const node of [document.body, hostContainer, host, root, rootElementInShadow]) {
+ node.addEventListener(type, event => {
+ if (event.target == node) {
+ events.push({type: event.type, target: event.target});
+ }
+ }, {capture: true});
+ }
+ }
+ rootElementInShadow.addEventListener("click", event => {
+ rootElementInShadow.remove();
+ events.push({type: event.type, target: event.target});
+ }, {once: true});
+ await new test_driver.Actions()
+ .pointerMove(rootElementInShadowRect.x + 10, rootElementInShadowRect.y + 10, {})
+ .pointerDown()
+ .pointerUp() // The clicked root element in the shadow is removed here.
+ .pointerMove(bodyRect.x + 10, bodyRect.y + 10, {}) // Then, move onto the <body>
+ .send();
+ // FYI: Comparing `mouseenter`s before `click` requires additional
+ // initialization, but it's out of scope of this bug. Therefore, we
+ // compare only events after `click`.
+ const expectedEvents = [ // no events should be fired on rootElementInShadow due to disconnected
+ { type: "mouseover", target: host}, // mouseover should be fired because of the mutation
+ { type: "mouseout", target: host}, // mouseout should be fired because of the mutation
+ { type: "mouseleave", target: host},
+ { type: "mouseleave", target: hostContainer},
+ { type: "mouseover", target: document.body},
+ { type: "mousemove", target: document.body},
+ ];
+ assert_equals(
+ stringifyEvents(eventsAfterClick(events)),
+ stringifyEvents(expectedEvents),
+ );
+ hostContainer.remove();
+ }, "After removing the root element in the shadow under the cursor, mouseleave events should be targeted outside the shadow, but redundant mouseenter events should not be fired");
+}, {once: true});
+</script>
+</head>
+<body style="padding-top: 32px"></body>
+</html>
diff --git a/testing/web-platform/tests/uievents/mouse/mouse_buttons_back_forward.html b/testing/web-platform/tests/uievents/mouse/mouse_buttons_back_forward.html
new file mode 100644
index 0000000000..2323bc1026
--- /dev/null
+++ b/testing/web-platform/tests/uievents/mouse/mouse_buttons_back_forward.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <title>Mouse Button Back/Forward</title>
+ <link rel="author" title="Google" href="http://www.google.com/" />
+ <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>
+ <script>
+ var testMouseUp = async_test('Tests that the mouseup is preventable.');
+ var received_back = false;
+ var received_forward = false;
+ const backButton = 3;
+ const forwardButton = 4;
+ var actions_promise;
+ window.addEventListener('mouseup', function(e) {
+ if (e.button == backButton) {
+ received_back = true;
+ e.preventDefault();
+ } else if (e.button == forwardButton) {
+ received_forward = true;
+ e.preventDefault();
+ }
+ if (received_back && received_forward) {
+ // Make sure the test finishes after all the input actions are completed.
+ actions_promise.then( () => {
+ testMouseUp.done();
+ });
+ }
+ });
+
+ function inject_input() {
+ // First click on back button and then forward button.
+ var actions = new test_driver.Actions();
+ actions_promise = actions.pointerMove(0, 0, {origin: target})
+ .pointerDown({button: actions.ButtonType.BACK})
+ .pointerUp({button: actions.ButtonType.BACK})
+ .pointerDown({button: actions.ButtonType.FORWARD})
+ .pointerUp({button: actions.ButtonType.FORWARD})
+ .send();
+ }
+ </script>
+
+ </head>
+ <body id="target" onload="inject_input()">
+ <h4>Test Description: Tests that the mouseup event is prevented.
+ <ol>
+ <li>Click the back mouse button</li>
+ <li>Click the back mouse forward</li>
+ </ol>
+ </h4>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/uievents/mouse/mouseenter-mouseleave-on-drag.html b/testing/web-platform/tests/uievents/mouse/mouseenter-mouseleave-on-drag.html
new file mode 100644
index 0000000000..c36a1501c0
--- /dev/null
+++ b/testing/web-platform/tests/uievents/mouse/mouseenter-mouseleave-on-drag.html
@@ -0,0 +1,187 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for redundant mouseenter or mouseleave 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>
+</head>
+<style>
+#outer {
+ background: grey;
+ position: absolute;
+ left: 100px;
+ top: 100px;
+ width: 100px;
+ height: 100px;
+}
+#inner {
+ background: red;
+ position: absolute;
+ left: 30px;
+ top: 30px;
+ width: 40px;
+ height: 40px;
+}
+</style>
+
+<body>
+ <!-- Verifies that dragging mouse in/out of an element doesn't fire redundant
+ mouseenter or mouseleave events (crbug.com/356090 & crbug.com/470258) -->
+ <div id="outer">
+ <div id="inner"></div>
+ </div>
+</body>
+<script>
+let eventLog = [];
+let nextUncheckedEventIndex = 0;
+
+// Ensure match to the next sequence of events in the event log.
+function assert_next_events(target, expectedEventNames, message) {
+ for (let i = 0; i < expectedEventNames.length; i++) {
+ assert_true(nextUncheckedEventIndex < eventLog.length,
+ `${message}: empty event queue`);
+ const observed = eventLog[nextUncheckedEventIndex++];
+ const expected = `${expectedEventNames[i]}@${target.id}`;
+ assert_equals(observed, expected,`${message}: Event mismatch`);
+ }
+}
+
+// After validating the expected events, all entries in the event map
+// must be false or we have recorded an unexpected event.
+function assert_empty_event_queue(message) {
+ const uncheckedEvents = eventLog.length - nextUncheckedEventIndex;
+ assert_equals(uncheckedEvents, 0,
+ `${message}: Unexpected events ` +
+ `${eventLog.slice(-uncheckedEvents).join(", ")}`);
+}
+
+function addEventListeners(test) {
+ const eventTypes = [
+ 'mousedown',
+ 'mouseenter',
+ 'mouseleave',
+ 'mousemove',
+ 'mouseout',
+ 'mouseover',
+ 'mouseup'
+ ];
+ ['inner', 'outer'].forEach(id => {
+ const element = document.getElementById(id);
+ eventTypes.forEach(eventType => {
+ const listener = (e) => {
+ if (e.eventPhase == Event.AT_TARGET) {
+ eventLog.push(`${eventType}@${id}`);
+ }
+ };
+ element.addEventListener(eventType, listener);
+ test.add_cleanup(() => {
+ element.removeEventListener(eventType, listener);
+ });
+ })
+ });
+}
+
+// At the end of each action sequence we move the mouse over the root element.
+// Once this event is detected, all other upstream events must be logged and
+// we can proceed with the checks.
+async function mousemoveOverRootElement() {
+ return new Promise(resolve => {
+ const listener = (e) => {
+ if (e.eventPhase == Event.AT_TARGET) {
+ document.documentElement.removeEventListener('mousemove', listener);
+ resolve();
+ }
+ };
+ document.documentElement.addEventListener('mousemove', listener);
+ });
+}
+
+window.onload = async () => {
+ const outer = document.getElementById('outer');
+ const inner = document.getElementById('inner');
+ const leftOuter = 100;
+ const rightOuter = 200;
+ const leftInner = 130;
+ const rightInner = 170;
+ const centerY = 150;
+
+ promise_test(async t => {
+ addEventListeners(t);
+ const completionPromise = mousemoveOverRootElement();
+ const actions =new test_driver.Actions();
+ actions.pointerMove(leftOuter + 10, centerY)
+ .pointerDown({button: actions.ButtonType.LEFT})
+ .pointerMove(rightOuter - 10, centerY)
+ .pointerUp({button: actions.ButtonType.LEFT})
+ .pointerMove(0, 0)
+ .send();
+ await actions;
+ await completionPromise;
+
+ assert_next_events(outer, ['mouseover', 'mouseenter', 'mousemove'],
+ 'Move over outer element');
+ assert_next_events(outer, ['mousedown', 'mousemove', 'mouseup'],
+ 'Drag across outer element');
+ assert_next_events(outer, ['mouseout', 'mouseleave'],
+ 'Move to origin');
+ assert_empty_event_queue('Drag across outer element');
+ }, 'Test dragging across inner div');
+
+ promise_test(async t => {
+ addEventListeners(t);
+ const completionPromise = mousemoveOverRootElement();
+ const actions =new test_driver.Actions();
+ actions.pointerMove(leftOuter + 10, centerY)
+ .pointerDown({button: actions.ButtonType.LEFT})
+ .pointerMove(leftInner + 10, centerY)
+ .pointerUp({button: actions.ButtonType.LEFT})
+ .pointerMove(0, 0)
+ .send();
+ await actions;
+ await completionPromise;
+
+ assert_next_events(outer, ['mouseover', 'mouseenter', 'mousemove'],
+ 'Move over outer element');
+ assert_next_events(outer, ['mousedown', 'mouseout'],
+ 'Initiate drag');
+ assert_next_events(inner,
+ ['mouseover', 'mouseenter', 'mousemove', 'mouseup'],
+ 'Drag into inner element');
+ assert_next_events(inner, ['mouseout', 'mouseleave'],
+ 'Move to origin');
+ assert_next_events(outer, [ 'mouseleave'],
+ 'Move to origin');
+ assert_empty_event_queue('Drag into inner element');
+ }, 'Test dragging into inner div');
+
+ promise_test(async t => {
+ addEventListeners(t);
+ const completionPromise = mousemoveOverRootElement();
+ const actions =new test_driver.Actions();
+ actions.pointerMove(leftInner + 10, centerY)
+ .pointerDown({button: actions.ButtonType.LEFT})
+ .pointerMove(rightInner + 10, centerY)
+ .pointerUp({button: actions.ButtonType.LEFT})
+ .pointerMove(0, 0)
+ .send();
+ await actions;
+ await completionPromise;
+
+ assert_next_events(inner, ['mouseover'], 'Move over inner element');
+ assert_next_events(outer, ['mouseenter'], 'Enter outer');
+ assert_next_events(inner, ['mouseenter', 'mousemove'],
+ 'Move across inner element');
+ assert_next_events(inner, ['mousedown', 'mouseout', 'mouseleave'],
+ 'Drag out of inner');
+ assert_next_events(outer, ['mouseover', 'mousemove', 'mouseup'],
+ 'Drag into outer');
+ assert_next_events(outer, ['mouseout', 'mouseleave'],
+ 'Move to origin');
+ assert_empty_event_queue('Drag into inner element');
+ }, 'Test dragging out of inner div');
+};
+</script>
+</html>
diff --git a/testing/web-platform/tests/uievents/mouse/mouseevent_move_button.html b/testing/web-platform/tests/uievents/mouse/mouseevent_move_button.html
new file mode 100644
index 0000000000..edde11d022
--- /dev/null
+++ b/testing/web-platform/tests/uievents/mouse/mouseevent_move_button.html
@@ -0,0 +1,102 @@
+<!doctype html>
+<html>
+ <head>
+ <title>Mouse Events with button depressed</title>
+ <meta name="timeout" content="long">
+ <meta name="viewport" content="width=device-width">
+ <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.box {
+ border: 2px solid lightgray;
+ margin: 25px;
+ padding: 25px;
+ float: left;
+ }
+ #lightyellow {
+ background-color: lightyellow;
+ }
+ #lightblue {
+ background-color: lightblue;
+ }
+ #lightgreen {
+ background-color: lightgreen;
+ }
+ </style>
+ </head>
+ <body onload="run()">
+ <h2>Mouse Events</h2>
+ <h4>Test Description: This test checks if mouse events set button property correctly
+ <ol>
+ <li>Put your mouse over the green rectangle</li>
+ <li>Press a non-primary button and hold it</li>
+ <li>Drag mouse to blue rectangle</li>
+ <li>Release mouse button</li>
+ </ol>
+ </h4>
+ <div class="box" id="lightyellow">
+ <div class="box" id="lightgreen"></div>
+ <div class="box" id="lightblue"></div>
+ </div>
+ <script>
+ var test = async_test("mouse events fired without button state");
+ var button = -1;
+ var actions_promise;
+
+ function run() {
+ var lightgreen = document.getElementById("lightgreen");
+ var lightyellow = document.getElementById("lightyellow");
+ var lightblue = document.getElementById("lightblue");
+
+ on_event(lightgreen, "contextmenu", function (event) {
+ event.preventDefault();
+ });
+
+ on_event(lightgreen, "mousedown", function (event) {
+ test.step(function() {assert_equals(button, -1, "There must only be one mouse down event.");});
+ test.step(function() {assert_not_equals(event.button, 0, "Must not be primary button.");});
+ button = event.button;
+ });
+ on_event(lightyellow, "click", function (event) {
+ test.step(function() {assert_equals(event.button, button, "Button must be the same as mousedown.");});
+ });
+ on_event(lightyellow, "mousemove", function (event) {
+ if (button != -1) {
+ test.step(function() {assert_equals(event.button, 0, "Button must be un-initialized for mousemove.");});
+ }
+ });
+ on_event(lightgreen, "mouseleave", function (event) {
+ if (button != -1) {
+ test.step(function() {assert_equals(event.button, 0, "Button must be un-initialized for mouseleave.");});
+ }
+ });
+ on_event(lightblue, "mouseenter", function (event) {
+ if (button != -1) {
+ test.step(function() {assert_equals(event.button, 0, "Button must be un-initialized for mouseenter.");});
+ }
+ });
+ on_event(lightblue, "mouseup", function (event) {
+ if (button != -1) {
+ test.step(function() {assert_equals(event.button, button, "Button must be the same as mousedown.");});
+ // Make sure the test finishes after all the input actions are completed.
+ actions_promise.then( () => {
+ test.done();
+ });
+ }
+ });
+
+ // Inject mouse inputs.
+ var actions = new test_driver.Actions();
+ actions_promise = actions.pointerMove(0, 0, {origin: lightgreen})
+ .pointerDown({button: actions.ButtonType.MIDDLE})
+ .pointerMove(0, 0, {origin: lightyellow})
+ .pointerMove(0, 0, {origin: lightblue})
+ .pointerUp({button: actions.ButtonType.MIDDLE})
+ .send();
+ }
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/uievents/mouse/mousemove_prevent_default_action.tentative.html b/testing/web-platform/tests/uievents/mouse/mousemove_prevent_default_action.tentative.html
new file mode 100644
index 0000000000..4caf98087f
--- /dev/null
+++ b/testing/web-platform/tests/uievents/mouse/mousemove_prevent_default_action.tentative.html
@@ -0,0 +1,98 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>mousemove event: preventDefault()</title>
+<link rel="author" title="Mirko Brodesser" href="mailto:mbrodesser@mozilla.com">
+<link rel="help" href="https://github.com/w3c/uievents/issues/278">
+<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>
+<script src=resources/utils.js></script>
+
+<body>
+ <div id="a">div a</div>
+ <div id="b">div b</div>
+ <div id="c" draggable="true">div c</div>
+</body>
+
+<script>
+ 'use strict';
+
+ let event_log = [];
+
+ function logEvents(e) {
+ event_log.push(e.type);
+ }
+
+ function initialize(test) {
+ // Deliberately avoiding mouseup here because the last selectionchange
+ // may be fired before or after the mouseup.
+ addTestScopedListener(document, "mousedown", logEvents, test);
+ addTestScopedListener(document, "mousemove", e => e.preventDefault(), test);
+ event_log = [];
+ }
+
+ promise_test(async test => {
+ initialize(test);
+ addTestScopedListener(document, "selectionchange", logEvents, test);
+
+ const a = document.getElementById("a");
+ const b = document.getElementById("b");
+
+ let mouseup_promise = getEvent("mouseup", document);
+
+ await new test_driver.Actions()
+ .pointerMove(0, 0, {origin: a})
+ .pointerDown()
+ .addTick()
+ .addTick()
+ .pointerMove(0, 0, {origin: b})
+ .addTick()
+ .addTick()
+ .pointerUp()
+ .send();
+
+ await mouseup_promise;
+
+ const expected_events = ["mousedown", "selectionchange", "selectionchange"];
+
+ assert_equals(event_log.toString(), expected_events.toString(),
+ "received events");
+ }, "selectionchange event firing when mousemove event is prevented");
+
+ promise_test(async test => {
+ initialize(test);
+ addTestScopedListener(document, 'dragstart', (event) => {
+ // For this test, it is enough to see the dragstart event. The event is
+ // cancelled here to suppress the actual drag operation because Blink's
+ // implementation of test_driver doesn't seem to be able to dispatch any
+ // event once the dragging is active.
+ event.preventDefault();
+ logEvents(event);
+ }, test);
+
+ const b = document.getElementById("b");
+ const c = document.getElementById("c");
+
+ const mouseup_promise = getEvent('mouseup', document);
+
+ await new test_driver.Actions()
+ .pointerMove(0, 0, {origin: c})
+ .pointerDown()
+ .addTick()
+ .addTick()
+ .pointerMove(0, 0, {origin: b})
+ .addTick()
+ .addTick()
+ .pointerUp()
+ .send();
+
+ await mouseup_promise;
+
+ const expected_events = ["mousedown", "dragstart"];
+
+ assert_equals(event_log.toString(), expected_events.toString(),
+ "received events");
+ }, "dragstart event firing when mousemove event is prevented");
+</script>
diff --git a/testing/web-platform/tests/uievents/mouse/mouseover-at-removing-mousedown-target.html b/testing/web-platform/tests/uievents/mouse/mouseover-at-removing-mousedown-target.html
new file mode 100644
index 0000000000..959dcef681
--- /dev/null
+++ b/testing/web-platform/tests/uievents/mouse/mouseover-at-removing-mousedown-target.html
@@ -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>
diff --git a/testing/web-platform/tests/uievents/mouse/resources/mouse-event-reporter-subframe.html b/testing/web-platform/tests/uievents/mouse/resources/mouse-event-reporter-subframe.html
new file mode 100644
index 0000000000..f12f429491
--- /dev/null
+++ b/testing/web-platform/tests/uievents/mouse/resources/mouse-event-reporter-subframe.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<script src="utils.js"></script>
+<style>
+ body, html {
+ margin: 0;
+ padding; 0;
+ background-color: #bfb;
+ }
+</style>
+<body>
+ <div>Child frame</div>
+</body>
+<script>
+ "use strict";
+ let cancel_mousedown = false;
+
+ window.addEventListener("load", () => {
+ window.addEventListener("message", event => {
+ let data = event.data;
+ if (data.type == "cancel-mousedown")
+ cancel_mousedown = data.param;
+ });
+
+ window.addEventListener("mousedown", e => {
+ let msg = "not-canceled";
+ if (cancel_mousedown) {
+ e.preventDefault();
+ msg = "canceled";
+ }
+ sendMessage(window.top, "mousedown", msg);
+ });
+
+ window.addEventListener("mouseup", e => sendMessage(window.top, "mouseup"));
+
+ sendMessage(parent, "load");
+ });
+</script>
diff --git a/testing/web-platform/tests/uievents/mouse/resources/utils.js b/testing/web-platform/tests/uievents/mouse/resources/utils.js
new file mode 100644
index 0000000000..6f5f6f4b6c
--- /dev/null
+++ b/testing/web-platform/tests/uievents/mouse/resources/utils.js
@@ -0,0 +1,35 @@
+// Sends to Window |w| the object |{type, param}|.
+function sendMessage(w, type, param) {
+ w.postMessage({"type": type, "param": param}, "*");
+}
+
+// Returns a |Promise| that gets resolved with the event object when |target|
+// receives an event of type |event_type|.
+function getEvent(event_type, target) {
+ return new Promise(resolve => {
+ target.addEventListener(event_type, e => resolve(e), {once: true});
+ });
+}
+
+// Adds a listener that is automatically removed at the end of the test.
+function addTestScopedListener(target, type, listener, test) {
+ target.addEventListener(type, listener);
+ test.add_cleanup(() => {
+ target.removeEventListener(type, listener);
+ });
+}
+
+// Returns a |Promise| that gets resolved with |event.data| when |window|
+// receives from |source| a "message" event whose |event.data.type| matches the string
+// |message_data_type|.
+function getMessageData(message_data_type, source) {
+ return new Promise(resolve => {
+ function waitAndRemove(e) {
+ if (e.source != source || !e.data || e.data.type != message_data_type)
+ return;
+ window.removeEventListener("message", waitAndRemove);
+ resolve(e.data);
+ }
+ window.addEventListener("message", waitAndRemove);
+ });
+}
diff --git a/testing/web-platform/tests/uievents/mouse/synthetic-mouse-enter-leave-over-out-button-state-after-target-removed.tentative.html b/testing/web-platform/tests/uievents/mouse/synthetic-mouse-enter-leave-over-out-button-state-after-target-removed.tentative.html
new file mode 100644
index 0000000000..fad82f850d
--- /dev/null
+++ b/testing/web-platform/tests/uievents/mouse/synthetic-mouse-enter-leave-over-out-button-state-after-target-removed.tentative.html
@@ -0,0 +1,240 @@
+<!doctype html>
+<html>
+<head>
+<meta charset="utf-8">
+<meta name="variant" content="?buttonType=LEFT&button=0&buttons=1">
+<meta name="variant" content="?buttonType=MIDDLE&button=1&buttons=4">
+<title>Testing button state of synthesized mouse(out|over|leave|enter) events</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<style>
+#parent {
+ background-color: lightseagreen;
+ padding: 0;
+ height: 40px;
+ width: 40px;
+}
+#child {
+ background-color: red;
+ margin: 0;
+ height: 30px;
+ width: 30px;
+}
+</style>
+</head>
+<body>
+<div id="parent"><div id="child">abc</div></div>
+<script>
+const searchParams = new URLSearchParams(document.location.search);
+const buttonType = searchParams.get("buttonType");
+const button = parseInt(searchParams.get("button"));
+const buttons = parseInt(searchParams.get("buttons"));
+
+let events = [];
+function eventToString(data) {
+ if (!data) {
+ return "{}";
+ }
+ return `{ '${data.type}' on '${data.target}': button=${data.button}, buttons=${data.buttons} }`;
+}
+
+function eventsToString(events) {
+ if (!events.length) {
+ return "[]";
+ }
+ let ret = "[";
+ for (const data of events) {
+ if (ret != "[") {
+ ret += ", ";
+ }
+ ret += eventToString(data);
+ }
+ return ret + "]";
+}
+
+function removeEventsBefore(eventType) {
+ while (events[0]?.type != eventType) {
+ events.shift();
+ }
+}
+
+const parentElement = document.getElementById("parent");
+const childElement = document.getElementById("child");
+
+function promiseLayout() {
+ return new Promise(resolve => {
+ (childElement.isConnected ? childElement : parentElement).getBoundingClientRect();
+ requestAnimationFrame(() => requestAnimationFrame(resolve));
+ });
+}
+
+promise_test(async () => {
+ await new Promise(resolve => {
+ addEventListener("load", resolve, { once: true });
+ });
+
+ ["mouseout", "mouseover", "mouseleave", "mouseenter", "mousemove", "mousedown"].forEach(eventType => {
+ parentElement.addEventListener(eventType, event => {
+ if (event.target != parentElement) {
+ return;
+ }
+ events.push({
+ type: event.type,
+ target: "parent",
+ button: event.button,
+ buttons: event.buttons,
+ });
+ });
+ childElement.addEventListener(eventType, event => {
+ if (event.target != childElement) {
+ return;
+ }
+ events.push({
+ type: event.type,
+ target: "child",
+ button: event.button,
+ buttons: event.buttons,
+ });
+ });
+ });
+}, "Setup event listeners and wait for load");
+
+promise_test(async t => {
+ events = [];
+ await promiseLayout();
+ childElement.addEventListener("mousedown", () => childElement.remove(), {once: true});
+ const {x, y} = (function () {
+ const rect = childElement.getBoundingClientRect();
+ return { x: rect.left, y: rect.top };
+ })();
+ const actions = new test_driver.Actions();
+ await actions.pointerMove(10, 10, {origin: childElement})
+ .pointerDown({button: actions.ButtonType[buttonType]})
+ .pause(100) // Allow browsers to synthesize mouseout, etc
+ .pointerUp({button: actions.ButtonType[buttonType]})
+ .send();
+ await promiseLayout();
+ removeEventsBefore("mousedown");
+ test(() => {
+ const maybeMouseDownEvent =
+ events.length && events[0].type == "mousedown" ? events.shift() : undefined;
+ assert_equals(
+ eventToString(maybeMouseDownEvent),
+ eventToString({ type: "mousedown", target: "child", button, buttons })
+ );
+ }, `${t.name}: mousedown should've been fired`);
+ assert_true(events.length > 0, `${t.name}: Some events should've been fired after mousedown`);
+ test(() => {
+ // Before `mousedown` is fired, both parent and child must have received
+ // `mouseenter`, only the child must have received `mouseover`. Then, the
+ // child is now moved away by the `mousedown` listener. Therefore,
+ // `mouseout` and `mouseleave` should be fired on the child as the spec of
+ // UI Events defines. Then, they are not a button press events. Therefore,
+ // the `button` should be 0, but buttons should be set to 4 because of
+ // pressing the middle button.
+ let mouseOutOrLeave = [];
+ while (events[0]?.type == "mouseout" || events[0]?.type == "mouseleave") {
+ mouseOutOrLeave.push(events.shift());
+ }
+ assert_equals(
+ eventsToString(mouseOutOrLeave),
+ eventsToString([
+ { type: "mouseout", target: "child", button: 0, buttons },
+ { type: "mouseleave", target: "child", button: 0, buttons },
+ ])
+ );
+ }, `${t.name}: mouseout and mouseleave should've been fired on the removed child`);
+ test(() => {
+ // And `mouseover` should be fired on the parent as the spec of UI Events
+ // defines.
+ let mouseOver = [];
+ while (events[0]?.type == "mouseover") {
+ mouseOver.push(events.shift());
+ }
+ assert_equals(
+ eventsToString(mouseOver),
+ eventsToString([{ type: "mouseover", target: "parent", button: 0, buttons }])
+ );
+ }, `${t.name}: mouseover should've been fired on the parent`);
+ test(() => {
+ // On the other hand, it's unclear about `mouseenter`. The mouse cursor has
+ // never been moved out from the parent. Therefore, it shouldn't be fired
+ // on the parent ideally, but all browsers do not pass this test and there
+ // is no clear definition about this case.
+ let mouseEnter = [];
+ while (events.length && events[0].type == "mouseenter") {
+ mouseEnter.push(events.shift());
+ }
+ assert_equals(eventsToString(mouseEnter), eventsToString([]));
+ }, `${t.name}: mouseenter should not have been fired on the parent`);
+ assert_equals(eventsToString(events), eventsToString([]), "All events should've been checked");
+ parentElement.appendChild(childElement);
+}, "Removing an element at mousedown");
+
+promise_test(async t => {
+ events = [];
+ await promiseLayout();
+ childElement.addEventListener("mouseup", () => childElement.remove(), {once: true});
+ const {x, y} = (function () {
+ const rect = childElement.getBoundingClientRect();
+ return { x: rect.left, y: rect.top };
+ })();
+ const actions = new test_driver.Actions();
+ await actions.pointerMove(10, 10, {origin: childElement})
+ .pointerDown({button: actions.ButtonType[buttonType]})
+ .pointerUp({button: actions.ButtonType[buttonType]})
+ .send();
+ await promiseLayout();
+ removeEventsBefore("mousedown");
+ test(() => {
+ const maybeMouseDownEvent =
+ events.length && events[0].type == "mousedown" ? events.shift() : undefined;
+ assert_equals(
+ eventToString(maybeMouseDownEvent),
+ eventToString({ type: "mousedown", target: "child", button, buttons })
+ );
+ }, `${t.name}: mousedown should've been fired`);
+ assert_true(events.length > 0, `${t.name}: Some events should've been fired after mousedown`);
+ // Same as the `mousedown` case except `buttons` value because `mouseout`,
+ // `mouseleave`, `mouseover` and `mouseenter` should (or may) be fired
+ // after the `mouseup`. Therefore, `.buttons` should not have the button
+ // flag.
+ test(() => {
+ let mouseOutOrLeave = [];
+ while (events[0]?.type == "mouseout" || events[0]?.type == "mouseleave") {
+ mouseOutOrLeave.push(events.shift());
+ }
+ assert_equals(
+ eventsToString(mouseOutOrLeave),
+ eventsToString([
+ { type: "mouseout", target: "child", button: 0, buttons: 0 },
+ { type: "mouseleave", target: "child", button: 0, buttons: 0 },
+ ])
+ );
+ }, `${t.name}: mouseout and mouseleave should've been fired on the removed child`);
+ test(() => {
+ let mouseOver = [];
+ while (events[0]?.type == "mouseover") {
+ mouseOver.push(events.shift());
+ }
+ assert_equals(
+ eventsToString(mouseOver),
+ eventsToString([{ type: "mouseover", target: "parent", button: 0, buttons: 0 }])
+ );
+ }, `${t.name}: mouseover should've been fired on the parent`);
+ test(() => {
+ let mouseEnter = [];
+ while (events[0]?.type == "mouseenter") {
+ mouseEnter.push(events.shift());
+ }
+ assert_equals(eventsToString(mouseEnter), eventsToString([]));
+ }, `${t.name}: mouseenter should not have been fired on the parent`);
+ assert_equals(eventsToString(events), eventsToString([]), "All events should've been checked");
+ parentElement.appendChild(childElement);
+}, "Removing an element at mouseup");
+</script>
+</body>
+</html>