diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /testing/web-platform/tests/uievents/mouse | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/uievents/mouse')
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> |