pointerevent_click_during_parent_capture.html (9939B)
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="utf-8"> 5 <meta name="variant" content="?pointerType=mouse&preventDefault="> 6 <meta name="variant" content="?pointerType=mouse&preventDefault=pointerdown"> 7 <meta name="variant" content="?pointerType=touch&preventDefault="> 8 <meta name="variant" content="?pointerType=touch&preventDefault=pointerdown"> 9 <meta name="variant" content="?pointerType=touch&preventDefault=touchstart"> 10 <title>Test `click` event target when a parent element captures the pointer</title> 11 <style> 12 #parent { 13 background: green; 14 border: 1px solid black; 15 width: 40px; 16 height: 40px; 17 } 18 19 #target { 20 background: blue; 21 border: 1px solid black; 22 width: 20px; 23 height: 20px; 24 margin: 10px; 25 } 26 </style> 27 <script src="/resources/testharness.js"></script> 28 <script src="/resources/testharnessreport.js"></script> 29 <script src="/resources/testdriver.js"></script> 30 <script src="/resources/testdriver-vendor.js"></script> 31 <script src="/resources/testdriver-actions.js"></script> 32 <script> 33 "use strict"; 34 35 const searchParams = new URLSearchParams(document.location.search); 36 const pointerType = searchParams.get("pointerType"); 37 const preventDefaultType = searchParams.get("preventDefault"); 38 39 addEventListener( 40 "load", 41 () => { 42 const iframe = document.querySelector("iframe"); 43 iframe.contentDocument.head.innerHTML = `<style>${ 44 document.querySelector("style").textContent 45 }</style>`; 46 47 async function runTest(t, win, doc) { 48 let pointerId; 49 const parent = doc.getElementById("parent"); 50 const target = doc.getElementById("target"); 51 const body = doc.body; 52 const html = doc.documentElement; 53 let eventTypes = []; 54 let composedPaths = []; 55 function stringifyIfElement(eventTarget) { 56 if (!(eventTarget instanceof win.Node)) { 57 return eventTarget; 58 } 59 switch (eventTarget.nodeType) { 60 case win.Node.ELEMENT_NODE: 61 return `<${eventTarget.localName}${ 62 eventTarget.id ? ` id="${eventTarget.id}"` : "" 63 }>`; 64 default: 65 return eventTarget; 66 } 67 } 68 function stringifyElements(eventTargets) { 69 return eventTargets.map(stringifyIfElement); 70 } 71 function captureEvent(e) { 72 eventTypes.push(e.type); 73 composedPaths.push(e.composedPath()); 74 } 75 const expectedEvents = (() => { 76 const pathToTarget = [target, parent, body, html, doc, win]; 77 const pathToParent = [parent, body, html, doc, win]; 78 function getExpectedEventsForMouse() { 79 switch (preventDefaultType) { 80 case "pointerdown": 81 return { 82 types: ["pointerdown", "pointerup", "click"], 83 composedPaths: [ 84 pathToTarget, // pointerdown 85 pathToParent, // pointerup 86 // Captured by the parent element, `click` should be fired on 87 // it. 88 pathToParent, // click 89 ], 90 }; 91 default: 92 return { 93 types: [ 94 "pointerdown", 95 "mousedown", 96 "pointerup", 97 "mouseup", 98 "click", 99 ], 100 composedPaths: [ 101 pathToTarget, // pointerdown 102 // `mousedown` target should be considered without the 103 // capturing element. 104 pathToTarget, // mousedown 105 pathToParent, // pointerup 106 // However, `mouseup` target should be considered with the 107 // capturing element. 108 pathToParent, // mouseup 109 // Captured by the parent element, `click` should be fired on 110 // it. 111 pathToParent, // click 112 ], 113 }; 114 } 115 } 116 function getExpectedEventsForTouch() { 117 switch (preventDefaultType) { 118 case "pointerdown": 119 return { 120 types: [ 121 "pointerdown", 122 "touchstart", 123 "pointerup", 124 "touchend", 125 "click", 126 ], 127 composedPaths: [ 128 pathToTarget, // pointerdown 129 // `touchstart` target should be considered without the 130 // capturing element. 131 pathToTarget, // touchstart 132 pathToParent, // pointerup 133 // Different from `mouseup`, `touchend` should always be fired 134 // on same target as `touchstart`. 135 pathToTarget, // touchend 136 // `click` event is NOT a compatibility mouse event of Touch 137 // Events because canceling `pointerdown` should cancel them. 138 // So, the event target should be considered with `userEvent` 139 // which caused this `click` event. In this case, it's the 140 // preceding `pointerup`. Therefore, this should be 141 // considered with the capturing element. 142 pathToParent, // click 143 ], 144 }; 145 case "touchstart": 146 return { 147 types: ["pointerdown", "touchstart", "pointerup", "touchend"], 148 composedPaths: [ 149 pathToTarget, // pointerdown 150 // `touchstart` target should be considered without the 151 // capturing element. 152 pathToTarget, // touchstart 153 pathToParent, // pointerup 154 // Different from `mouseup`, `touchend` should always be fired 155 // on same target as `touchstart`. 156 pathToTarget, // touchend 157 // `click` shouldn't be fired if `touchstart` is canceled 158 // especially for the backward compatibility. 159 ], 160 }; 161 default: 162 return { 163 types: [ 164 "pointerdown", 165 "touchstart", 166 "pointerup", 167 "touchend", 168 "mousedown", 169 "mouseup", 170 "click", 171 ], 172 composedPaths: [ 173 pathToTarget, // pointerdown 174 // `touchstart` target should be considered without the 175 // capturing element. 176 pathToTarget, // touchstart 177 pathToParent, // touchup 178 // Different from `mouseup`, `touchend` should always be fired 179 // on same target as `touchstart`. 180 pathToTarget, // touchend 181 // Compatibility mouse events should be fired on the element 182 // at the touch point. 183 pathToTarget, // mousedown 184 pathToTarget, // mouseup 185 // `click` should NOT be a compatibility mouse event of the 186 // Touch Events since touchstart was not consumed. So, 187 // captured by the parent element, `click` should be fired on 188 // it. 189 pathToParent, //click 190 ], 191 }; 192 } 193 } 194 return pointerType == "mouse" 195 ? getExpectedEventsForMouse() 196 : getExpectedEventsForTouch(); 197 })(); 198 199 win.addEventListener( 200 "pointerdown", 201 e => { 202 captureEvent(e); 203 pointerId = e.pointerId; 204 parent.setPointerCapture(pointerId); 205 if (preventDefaultType == e.type) { 206 e.preventDefault(); 207 } 208 }, 209 { once: true, passive: false } 210 ); 211 win.addEventListener( 212 "pointerup", 213 e => { 214 captureEvent(e); 215 parent.releasePointerCapture(pointerId); 216 }, 217 { once: true } 218 ); 219 win.addEventListener( 220 "touchstart", 221 e => { 222 captureEvent(e); 223 if (preventDefaultType == e.type) { 224 e.preventDefault(); 225 } 226 }, 227 { once: true, passive: false } 228 ); 229 win.addEventListener( 230 "touchend", 231 captureEvent, 232 { once: true } 233 ); 234 win.addEventListener("mousedown", captureEvent, { once: true }); 235 win.addEventListener("mouseup", captureEvent, { once: true }); 236 win.addEventListener("click", captureEvent, { once: true }); 237 238 await new test_driver.Actions() 239 .addPointer("TestPointer", pointerType) 240 .pointerMove(0, 0, { origin: target }) 241 .pointerDown() 242 .pointerUp() 243 .pause(100) // XXX Required for preventing intermittent failure of Firefox 244 .send(); 245 246 test(() => { 247 assert_array_equals(eventTypes, expectedEvents.types); 248 }, `${t.name}: all expected events should be fired`); 249 for (let i = 0; i < eventTypes.length; i++) { 250 const eventType = eventTypes[i]; 251 const expectedEventIndex = expectedEvents.types.indexOf(eventType); 252 if (expectedEventIndex < 0) { 253 continue; 254 } 255 test(() => { 256 assert_array_equals( 257 stringifyElements(composedPaths[i]), 258 stringifyElements(expectedEvents.composedPaths[expectedEventIndex]) 259 ); 260 }, `${t.name}: "${eventType}" event should be fired on expected target`); 261 } 262 } 263 264 promise_test(async t => { 265 await runTest(t, window, document); 266 }, "Test in the topmost document"); 267 promise_test(async t => { 268 await runTest(t, iframe.contentWindow, iframe.contentDocument); 269 }, "Test in the iframe"); 270 }, 271 { once: true } 272 ); 273 </script> 274 </head> 275 <body> 276 <div id="parent"> 277 <div id="target"></div> 278 </div> 279 <iframe srcdoc="<div id='parent'><div id='target'></div></div>"></iframe> 280 </body> 281 </html>