| <!DOCTYPE html> |
| <title>Shadow DOM: Imperative Slot API slotchange event</title> |
| <meta name="author" title="Yu Han" href="mailto:yuzhehan@chromium.org"> |
| <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> |