<!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>
