pointerevent_click_during_parent_capture.html (9471B)
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(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 if (pointerType == "mouse") { 79 if (preventDefaultType == "pointerdown") { 80 return { 81 types: ["pointerdown", "pointerup", "click"], 82 composedPaths: [ 83 pathToTarget, // pointerdown 84 pathToParent, // pointerup 85 // Captured by the parent element, `click` should be fired on it. 86 pathToParent, // click 87 ], 88 }; 89 } 90 return { 91 types: [ 92 "pointerdown", 93 "mousedown", 94 "pointerup", 95 "mouseup", 96 "click", 97 ], 98 composedPaths: [ 99 pathToTarget, // pointerdown 100 // `mousedown` target should be considered without the capturing 101 // element. 102 pathToTarget, // mousedown 103 pathToParent, // pointerup 104 // However, `mouseup` target should be considered with the capturing 105 // element. 106 pathToParent, // mouseup 107 // Captured by the parent element, `click` should be fired on it. 108 pathToParent, // click 109 ], 110 }; 111 } 112 if (preventDefaultType == "pointerdown") { 113 return { 114 types: [ 115 "pointerdown", 116 "touchstart", 117 "pointerup", 118 "touchend", 119 "click", 120 ], 121 composedPaths: [ 122 pathToTarget, // pointerdown 123 // `touchstart` target should be considered without the capturing 124 // element. 125 pathToTarget, // touchstart 126 pathToParent, // pointerup 127 // Different from `mouseup`, `touchend` should always be fired on 128 // same target as `touchstart`. 129 pathToTarget, // touchend 130 // `click` event is NOT a compatibility mouse event of Touch 131 // Events because canceling `pointerdown` should cancel them. 132 // So, the event target should be considered with `userEvent` 133 // which caused this `click` event. In this case, it's the 134 // preceding `pointerup`. Therefore, this should be considered 135 // with the capturing element. 136 pathToParent, // click 137 ], 138 }; 139 } 140 if (preventDefaultType == "touchstart") { 141 return { 142 types: ["pointerdown", "touchstart", "pointerup", "touchend"], 143 composedPaths: [ 144 pathToTarget, // pointerdown 145 // `touchstart` target should be considered without the capturing 146 // element. 147 pathToTarget, // touchstart 148 pathToParent, // pointerup 149 // Different from `mouseup`, `touchend` should always be fired on 150 // same target as `touchstart`. 151 pathToTarget, // touchend 152 // `click` shouldn't be fired if `touchstart` is canceled especially 153 // for the backward compatibility. 154 ], 155 }; 156 } 157 return { 158 types: [ 159 "pointerdown", 160 "touchstart", 161 "pointerup", 162 "touchend", 163 "mousedown", 164 "mouseup", 165 "click", 166 ], 167 composedPaths: [ 168 pathToTarget, // pointerdown 169 // `touchstart` target should be considered without the capturing 170 // element. 171 pathToTarget, // touchstart 172 pathToParent, // touchup 173 // Different from `mouseup`, `touchend` should always be fired on 174 // same target as `touchstart`. 175 pathToTarget, // touchend 176 // Compatibility mouse events should be fired on the element at the 177 // touch point. 178 pathToTarget, // mousedown 179 pathToTarget, // mouseup 180 // `click` should NOT be a compatibility mouse event of the Touch 181 // Events since touchstart was not consumed. So, captured by the 182 // parent element, `click` should be fired on it. 183 pathToParent, //click 184 ], 185 }; 186 })(); 187 188 win.addEventListener( 189 "pointerdown", 190 e => { 191 captureEvent(e); 192 pointerId = e.pointerId; 193 parent.setPointerCapture(pointerId); 194 if (preventDefaultType == e.type) { 195 e.preventDefault(); 196 } 197 }, 198 { once: true, passive: false } 199 ); 200 win.addEventListener( 201 "pointerup", 202 e => { 203 captureEvent(e); 204 parent.releasePointerCapture(pointerId); 205 }, 206 { once: true } 207 ); 208 win.addEventListener( 209 "touchstart", 210 e => { 211 captureEvent(e); 212 if (preventDefaultType == e.type) { 213 e.preventDefault(); 214 } 215 }, 216 { once: true, passive: false } 217 ); 218 win.addEventListener( 219 "touchend", 220 captureEvent, 221 { once: true } 222 ); 223 win.addEventListener("mousedown", captureEvent, { once: true }); 224 win.addEventListener("mouseup", captureEvent, { once: true }); 225 win.addEventListener("click", captureEvent, { once: true }); 226 227 // Unfortunately, async synthesizing of the touch event will be handled 228 // in some event loops until dispatching the last `click`. THerefore, 229 // we need to wait it with a promise and check no redundant events with 230 // waiting some more ticks. 231 const promisePointerUp = new Promise(resolve => { 232 win.addEventListener( 233 expectedEvents.types[expectedEvents.types.length - 1], 234 () => requestAnimationFrame( 235 () => requestAnimationFrame(resolve) 236 ), 237 { once: true } 238 ); 239 }); 240 await new test_driver.Actions() 241 .addPointer("TestPointer", pointerType) 242 .pointerMove(0, 0, { origin: target }) 243 .pointerDown() 244 .pointerUp() 245 .send(); 246 await promisePointerUp; 247 248 assert_array_equals( 249 eventTypes, 250 expectedEvents.types, 251 "all expected events should be fired" 252 ); 253 for (let i = 0; i < expectedEvents.types.length; i++) { 254 assert_array_equals( 255 stringifyElements(composedPaths[i]), 256 stringifyElements(expectedEvents.composedPaths[i]), 257 `"${expectedEvents.types[i]}" event should be fired on expected target` 258 ); 259 } 260 } 261 262 promise_test(async () => { 263 await runTest(window, document); 264 }, "Test in the topmost document"); 265 promise_test(async () => { 266 await runTest(iframe.contentWindow, iframe.contentDocument); 267 }, "Test in the iframe"); 268 }, 269 { once: true } 270 ); 271 </script> 272 </head> 273 <body> 274 <div id="parent"> 275 <div id="target"></div> 276 </div> 277 <iframe srcdoc="<div id='parent'><div id='target'></div></div>"></iframe> 278 </body> 279 </html>