tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 5dd83104b28eb38d47eab54991e5afe4f2c2117b
parent debdf7f1910f89980aa91285f0c8669940ec931a
Author: Masayuki Nakano <masayuki@d-toybox.com>
Date:   Fri, 17 Oct 2025 02:09:27 +0000

Bug 1994100 - Add WPTs for mouse/pointer boundary events when `usemap` of `<img>` or `coord` of `<area>` is modified r=smaug,dom-core

It's unclear that how `<area>` is treated within CSS.  However, it's
treated as a transparent object at least when a hit-test of pointing
device events and `<area>` can be a target of mouse events and pointer
events.

When the pointer is moved over the shape of an `<area>`, browsers
dispatch `mouseover` and `pointerover` on the `<area>` instead of the
`<img>`. Then, when it's moved out from the shape, browsers dispatch
`mouseout` and `pointerout`.

However, Chrome and Safari do not dispatch boundary events when the
underneath element of the last pointer position becomes another element.
This does not make sense because modifying the layout, e.g., modifying
the `<img>` size, causes `mouseover` and `pointerover` on the new
element anyway.  Therefore, modifying `usemap` attribute of `<img>`
and `coord` attribute of `<area>` should be treated as a layout change.

Differential Revision: https://phabricator.services.mozilla.com/D268655

Diffstat:
Mlayout/generic/nsImageMap.cpp | 8++++++++
Atesting/web-platform/tests/pointerevents/pointerevent_boundary_events_on_image_map.html | 376+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atesting/web-platform/tests/uievents/mouse/mouse_boundary_events_on_image_map.html | 376+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 760 insertions(+), 0 deletions(-)

