diff options
Diffstat (limited to '')
-rw-r--r-- | testing/web-platform/tests/shadow-dom/slotchange-event.html | 602 |
1 files changed, 602 insertions, 0 deletions
diff --git a/testing/web-platform/tests/shadow-dom/slotchange-event.html b/testing/web-platform/tests/shadow-dom/slotchange-event.html new file mode 100644 index 0000000000..f1d06297ff --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/slotchange-event.html @@ -0,0 +1,602 @@ +<!DOCTYPE html> +<html> +<head> +<title>Shadow DOM: slotchange event</title> +<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org"> +<meta name="author" title="Hayato Ito" href="mailto:hayato@google.com"> +<link rel="help" href="https://dom.spec.whatwg.org/#signaling-slot-change"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +</head> +<body> +<div id="log"></div> +<script> +function treeName(mode, connectedToDocument) +{ + return (mode == 'open' ? 'an ' : 'a ') + mode + ' shadow root ' + + (connectedToDocument ? '' : ' not') + ' in a document'; +} + +function testAppendingSpanToShadowRootWithDefaultSlot(mode, connectedToDocument) +{ + var test = async_test('slotchange event must fire on a default slot element inside ' + + treeName(mode, connectedToDocument)); + + var host; + var slot; + var eventCount = 0; + + test.step(function () { + host = document.createElement('div'); + if (connectedToDocument) + document.body.appendChild(host); + + var shadowRoot = host.attachShadow({'mode': mode}); + slot = document.createElement('slot'); + + slot.addEventListener('slotchange', function (event) { + if (event.isFakeEvent) + return; + + test.step(function () { + assert_equals(event.type, 'slotchange', 'slotchange event\'s type must be "slotchange"'); + assert_equals(event.target, slot, 'slotchange event\'s target must be the slot element'); + assert_equals(event.relatedTarget, undefined, 'slotchange must not set relatedTarget'); + }); + eventCount++; + }); + + shadowRoot.appendChild(slot); + + host.appendChild(document.createElement('span')); + host.appendChild(document.createElement('b')); + + assert_equals(eventCount, 0, 'slotchange event must not be fired synchronously'); + }); + + setTimeout(function () { + test.step(function () { + assert_equals(eventCount, 1, 'slotchange must be fired exactly once after the assigned nodes changed'); + + host.appendChild(document.createElement('i')); + }); + + setTimeout(function () { + test.step(function () { + assert_equals(eventCount, 2, 'slotchange must be fired exactly once after the assigned nodes changed'); + + host.appendChild(document.createTextNode('hello')); + + var fakeEvent = new Event('slotchange'); + fakeEvent.isFakeEvent = true; + slot.dispatchEvent(fakeEvent); + }); + + setTimeout(function () { + test.step(function () { + assert_equals(eventCount, 3, 'slotchange must be fired exactly once after the assigned nodes changed' + + ' event if there was a synthetic slotchange event fired'); + }); + test.done(); + }, 1); + }, 1); + }, 1); +} + +testAppendingSpanToShadowRootWithDefaultSlot('open', true); +testAppendingSpanToShadowRootWithDefaultSlot('closed', true); +testAppendingSpanToShadowRootWithDefaultSlot('open', false); +testAppendingSpanToShadowRootWithDefaultSlot('closed', false); + +function testAppendingSpanToShadowRootWithNamedSlot(mode, connectedToDocument) +{ + var test = async_test('slotchange event must fire on a named slot element inside' + + treeName(mode, connectedToDocument)); + + var host; + var slot; + var eventCount = 0; + + test.step(function () { + host = document.createElement('div'); + if (connectedToDocument) + document.body.appendChild(host); + + var shadowRoot = host.attachShadow({'mode': mode}); + slot = document.createElement('slot'); + slot.name = 'someSlot'; + + slot.addEventListener('slotchange', function (event) { + if (event.isFakeEvent) + return; + + test.step(function () { + assert_equals(event.type, 'slotchange', 'slotchange event\'s type must be "slotchange"'); + assert_equals(event.target, slot, 'slotchange event\'s target must be the slot element'); + assert_equals(event.relatedTarget, undefined, 'slotchange must not set relatedTarget'); + }); + eventCount++; + }); + + shadowRoot.appendChild(slot); + + var span = document.createElement('span'); + span.slot = 'someSlot'; + host.appendChild(span); + + var b = document.createElement('b'); + b.slot = 'someSlot'; + host.appendChild(b); + + assert_equals(eventCount, 0, 'slotchange event must not be fired synchronously'); + }); + + setTimeout(function () { + test.step(function () { + assert_equals(eventCount, 1, 'slotchange must be fired exactly once after the assigned nodes changed'); + + var i = document.createElement('i'); + i.slot = 'someSlot'; + host.appendChild(i); + }); + + setTimeout(function () { + test.step(function () { + assert_equals(eventCount, 2, 'slotchange must be fired exactly once after the assigned nodes changed'); + + var em = document.createElement('em'); + em.slot = 'someSlot'; + host.appendChild(em); + + var fakeEvent = new Event('slotchange'); + fakeEvent.isFakeEvent = true; + slot.dispatchEvent(fakeEvent); + }); + + setTimeout(function () { + test.step(function () { + assert_equals(eventCount, 3, 'slotchange must be fired exactly once after the assigned nodes changed' + + ' event if there was a synthetic slotchange event fired'); + }); + test.done(); + }, 1); + + }, 1); + }, 1); +} + +testAppendingSpanToShadowRootWithNamedSlot('open', true); +testAppendingSpanToShadowRootWithNamedSlot('closed', true); +testAppendingSpanToShadowRootWithNamedSlot('open', false); +testAppendingSpanToShadowRootWithNamedSlot('closed', false); + +function testSlotchangeDoesNotFireWhenOtherSlotsChange(mode, connectedToDocument) +{ + var test = async_test('slotchange event must not fire on a slot element inside ' + + treeName(mode, connectedToDocument) + + ' when another slot\'s assigned nodes change'); + + var host; + var defaultSlotEventCount = 0; + var namedSlotEventCount = 0; + + test.step(function () { + host = document.createElement('div'); + if (connectedToDocument) + document.body.appendChild(host); + + var shadowRoot = host.attachShadow({'mode': mode}); + var defaultSlot = document.createElement('slot'); + defaultSlot.addEventListener('slotchange', function (event) { + test.step(function () { + assert_equals(event.target, defaultSlot, 'slotchange event\'s target must be the slot element'); + }); + defaultSlotEventCount++; + }); + + var namedSlot = document.createElement('slot'); + namedSlot.name = 'slotName'; + namedSlot.addEventListener('slotchange', function (event) { + test.step(function () { + assert_equals(event.target, namedSlot, 'slotchange event\'s target must be the slot element'); + }); + namedSlotEventCount++; + }); + + shadowRoot.appendChild(defaultSlot); + shadowRoot.appendChild(namedSlot); + + host.appendChild(document.createElement('span')); + + assert_equals(defaultSlotEventCount, 0, 'slotchange event must not be fired synchronously'); + assert_equals(namedSlotEventCount, 0, 'slotchange event must not be fired synchronously'); + }); + + setTimeout(function () { + test.step(function () { + assert_equals(defaultSlotEventCount, 1, + 'slotchange must be fired exactly once after the assigned nodes change on a default slot'); + assert_equals(namedSlotEventCount, 0, + 'slotchange must not be fired on a named slot after the assigned nodes change on a default slot'); + + var span = document.createElement('span'); + span.slot = 'slotName'; + host.appendChild(span); + }); + + setTimeout(function () { + test.step(function () { + assert_equals(defaultSlotEventCount, 1, + 'slotchange must not be fired on a default slot after the assigned nodes change on a named slot'); + assert_equals(namedSlotEventCount, 1, + 'slotchange must be fired exactly once after the assigned nodes change on a default slot'); + }); + test.done(); + }, 1); + }, 1); +} + +testSlotchangeDoesNotFireWhenOtherSlotsChange('open', true); +testSlotchangeDoesNotFireWhenOtherSlotsChange('closed', true); +testSlotchangeDoesNotFireWhenOtherSlotsChange('open', false); +testSlotchangeDoesNotFireWhenOtherSlotsChange('closed', false); + +function testSlotchangeDoesFireAtInsertedAndDoesNotFireForMutationAfterRemoved(mode, connectedToDocument) +{ + var test = async_test('slotchange event must fire on a slot element when a shadow host has a slottable and the slot was inserted' + + ' and must not fire when the shadow host was mutated after the slot was removed inside ' + + treeName(mode, connectedToDocument)); + + var host; + var slot; + var eventCount = 0; + + test.step(function () { + host = document.createElement('div'); + if (connectedToDocument) + document.body.appendChild(host); + + var shadowRoot = host.attachShadow({'mode': mode}); + slot = document.createElement('slot'); + slot.addEventListener('slotchange', function (event) { + test.step(function () { + assert_equals(event.target, slot, 'slotchange event\'s target must be the slot element'); + }); + eventCount++; + }); + + host.appendChild(document.createElement('span')); + shadowRoot.appendChild(slot); + + assert_equals(eventCount, 0, 'slotchange event must not be fired synchronously'); + }); + + setTimeout(function () { + test.step(function () { + assert_equals(eventCount, 1, + 'slotchange must be fired on a slot element if there is assigned nodes when the slot was inserted'); + host.removeChild(host.firstChild); + }); + + setTimeout(function () { + test.step(function () { + assert_equals(eventCount, 2, + 'slotchange must be fired after the assigned nodes change on a slot while the slot element was in the tree'); + slot.parentNode.removeChild(slot); + host.appendChild(document.createElement('span')); + }); + + setTimeout(function () { + assert_equals(eventCount, 2, + 'slotchange must not be fired on a slot element if the assigned nodes changed after the slot was removed'); + test.done(); + }, 1); + }, 1); + }, 1); +} + +testSlotchangeDoesFireAtInsertedAndDoesNotFireForMutationAfterRemoved('open', true); +testSlotchangeDoesFireAtInsertedAndDoesNotFireForMutationAfterRemoved('closed', true); +testSlotchangeDoesFireAtInsertedAndDoesNotFireForMutationAfterRemoved('open', false); +testSlotchangeDoesFireAtInsertedAndDoesNotFireForMutationAfterRemoved('closed', false); + +function testSlotchangeFiresOnTransientlyPresentSlot(mode, connectedToDocument) +{ + var test = async_test('slotchange event must fire on a slot element inside ' + + treeName(mode, connectedToDocument) + + ' even if the slot was removed immediately after the assigned nodes were mutated'); + + var host; + var slot; + var anotherSlot; + var slotEventCount = 0; + var anotherSlotEventCount = 0; + + test.step(function () { + host = document.createElement('div'); + if (connectedToDocument) + document.body.appendChild(host); + + var shadowRoot = host.attachShadow({'mode': mode}); + slot = document.createElement('slot'); + slot.name = 'someSlot'; + slot.addEventListener('slotchange', function (event) { + test.step(function () { + assert_equals(event.target, slot, 'slotchange event\'s target must be the slot element'); + }); + slotEventCount++; + }); + + anotherSlot = document.createElement('slot'); + anotherSlot.name = 'someSlot'; + anotherSlot.addEventListener('slotchange', function (event) { + test.step(function () { + assert_equals(event.target, anotherSlot, 'slotchange event\'s target must be the slot element'); + }); + anotherSlotEventCount++; + }); + + shadowRoot.appendChild(slot); + + var span = document.createElement('span'); + span.slot = 'someSlot'; + host.appendChild(span); + + shadowRoot.removeChild(slot); + shadowRoot.appendChild(anotherSlot); + + var span = document.createElement('span'); + span.slot = 'someSlot'; + host.appendChild(span); + + shadowRoot.removeChild(anotherSlot); + + assert_equals(slotEventCount, 0, 'slotchange event must not be fired synchronously'); + assert_equals(anotherSlotEventCount, 0, 'slotchange event must not be fired synchronously'); + }); + + setTimeout(function () { + test.step(function () { + assert_equals(slotEventCount, 1, + 'slotchange must be fired on a slot element if the assigned nodes changed while the slot was present'); + assert_equals(anotherSlotEventCount, 1, + 'slotchange must be fired on a slot element if the assigned nodes changed while the slot was present'); + }); + test.done(); + }, 1); +} + +testSlotchangeFiresOnTransientlyPresentSlot('open', true); +testSlotchangeFiresOnTransientlyPresentSlot('closed', true); +testSlotchangeFiresOnTransientlyPresentSlot('open', false); +testSlotchangeFiresOnTransientlyPresentSlot('closed', false); + +function testSlotchangeFiresOnInnerHTML(mode, connectedToDocument) +{ + var test = async_test('slotchange event must fire on a slot element inside ' + + treeName(mode, connectedToDocument) + + ' when innerHTML modifies the children of the shadow host'); + + var host; + var defaultSlot; + var namedSlot; + var defaultSlotEventCount = 0; + var namedSlotEventCount = 0; + + test.step(function () { + host = document.createElement('div'); + if (connectedToDocument) + document.body.appendChild(host); + + var shadowRoot = host.attachShadow({'mode': mode}); + defaultSlot = document.createElement('slot'); + defaultSlot.addEventListener('slotchange', function (event) { + test.step(function () { + assert_equals(event.target, defaultSlot, 'slotchange event\'s target must be the slot element'); + }); + defaultSlotEventCount++; + }); + + namedSlot = document.createElement('slot'); + namedSlot.name = 'someSlot'; + namedSlot.addEventListener('slotchange', function (event) { + test.step(function () { + assert_equals(event.target, namedSlot, 'slotchange event\'s target must be the slot element'); + }); + namedSlotEventCount++; + }); + shadowRoot.appendChild(namedSlot); + shadowRoot.appendChild(defaultSlot); + host.innerHTML = 'foo <b>bar</b>'; + + assert_equals(defaultSlotEventCount, 0, 'slotchange event must not be fired synchronously'); + assert_equals(namedSlotEventCount, 0, 'slotchange event must not be fired synchronously'); + }); + + setTimeout(function () { + test.step(function () { + assert_equals(defaultSlotEventCount, 1, + 'slotchange must be fired on a slot element if the assigned nodes are changed by innerHTML'); + assert_equals(namedSlotEventCount, 0, + 'slotchange must not be fired on a slot element if the assigned nodes are not changed by innerHTML'); + host.innerHTML = 'baz'; + }); + setTimeout(function () { + test.step(function () { + assert_equals(defaultSlotEventCount, 2, + 'slotchange must be fired on a slot element if the assigned nodes are changed by innerHTML'); + assert_equals(namedSlotEventCount, 0, + 'slotchange must not be fired on a slot element if the assigned nodes are not changed by innerHTML'); + host.innerHTML = ''; + }); + setTimeout(function () { + test.step(function () { + assert_equals(defaultSlotEventCount, 3, + 'slotchange must be fired on a slot element if the assigned nodes are changed by innerHTML'); + assert_equals(namedSlotEventCount, 0, + 'slotchange must not be fired on a slot element if the assigned nodes are not changed by innerHTML'); + host.innerHTML = '<b slot="someSlot">content</b>'; + }); + setTimeout(function () { + test.step(function () { + assert_equals(defaultSlotEventCount, 3, + 'slotchange must not be fired on a slot element if the assigned nodes are not changed by innerHTML'); + assert_equals(namedSlotEventCount, 1, + 'slotchange must not be fired on a slot element if the assigned nodes are changed by innerHTML'); + host.innerHTML = ''; + }); + setTimeout(function () { + test.step(function () { + // FIXME: This test would fail in the current implementation because we can't tell + // whether a text node was removed in AllChildrenRemoved or not. +// assert_equals(defaultSlotEventCount, 3, +// 'slotchange must not be fired on a slot element if the assigned nodes are not changed by innerHTML'); + assert_equals(namedSlotEventCount, 2, + 'slotchange must not be fired on a slot element if the assigned nodes are changed by innerHTML'); + }); + test.done(); + }, 1); + }, 1); + }, 1); + }, 1); + }, 1); +} + +testSlotchangeFiresOnInnerHTML('open', true); +testSlotchangeFiresOnInnerHTML('closed', true); +testSlotchangeFiresOnInnerHTML('open', false); +testSlotchangeFiresOnInnerHTML('closed', false); + +function testSlotchangeFiresWhenNestedSlotChange(mode, connectedToDocument) +{ + var test = async_test('slotchange event must fire on a slot element inside ' + + treeName(mode, connectedToDocument) + + ' when nested slots\'s contents change'); + + var outerHost; + var innerHost; + var outerSlot; + var innerSlot; + var outerSlotEventCount = 0; + var innerSlotEventCount = 0; + + test.step(function () { + outerHost = document.createElement('div'); + if (connectedToDocument) + document.body.appendChild(outerHost); + + var outerShadow = outerHost.attachShadow({'mode': mode}); + outerShadow.appendChild(document.createElement('span')); + outerSlot = document.createElement('slot'); + outerSlot.addEventListener('slotchange', function (event) { + test.step(function () { + assert_equals(event.target, outerSlot, 'slotchange event\'s target must be the slot element'); + }); + outerSlotEventCount++; + }); + + innerHost = document.createElement('div'); + innerHost.appendChild(outerSlot); + outerShadow.appendChild(innerHost); + + var innerShadow = innerHost.attachShadow({'mode': mode}); + innerShadow.appendChild(document.createElement('span')); + innerSlot = document.createElement('slot'); + innerSlot.addEventListener('slotchange', function (event) { + event.stopPropagation(); + test.step(function () { + if (innerSlotEventCount === 0) { + assert_equals(event.target, innerSlot, 'slotchange event\'s target must be the inner slot element at 1st slotchange'); + } else if (innerSlotEventCount === 1) { + assert_equals(event.target, outerSlot, 'slotchange event\'s target must be the outer slot element at 2nd sltochange'); + } + }); + innerSlotEventCount++; + }); + innerShadow.appendChild(innerSlot); + + outerHost.appendChild(document.createElement('span')); + + assert_equals(innerSlotEventCount, 0, 'slotchange event must not be fired synchronously'); + assert_equals(outerSlotEventCount, 0, 'slotchange event must not be fired synchronously'); + }); + + setTimeout(function () { + test.step(function () { + assert_equals(outerSlotEventCount, 1, + 'slotchange must be fired on a slot element if the assigned nodes changed'); + assert_equals(innerSlotEventCount, 2, + 'slotchange must be fired on a slot element and must bubble'); + }); + test.done(); + }, 1); +} + +testSlotchangeFiresWhenNestedSlotChange('open', true); +testSlotchangeFiresWhenNestedSlotChange('closed', true); +testSlotchangeFiresWhenNestedSlotChange('open', false); +testSlotchangeFiresWhenNestedSlotChange('closed', false); + +function testSlotchangeFiresAtEndOfMicroTask(mode, connectedToDocument) +{ + var test = async_test('slotchange event must fire at the end of current microtask after mutation observers are invoked inside ' + + treeName(mode, connectedToDocument) + ' when slots\'s contents change'); + + var host; + var slot; + var eventCount = 0; + + test.step(function () { + host = document.createElement('div'); + if (connectedToDocument) + document.body.appendChild(host); + + var shadowRoot = host.attachShadow({'mode': mode}); + slot = document.createElement('slot'); + + slot.addEventListener('slotchange', function (event) { + test.step(function () { + assert_equals(event.type, 'slotchange', 'slotchange event\'s type must be "slotchange"'); + assert_equals(event.target, slot, 'slotchange event\'s target must be the slot element'); + }); + eventCount++; + }); + + shadowRoot.appendChild(slot); + }); + + var element = document.createElement('div'); + + new MutationObserver(function () { + test.step(function () { + assert_equals(eventCount, 0, 'slotchange event must not be fired before mutation records are delivered'); + }); + host.appendChild(document.createElement('span')); + element.setAttribute('title', 'bar'); + }).observe(element, {attributes: true, attributeFilter: ['id']}); + + new MutationObserver(function () { + test.step(function () { + assert_equals(eventCount, 1, 'slotchange event must be fired during a single compound microtask'); + }); + }).observe(element, {attributes: true, attributeFilter: ['title']}); + + element.setAttribute('id', 'foo'); + host.appendChild(document.createElement('div')); + + setTimeout(function () { + test.step(function () { + assert_equals(eventCount, 2, + 'a distinct slotchange event must be enqueued for changes made during a mutation observer delivery'); + }); + test.done(); + }, 0); +} + +testSlotchangeFiresAtEndOfMicroTask('open', true); +testSlotchangeFiresAtEndOfMicroTask('closed', true); +testSlotchangeFiresAtEndOfMicroTask('open', false); +testSlotchangeFiresAtEndOfMicroTask('closed', false); +</script> +</body> +</html> |