<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Redundant "pointerenter" shouldn't be fired without "pointerleave"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 ["pointerenter", "pointerleave", "pointerover", "pointerout", "pointermove"]) {
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 `pointerenter`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: "pointerleave", target: div2}, // no pointerout because of first pointer move after the mutation
{ type: "pointerleave", target: div1},
{ type: "pointerover", target: document.body},
{ type: "pointermove", target: document.body},
];
assert_equals(
stringifyEvents(eventsAfterClick(events)),
stringifyEvents(expectedEvents),
);
div1.remove();
}, "After removing the last over element, redundant pointerenter 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 ["pointerenter", "pointerleave", "pointerover", "pointerout", "pointermove"]) {
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 `pointerenter`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: "pointerleave", target: host}, // no pointerout because of first pointer move after the mutation
{ type: "pointerleave", target: hostContainer},
{ type: "pointerover", target: document.body},
{ type: "pointermove", target: document.body},
];
assert_equals(
stringifyEvents(eventsAfterClick(events)),
stringifyEvents(expectedEvents),
);
hostContainer.remove();
}, "After removing the root element in the shadow under the cursor, pointerleave events should be targeted outside the shadow, but redundant pointerenter events should not be fired");
}, {once: true});
</script>
</head>
<body style="padding-top: 32px"></body>
</html>