blob: 015d3b75ac73cfa84d7bb3a05d844863e5dc7e3b [file] [log] [blame]
<!DOCTYPE html>
<html>
<head>
<title>Shadow DOM: slotchange event when inserting, removing, or renaming a slot element</title>
<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
<link rel="help" href="https://dom.spec.whatwg.org/#shadow-tree-slots">
<link rel="help" href="https://dom.spec.whatwg.org/#concept-node-insert">
<link rel="help" href="https://dom.spec.whatwg.org/#concept-node-remove">
<link rel="help" href="https://dom.spec.whatwg.org/#assign-slotables-for-a-tree">
<script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script>
</head>
<body>
<div id="log"></div>
<script>
function generateTests(...args) {
testMutatingSlot('closed', true, ...args);
testMutatingSlot('closed', false, ...args);
testMutatingSlot('open', true, ...args);
testMutatingSlot('open', false, ...args);
}
let slotchangeEventTargets;
function testMutatingSlot(mode, connectedToDocument, hostContent, shadowContent, mutateSlot, expectedTargets, description)
{
promise_test(async function () {
const host = document.createElement('div');
if (connectedToDocument)
document.body.appendChild(host);
if (hostContent)
host.innerHTML = hostContent;
const shadowRoot = host.attachShadow({mode});
slotchangeEventTargets = [];
if (shadowContent) {
shadowRoot.innerHTML = shadowContent;
addEventListners(shadowRoot);
}
await Promise.resolve();
slotchangeEventTargets.length = 0;
mutateSlot(shadowRoot);
await Promise.resolve();
assert_array_equals(slotchangeEventTargets, expectedTargets);
host.remove();
}, description + ` in a ${connectedToDocument ? 'connected' : 'disconnected'} ${mode} mode shadow root`);
}
function addEventListners(container)
{
for (const existingSlots of container.querySelectorAll('slot'))
existingSlots.addEventListener('slotchange', (event) => slotchangeEventTargets.push(event.target.id));
}
function newSlot(slotName)
{
const slot = document.createElement('slot');
slot.id = 'newSlot';
if (slotName)
slot.name = slotName;
slot.addEventListener('slotchange', (event) => slotchangeEventTargets.push(event.target.id));
return slot;
}
// Insertions
generateTests(null, null, (shadowRoot) => shadowRoot.append(newSlot()), [],
'slotchange event should not fire when inserting a default slot element into a shadow host with no children');
generateTests(null, null, (shadowRoot) => shadowRoot.append(newSlot('foo')), [],
'slotchange event should not fire when inserting a named slot element into a shadow host with no children');
generateTests('<span slot="bar"></span>', null, (shadowRoot) => shadowRoot.append(newSlot()), [],
'slotchange event should not fire when inserting a default slot element when there is an element assigned to another slot');
generateTests('<span></span>', null, (shadowRoot) => shadowRoot.append(newSlot()), ['newSlot'],
'slotchange event should fire when inserting a default slot element when there is an element assigned to the default slot');
generateTests('hello', null, (shadowRoot) => shadowRoot.append(newSlot()), ['newSlot'],
'slotchange event should fire when inserting a default slot element when there is a Text node assigned to the default slot');
generateTests('<span slot="bar"></span>', null, (shadowRoot) => shadowRoot.append(newSlot('foo')), [],
'slotchange event should not fire when inserting a named slot element when there is an element assigned to another slot');
generateTests('<span></span>', null, (shadowRoot) => shadowRoot.append(newSlot('foo')), [],
'slotchange event should not fire when inserting a named slot element when there is an element assigned to the default slot');
generateTests('<span slot="foo"></span>', null, (shadowRoot) => shadowRoot.append(newSlot('foo')), ['newSlot'],
'slotchange event should fire when inserting a named slot element when there is an element assigned to the slot');
generateTests('hello', null, (shadowRoot) => shadowRoot.append(newSlot('foo')), [],
'slotchange event should not fire when inserting a named slot element when there is a Text node assigned to the default slot');
generateTests('<span></span>', '<slot id="oldSlot"></slot>',
(shadowRoot) => shadowRoot.append(newSlot()), [],
'slotchange event should not fire when inserting a default slot element after an existing default slot');
generateTests('<span></span>', '<slot id="oldSlot"></slot>',
(shadowRoot) => shadowRoot.prepend(newSlot()), ['newSlot', 'oldSlot'],
'slotchange event should fire when inserting a default slot element before an existing default slot in the tree order');
generateTests('<span></span>', '<slot id="oldSlot1"></slot><slot id="oldSlot2"></slot>',
(shadowRoot) => shadowRoot.firstChild.after(newSlot()), [],
'slotchange event should not fire when inserting a default slot element between two existing default slots');
generateTests('<span slot="foo"></span>', '<slot id="oldSlot" name="foo"></slot>',
(shadowRoot) => shadowRoot.append(newSlot('foo')), [],
'slotchange event should not fire when inserting a named slot element after an existing slot of the same name');
generateTests('<span slot="foo"></span>', '<slot id="oldSlot" name="foo"></slot>',
(shadowRoot) => shadowRoot.prepend(newSlot('foo')), ['newSlot', 'oldSlot'],
'slotchange event should fire when inserting a named slot element before an existing slot of the same name in the tree order');
generateTests('<span name="foo"></span>', '<slot id="oldSlot1" name="foo"></slot><slot id="oldSlot2" name="foo"></slot>',
(shadowRoot) => shadowRoot.firstChild.after(newSlot('foo')), [],
'slotchange event should not fire when inserting a named slot element between two existing slots of the same name');
generateTests('<span></span>', '<div><slot id="oldSlot"></slot></div>',
(shadowRoot) => {
const ancestor = document.createElement('div');
ancestor.innerHTML = '<span><b><slot id="newSlot"></slot></b></span>';
addEventListners(ancestor);
shadowRoot.prepend(ancestor);
}, ['newSlot', 'oldSlot'],
'slotchange event should fire when inserting the ancestor of a default slot element before an existing default slot in the tree order');
generateTests('<span slot="foo"></span>', '<div><slot name="foo" id="oldSlot"></slot></div>',
(shadowRoot) => {
const ancestor = document.createElement('div');
ancestor.innerHTML = '<span><b><slot id="newSlot" name="foo"></slot></b></span>';
addEventListners(ancestor);
shadowRoot.prepend(ancestor);
}, ['newSlot', 'oldSlot'],
'slotchange event should fire when inserting the ancestor of a named slot element before an existing named slot element in the tree order');
generateTests('<span></span>', null,
(shadowRoot) => {
const ancestor = document.createElement('div');
ancestor.innerHTML = '<p><slot id="slot1"></slot><b><slot id="slot2"></slot></b><slot id="slot3"></slot></p>';
addEventListners(ancestor);
shadowRoot.append(ancestor);
}, ['slot1'],
'slotchange event should fire on the first default slot inserted in the tree order');
generateTests('<span slot="foo"></span>', null,
(shadowRoot) => {
const ancestor = document.createElement('div');
ancestor.innerHTML = '<p><slot id="slot1" name="foo"></slot><b><slot id="slot2" name="foo"></slot></b><slot id="slot3" name="foo"></slot></p>';
addEventListners(ancestor);
shadowRoot.append(ancestor);
}, ['slot1'],
'slotchange event should fire on the first named slot inserted in the tree order');
generateTests('<span slot="b"></span>', null,
(shadowRoot) => {
const ancestor = document.createElement('div');
ancestor.innerHTML = '<p><slot id="slot1" name="a"></slot><b><slot id="slot2" name="b"></slot></b><slot id="slot3" name="b"></slot></p>';
addEventListners(ancestor);
shadowRoot.append(ancestor);
}, ['slot2'],
'slotchange event should fire on the first named slot of the same name inserted in the tree order');
generateTests('<span slot="a"></span><span slot="b"></span>', '<slot id="oldSlot" name="a"></slot><slot></slot>',
(shadowRoot) => shadowRoot.prepend(newSlot('a')), ['newSlot', 'oldSlot'],
'slotchange event should fire when inserting a named slot element before an existing slot of the same name before a default slot in the tree order');
generateTests('<span></span><span slot="a"></span>', '<slot id="oldDefault"></slot><slot id="oldA" name="a"></slot>',
(shadowRoot) => {
const ancestor = document.createElement('div');
ancestor.innerHTML = `<span><slot id="newDefault1"></slot><b><slot id="newDefault2"></slot><slot id="newB" name="b"></slot><slot id="newA" name="a"></slot></b></span>`;
addEventListners(ancestor);
shadowRoot.prepend(ancestor);
}, ['newDefault1', 'newA', 'oldDefault', 'oldA'],
'slotchange event should fire on all newly inserted slots with assigned nodes and their previously-first counterparts in the tree order');
generateTests('<span></span><span slot="a"></span>', '<slot id="oldDefault"></slot><slot id="oldA" name="a"></slot><slot id="oldB" name="b"></slot>',
(shadowRoot) => {
const ancestor = document.createElement('div');
ancestor.innerHTML = `<span><slot id="newDefault1"></slot><b><slot id="newDefault2"></slot><slot id="newB" name="b"></slot><slot id="newA" name="a"></slot></b></span>`;
addEventListners(ancestor);
shadowRoot.prepend(ancestor);
}, ['newDefault1', 'newA', 'oldDefault', 'oldA'],
'slotchange event should fire on all newly inserted slots with assigned nodes but not on those without in the tree order');
// Removals
generateTests(null, '<slot id="oldSlot"></slot>', (shadowRoot) => shadowRoot.querySelector('slot').remove(), [],
'slotchange event should not fire when removing a default slot element into a shadow host with no children');
generateTests(null, '<slot id="oldSlot" name="foo"></slot>', (shadowRoot) => shadowRoot.querySelector('slot').remove(), [],
'slotchange event should not fire when removing a named slot element into a shadow host with no children');
generateTests('<span slot="bar"></span>', '<slot id="oldSlot"></slot>', (shadowRoot) => shadowRoot.querySelector('slot').remove(), [],
'slotchange event should not fire when removing a default slot element when there is an element assigned to another slot');
generateTests('<span></span>', '<slot id="oldSlot"></slot>', (shadowRoot) => shadowRoot.querySelector('slot').remove(), ['oldSlot'],
'slotchange event should fire when removing a default slot element when there is an element assigned to the default slot');
generateTests('hello', '<slot id="oldSlot"></slot>', (shadowRoot) => shadowRoot.querySelector('slot').remove(), ['oldSlot'],
'slotchange event should fire when removing a default slot element when there is a Text node assigned to the default slot');
generateTests('<span slot="bar"></span>', '<slot id="oldSlot" name="foo"></slot>', (shadowRoot) => shadowRoot.querySelector('slot').remove(), [],
'slotchange event should not fire when removing a named slot element when there is an element assigned to another slot');
generateTests('<span></span>', '<slot id="oldSlot" name="foo"></slot>', (shadowRoot) => shadowRoot.querySelector('slot').remove(), [],
'slotchange event should not fire when removing a named slot element when there is an element assigned to the default slot');
generateTests('<span slot="foo"></span>', '<slot id="oldSlot" name="foo"></slot>', (shadowRoot) => shadowRoot.querySelector('slot').remove(), ['oldSlot'],
'slotchange event should fire when removing a named slot element when there is an element assigned to the slot');
generateTests('hello', '<slot id="oldSlot" name="foo"></slot>', (shadowRoot) => shadowRoot.querySelector('slot').remove(), [],
'slotchange event should not fire when removing a named slot element when there is a Text node assigned to the default slot');
generateTests('<span></span>', '<slot id="slot1"></slot><slot id="slot2"></slot>',
(shadowRoot) => shadowRoot.getElementById('slot2').remove(), [],
'slotchange event should not fire when removing a default slot element after an existing default slot');
generateTests('<span></span>', '<slot id="slot1"></slot><slot id="slot2"></slot>',
(shadowRoot) => shadowRoot.getElementById('slot1').remove(), ['slot2', 'slot1'],
'slotchange event should fire when removing the first default slot element even if it had duplicates');
generateTests('<span></span>', '<slot id="slot1"><slot id="slot2"></slot></slot><slot id="slot3"></slot>',
(shadowRoot) => shadowRoot.getElementById('slot2').remove(), [],
'slotchange event should not fire when removing a duplicate default slot, which is the first child of a default slot element');
generateTests('<span slot="foo"></span>', '<slot id="slot1" name="foo"></slot><slot id="slot2" name="foo"></slot>',
(shadowRoot) => shadowRoot.getElementById('slot2').remove(), [],
'slotchange event should not fire when removing a named slot element after an existing named slot of the same name');
generateTests('<span slot="foo"></span>', '<slot id="slot1" name="foo"></slot><slot id="slot2" name="foo"></slot>',
(shadowRoot) => shadowRoot.getElementById('slot1').remove(), ['slot2', 'slot1'],
'slotchange event should fire when removing the first named slot element even if it had duplicates');
generateTests('<span slot="foo"></span>', '<slot id="slot1" name="foo"><slot id="slot2" name="foo"></slot></slot><slot id="slot3" name="foo"></slot>',
(shadowRoot) => shadowRoot.getElementById('slot2').remove(), [],
'slotchange event should not fire when removing a duplicate named slot, which is the first child of a named slot element of the same name');
generateTests('<span></span>', '<div><span><b><slot id="slot1"></slot></b></span></div><slot id="slot2"></slot>',
(shadowRoot) => shadowRoot.querySelector('div').remove(), ['slot2', 'slot1'],
'slotchange event should fire when removing the ancestor of a default slot element before another default slot in the tree order');
generateTests('<span></span>', '<div><span><b><slot id="slot1"></slot></b></span></div><slot id="slot2"></slot>',
(shadowRoot) => shadowRoot.querySelector('div').remove(), ['slot2', 'slot1'],
'slotchange event should fire when removing the ancestor of a named slot element before another slot of the same name in the tree order');
generateTests('<span></span>', '<div><p><slot id="slot1"></slot><b><slot id="slot2"></slot></b><slot id="slot3"></slot></p></div>',
(shadowRoot) => shadowRoot.querySelector('p').remove(), ['slot1'],
'slotchange event should fire on the first default slot removed in the tree order');
generateTests('<span slot="foo"></span>',
'<div><p><slot id="slot1" name="foo"></slot><b><slot id="slot2" name="foo"></slot></b><slot id="slot3" name="foo"></slot></p></div>',
(shadowRoot) => shadowRoot.querySelector('p').remove(), ['slot1'],
'slotchange event should fire on the first named slot removed in the tree order');
generateTests('<span></span>', '<div><p><slot id="slot1"></slot><b><slot id="slot2"></slot></b><slot id="slot3"></slot></p></div>',
(shadowRoot) => shadowRoot.querySelector('p').textContent = '', ['slot1'],
'slotchange event should fire on the first default slot removed in the tree order when replacing all children');
generateTests('<span></span>', '<div><p><slot id="slot1"></slot><b><slot id="slot2"></slot></b><slot id="slot3"></slot></p></div>',
(shadowRoot) => shadowRoot.querySelector('p').textContent = '', ['slot1'],
'slotchange event should fire on the first default slot removed in the tree order when replacing all children 2');
generateTests('<span slot="b"></span>',
'<p><slot id="slot1" name="a"></slot><b><slot id="slot2" name="b"></slot></b><slot id="slot3" name="b"></slot></p>',
(shadowRoot) => shadowRoot.querySelector('p').remove(), ['slot2'],
'slotchange event should fire on the first named slot of the same name removed in the tree order');
generateTests('<span slot="a"></span><span slot="b"></span>', '<slot id="a1" name="a"></slot><slot id="a2" name="a"></slot><slot></slot>',
(shadowRoot) => shadowRoot.getElementById('a1').remove(), ['a2', 'a1'],
'slotchange event should fire when inserting a named slot element before an existing slot of the same name before a default slot in the tree order 2');
generateTests('<span></span><span slot="a"></span>', '<span><slot id="default1"></slot><b><slot id="b1" name="b"></slot>'
+ '<slot id="a1" name="a"></slot></b></span><slot id="default2"></slot><slot id="a2" name="a"></slot>',
(shadowRoot) => shadowRoot.querySelector('span').remove(), ['default2', 'a2', 'default1', 'a1'],
'slotchange event should fire on all first slots of its kind with assigned nodes and their previously-first counterparts in the tree order');
generateTests('<span></span><span slot="a"></span>', '<span><slot id="default1"></slot><b><slot id="default2"></slot>'
+ '<slot id="b1" name="b"></slot><slot id="a1" name="a"></slot></b></span>'
+ '<slot id="default3"></slot><slot id="a2" name="a"></slot><slot id="b2" name="b"></slot>',
(shadowRoot) => shadowRoot.querySelector('span').remove(), ['default3', 'a2', 'default1', 'a1'],
'slotchange event should fire on all removed slots with assigned nodes but not on those without in the tree order');
// Changing the name
generateTests(null, '<slot id="slot"></slot>', (shadowRoot) => shadowRoot.querySelector('slot').name = 'foo', [],
'slotchange event should not fire when renaming a default slot element to a named slot when there are no assigned nodes');
generateTests('<span slot="foo"></span>', '<slot id="slot"></slot>', (shadowRoot) => shadowRoot.querySelector('slot').name = 'foo', ['slot'],
'slotchange event should fire when renaming a default slot element to a named slot when there is an assigned node to the named slot');
generateTests('hello', '<slot id="slot"></slot>', (shadowRoot) => shadowRoot.querySelector('slot').name = 'foo', ['slot'],
'slotchange event should fire when renaming a default slot element to a named slot when there is an assigned node to the default slot');
generateTests(null, '<slot id="slot" name="foo"></slot>', (shadowRoot) => shadowRoot.querySelector('slot').name = null, [],
'slotchange event should not fire when renaming a named slot element to a default slot when there are no assigned nodes');
generateTests('<span></span>', '<slot id="slot" name="foo"></slot>', (shadowRoot) => shadowRoot.querySelector('slot').name = '', ['slot'],
'slotchange event should fire when renaming a named slot element to a default slot when there is a node assigned to the default slot');
generateTests('<span slot="foo"></span>', '<slot id="slot" name="foo"></slot>', (shadowRoot) => shadowRoot.querySelector('slot').name = '', ['slot'],
'slotchange event should fire when renaming a named slot element to a default slot when there is a node assigned to the named slot');
generateTests(null, '<slot id="slot" name="foo"></slot>', (shadowRoot) => shadowRoot.querySelector('slot').name = 'bar', [],
'slotchange event should not fire when renaming a named slot element when there are no assigned nodes');
generateTests('<span slot="bar"></span>', '<slot id="slot" name="foo"></slot>', (shadowRoot) => shadowRoot.querySelector('slot').name = 'bar', ['slot'],
'slotchange event should fire when renaming a named slot element when there is an assigned node to the slot of the new name');
generateTests('<span slot="foo"></span>', '<slot id="slot" name="foo"></slot>', (shadowRoot) => shadowRoot.querySelector('slot').name = 'bar', ['slot'],
'slotchange event should fire when renaming a named slot element when there is an assigned node to the slot of the old name');
generateTests('<span slot="foo"></span>', '<slot id="slot1" name="foo"></slot><slot id="slot2" name="foo"></slot>',
(shadowRoot) => shadowRoot.getElementById('slot2').name = 'bar', [],
'slotchange event should not fire when renaming the second slot element of a given name and there is no assigned node of the new name');
generateTests('<span slot="foo"></span>', '<slot id="slot1" name="foo"></slot><slot id="slot2" name="foo"></slot>',
(shadowRoot) => shadowRoot.getElementById('slot1').name = 'bar', ['slot1', 'slot2'],
'slotchange event should fire when renaming the slot element of a given name and there is an assigned node and a second slot element');
generateTests('<span slot="foo"></span><span slot="bar"></span>',
'<slot id="slot1" name="foo"></slot><slot id="slot2" name="bar"></slot><slot id="slot3" name="foo"></slot>',
(shadowRoot) => shadowRoot.getElementById('slot1').name = 'bar', ['slot1', 'slot2', 'slot3'],
'slotchange event should fire on all three slot elements whose assigned nodes were affected in the tree order when renaming a slot element');
generateTests('<span slot="bar"></span>', '<slot id="slot1" name="foo"></slot><slot id="slot2" name="bar"></slot><slot id="slot3" name="foo"></slot>',
(shadowRoot) => shadowRoot.getElementById('slot1').name = 'bar', ['slot1', 'slot2'],
'slotchange event should fire on slot elements with assigned nodes in the tree order when renaming a slot element but not on those without');
generateTests('<span slot="foo"></span><span slot="bar"></span>',
'<slot id="slot1" name="foo"></slot><slot id="slot2" name="bar"></slot><slot id="slot3" name="foo"></slot>',
(shadowRoot) => shadowRoot.getElementById('slot2').name = 'foo', ['slot2'],
'slotchange event should only fire on the renamed slot when there is another slot with the same name earlier in the tree');
</script>
</body>
</html>