<!DOCTYPE html>
<html>
<head>
<title>Shadow DOM: slotchange event</title>
<meta name="author" title="Ryosuke Niwa" href="mailto:[email protected]">
<meta name="author" title="Hayato Ito" href="mailto:[email protected]">
<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>