Source code
Revision control
Copy as Markdown
Other Tools
Test Info:
- This WPT test may be referenced by the following Test IDs:
            
- /shadow-dom/imperative-slot-api-slotchange.html - WPT Dashboard Interop Dashboard
 
 
<!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>
<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>