blob: 32a36624a8e367dd7cef3a287498709569fc1562 [file] [log] [blame]
<!DOCTYPE html>
<html>
<head>
<title>Shadow DOM: slotchange event</title>
<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
<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>
<script>
function create_slotchange_observer() {
let log = [];
const listener = function (event) {
log.push({node: this, event: event, eventType: event.type, eventTarget: event.target});
}
return {
observe: (node) => node.addEventListener('slotchange', listener),
takeLog: () => {
const currentLog = log;
log = [];
return currentLog;
}
};
}
function assert_slotchange_log(logEntry, node, target, description) {
assert_equals(logEntry.node, node, description);
assert_equals(logEntry.eventType, 'slotchange', description);
assert_equals(logEntry.eventTarget, target, description);
}
function test_slotchange_event_bubbles(mode, connected) {
promise_test(() => {
const host = document.createElement('div');
if (connected)
document.body.appendChild(host);
const shadowRoot = host.attachShadow({'mode': mode});
shadowRoot.innerHTML = '<div><slot></slot></div>';
const container = shadowRoot.querySelector('div');
const slot = shadowRoot.querySelector('slot');
const observer = create_slotchange_observer();
observer.observe(slot);
observer.observe(container);
observer.observe(shadowRoot);
observer.observe(host);
observer.observe(document);
observer.observe(window);
shadowRoot.appendChild(container);
host.appendChild(document.createElement('span'));
host.appendChild(document.createElement('b'));
assert_array_equals(observer.takeLog(), [], 'slotchange event must not be fired synchronously');
return Promise.resolve().then(() => {
const log = observer.takeLog();
const events = new Set(log.map((entry) => entry.event));
assert_equals(events.size, 1, 'Mutating the assigned content of a slot must fire exactly one slotchange event');
assert_slotchange_log(log[0], slot, slot, 'slotchange event must be dispatched at the slot element first');
assert_slotchange_log(log[1], container, slot, 'slotchange event must bubble up to the parent node of the slot');
assert_slotchange_log(log[2], shadowRoot, slot, 'slotchange event must bubble up to the shadow root');
assert_equals(log.length, 3, 'slotchange must not bubble beyond the shadow root');
});
}, `slotchange event must bubble in a ${connected ? 'connected' : 'disconnected'} ${mode}-mode shadow tree`);
}
test_slotchange_event_bubbles('closed', false);
test_slotchange_event_bubbles('closed', true);
test_slotchange_event_bubbles('open', false);
test_slotchange_event_bubbles('open', true);
function test_single_slotchange_event_for_nested_slots(outerMode, innerMode, connected) {
promise_test(async () => {
const outerHost = document.createElement('outer-host');
if (connected)
document.body.appendChild(outerHost);
const outerShadow = outerHost.attachShadow({'mode': outerMode});
outerShadow.innerHTML = '<div><inner-host><slot></slot></inner-host></div>';
const outerHostParent = outerShadow.querySelector('div');
const outerSlot = outerShadow.querySelector('slot');
const innerHost = outerShadow.querySelector('inner-host');
const innerShadow = innerHost.attachShadow({'mode': innerMode});
innerShadow.innerHTML = '<div><slot></slot></div>';
const innerSlotParent = innerShadow.querySelector('div');
const innerSlot = innerShadow.querySelector('slot');
await Promise.resolve();
const observer = create_slotchange_observer();
observer.observe(outerSlot);
observer.observe(innerHost);
observer.observe(window);
observer.observe(document);
observer.observe(outerHost);
observer.observe(outerShadow);
observer.observe(outerHostParent);
observer.observe(outerSlot);
observer.observe(innerHost);
observer.observe(innerShadow);
observer.observe(innerSlotParent);
observer.observe(innerSlot);
outerHost.textContent = ' ';
assert_array_equals(observer.takeLog(), [], 'slotchange event must not be fired synchronously');
await Promise.resolve();
const log = observer.takeLog();
const events = new Set(log.map((entry) => entry.event));
assert_equals(events.size, 1, 'Mutating the assigned content of a slot must fire exactly one slotchange event');
assert_slotchange_log(log[0], outerSlot, outerSlot, 'slotchange event must be dispatched at the slot element first');
assert_slotchange_log(log[1], innerSlot, outerSlot, 'slotchange event must bubble up from a slot element to its assigned slot');
assert_slotchange_log(log[2], innerSlotParent, outerSlot, 'slotchange event must bubble up to the parent node of a slot');
assert_slotchange_log(log[3], innerShadow, outerSlot, 'slotchange event must bubble up to the shadow root');
assert_slotchange_log(log[4], innerHost, outerSlot,
'slotchange event must bubble up to the shadow host if the host is a descendent of the tree in which the event was fired');
assert_slotchange_log(log[5], outerHostParent, outerSlot,
'slotchange event must bubble up to the parent of an inner shadow host');
assert_slotchange_log(log[6], outerShadow, outerSlot, 'slotchange event must bubble up to the shadow root');
assert_equals(log.length, 7, 'slotchange must not bubble beyond the shadow root in which the event was fired');
}, `A single slotchange event must bubble from a ${connected ? 'connected' : 'disconnected'} ${innerMode}-mode shadow tree to`
+ `a slot in its parent ${outerMode}-mode shadow tree`);
}
test_single_slotchange_event_for_nested_slots('closed', 'closed', false);
test_single_slotchange_event_for_nested_slots('closed', 'closed', true);
test_single_slotchange_event_for_nested_slots('closed', 'open', false);
test_single_slotchange_event_for_nested_slots('closed', 'open', true);
test_single_slotchange_event_for_nested_slots('open', 'closed', false);
test_single_slotchange_event_for_nested_slots('open', 'closed', true);
test_single_slotchange_event_for_nested_slots('open', 'open', false);
test_single_slotchange_event_for_nested_slots('open', 'open', true);
function test_slotchange_event_fired_without_listener_on_slot(mode, connected) {
promise_test(() => {
const host = document.createElement('div');
if (connected)
document.body.appendChild(host);
const shadowRoot = host.attachShadow({'mode': mode});
shadowRoot.innerHTML = '<div><slot></slot></div>';
const container = shadowRoot.querySelector('div');
const slot = shadowRoot.querySelector('slot');
const observer = create_slotchange_observer();
observer.observe(container);
observer.observe(shadowRoot);
observer.observe(host);
observer.observe(document);
observer.observe(window);
shadowRoot.appendChild(container);
host.appendChild(document.createElement('span'));
host.appendChild(document.createElement('b'));
assert_array_equals(observer.takeLog(), [], 'slotchange event must not be fired synchronously');
return Promise.resolve().then(() => {
const log = observer.takeLog();
const events = new Set(log.map((entry) => entry.event));
assert_equals(events.size, 1, 'Mutating the assigned content of a slot must fire exactly one slotchange event');
assert_slotchange_log(log[0], container, slot, 'slotchange event must bubble up to the parent node of the slot');
assert_slotchange_log(log[1], shadowRoot, slot, 'slotchange event must bubble up to the shadow root');
assert_equals(log.length, 2, 'slotchange must not bubble beyond the shadow root');
});
}, `slotchange event must be fired in a ${connected ? 'connected' : 'disconnected'} ${mode}-mode shadow tree`
+ ` even when the slot element itself lacks a event listener.`);
}
test_slotchange_event_fired_without_listener_on_slot('closed', false);
test_slotchange_event_fired_without_listener_on_slot('closed', true);
test_slotchange_event_fired_without_listener_on_slot('open', false);
test_slotchange_event_fired_without_listener_on_slot('open', true);
</script>
</body>
</html>