diff --git a/layout/generic/nsImageMap.cpp b/layout/generic/nsImageMap.cpp @@ -8,6 +8,7 @@ #include "nsImageMap.h" +#include "mozilla/PresShell.h" #include "mozilla/UniquePtr.h" #include "mozilla/dom/Element.h" #include "mozilla/dom/Event.h" // for Event @@ -762,6 +763,13 @@ void nsImageMap::DrawFocus(nsIFrame* aFrame, DrawTarget& aDrawTarget, void nsImageMap::MaybeUpdateAreas(nsIContent* aContent) { if (aContent == mMap || mConsiderWholeSubtree) { UpdateAreas(); + + // If the mouse cursor hovered an <area> or will hover an <area>, we may + // need to update the cursor and dispatch mouse/pointer boundary events. + // So, let's enqueue a synthesized mouse move. + if (PresShell* const presShell = aContent->OwnerDoc()->GetPresShell()) { + presShell->SynthesizeMouseMove(false); + } } } diff --git a/testing/web-platform/tests/pointerevents/pointerevent_boundary_events_on_image_map.html b/testing/web-platform/tests/pointerevents/pointerevent_boundary_events_on_image_map.html @@ -0,0 +1,376 @@ +<!doctype html> +<html> +<head> +<meta charset="utf-8"> +<meta name="viewport" content="width=device-width, user-scalable=no"> +<title>Event targets of boundary events over an image map</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"; + +addEventListener("load", () => { + const initialDiv = document.getElementById("init"); + const container = document.getElementById("container"); + const img1 = document.getElementById("img1"); + const img2 = document.getElementById("img2"); + const area1_1 = document.getElementById("area1-1"); + const area1_2 = document.createElement("area"); + area1_2.setAttribute("id", "area1-2"); + area1_2.setAttribute("shape", "rect"); + area1_2.setAttribute("coords", "0,0,100,100"); + area1_2.setAttribute("href", "#"); + const area2_1 = document.getElementById("area2-1"); + const map1 = document.getElementById("map1"); + const map2 = document.getElementById("map2"); + + function stringifyEvents(arrayOfEvents) { + function stringifyEvent(event) { + return `${event.type}@${event.target.localName}${ + event.target.id ? `#${event.target.id}` : "" + }` + } + let str = "["; + for (const event of arrayOfEvents) { + if (str != "[") { + str += ", "; + } + str += stringifyEvent(event); + } + return str + "]"; + } + + let events = []; + function pushEvent(event) { + events.push(event); + } + for (const type of ["pointermove", "pointerover", "pointerenter", "pointerout", "pointerleave"]) { + container.addEventListener(type, pushEvent, {capture: true}); + } + + promise_test(async () => { + events = []; + await new test_driver.Actions() + .pointerMove(0, 0, {origin: initialDiv}) + .pointerMove(0, 0, {origin: img1}) // actually moved over area1-1 + .pointerMove(0, 0, {origin: initialDiv}) + .send(); + assert_equals( + stringifyEvents(events), + stringifyEvents([ + {type: "pointerover", target: area1_1}, + {type: "pointerenter", target: container}, + {type: "pointerenter", target: map1}, + {type: "pointerenter", target: area1_1}, + {type: "pointermove", target: area1_1}, + {type: "pointerout", target: area1_1}, + {type: "pointerleave", target: area1_1}, + {type: "pointerleave", target: map1}, + {type: "pointerleave", target: container}, + ]) + ); + }, "pointer boundary events when simple over/out"); + + promise_test(async () => { + events = []; + await new test_driver.Actions() + .pointerMove(0, 0, {origin: initialDiv}) + .pointerMove(0, 0, {origin: img1}) // actually moved over area1-1 + .pointerMove(0, 0, {origin: img2}) // actually moved in area1-1 + .pointerMove(0, 0, {origin: initialDiv}) + .send(); + assert_equals( + stringifyEvents(events), + stringifyEvents([ + {type: "pointerover", target: area1_1}, + {type: "pointerenter", target: container}, + {type: "pointerenter", target: map1}, + {type: "pointerenter", target: area1_1}, + {type: "pointermove", target: area1_1}, + // boundary events shouldn't be fired when moving from img1 to img2 + {type: "pointermove", target: area1_1}, + {type: "pointerout", target: area1_1}, + {type: "pointerleave", target: area1_1}, + {type: "pointerleave", target: map1}, + {type: "pointerleave", target: container}, + ]) + ); + }, "pointer boundary events when moved from an <area> to the same <area> shared by another <img>"); + + promise_test(async t => { + events = []; + function shrinkArea1_1() { + area1_1.setAttribute("coords", "0,0,10,10"); + t.add_cleanup(() => { + area1_1.setAttribute("coords", "0,0,100,100"); + }); + } + area1_1.addEventListener("pointermove", shrinkArea1_1, {once: true}); + t.add_cleanup(() => area1_1.removeEventListener("pointermove", shrinkArea1_1)); + await new test_driver.Actions() + .pointerMove(0, 0, {origin: initialDiv}) + .pointerMove(0, 0, {origin: img1}) // actually moved over area1-1 + .addTick(100) // now, over the <img> + .addTick() // for Firefox bug 1994340 + .pointerMove(0, 0, {origin: initialDiv}) + .send(); + assert_equals( + stringifyEvents(events), + stringifyEvents([ + {type: "pointerover", target: area1_1}, + {type: "pointerenter", target: container}, + {type: "pointerenter", target: map1}, + {type: "pointerenter", target: area1_1}, + {type: "pointermove", target: area1_1}, + // Now, the <area> is shrunken and the cursor is not over the <area>. + {type: "pointerout", target: area1_1}, + {type: "pointerleave", target: area1_1}, + {type: "pointerleave", target: map1}, + {type: "pointerover", target: img1}, + {type: "pointerenter", target: img1}, + // Then, move out from the <img>. + {type: "pointerout", target: img1}, + {type: "pointerleave", target: img1}, + {type: "pointerleave", target: container}, + ]) + ); + }, "pointer boundary events when the <area> is resized"); + + promise_test(async t => { + events = []; + function shrinkArea1_1() { + area1_1.setAttribute("coords", "0,0,10,10"); + img1.setAttribute("width", "200"); + img1.getBoundingClientRect(); + t.add_cleanup(() => { + img1.setAttribute("width", "100"); + area1_1.setAttribute("coords", "0,0,100,100"); + img1.getBoundingClientRect(); + }); + } + area1_1.addEventListener("pointermove", shrinkArea1_1, {once: true}); + t.add_cleanup(() => area1_1.removeEventListener("pointermove", shrinkArea1_1)); + await new test_driver.Actions() + .pointerMove(0, 0, {origin: initialDiv}) + .pointerMove(0, 0, {origin: img1}) // actually moved over area1-1 + .addTick() // now over the <img> + .pointerMove(0, 0, {origin: initialDiv}) + .send(); + assert_equals( + stringifyEvents(events), + stringifyEvents([ + {type: "pointerover", target: area1_1}, + {type: "pointerenter", target: container}, + {type: "pointerenter", target: map1}, + {type: "pointerenter", target: area1_1}, + {type: "pointermove", target: area1_1}, + // Now, the <area> is shrunken and the cursor is not over the <area>. + {type: "pointerout", target: area1_1}, + {type: "pointerleave", target: area1_1}, + {type: "pointerleave", target: map1}, + {type: "pointerover", target: img1}, + {type: "pointerenter", target: img1}, + // Then, move out from the <img>. + {type: "pointerout", target: img1}, + {type: "pointerleave", target: img1}, + {type: "pointerleave", target: container}, + ]) + ); + }, "pointer boundary events when both <area> and <img> are resized"); + + promise_test(async t => { + events = []; + function switchMap() { + img1.setAttribute("usemap", "#map2"); + t.add_cleanup(() => { + img1.setAttribute("usemap", "#map1"); + }); + } + area1_1.addEventListener("pointermove", switchMap, {once: true}); + t.add_cleanup(() => area1_1.removeEventListener("pointermove", switchMap)); + await new test_driver.Actions() + .pointerMove(0, 0, {origin: initialDiv}) + .pointerMove(0, 0, {origin: img1}) // actually moved over area1-1 + .addTick(100) // now over area2-1 + .addTick() // for Firefox bug 1994340 + .pointerMove(0, 0, {origin: initialDiv}) + .send(); + assert_equals( + stringifyEvents(events), + stringifyEvents([ + {type: "pointerover", target: area1_1}, + {type: "pointerenter", target: container}, + {type: "pointerenter", target: map1}, + {type: "pointerenter", target: area1_1}, + {type: "pointermove", target: area1_1}, + // Now, the #map2 is the image map definition and over its area2-1 + {type: "pointerout", target: area1_1}, + {type: "pointerleave", target: area1_1}, + {type: "pointerleave", target: map1}, + {type: "pointerover", target: area2_1}, + {type: "pointerenter", target: map2}, + {type: "pointerenter", target: area2_1}, + // Then, move out from the <area>. + {type: "pointerout", target: area2_1}, + {type: "pointerleave", target: area2_1}, + {type: "pointerleave", target: map2}, + {type: "pointerleave", target: container}, + ]) + ); + }, "pointer boundary events when usemap is modified"); + + promise_test(async t => { + events = []; + function switchMap() { + img1.setAttribute("usemap", "#map2"); + img1.setAttribute("width", "200"); + img1.getBoundingClientRect(); + t.add_cleanup(() => { + img1.setAttribute("usemap", "#map1"); + img1.setAttribute("width", "100"); + img1.getBoundingClientRect(); + }); + } + area1_1.addEventListener("pointermove", switchMap, {once: true}); + t.add_cleanup(() => area1_1.removeEventListener("pointermove", switchMap)); + await new test_driver.Actions() + .pointerMove(0, 0, {origin: initialDiv}) + .pointerMove(0, 0, {origin: img1}) // actually moved over area1-1 + .addTick() // now over area2-1 + .pointerMove(0, 0, {origin: initialDiv}) + .send(); + assert_equals( + stringifyEvents(events), + stringifyEvents([ + {type: "pointerover", target: area1_1}, + {type: "pointerenter", target: container}, + {type: "pointerenter", target: map1}, + {type: "pointerenter", target: area1_1}, + {type: "pointermove", target: area1_1}, + // Now, the #map2 is the image map definition and over its area2-1 + {type: "pointerout", target: area1_1}, + {type: "pointerleave", target: area1_1}, + {type: "pointerleave", target: map1}, + {type: "pointerover", target: area2_1}, + {type: "pointerenter", target: map2}, + {type: "pointerenter", target: area2_1}, + // Then, move out from the <area>. + {type: "pointerout", target: area2_1}, + {type: "pointerleave", target: area2_1}, + {type: "pointerleave", target: map2}, + {type: "pointerleave", target: container}, + ]) + ); + }, "pointer boundary events when usemap is modified and <img> is resized"); + + promise_test(async t => { + events = []; + function appendArea1_2() { + map1.insertBefore(area1_2, area1_1); + t.add_cleanup(() => { + area1_2.remove(); + }); + } + area1_1.addEventListener("pointermove", appendArea1_2, {once: true}); + t.add_cleanup(() => area1_1.removeEventListener("pointermove", appendArea1_2)); + await new test_driver.Actions() + .pointerMove(0, 0, {origin: initialDiv}) + .pointerMove(0, 0, {origin: img1}) // actually moved over area1-1 + .addTick(100) // now, over the area1-2 + .addTick() // for Firefox bug 1994340 + .pointerMove(0, 0, {origin: initialDiv}) + .send(); + assert_equals( + stringifyEvents(events), + stringifyEvents([ + {type: "pointerover", target: area1_1}, + {type: "pointerenter", target: container}, + {type: "pointerenter", target: map1}, + {type: "pointerenter", target: area1_1}, + {type: "pointermove", target: area1_1}, + // Now, new <area> is inserted and it's the top-most. + {type: "pointerout", target: area1_1}, + {type: "pointerleave", target: area1_1}, + {type: "pointerover", target: area1_2}, + {type: "pointerenter", target: area1_2}, + // Then, move out from the <img>. + {type: "pointerout", target: area1_2}, + {type: "pointerleave", target: area1_2}, + {type: "pointerleave", target: map1}, + {type: "pointerleave", target: container}, + ]) + ); + }, "pointer boundary events when new <area> is available"); + + promise_test(async t => { + events = []; + function appendArea1_2() { + map1.insertBefore(area1_2, area1_1); + img1.setAttribute("width", "200"); + img1.getBoundingClientRect(); + t.add_cleanup(() => { + area1_2.remove(); + img1.setAttribute("width", "100"); + img1.getBoundingClientRect(); + }); + } + area1_1.addEventListener("pointermove", appendArea1_2, {once: true}); + t.add_cleanup(() => area1_1.removeEventListener("pointermove", appendArea1_2)); + await new test_driver.Actions() + .pointerMove(0, 0, {origin: initialDiv}) + .pointerMove(0, 0, {origin: img1}) // actually moved over area1-1 + .addTick() // now, over the area1-2 + .pointerMove(0, 0, {origin: initialDiv}) + .send(); + assert_equals( + stringifyEvents(events), + stringifyEvents([ + {type: "pointerover", target: area1_1}, + {type: "pointerenter", target: container}, + {type: "pointerenter", target: map1}, + {type: "pointerenter", target: area1_1}, + {type: "pointermove", target: area1_1}, + // Now, new <area> is inserted and it's the top-most. + {type: "pointerout", target: area1_1}, + {type: "pointerleave", target: area1_1}, + {type: "pointerover", target: area1_2}, + {type: "pointerenter", target: area1_2}, + // Then, move out from the <img>. + {type: "pointerout", target: area1_2}, + {type: "pointerleave", target: area1_2}, + {type: "pointerleave", target: map1}, + {type: "pointerleave", target: container}, + ]) + ); + }, "pointer boundary events when new <area> is available and the <img> is resized"); +}, {once: true}); +</script> +<style> +img { + margin: 0; + border: none; +} +div { + margin: 0; + width: 200px; + white-space: nowrap; +} +</style> +</head> +<body> + <div id="init">initial position</div> + <div id="container"> + <map id="map1"> + <area id="area1-1" shape="rect" coords="0,0,100,100" href="#"> + </map> + <map id="map2"> + <area id="area2-1" shape="rect" coords="0,0,100,100" href="#"> + </map> + <img id="img1" usemap="#map1" src="../images/green-16x16.png" width="100" height="100"> + <img id="img2" usemap="#map1" src="../images/green-16x16.png" width="100" height="100"> + </div> +</body> +</html> diff --git a/testing/web-platform/tests/uievents/mouse/mouse_boundary_events_on_image_map.html b/testing/web-platform/tests/uievents/mouse/mouse_boundary_events_on_image_map.html @@ -0,0 +1,376 @@ +<!doctype html> +<html> +<head> +<meta charset="utf-8"> +<meta name="viewport" content="width=device-width, user-scalable=no"> +<title>Event targets of boundary events over an image map</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"; + +addEventListener("load", () => { + const initialDiv = document.getElementById("init"); + const container = document.getElementById("container"); + const img1 = document.getElementById("img1"); + const img2 = document.getElementById("img2"); + const area1_1 = document.getElementById("area1-1"); + const area1_2 = document.createElement("area"); + area1_2.setAttribute("id", "area1-2"); + area1_2.setAttribute("shape", "rect"); + area1_2.setAttribute("coords", "0, 0, 100, 100"); + area1_2.setAttribute("href", "#"); + const area2_1 = document.getElementById("area2-1"); + const map1 = document.getElementById("map1"); + const map2 = document.getElementById("map2"); + + function stringifyEvents(arrayOfEvents) { + function stringifyEvent(event) { + return `${event.type}@${event.target.localName}${ + event.target.id ? `#${event.target.id}` : "" + }` + } + let str = "["; + for (const event of arrayOfEvents) { + if (str != "[") { + str += ", "; + } + str += stringifyEvent(event); + } + return str + "]"; + } + + let events = []; + function pushEvent(event) { + events.push(event); + } + for (const type of ["mousemove", "mouseover", "mouseenter", "mouseout", "mouseleave"]) { + container.addEventListener(type, pushEvent, {capture: true}); + } + + promise_test(async () => { + events = []; + await new test_driver.Actions() + .pointerMove(0, 0, {origin: initialDiv}) + .pointerMove(0, 0, {origin: img1}) // actually moved over area1-1 + .pointerMove(0, 0, {origin: initialDiv}) + .send(); + assert_equals( + stringifyEvents(events), + stringifyEvents([ + {type: "mouseover", target: area1_1}, + {type: "mouseenter", target: container}, + {type: "mouseenter", target: map1}, + {type: "mouseenter", target: area1_1}, + {type: "mousemove", target: area1_1}, + {type: "mouseout", target: area1_1}, + {type: "mouseleave", target: area1_1}, + {type: "mouseleave", target: map1}, + {type: "mouseleave", target: container}, + ]) + ); + }, "mouse boundary events when simple over/out"); + + promise_test(async () => { + events = []; + await new test_driver.Actions() + .pointerMove(0, 0, {origin: initialDiv}) + .pointerMove(0, 0, {origin: img1}) // actually moved over area1-1 + .pointerMove(0, 0, {origin: img2}) // actually moved in area1-1 + .pointerMove(0, 0, {origin: initialDiv}) + .send(); + assert_equals( + stringifyEvents(events), + stringifyEvents([ + {type: "mouseover", target: area1_1}, + {type: "mouseenter", target: container}, + {type: "mouseenter", target: map1}, + {type: "mouseenter", target: area1_1}, + {type: "mousemove", target: area1_1}, + // boundary events shouldn't be fired when moving from img1 to img2 + {type: "mousemove", target: area1_1}, + {type: "mouseout", target: area1_1}, + {type: "mouseleave", target: area1_1}, + {type: "mouseleave", target: map1}, + {type: "mouseleave", target: container}, + ]) + ); + }, "mouse boundary events when moved from an <area> to the same <area> shared by another <img>"); + + promise_test(async t => { + events = []; + function shrinkArea1_1() { + area1_1.setAttribute("coords", "0,0,10,10"); + t.add_cleanup(() => { + area1_1.setAttribute("coords", "0,0,100,100"); + }); + } + area1_1.addEventListener("mousemove", shrinkArea1_1, {once: true}); + t.add_cleanup(() => area1_1.removeEventListener("mousemove", shrinkArea1_1)); + await new test_driver.Actions() + .pointerMove(0, 0, {origin: initialDiv}) + .pointerMove(0, 0, {origin: img1}) // actually moved over area1-1 + .addTick(100) // now, over the <img> + .addTick() // for Firefox bug 1994340 + .pointerMove(0, 0, {origin: initialDiv}) + .send(); + assert_equals( + stringifyEvents(events), + stringifyEvents([ + {type: "mouseover", target: area1_1}, + {type: "mouseenter", target: container}, + {type: "mouseenter", target: map1}, + {type: "mouseenter", target: area1_1}, + {type: "mousemove", target: area1_1}, + // Now, the <area> is shrunken and the cursor is not over the <area>. + {type: "mouseout", target: area1_1}, + {type: "mouseleave", target: area1_1}, + {type: "mouseleave", target: map1}, + {type: "mouseover", target: img1}, + {type: "mouseenter", target: img1}, + // Then, move out from the <img>. + {type: "mouseout", target: img1}, + {type: "mouseleave", target: img1}, + {type: "mouseleave", target: container}, + ]) + ); + }, "mouse boundary events when the <area> is resized"); + + promise_test(async t => { + events = []; + function shrinkArea1_1() { + area1_1.setAttribute("coords", "0,0,10,10"); + img1.setAttribute("width", "200"); + img1.getBoundingClientRect(); + t.add_cleanup(() => { + img1.setAttribute("width", "100"); + area1_1.setAttribute("coords", "0,0,100,100"); + img1.getBoundingClientRect(); + }); + } + area1_1.addEventListener("mousemove", shrinkArea1_1, {once: true}); + t.add_cleanup(() => area1_1.removeEventListener("mousemove", shrinkArea1_1)); + await new test_driver.Actions() + .pointerMove(0, 0, {origin: initialDiv}) + .pointerMove(0, 0, {origin: img1}) // actually moved over area1-1 + .addTick() // now over the <img> + .pointerMove(0, 0, {origin: initialDiv}) + .send(); + assert_equals( + stringifyEvents(events), + stringifyEvents([ + {type: "mouseover", target: area1_1}, + {type: "mouseenter", target: container}, + {type: "mouseenter", target: map1}, + {type: "mouseenter", target: area1_1}, + {type: "mousemove", target: area1_1}, + // Now, the <area> is shrunken and the cursor is not over the <area>. + {type: "mouseout", target: area1_1}, + {type: "mouseleave", target: area1_1}, + {type: "mouseleave", target: map1}, + {type: "mouseover", target: img1}, + {type: "mouseenter", target: img1}, + // Then, move out from the <img>. + {type: "mouseout", target: img1}, + {type: "mouseleave", target: img1}, + {type: "mouseleave", target: container}, + ]) + ); + }, "mouse boundary events when both <area> and <img> are resized"); + + promise_test(async t => { + events = []; + function switchMap() { + img1.setAttribute("usemap", "#map2"); + t.add_cleanup(() => { + img1.setAttribute("usemap", "#map1"); + }); + } + area1_1.addEventListener("mousemove", switchMap, {once: true}); + t.add_cleanup(() => area1_1.removeEventListener("mousemove", switchMap)); + await new test_driver.Actions() + .pointerMove(0, 0, {origin: initialDiv}) + .pointerMove(0, 0, {origin: img1}) // actually moved over area1-1 + .addTick(100) // now over area2-1 + .addTick() // for Firefox bug 1994340 + .pointerMove(0, 0, {origin: initialDiv}) + .send(); + assert_equals( + stringifyEvents(events), + stringifyEvents([ + {type: "mouseover", target: area1_1}, + {type: "mouseenter", target: container}, + {type: "mouseenter", target: map1}, + {type: "mouseenter", target: area1_1}, + {type: "mousemove", target: area1_1}, + // Now, the #map2 is the image map definition and over its area2-1 + {type: "mouseout", target: area1_1}, + {type: "mouseleave", target: area1_1}, + {type: "mouseleave", target: map1}, + {type: "mouseover", target: area2_1}, + {type: "mouseenter", target: map2}, + {type: "mouseenter", target: area2_1}, + // Then, move out from the <area>. + {type: "mouseout", target: area2_1}, + {type: "mouseleave", target: area2_1}, + {type: "mouseleave", target: map2}, + {type: "mouseleave", target: container}, + ]) + ); + }, "mouse boundary events when usemap is modified"); + + promise_test(async t => { + events = []; + function switchMap() { + img1.setAttribute("usemap", "#map2"); + img1.setAttribute("width", "200"); + img1.getBoundingClientRect(); + t.add_cleanup(() => { + img1.setAttribute("usemap", "#map1"); + img1.setAttribute("width", "100"); + img1.getBoundingClientRect(); + }); + } + area1_1.addEventListener("mousemove", switchMap, {once: true}); + t.add_cleanup(() => area1_1.removeEventListener("mousemove", switchMap)); + await new test_driver.Actions() + .pointerMove(0, 0, {origin: initialDiv}) + .pointerMove(0, 0, {origin: img1}) // actually moved over area1-1 + .addTick() // now over area2-1 + .pointerMove(0, 0, {origin: initialDiv}) + .send(); + assert_equals( + stringifyEvents(events), + stringifyEvents([ + {type: "mouseover", target: area1_1}, + {type: "mouseenter", target: container}, + {type: "mouseenter", target: map1}, + {type: "mouseenter", target: area1_1}, + {type: "mousemove", target: area1_1}, + // Now, the #map2 is the image map definition and over its area2-1 + {type: "mouseout", target: area1_1}, + {type: "mouseleave", target: area1_1}, + {type: "mouseleave", target: map1}, + {type: "mouseover", target: area2_1}, + {type: "mouseenter", target: map2}, + {type: "mouseenter", target: area2_1}, + // Then, move out from the <area>. + {type: "mouseout", target: area2_1}, + {type: "mouseleave", target: area2_1}, + {type: "mouseleave", target: map2}, + {type: "mouseleave", target: container}, + ]) + ); + }, "mouse boundary events when usemap is modified and <img> is resized"); + + promise_test(async t => { + events = []; + function appendArea1_2() { + map1.insertBefore(area1_2, area1_1); + t.add_cleanup(() => { + area1_2.remove(); + }); + } + area1_1.addEventListener("mousemove", appendArea1_2, {once: true}); + t.add_cleanup(() => area1_1.removeEventListener("mousemove", appendArea1_2)); + await new test_driver.Actions() + .pointerMove(0, 0, {origin: initialDiv}) + .pointerMove(0, 0, {origin: img1}) // actually moved over area1-1 + .addTick(100) // now, over the area1-2 + .addTick() // for Firefox bug 1994340 + .pointerMove(0, 0, {origin: initialDiv}) + .send(); + assert_equals( + stringifyEvents(events), + stringifyEvents([ + {type: "mouseover", target: area1_1}, + {type: "mouseenter", target: container}, + {type: "mouseenter", target: map1}, + {type: "mouseenter", target: area1_1}, + {type: "mousemove", target: area1_1}, + // Now, new <area> is inserted and it's the top-most. + {type: "mouseout", target: area1_1}, + {type: "mouseleave", target: area1_1}, + {type: "mouseover", target: area1_2}, + {type: "mouseenter", target: area1_2}, + // Then, move out from the <img>. + {type: "mouseout", target: area1_2}, + {type: "mouseleave", target: area1_2}, + {type: "mouseleave", target: map1}, + {type: "mouseleave", target: container}, + ]) + ); + }, "mouse boundary events when new <area> is available"); + + promise_test(async t => { + events = []; + function appendArea1_2() { + map1.insertBefore(area1_2, area1_1); + img1.setAttribute("width", "200"); + img1.getBoundingClientRect(); + t.add_cleanup(() => { + area1_2.remove(); + img1.setAttribute("width", "100"); + img1.getBoundingClientRect(); + }); + } + area1_1.addEventListener("mousemove", appendArea1_2, {once: true}); + t.add_cleanup(() => area1_1.removeEventListener("mousemove", appendArea1_2)); + await new test_driver.Actions() + .pointerMove(0, 0, {origin: initialDiv}) + .pointerMove(0, 0, {origin: img1}) // actually moved over area1-1 + .addTick() // now, over the area1-2 + .pointerMove(0, 0, {origin: initialDiv}) + .send(); + assert_equals( + stringifyEvents(events), + stringifyEvents([ + {type: "mouseover", target: area1_1}, + {type: "mouseenter", target: container}, + {type: "mouseenter", target: map1}, + {type: "mouseenter", target: area1_1}, + {type: "mousemove", target: area1_1}, + // Now, new <area> is inserted and it's the top-most. + {type: "mouseout", target: area1_1}, + {type: "mouseleave", target: area1_1}, + {type: "mouseover", target: area1_2}, + {type: "mouseenter", target: area1_2}, + // Then, move out from the <img>. + {type: "mouseout", target: area1_2}, + {type: "mouseleave", target: area1_2}, + {type: "mouseleave", target: map1}, + {type: "mouseleave", target: container}, + ]) + ); + }, "mouse boundary events when new <area> is available and the <img> is resized"); +}, {once: true}); +</script> +<style> +img { + margin: 0; + border: none; +} +div { + margin: 0; + width: 200px; + white-space: nowrap; +} +</style> +</head> +<body> + <div id="init">initial position</div> + <div id="container"> + <map id="map1"> + <area id="area1-1" shape="rect" coords="0,0,100,100" href="#"> + </map> + <map id="map2"> + <area id="area2-1" shape="rect" coords="0,0,100,100" href="#"> + </map> + <img id="img1" usemap="#map1" src="../../images/green-16x16.png" width="100" height="100"> + <img id="img2" usemap="#map1" src="../../images/green-16x16.png" width="100" height="100"> + </div> +</body> +</html>