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