190 lines
8.6 KiB
HTML
190 lines
8.6 KiB
HTML
<!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>
|