<!DOCTYPE html>
<title>Shadow DOM: Imperative Slot API slotchange event</title>
<meta name="author" title="Yu Han" href="mailto:[email protected]">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<link rel="help" href="https://dom.spec.whatwg.org/#signaling-slot-change">
<script src="resources/shadow-dom.js"></script>
<div id="test_slotchange">
<div id="host">
<template id="shadow_root" data-mode="open" data-slot-assignment="manual">
<slot id="s1"><div id="fb">fallback</div></slot>
<slot id="s2"></slot>
<div>
<slot id="s2.5"></slot>
</div>
<slot id="s3"></slot>
</template>
<div id="c1"></div>
<div id="c2"></div>
</div>
<div id="c4"></div>
</div>
<script>
function getDataCollection() {
return {
s1EventCount: 0,
s2EventCount: 0,
s3EventCount: 0,
s1ResolveFn: null,
s2ResolveFn: null,
s3ResolveFn: null,
}
}
function setupShadowDOM(id, test, data) {
let tTree = createTestTree(id);
tTree.s1.addEventListener('slotchange', (event) => {
if (!event.isFakeEvent) {
test.step(function () {
assert_equals(event.type, 'slotchange', 'slotchange event\'s type must be "slotchange"');
assert_equals(event.target, tTree.s1, 'slotchange event\'s target must be the slot element');
assert_equals(event.relatedTarget, undefined, 'slotchange must not set relatedTarget');
});
data.s1EventCount++;
}
data.s1ResolveFn();
});
tTree.s2.addEventListener('slotchange', (event) => {
if (!event.isFakeEvent) {
test.step(function () {
assert_equals(event.type, 'slotchange', 'slotchange event\'s type must be "slotchange"');
assert_equals(event.target, tTree.s2, 'slotchange event\'s target must be the slot element');
assert_equals(event.relatedTarget, undefined, 'slotchange must not set relatedTarget');
});
data.s2EventCount++;
}
data.s2ResolveFn();
});
tTree.s3.addEventListener('slotchange', (event) => {
if (!event.isFakeEvent) {
test.step(function () {
assert_equals(event.type, 'slotchange', 'slotchange event\'s type must be "slotchange"');
// listen to bubbling events.
assert_equals(event.relatedTarget, undefined, 'slotchange must not set relatedTarget');
});
data.s3EventCount++;
}
data.s3ResolveFn();
});
return tTree;
}
function monitorSlots(data) {
const s1Promise = new Promise((resolve, reject) => {
data.s1ResolveFn = resolve;
});
const s2Promise = new Promise((resolve, reject) => {
data.s2ResolveFn = resolve;
});
const s3Promise = new Promise((resolve, reject) => {
data.s3ResolveFn = resolve;
});
return [s1Promise, s2Promise, s3Promise];
}
</script>
<script>
// Tests:
async_test((test) => {
const data = getDataCollection();
let tTree = setupShadowDOM(test_slotchange, test, data);
let [s1Promise, s2Promise] = monitorSlots(data);
tTree.s1.assign(tTree.c1);
tTree.s2.assign(tTree.c2);
assert_equals(data.s1EventCount, 0, 'slotchange event must not be fired synchronously');
assert_equals(data.s2EventCount, 0);
Promise.all([s1Promise, s2Promise]).then(test.step_func_done(() => {
assert_equals(data.s1EventCount, 1);
assert_equals(data.s2EventCount, 1);
}));
}, 'slotchange event must not fire synchronously.');
async_test((test) => {
const data = getDataCollection();
let tTree = setupShadowDOM(test_slotchange, test, data);
let [s1Promise, s2Promise] = monitorSlots(data);
tTree.s1.assign();;
tTree.s2.assign();
tTree.host.insertBefore(tTree.c4, tTree.c1);
Promise.all([s1Promise, s2Promise]).then(test.step_func_done(() => {
assert_equals(data.s1EventCount, 0);
assert_equals(data.s2EventCount, 0);
}));
// use fake event to trigger event handler.
let fakeEvent = new Event('slotchange');
fakeEvent.isFakeEvent = true;
tTree.s1.dispatchEvent(fakeEvent);
tTree.s2.dispatchEvent(fakeEvent);
}, 'slotchange event should not fire when assignments do not change assignedNodes.');
async_test((test) => {
const data = getDataCollection();
let tTree = setupShadowDOM(test_slotchange,test, data);
let [s1Promise] = monitorSlots(data);
tTree.s1.assign(tTree.c1, tTree.c2);
s1Promise.then(test.step_func(() => {
assert_equals(data.s1EventCount, 1);
[s1Promise] = monitorSlots(data);
tTree.s1.assign(tTree.c1, tTree.c2);
tTree.s1.assign(tTree.c1, tTree.c2, tTree.c1, tTree.c2, tTree.c2);
s1Promise.then(test.step_func_done(() => {
assert_equals(data.s1EventCount, 1);
}));
let fakeEvent = new Event('slotchange');
fakeEvent.isFakeEvent = true;
tTree.s1.dispatchEvent(fakeEvent);
}));
}, 'slotchange event should not fire when same node is assigned.');
async_test((test) => {
const data = getDataCollection();
let tTree = setupShadowDOM(test_slotchange, test, data);
let [s1Promise, s2Promise] = monitorSlots(data);
tTree.s1.assign(tTree.c1);
tTree.s2.assign(tTree.c2);
Promise.all([s1Promise, s2Promise]).then(test.step_func_done(() => {
assert_equals(data.s1EventCount, 1);
assert_equals(data.s2EventCount, 1);
}));
}, "Fire slotchange event when slot's assigned nodes changes.");
async_test((test) => {
const data = getDataCollection();
let tTree = setupShadowDOM(test_slotchange, test, data);
let [s1Promise, s2Promise] = monitorSlots(data);
tTree.s1.assign(tTree.c1);
s1Promise.then(test.step_func(() => {
assert_equals(data.s1EventCount, 1);
[s1Promise, s2Promise] = monitorSlots(data);
tTree.s2.assign(tTree.c1);
Promise.all([s1Promise, s2Promise]).then(test.step_func_done(() => {
assert_equals(data.s1EventCount, 2);
assert_equals(data.s2EventCount, 1);
}));
}));
}, "Fire slotchange event on previous slot and new slot when node is reassigned.");
async_test((test) => {
const data = getDataCollection();
let tTree = setupShadowDOM(test_slotchange, test, data);
let [s1Promise] = monitorSlots(data);
tTree.s1.assign(tTree.c1);
s1Promise.then(test.step_func(() => {
assert_equals(data.s1EventCount, 1);
[s1Promise] = monitorSlots(data);
tTree.s1.assign();
s1Promise.then(test.step_func_done(() => {
assert_equals(data.s1EventCount, 2);
}));
}));
}, "Fire slotchange event on node assignment and when assigned node is removed.");
async_test((test) => {
const data = getDataCollection();
let tTree = setupShadowDOM(test_slotchange, test, data);
let [s1Promise] = monitorSlots(data);
tTree.s1.assign(tTree.c1, tTree.c2);
s1Promise.then(test.step_func(() => {
assert_equals(data.s1EventCount, 1);
[s1Promise] = monitorSlots(data);
tTree.s1.assign(tTree.c2, tTree.c1);
s1Promise.then(test.step_func_done(() => {
assert_equals(data.s1EventCount, 2);
}));
}));
}, "Fire slotchange event when order of assigned nodes changes.");
promise_test((test) => {
const data = getDataCollection();
let tTree = setupShadowDOM(test_slotchange, test, data);
let [s1Promise] = monitorSlots(data);
tTree.s1.assign(tTree.c1);
return s1Promise.then(test.step_func(() => {
assert_equals(data.s1EventCount, 1);
[s1Promise] = monitorSlots(data);
tTree.c1.remove();
return s1Promise;
}))
.then(test.step_func(() => {
assert_equals(data.s1EventCount, 2);
}));
}, "Fire slotchange event when assigned node is removed.");
promise_test((test) => {
const data = getDataCollection();
let tTree = setupShadowDOM(test_slotchange, test, data);
[s1Promise] = monitorSlots(data);
tTree.s1.assign(tTree.c1);
return s1Promise.then(test.step_func(() => {
assert_equals(data.s1EventCount, 1);
[s1Promise] = monitorSlots(data);
tTree.s1.remove();
return s1Promise;
}))
.then(test.step_func(() => {
assert_equals(data.s1EventCount, 2);
}));
}, "Fire slotchange event when removing a slot from Shadows Root that changes its assigned nodes.");
async_test((test) => {
const data = getDataCollection();
let tTree = setupShadowDOM(test_slotchange, test, data);
let [s1Promise] = monitorSlots(data);
tTree.s1.remove();
let fakeEvent = new Event('slotchange');
fakeEvent.isFakeEvent = true;
tTree.s1.dispatchEvent(fakeEvent);
s1Promise.then(test.step_func(() => {
assert_equals(data.s2EventCount, 0);
[s1Promise, s2Promise] = monitorSlots(data);
tTree.shadow_root.insertBefore(tTree.s1, tTree.s2);
tTree.s1.dispatchEvent(fakeEvent);
tTree.s2.dispatchEvent(fakeEvent);
Promise.all([s1Promise, s2Promise]).then(test.step_func_done(() => {
assert_equals(data.s1EventCount, 0);
assert_equals(data.s2EventCount, 0);
}));
}));
}, "No slotchange event when adding or removing an empty slot.");
async_test((test) => {
const data = getDataCollection();
let tTree = setupShadowDOM(test_slotchange, test, data);
let [s1Promise, s2Promise] = monitorSlots(data);
tTree.host.appendChild(document.createElement("div"));
let fakeEvent = new Event('slotchange');
fakeEvent.isFakeEvent = true;
tTree.s1.dispatchEvent(fakeEvent);
tTree.s2.dispatchEvent(fakeEvent);
Promise.all([s1Promise, s2Promise]).then(test.step_func(() => {
assert_equals(data.s1EventCount, 0);
assert_equals(data.s2EventCount, 0);
[s1Promise, s2Promise] = monitorSlots(data);
tTree.shadow_root.insertBefore(document.createElement("div"), tTree.s2);
tTree.s1.dispatchEvent(fakeEvent);
tTree.s2.dispatchEvent(fakeEvent);
Promise.all([s1Promise, s2Promise]).then(test.step_func_done(() => {
assert_equals(data.s1EventCount, 0);
assert_equals(data.s2EventCount, 0);
}));
}));
}, "No slotchange event when adding another slotable.");
</script>
<div id="test_nested_slotchange">
<div>
<template data-mode="open" data-slot-assignment="manual">
<div>
<template data-mode="open" data-slot-assignment="manual">
<slot id="s2"></slot>
<slot id="s3"></slot>
</template>
<slot id="s1"></slot>
</div>
</template>
<div id="c1"></div>
</div>
</div>
<script>
async_test((test) => {
const data = getDataCollection();
let tTree = setupShadowDOM(test_nested_slotchange, test, data);
let [s1Promise, s2Promise, s3Promise] = monitorSlots(data);
tTree.s3.assign(tTree.s1);
s3Promise.then(test.step_func(() => {
assert_equals(data.s3EventCount, 1);
[s1Promise, s2Promise, s3Promise] = monitorSlots(data);
tTree.s1.assign(tTree.c1);
Promise.all([s1Promise, s3Promise]).then(test.step_func_done(() => {
assert_equals(data.s1EventCount, 1);
assert_equals(data.s3EventCount, 2);
}));
}));
}, "Fire slotchange event when assign node to nested slot, ensure event bubbles ups.");
promise_test(async t => {
async function mutationObserversRun() {
return new Promise(r => {
t.step_timeout(r, 0);
});
}
let tTree = createTestTree(test_slotchange);
tTree.s1.assign(tTree.c1);
tTree["s2.5"].assign(tTree.c2);
let slotChangedOrder = [];
// Clears out pending mutation observers
await mutationObserversRun();
tTree.s1.addEventListener("slotchange", function() {
slotChangedOrder.push("s1");
});
tTree.s3.addEventListener("slotchange", function() {
slotChangedOrder.push("s3");
});
tTree["s2.5"].addEventListener("slotchange", function() {
slotChangedOrder.push("s2.5");
});
tTree.s3.assign(tTree.c2, tTree.c1);
await mutationObserversRun();
assert_array_equals(slotChangedOrder, ["s1", "s2.5", "s3"]);
}, 'Signal a slot change should be done in tree order.');
</script>