slotchange-event.html (24032B)
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <title>Shadow DOM: slotchange event</title> 5 <meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org"> 6 <meta name="author" title="Hayato Ito" href="mailto:hayato@google.com"> 7 <link rel="help" href="https://dom.spec.whatwg.org/#signaling-slot-change"> 8 <script src="/resources/testharness.js"></script> 9 <script src="/resources/testharnessreport.js"></script> 10 </head> 11 <body> 12 <div id="log"></div> 13 <script> 14 function treeName(mode, connectedToDocument) 15 { 16 return (mode == 'open' ? 'an ' : 'a ') + mode + ' shadow root ' 17 + (connectedToDocument ? '' : ' not') + ' in a document'; 18 } 19 20 function testAppendingSpanToShadowRootWithDefaultSlot(mode, connectedToDocument) 21 { 22 var test = async_test('slotchange event must fire on a default slot element inside ' 23 + treeName(mode, connectedToDocument)); 24 25 var host; 26 var slot; 27 var eventCount = 0; 28 29 test.step(function () { 30 host = document.createElement('div'); 31 if (connectedToDocument) 32 document.body.appendChild(host); 33 34 var shadowRoot = host.attachShadow({'mode': mode}); 35 slot = document.createElement('slot'); 36 37 slot.addEventListener('slotchange', function (event) { 38 if (event.isFakeEvent) 39 return; 40 41 test.step(function () { 42 assert_equals(event.type, 'slotchange', 'slotchange event\'s type must be "slotchange"'); 43 assert_equals(event.target, slot, 'slotchange event\'s target must be the slot element'); 44 assert_equals(event.relatedTarget, undefined, 'slotchange must not set relatedTarget'); 45 }); 46 eventCount++; 47 }); 48 49 shadowRoot.appendChild(slot); 50 51 host.appendChild(document.createElement('span')); 52 host.appendChild(document.createElement('b')); 53 54 assert_equals(eventCount, 0, 'slotchange event must not be fired synchronously'); 55 }); 56 57 setTimeout(function () { 58 test.step(function () { 59 assert_equals(eventCount, 1, 'slotchange must be fired exactly once after the assigned nodes changed'); 60 61 host.appendChild(document.createElement('i')); 62 }); 63 64 setTimeout(function () { 65 test.step(function () { 66 assert_equals(eventCount, 2, 'slotchange must be fired exactly once after the assigned nodes changed'); 67 68 host.appendChild(document.createTextNode('hello')); 69 70 var fakeEvent = new Event('slotchange'); 71 fakeEvent.isFakeEvent = true; 72 slot.dispatchEvent(fakeEvent); 73 }); 74 75 setTimeout(function () { 76 test.step(function () { 77 assert_equals(eventCount, 3, 'slotchange must be fired exactly once after the assigned nodes changed' 78 + ' event if there was a synthetic slotchange event fired'); 79 }); 80 test.done(); 81 }, 1); 82 }, 1); 83 }, 1); 84 } 85 86 testAppendingSpanToShadowRootWithDefaultSlot('open', true); 87 testAppendingSpanToShadowRootWithDefaultSlot('closed', true); 88 testAppendingSpanToShadowRootWithDefaultSlot('open', false); 89 testAppendingSpanToShadowRootWithDefaultSlot('closed', false); 90 91 function testAppendingSpanToShadowRootWithNamedSlot(mode, connectedToDocument) 92 { 93 var test = async_test('slotchange event must fire on a named slot element inside' 94 + treeName(mode, connectedToDocument)); 95 96 var host; 97 var slot; 98 var eventCount = 0; 99 100 test.step(function () { 101 host = document.createElement('div'); 102 if (connectedToDocument) 103 document.body.appendChild(host); 104 105 var shadowRoot = host.attachShadow({'mode': mode}); 106 slot = document.createElement('slot'); 107 slot.name = 'someSlot'; 108 109 slot.addEventListener('slotchange', function (event) { 110 if (event.isFakeEvent) 111 return; 112 113 test.step(function () { 114 assert_equals(event.type, 'slotchange', 'slotchange event\'s type must be "slotchange"'); 115 assert_equals(event.target, slot, 'slotchange event\'s target must be the slot element'); 116 assert_equals(event.relatedTarget, undefined, 'slotchange must not set relatedTarget'); 117 }); 118 eventCount++; 119 }); 120 121 shadowRoot.appendChild(slot); 122 123 var span = document.createElement('span'); 124 span.slot = 'someSlot'; 125 host.appendChild(span); 126 127 var b = document.createElement('b'); 128 b.slot = 'someSlot'; 129 host.appendChild(b); 130 131 assert_equals(eventCount, 0, 'slotchange event must not be fired synchronously'); 132 }); 133 134 setTimeout(function () { 135 test.step(function () { 136 assert_equals(eventCount, 1, 'slotchange must be fired exactly once after the assigned nodes changed'); 137 138 var i = document.createElement('i'); 139 i.slot = 'someSlot'; 140 host.appendChild(i); 141 }); 142 143 setTimeout(function () { 144 test.step(function () { 145 assert_equals(eventCount, 2, 'slotchange must be fired exactly once after the assigned nodes changed'); 146 147 var em = document.createElement('em'); 148 em.slot = 'someSlot'; 149 host.appendChild(em); 150 151 var fakeEvent = new Event('slotchange'); 152 fakeEvent.isFakeEvent = true; 153 slot.dispatchEvent(fakeEvent); 154 }); 155 156 setTimeout(function () { 157 test.step(function () { 158 assert_equals(eventCount, 3, 'slotchange must be fired exactly once after the assigned nodes changed' 159 + ' event if there was a synthetic slotchange event fired'); 160 }); 161 test.done(); 162 }, 1); 163 164 }, 1); 165 }, 1); 166 } 167 168 testAppendingSpanToShadowRootWithNamedSlot('open', true); 169 testAppendingSpanToShadowRootWithNamedSlot('closed', true); 170 testAppendingSpanToShadowRootWithNamedSlot('open', false); 171 testAppendingSpanToShadowRootWithNamedSlot('closed', false); 172 173 function testSlotchangeDoesNotFireWhenOtherSlotsChange(mode, connectedToDocument) 174 { 175 var test = async_test('slotchange event must not fire on a slot element inside ' 176 + treeName(mode, connectedToDocument) 177 + ' when another slot\'s assigned nodes change'); 178 179 var host; 180 var defaultSlotEventCount = 0; 181 var namedSlotEventCount = 0; 182 183 test.step(function () { 184 host = document.createElement('div'); 185 if (connectedToDocument) 186 document.body.appendChild(host); 187 188 var shadowRoot = host.attachShadow({'mode': mode}); 189 var defaultSlot = document.createElement('slot'); 190 defaultSlot.addEventListener('slotchange', function (event) { 191 test.step(function () { 192 assert_equals(event.target, defaultSlot, 'slotchange event\'s target must be the slot element'); 193 }); 194 defaultSlotEventCount++; 195 }); 196 197 var namedSlot = document.createElement('slot'); 198 namedSlot.name = 'slotName'; 199 namedSlot.addEventListener('slotchange', function (event) { 200 test.step(function () { 201 assert_equals(event.target, namedSlot, 'slotchange event\'s target must be the slot element'); 202 }); 203 namedSlotEventCount++; 204 }); 205 206 shadowRoot.appendChild(defaultSlot); 207 shadowRoot.appendChild(namedSlot); 208 209 host.appendChild(document.createElement('span')); 210 211 assert_equals(defaultSlotEventCount, 0, 'slotchange event must not be fired synchronously'); 212 assert_equals(namedSlotEventCount, 0, 'slotchange event must not be fired synchronously'); 213 }); 214 215 setTimeout(function () { 216 test.step(function () { 217 assert_equals(defaultSlotEventCount, 1, 218 'slotchange must be fired exactly once after the assigned nodes change on a default slot'); 219 assert_equals(namedSlotEventCount, 0, 220 'slotchange must not be fired on a named slot after the assigned nodes change on a default slot'); 221 222 var span = document.createElement('span'); 223 span.slot = 'slotName'; 224 host.appendChild(span); 225 }); 226 227 setTimeout(function () { 228 test.step(function () { 229 assert_equals(defaultSlotEventCount, 1, 230 'slotchange must not be fired on a default slot after the assigned nodes change on a named slot'); 231 assert_equals(namedSlotEventCount, 1, 232 'slotchange must be fired exactly once after the assigned nodes change on a default slot'); 233 }); 234 test.done(); 235 }, 1); 236 }, 1); 237 } 238 239 testSlotchangeDoesNotFireWhenOtherSlotsChange('open', true); 240 testSlotchangeDoesNotFireWhenOtherSlotsChange('closed', true); 241 testSlotchangeDoesNotFireWhenOtherSlotsChange('open', false); 242 testSlotchangeDoesNotFireWhenOtherSlotsChange('closed', false); 243 244 function testSlotchangeDoesFireAtInsertedAndDoesNotFireForMutationAfterRemoved(mode, connectedToDocument) 245 { 246 var test = async_test('slotchange event must fire on a slot element when a shadow host has a slottable and the slot was inserted' 247 + ' and must not fire when the shadow host was mutated after the slot was removed inside ' 248 + treeName(mode, connectedToDocument)); 249 250 var host; 251 var slot; 252 var eventCount = 0; 253 254 test.step(function () { 255 host = document.createElement('div'); 256 if (connectedToDocument) 257 document.body.appendChild(host); 258 259 var shadowRoot = host.attachShadow({'mode': mode}); 260 slot = document.createElement('slot'); 261 slot.addEventListener('slotchange', function (event) { 262 test.step(function () { 263 assert_equals(event.target, slot, 'slotchange event\'s target must be the slot element'); 264 }); 265 eventCount++; 266 }); 267 268 host.appendChild(document.createElement('span')); 269 shadowRoot.appendChild(slot); 270 271 assert_equals(eventCount, 0, 'slotchange event must not be fired synchronously'); 272 }); 273 274 setTimeout(function () { 275 test.step(function () { 276 assert_equals(eventCount, 1, 277 'slotchange must be fired on a slot element if there is assigned nodes when the slot was inserted'); 278 host.removeChild(host.firstChild); 279 }); 280 281 setTimeout(function () { 282 test.step(function () { 283 assert_equals(eventCount, 2, 284 'slotchange must be fired after the assigned nodes change on a slot while the slot element was in the tree'); 285 slot.parentNode.removeChild(slot); 286 host.appendChild(document.createElement('span')); 287 }); 288 289 setTimeout(function () { 290 assert_equals(eventCount, 2, 291 'slotchange must not be fired on a slot element if the assigned nodes changed after the slot was removed'); 292 test.done(); 293 }, 1); 294 }, 1); 295 }, 1); 296 } 297 298 testSlotchangeDoesFireAtInsertedAndDoesNotFireForMutationAfterRemoved('open', true); 299 testSlotchangeDoesFireAtInsertedAndDoesNotFireForMutationAfterRemoved('closed', true); 300 testSlotchangeDoesFireAtInsertedAndDoesNotFireForMutationAfterRemoved('open', false); 301 testSlotchangeDoesFireAtInsertedAndDoesNotFireForMutationAfterRemoved('closed', false); 302 303 function testSlotchangeFiresOnTransientlyPresentSlot(mode, connectedToDocument) 304 { 305 var test = async_test('slotchange event must fire on a slot element inside ' 306 + treeName(mode, connectedToDocument) 307 + ' even if the slot was removed immediately after the assigned nodes were mutated'); 308 309 var host; 310 var slot; 311 var anotherSlot; 312 var slotEventCount = 0; 313 var anotherSlotEventCount = 0; 314 315 test.step(function () { 316 host = document.createElement('div'); 317 if (connectedToDocument) 318 document.body.appendChild(host); 319 320 var shadowRoot = host.attachShadow({'mode': mode}); 321 slot = document.createElement('slot'); 322 slot.name = 'someSlot'; 323 slot.addEventListener('slotchange', function (event) { 324 test.step(function () { 325 assert_equals(event.target, slot, 'slotchange event\'s target must be the slot element'); 326 }); 327 slotEventCount++; 328 }); 329 330 anotherSlot = document.createElement('slot'); 331 anotherSlot.name = 'someSlot'; 332 anotherSlot.addEventListener('slotchange', function (event) { 333 test.step(function () { 334 assert_equals(event.target, anotherSlot, 'slotchange event\'s target must be the slot element'); 335 }); 336 anotherSlotEventCount++; 337 }); 338 339 shadowRoot.appendChild(slot); 340 341 var span = document.createElement('span'); 342 span.slot = 'someSlot'; 343 host.appendChild(span); 344 345 shadowRoot.removeChild(slot); 346 shadowRoot.appendChild(anotherSlot); 347 348 var span = document.createElement('span'); 349 span.slot = 'someSlot'; 350 host.appendChild(span); 351 352 shadowRoot.removeChild(anotherSlot); 353 354 assert_equals(slotEventCount, 0, 'slotchange event must not be fired synchronously'); 355 assert_equals(anotherSlotEventCount, 0, 'slotchange event must not be fired synchronously'); 356 }); 357 358 setTimeout(function () { 359 test.step(function () { 360 assert_equals(slotEventCount, 1, 361 'slotchange must be fired on a slot element if the assigned nodes changed while the slot was present'); 362 assert_equals(anotherSlotEventCount, 1, 363 'slotchange must be fired on a slot element if the assigned nodes changed while the slot was present'); 364 }); 365 test.done(); 366 }, 1); 367 } 368 369 testSlotchangeFiresOnTransientlyPresentSlot('open', true); 370 testSlotchangeFiresOnTransientlyPresentSlot('closed', true); 371 testSlotchangeFiresOnTransientlyPresentSlot('open', false); 372 testSlotchangeFiresOnTransientlyPresentSlot('closed', false); 373 374 function testSlotchangeFiresOnInnerHTML(mode, connectedToDocument) 375 { 376 var test = async_test('slotchange event must fire on a slot element inside ' 377 + treeName(mode, connectedToDocument) 378 + ' when innerHTML modifies the children of the shadow host'); 379 380 var host; 381 var defaultSlot; 382 var namedSlot; 383 var defaultSlotEventCount = 0; 384 var namedSlotEventCount = 0; 385 386 test.step(function () { 387 host = document.createElement('div'); 388 if (connectedToDocument) 389 document.body.appendChild(host); 390 391 var shadowRoot = host.attachShadow({'mode': mode}); 392 defaultSlot = document.createElement('slot'); 393 defaultSlot.addEventListener('slotchange', function (event) { 394 test.step(function () { 395 assert_equals(event.target, defaultSlot, 'slotchange event\'s target must be the slot element'); 396 }); 397 defaultSlotEventCount++; 398 }); 399 400 namedSlot = document.createElement('slot'); 401 namedSlot.name = 'someSlot'; 402 namedSlot.addEventListener('slotchange', function (event) { 403 test.step(function () { 404 assert_equals(event.target, namedSlot, 'slotchange event\'s target must be the slot element'); 405 }); 406 namedSlotEventCount++; 407 }); 408 shadowRoot.appendChild(namedSlot); 409 shadowRoot.appendChild(defaultSlot); 410 host.innerHTML = 'foo <b>bar</b>'; 411 412 assert_equals(defaultSlotEventCount, 0, 'slotchange event must not be fired synchronously'); 413 assert_equals(namedSlotEventCount, 0, 'slotchange event must not be fired synchronously'); 414 }); 415 416 setTimeout(function () { 417 test.step(function () { 418 assert_equals(defaultSlotEventCount, 1, 419 'slotchange must be fired on a slot element if the assigned nodes are changed by innerHTML'); 420 assert_equals(namedSlotEventCount, 0, 421 'slotchange must not be fired on a slot element if the assigned nodes are not changed by innerHTML'); 422 host.innerHTML = 'baz'; 423 }); 424 setTimeout(function () { 425 test.step(function () { 426 assert_equals(defaultSlotEventCount, 2, 427 'slotchange must be fired on a slot element if the assigned nodes are changed by innerHTML'); 428 assert_equals(namedSlotEventCount, 0, 429 'slotchange must not be fired on a slot element if the assigned nodes are not changed by innerHTML'); 430 host.innerHTML = ''; 431 }); 432 setTimeout(function () { 433 test.step(function () { 434 assert_equals(defaultSlotEventCount, 3, 435 'slotchange must be fired on a slot element if the assigned nodes are changed by innerHTML'); 436 assert_equals(namedSlotEventCount, 0, 437 'slotchange must not be fired on a slot element if the assigned nodes are not changed by innerHTML'); 438 host.innerHTML = '<b slot="someSlot">content</b>'; 439 }); 440 setTimeout(function () { 441 test.step(function () { 442 assert_equals(defaultSlotEventCount, 3, 443 'slotchange must not be fired on a slot element if the assigned nodes are not changed by innerHTML'); 444 assert_equals(namedSlotEventCount, 1, 445 'slotchange must not be fired on a slot element if the assigned nodes are changed by innerHTML'); 446 host.innerHTML = ''; 447 }); 448 setTimeout(function () { 449 test.step(function () { 450 // FIXME: This test would fail in the current implementation because we can't tell 451 // whether a text node was removed in AllChildrenRemoved or not. 452 // assert_equals(defaultSlotEventCount, 3, 453 // 'slotchange must not be fired on a slot element if the assigned nodes are not changed by innerHTML'); 454 assert_equals(namedSlotEventCount, 2, 455 'slotchange must not be fired on a slot element if the assigned nodes are changed by innerHTML'); 456 }); 457 test.done(); 458 }, 1); 459 }, 1); 460 }, 1); 461 }, 1); 462 }, 1); 463 } 464 465 testSlotchangeFiresOnInnerHTML('open', true); 466 testSlotchangeFiresOnInnerHTML('closed', true); 467 testSlotchangeFiresOnInnerHTML('open', false); 468 testSlotchangeFiresOnInnerHTML('closed', false); 469 470 function testSlotchangeFiresWhenNestedSlotChange(mode, connectedToDocument) 471 { 472 var test = async_test('slotchange event must fire on a slot element inside ' 473 + treeName(mode, connectedToDocument) 474 + ' when nested slots\'s contents change'); 475 476 var outerHost; 477 var innerHost; 478 var outerSlot; 479 var innerSlot; 480 var outerSlotEventCount = 0; 481 var innerSlotEventCount = 0; 482 483 test.step(function () { 484 outerHost = document.createElement('div'); 485 if (connectedToDocument) 486 document.body.appendChild(outerHost); 487 488 var outerShadow = outerHost.attachShadow({'mode': mode}); 489 outerShadow.appendChild(document.createElement('span')); 490 outerSlot = document.createElement('slot'); 491 outerSlot.addEventListener('slotchange', function (event) { 492 test.step(function () { 493 assert_equals(event.target, outerSlot, 'slotchange event\'s target must be the slot element'); 494 }); 495 outerSlotEventCount++; 496 }); 497 498 innerHost = document.createElement('div'); 499 innerHost.appendChild(outerSlot); 500 outerShadow.appendChild(innerHost); 501 502 var innerShadow = innerHost.attachShadow({'mode': mode}); 503 innerShadow.appendChild(document.createElement('span')); 504 innerSlot = document.createElement('slot'); 505 innerSlot.addEventListener('slotchange', function (event) { 506 event.stopPropagation(); 507 test.step(function () { 508 if (innerSlotEventCount === 0) { 509 assert_equals(event.target, innerSlot, 'slotchange event\'s target must be the inner slot element at 1st slotchange'); 510 } else if (innerSlotEventCount === 1) { 511 assert_equals(event.target, outerSlot, 'slotchange event\'s target must be the outer slot element at 2nd sltochange'); 512 } 513 }); 514 innerSlotEventCount++; 515 }); 516 innerShadow.appendChild(innerSlot); 517 518 outerHost.appendChild(document.createElement('span')); 519 520 assert_equals(innerSlotEventCount, 0, 'slotchange event must not be fired synchronously'); 521 assert_equals(outerSlotEventCount, 0, 'slotchange event must not be fired synchronously'); 522 }); 523 524 setTimeout(function () { 525 test.step(function () { 526 assert_equals(outerSlotEventCount, 1, 527 'slotchange must be fired on a slot element if the assigned nodes changed'); 528 assert_equals(innerSlotEventCount, 2, 529 'slotchange must be fired on a slot element and must bubble'); 530 }); 531 test.done(); 532 }, 1); 533 } 534 535 testSlotchangeFiresWhenNestedSlotChange('open', true); 536 testSlotchangeFiresWhenNestedSlotChange('closed', true); 537 testSlotchangeFiresWhenNestedSlotChange('open', false); 538 testSlotchangeFiresWhenNestedSlotChange('closed', false); 539 540 function testSlotchangeFiresAtEndOfMicroTask(mode, connectedToDocument) 541 { 542 var test = async_test('slotchange event must fire at the end of current microtask after mutation observers are invoked inside ' 543 + treeName(mode, connectedToDocument) + ' when slots\'s contents change'); 544 545 var host; 546 var slot; 547 var eventCount = 0; 548 549 test.step(function () { 550 host = document.createElement('div'); 551 if (connectedToDocument) 552 document.body.appendChild(host); 553 554 var shadowRoot = host.attachShadow({'mode': mode}); 555 slot = document.createElement('slot'); 556 557 slot.addEventListener('slotchange', function (event) { 558 test.step(function () { 559 assert_equals(event.type, 'slotchange', 'slotchange event\'s type must be "slotchange"'); 560 assert_equals(event.target, slot, 'slotchange event\'s target must be the slot element'); 561 }); 562 eventCount++; 563 }); 564 565 shadowRoot.appendChild(slot); 566 }); 567 568 var element = document.createElement('div'); 569 570 new MutationObserver(function () { 571 test.step(function () { 572 assert_equals(eventCount, 0, 'slotchange event must not be fired before mutation records are delivered'); 573 }); 574 host.appendChild(document.createElement('span')); 575 element.setAttribute('title', 'bar'); 576 }).observe(element, {attributes: true, attributeFilter: ['id']}); 577 578 new MutationObserver(function () { 579 test.step(function () { 580 assert_equals(eventCount, 1, 'slotchange event must be fired during a single compound microtask'); 581 }); 582 }).observe(element, {attributes: true, attributeFilter: ['title']}); 583 584 element.setAttribute('id', 'foo'); 585 host.appendChild(document.createElement('div')); 586 587 setTimeout(function () { 588 test.step(function () { 589 assert_equals(eventCount, 2, 590 'a distinct slotchange event must be enqueued for changes made during a mutation observer delivery'); 591 }); 592 test.done(); 593 }, 0); 594 } 595 596 testSlotchangeFiresAtEndOfMicroTask('open', true); 597 testSlotchangeFiresAtEndOfMicroTask('closed', true); 598 testSlotchangeFiresAtEndOfMicroTask('open', false); 599 testSlotchangeFiresAtEndOfMicroTask('closed', false); 600 </script> 601 </body> 602 </html>