synthetic-mouse-enter-leave-over-out-button-state-after-target-removed.tentative.html (8615B)
1 <!doctype html> 2 <html> 3 <head> 4 <meta charset="utf-8"> 5 <meta name="variant" content="?buttonType=LEFT&button=0&buttons=1"> 6 <meta name="variant" content="?buttonType=MIDDLE&button=1&buttons=4"> 7 <title>Testing button state of synthesized mouse(out|over|leave|enter) events</title> 8 <script src="/resources/testharness.js"></script> 9 <script src="/resources/testharnessreport.js"></script> 10 <script src="/resources/testdriver.js"></script> 11 <script src="/resources/testdriver-vendor.js"></script> 12 <script src="/resources/testdriver-actions.js"></script> 13 <style> 14 #parent { 15 background-color: lightseagreen; 16 padding: 0; 17 height: 40px; 18 width: 40px; 19 } 20 #child { 21 background-color: red; 22 margin: 0; 23 height: 30px; 24 width: 30px; 25 } 26 </style> 27 </head> 28 <body> 29 <div id="parent"><div id="child">abc</div></div> 30 <script> 31 const searchParams = new URLSearchParams(document.location.search); 32 const buttonType = searchParams.get("buttonType"); 33 const button = parseInt(searchParams.get("button")); 34 const buttons = parseInt(searchParams.get("buttons")); 35 36 let events = []; 37 function eventToString(data) { 38 if (!data) { 39 return "{}"; 40 } 41 return `{ '${data.type}' on '${data.target}': button=${data.button}, buttons=${data.buttons} }`; 42 } 43 44 function eventsToString(events) { 45 if (!events.length) { 46 return "[]"; 47 } 48 let ret = "["; 49 for (const data of events) { 50 if (ret != "[") { 51 ret += ", "; 52 } 53 ret += eventToString(data); 54 } 55 return ret + "]"; 56 } 57 58 function removeEventsBefore(eventType) { 59 while (events[0]?.type != eventType) { 60 events.shift(); 61 } 62 } 63 64 const parentElement = document.getElementById("parent"); 65 const childElement = document.getElementById("child"); 66 67 function promiseLayout() { 68 return new Promise(resolve => { 69 (childElement.isConnected ? childElement : parentElement).getBoundingClientRect(); 70 requestAnimationFrame(() => requestAnimationFrame(resolve)); 71 }); 72 } 73 74 promise_test(async () => { 75 await new Promise(resolve => { 76 addEventListener("load", resolve, { once: true }); 77 }); 78 79 ["mouseout", "mouseover", "mouseleave", "mouseenter", "mousemove", "mousedown"].forEach(eventType => { 80 parentElement.addEventListener(eventType, event => { 81 if (event.target != parentElement) { 82 return; 83 } 84 events.push({ 85 type: event.type, 86 target: "parent", 87 button: event.button, 88 buttons: event.buttons, 89 }); 90 }); 91 childElement.addEventListener(eventType, event => { 92 if (event.target != childElement) { 93 return; 94 } 95 events.push({ 96 type: event.type, 97 target: "child", 98 button: event.button, 99 buttons: event.buttons, 100 }); 101 }); 102 }); 103 }, "Setup event listeners and wait for load"); 104 105 promise_test(async t => { 106 events = []; 107 await promiseLayout(); 108 childElement.addEventListener("mousedown", () => childElement.remove(), {once: true}); 109 const {x, y} = (function () { 110 const rect = childElement.getBoundingClientRect(); 111 return { x: rect.left, y: rect.top }; 112 })(); 113 const actions = new test_driver.Actions(); 114 await actions.pointerMove(10, 10, {origin: childElement}) 115 .pointerDown({button: actions.ButtonType[buttonType]}) 116 .pause(100) // Allow browsers to synthesize mouseout, etc 117 .pointerUp({button: actions.ButtonType[buttonType]}) 118 .send(); 119 await promiseLayout(); 120 removeEventsBefore("mousedown"); 121 test(() => { 122 const maybeMouseDownEvent = 123 events.length && events[0].type == "mousedown" ? events.shift() : undefined; 124 assert_equals( 125 eventToString(maybeMouseDownEvent), 126 eventToString({ type: "mousedown", target: "child", button, buttons }) 127 ); 128 }, `${t.name}: mousedown should've been fired`); 129 assert_true(events.length > 0, `${t.name}: Some events should've been fired after mousedown`); 130 test(() => { 131 // Before `mousedown` is fired, both parent and child must have received 132 // `mouseenter`, only the child must have received `mouseover`. Then, the 133 // child is now moved away by the `mousedown` listener. Therefore, 134 // `mouseout` and `mouseleave` should be fired on the child as the spec of 135 // UI Events defines. Then, they are not a button press events. Therefore, 136 // the `button` should be 0, but buttons should be set to 4 because of 137 // pressing the middle button. 138 let mouseOutOrLeave = []; 139 while (events[0]?.type == "mouseout" || events[0]?.type == "mouseleave") { 140 mouseOutOrLeave.push(events.shift()); 141 } 142 assert_equals( 143 eventsToString(mouseOutOrLeave), 144 eventsToString([ 145 { type: "mouseout", target: "child", button: 0, buttons }, 146 { type: "mouseleave", target: "child", button: 0, buttons }, 147 ]) 148 ); 149 }, `${t.name}: mouseout and mouseleave should've been fired on the removed child`); 150 test(() => { 151 // And `mouseover` should be fired on the parent as the spec of UI Events 152 // defines. 153 let mouseOver = []; 154 while (events[0]?.type == "mouseover") { 155 mouseOver.push(events.shift()); 156 } 157 assert_equals( 158 eventsToString(mouseOver), 159 eventsToString([{ type: "mouseover", target: "parent", button: 0, buttons }]) 160 ); 161 }, `${t.name}: mouseover should've been fired on the parent`); 162 test(() => { 163 // On the other hand, it's unclear about `mouseenter`. The mouse cursor has 164 // never been moved out from the parent. Therefore, it shouldn't be fired 165 // on the parent ideally, but all browsers do not pass this test and there 166 // is no clear definition about this case. 167 let mouseEnter = []; 168 while (events.length && events[0].type == "mouseenter") { 169 mouseEnter.push(events.shift()); 170 } 171 assert_equals(eventsToString(mouseEnter), eventsToString([])); 172 }, `${t.name}: mouseenter should not have been fired on the parent`); 173 assert_equals(eventsToString(events), eventsToString([]), "All events should've been checked"); 174 parentElement.appendChild(childElement); 175 }, "Removing an element at mousedown"); 176 177 promise_test(async t => { 178 events = []; 179 await promiseLayout(); 180 childElement.addEventListener("mouseup", () => childElement.remove(), {once: true}); 181 const {x, y} = (function () { 182 const rect = childElement.getBoundingClientRect(); 183 return { x: rect.left, y: rect.top }; 184 })(); 185 const actions = new test_driver.Actions(); 186 await actions.pointerMove(10, 10, {origin: childElement}) 187 .pointerDown({button: actions.ButtonType[buttonType]}) 188 .pointerUp({button: actions.ButtonType[buttonType]}) 189 .send(); 190 await promiseLayout(); 191 removeEventsBefore("mousedown"); 192 test(() => { 193 const maybeMouseDownEvent = 194 events.length && events[0].type == "mousedown" ? events.shift() : undefined; 195 assert_equals( 196 eventToString(maybeMouseDownEvent), 197 eventToString({ type: "mousedown", target: "child", button, buttons }) 198 ); 199 }, `${t.name}: mousedown should've been fired`); 200 assert_true(events.length > 0, `${t.name}: Some events should've been fired after mousedown`); 201 // Same as the `mousedown` case except `buttons` value because `mouseout`, 202 // `mouseleave`, `mouseover` and `mouseenter` should (or may) be fired 203 // after the `mouseup`. Therefore, `.buttons` should not have the button 204 // flag. 205 test(() => { 206 let mouseOutOrLeave = []; 207 while (events[0]?.type == "mouseout" || events[0]?.type == "mouseleave") { 208 mouseOutOrLeave.push(events.shift()); 209 } 210 assert_equals( 211 eventsToString(mouseOutOrLeave), 212 eventsToString([ 213 { type: "mouseout", target: "child", button: 0, buttons: 0 }, 214 { type: "mouseleave", target: "child", button: 0, buttons: 0 }, 215 ]) 216 ); 217 }, `${t.name}: mouseout and mouseleave should've been fired on the removed child`); 218 test(() => { 219 let mouseOver = []; 220 while (events[0]?.type == "mouseover") { 221 mouseOver.push(events.shift()); 222 } 223 assert_equals( 224 eventsToString(mouseOver), 225 eventsToString([{ type: "mouseover", target: "parent", button: 0, buttons: 0 }]) 226 ); 227 }, `${t.name}: mouseover should've been fired on the parent`); 228 test(() => { 229 let mouseEnter = []; 230 while (events[0]?.type == "mouseenter") { 231 mouseEnter.push(events.shift()); 232 } 233 assert_equals(eventsToString(mouseEnter), eventsToString([])); 234 }, `${t.name}: mouseenter should not have been fired on the parent`); 235 assert_equals(eventsToString(events), eventsToString([]), "All events should've been checked"); 236 parentElement.appendChild(childElement); 237 }, "Removing an element at mouseup"); 238 </script> 239 </body> 240 </html>