imperative-slot-api-slotchange.html (11732B)
1 <!DOCTYPE html> 2 <title>Shadow DOM: Imperative Slot API slotchange event</title> 3 <meta name="author" title="Yu Han" href="mailto:yuzhehan@chromium.org"> 4 <script src="/resources/testharness.js"></script> 5 <script src="/resources/testharnessreport.js"></script> 6 <link rel="help" href="https://dom.spec.whatwg.org/#signaling-slot-change"> 7 <script src="resources/shadow-dom.js"></script> 8 9 <div id="test_slotchange"> 10 <div id="host"> 11 <template id="shadow_root" data-mode="open" data-slot-assignment="manual"> 12 <slot id="s1"><div id="fb">fallback</div></slot> 13 <slot id="s2"></slot> 14 <div> 15 <slot id="s2.5"></slot> 16 </div> 17 <slot id="s3"></slot> 18 </template> 19 <div id="c1"></div> 20 <div id="c2"></div> 21 </div> 22 <div id="c4"></div> 23 </div> 24 25 <script> 26 function getDataCollection() { 27 return { 28 s1EventCount: 0, 29 s2EventCount: 0, 30 s3EventCount: 0, 31 s1ResolveFn: null, 32 s2ResolveFn: null, 33 s3ResolveFn: null, 34 } 35 } 36 37 function setupShadowDOM(id, test, data) { 38 let tTree = createTestTree(id); 39 tTree.s1.addEventListener('slotchange', (event) => { 40 if (!event.isFakeEvent) { 41 test.step(function () { 42 assert_equals(event.type, 'slotchange', 'slotchange event\'s type must be "slotchange"'); 43 assert_equals(event.target, tTree.s1, 'slotchange event\'s target must be the slot element'); 44 assert_equals(event.relatedTarget, undefined, 'slotchange must not set relatedTarget'); 45 }); 46 data.s1EventCount++; 47 } 48 data.s1ResolveFn(); 49 }); 50 tTree.s2.addEventListener('slotchange', (event) => { 51 if (!event.isFakeEvent) { 52 test.step(function () { 53 assert_equals(event.type, 'slotchange', 'slotchange event\'s type must be "slotchange"'); 54 assert_equals(event.target, tTree.s2, 'slotchange event\'s target must be the slot element'); 55 assert_equals(event.relatedTarget, undefined, 'slotchange must not set relatedTarget'); 56 }); 57 data.s2EventCount++; 58 } 59 data.s2ResolveFn(); 60 }); 61 tTree.s3.addEventListener('slotchange', (event) => { 62 if (!event.isFakeEvent) { 63 test.step(function () { 64 assert_equals(event.type, 'slotchange', 'slotchange event\'s type must be "slotchange"'); 65 // listen to bubbling events. 66 assert_equals(event.relatedTarget, undefined, 'slotchange must not set relatedTarget'); 67 }); 68 data.s3EventCount++; 69 } 70 data.s3ResolveFn(); 71 }); 72 return tTree; 73 } 74 75 function monitorSlots(data) { 76 const s1Promise = new Promise((resolve, reject) => { 77 data.s1ResolveFn = resolve; 78 }); 79 const s2Promise = new Promise((resolve, reject) => { 80 data.s2ResolveFn = resolve; 81 }); 82 const s3Promise = new Promise((resolve, reject) => { 83 data.s3ResolveFn = resolve; 84 }); 85 return [s1Promise, s2Promise, s3Promise]; 86 } 87 </script> 88 89 <script> 90 // Tests: 91 async_test((test) => { 92 const data = getDataCollection(); 93 let tTree = setupShadowDOM(test_slotchange, test, data); 94 let [s1Promise, s2Promise] = monitorSlots(data); 95 96 tTree.s1.assign(tTree.c1); 97 tTree.s2.assign(tTree.c2); 98 99 assert_equals(data.s1EventCount, 0, 'slotchange event must not be fired synchronously'); 100 assert_equals(data.s2EventCount, 0); 101 102 Promise.all([s1Promise, s2Promise]).then(test.step_func_done(() => { 103 assert_equals(data.s1EventCount, 1); 104 assert_equals(data.s2EventCount, 1); 105 })); 106 }, 'slotchange event must not fire synchronously.'); 107 108 async_test((test) => { 109 const data = getDataCollection(); 110 let tTree = setupShadowDOM(test_slotchange, test, data); 111 let [s1Promise, s2Promise] = monitorSlots(data); 112 113 tTree.s1.assign();; 114 tTree.s2.assign(); 115 tTree.host.insertBefore(tTree.c4, tTree.c1); 116 117 Promise.all([s1Promise, s2Promise]).then(test.step_func_done(() => { 118 assert_equals(data.s1EventCount, 0); 119 assert_equals(data.s2EventCount, 0); 120 })); 121 122 // use fake event to trigger event handler. 123 let fakeEvent = new Event('slotchange'); 124 fakeEvent.isFakeEvent = true; 125 tTree.s1.dispatchEvent(fakeEvent); 126 tTree.s2.dispatchEvent(fakeEvent); 127 }, 'slotchange event should not fire when assignments do not change assignedNodes.'); 128 129 async_test((test) => { 130 const data = getDataCollection(); 131 let tTree = setupShadowDOM(test_slotchange,test, data); 132 let [s1Promise] = monitorSlots(data); 133 134 tTree.s1.assign(tTree.c1, tTree.c2); 135 136 s1Promise.then(test.step_func(() => { 137 assert_equals(data.s1EventCount, 1); 138 139 [s1Promise] = monitorSlots(data); 140 tTree.s1.assign(tTree.c1, tTree.c2); 141 tTree.s1.assign(tTree.c1, tTree.c2, tTree.c1, tTree.c2, tTree.c2); 142 143 s1Promise.then(test.step_func_done(() => { 144 assert_equals(data.s1EventCount, 1); 145 })); 146 147 let fakeEvent = new Event('slotchange'); 148 fakeEvent.isFakeEvent = true; 149 tTree.s1.dispatchEvent(fakeEvent); 150 })); 151 152 }, 'slotchange event should not fire when same node is assigned.'); 153 154 async_test((test) => { 155 const data = getDataCollection(); 156 let tTree = setupShadowDOM(test_slotchange, test, data); 157 let [s1Promise, s2Promise] = monitorSlots(data); 158 159 tTree.s1.assign(tTree.c1); 160 tTree.s2.assign(tTree.c2); 161 162 Promise.all([s1Promise, s2Promise]).then(test.step_func_done(() => { 163 assert_equals(data.s1EventCount, 1); 164 assert_equals(data.s2EventCount, 1); 165 })); 166 }, "Fire slotchange event when slot's assigned nodes changes."); 167 168 async_test((test) => { 169 const data = getDataCollection(); 170 let tTree = setupShadowDOM(test_slotchange, test, data); 171 let [s1Promise, s2Promise] = monitorSlots(data); 172 173 tTree.s1.assign(tTree.c1); 174 175 s1Promise.then(test.step_func(() => { 176 assert_equals(data.s1EventCount, 1); 177 178 [s1Promise, s2Promise] = monitorSlots(data); 179 tTree.s2.assign(tTree.c1); 180 181 Promise.all([s1Promise, s2Promise]).then(test.step_func_done(() => { 182 assert_equals(data.s1EventCount, 2); 183 assert_equals(data.s2EventCount, 1); 184 })); 185 })); 186 }, "Fire slotchange event on previous slot and new slot when node is reassigned."); 187 188 async_test((test) => { 189 const data = getDataCollection(); 190 let tTree = setupShadowDOM(test_slotchange, test, data); 191 let [s1Promise] = monitorSlots(data); 192 193 tTree.s1.assign(tTree.c1); 194 195 s1Promise.then(test.step_func(() => { 196 assert_equals(data.s1EventCount, 1); 197 198 [s1Promise] = monitorSlots(data); 199 tTree.s1.assign(); 200 201 s1Promise.then(test.step_func_done(() => { 202 assert_equals(data.s1EventCount, 2); 203 })); 204 })); 205 }, "Fire slotchange event on node assignment and when assigned node is removed."); 206 207 async_test((test) => { 208 const data = getDataCollection(); 209 let tTree = setupShadowDOM(test_slotchange, test, data); 210 let [s1Promise] = monitorSlots(data); 211 212 tTree.s1.assign(tTree.c1, tTree.c2); 213 214 s1Promise.then(test.step_func(() => { 215 assert_equals(data.s1EventCount, 1); 216 217 [s1Promise] = monitorSlots(data); 218 tTree.s1.assign(tTree.c2, tTree.c1); 219 220 s1Promise.then(test.step_func_done(() => { 221 assert_equals(data.s1EventCount, 2); 222 })); 223 })); 224 }, "Fire slotchange event when order of assigned nodes changes."); 225 226 promise_test((test) => { 227 const data = getDataCollection(); 228 let tTree = setupShadowDOM(test_slotchange, test, data); 229 let [s1Promise] = monitorSlots(data); 230 231 tTree.s1.assign(tTree.c1); 232 233 return s1Promise.then(test.step_func(() => { 234 assert_equals(data.s1EventCount, 1); 235 236 [s1Promise] = monitorSlots(data); 237 tTree.c1.remove(); 238 239 return s1Promise; 240 })) 241 .then(test.step_func(() => { 242 assert_equals(data.s1EventCount, 2); 243 })); 244 }, "Fire slotchange event when assigned node is removed."); 245 246 promise_test((test) => { 247 const data = getDataCollection(); 248 let tTree = setupShadowDOM(test_slotchange, test, data); 249 [s1Promise] = monitorSlots(data); 250 251 tTree.s1.assign(tTree.c1); 252 253 return s1Promise.then(test.step_func(() => { 254 assert_equals(data.s1EventCount, 1); 255 256 [s1Promise] = monitorSlots(data); 257 tTree.s1.remove(); 258 259 return s1Promise; 260 })) 261 .then(test.step_func(() => { 262 assert_equals(data.s1EventCount, 2); 263 })); 264 }, "Fire slotchange event when removing a slot from Shadows Root that changes its assigned nodes."); 265 266 async_test((test) => { 267 const data = getDataCollection(); 268 let tTree = setupShadowDOM(test_slotchange, test, data); 269 let [s1Promise] = monitorSlots(data); 270 271 tTree.s1.remove(); 272 273 let fakeEvent = new Event('slotchange'); 274 fakeEvent.isFakeEvent = true; 275 tTree.s1.dispatchEvent(fakeEvent); 276 277 s1Promise.then(test.step_func(() => { 278 assert_equals(data.s2EventCount, 0); 279 280 [s1Promise, s2Promise] = monitorSlots(data); 281 tTree.shadow_root.insertBefore(tTree.s1, tTree.s2); 282 283 tTree.s1.dispatchEvent(fakeEvent); 284 tTree.s2.dispatchEvent(fakeEvent); 285 286 Promise.all([s1Promise, s2Promise]).then(test.step_func_done(() => { 287 assert_equals(data.s1EventCount, 0); 288 assert_equals(data.s2EventCount, 0); 289 })); 290 })); 291 292 }, "No slotchange event when adding or removing an empty slot."); 293 294 async_test((test) => { 295 const data = getDataCollection(); 296 let tTree = setupShadowDOM(test_slotchange, test, data); 297 let [s1Promise, s2Promise] = monitorSlots(data); 298 299 tTree.host.appendChild(document.createElement("div")); 300 301 let fakeEvent = new Event('slotchange'); 302 fakeEvent.isFakeEvent = true; 303 tTree.s1.dispatchEvent(fakeEvent); 304 tTree.s2.dispatchEvent(fakeEvent); 305 306 Promise.all([s1Promise, s2Promise]).then(test.step_func(() => { 307 assert_equals(data.s1EventCount, 0); 308 assert_equals(data.s2EventCount, 0); 309 310 [s1Promise, s2Promise] = monitorSlots(data); 311 tTree.shadow_root.insertBefore(document.createElement("div"), tTree.s2); 312 313 tTree.s1.dispatchEvent(fakeEvent); 314 tTree.s2.dispatchEvent(fakeEvent); 315 316 Promise.all([s1Promise, s2Promise]).then(test.step_func_done(() => { 317 assert_equals(data.s1EventCount, 0); 318 assert_equals(data.s2EventCount, 0); 319 })); 320 })); 321 322 }, "No slotchange event when adding another slotable."); 323 324 </script> 325 326 <div id="test_nested_slotchange"> 327 <div> 328 <template data-mode="open" data-slot-assignment="manual"> 329 <div> 330 <template data-mode="open" data-slot-assignment="manual"> 331 <slot id="s2"></slot> 332 <slot id="s3"></slot> 333 </template> 334 <slot id="s1"></slot> 335 </div> 336 </template> 337 <div id="c1"></div> 338 </div> 339 </div> 340 341 <script> 342 async_test((test) => { 343 const data = getDataCollection(); 344 let tTree = setupShadowDOM(test_nested_slotchange, test, data); 345 let [s1Promise, s2Promise, s3Promise] = monitorSlots(data); 346 347 tTree.s3.assign(tTree.s1); 348 349 s3Promise.then(test.step_func(() => { 350 assert_equals(data.s3EventCount, 1); 351 [s1Promise, s2Promise, s3Promise] = monitorSlots(data); 352 353 tTree.s1.assign(tTree.c1); 354 355 Promise.all([s1Promise, s3Promise]).then(test.step_func_done(() => { 356 assert_equals(data.s1EventCount, 1); 357 assert_equals(data.s3EventCount, 2); 358 })); 359 })); 360 }, "Fire slotchange event when assign node to nested slot, ensure event bubbles ups."); 361 362 promise_test(async t => { 363 async function mutationObserversRun() { 364 return new Promise(r => { 365 t.step_timeout(r, 0); 366 }); 367 } 368 let tTree = createTestTree(test_slotchange); 369 370 tTree.s1.assign(tTree.c1); 371 tTree["s2.5"].assign(tTree.c2); 372 373 let slotChangedOrder = []; 374 375 // Clears out pending mutation observers 376 await mutationObserversRun(); 377 378 tTree.s1.addEventListener("slotchange", function() { 379 slotChangedOrder.push("s1"); 380 }); 381 382 tTree.s3.addEventListener("slotchange", function() { 383 slotChangedOrder.push("s3"); 384 }); 385 386 tTree["s2.5"].addEventListener("slotchange", function() { 387 slotChangedOrder.push("s2.5"); 388 }); 389 390 tTree.s3.assign(tTree.c2, tTree.c1); 391 await mutationObserversRun(); 392 assert_array_equals(slotChangedOrder, ["s1", "s2.5", "s3"]); 393 }, 'Signal a slot change should be done in tree order.'); 394 </script>