Bug 1053898 - Add integration tests for DevTools inspector shadow dom support;r=bgrins draft
authorJulian Descottes <jdescottes@mozilla.com>
Tue, 13 Feb 2018 20:03:39 +0100
changeset 774775 620b1eba4d234b1333672f31862f6f349f962e2c
parent 774748 d6723178568df5ebb736292851622e1e345ec365
child 774776 024ff11140b33ca2146eb43ba96d25b8e706aa36
push id104503
push userjdescottes@mozilla.com
push dateThu, 29 Mar 2018 15:12:09 +0000
reviewersbgrins
bugs1053898
milestone61.0a1
Bug 1053898 - Add integration tests for DevTools inspector shadow dom support;r=bgrins MozReview-Commit-ID: 7C56R5ZMQ4B
devtools/client/inspector/markup/markup.js
devtools/client/inspector/markup/test/browser.ini
devtools/client/inspector/markup/test/browser_markup_dragdrop_invalidNodes.js
devtools/client/inspector/markup/test/browser_markup_shadowdom.js
devtools/client/inspector/markup/test/browser_markup_shadowdom_clickreveal.js
devtools/client/inspector/markup/test/browser_markup_shadowdom_delete.js
devtools/client/inspector/markup/test/browser_markup_shadowdom_maxchildren.js
devtools/client/inspector/markup/test/browser_markup_shadowdom_mutations_shadow.js
devtools/client/inspector/markup/test/browser_markup_shadowdom_navigation.js
devtools/client/inspector/markup/test/browser_markup_shadowdom_noslot.js
devtools/client/inspector/markup/test/browser_markup_shadowdom_slotupdate.js
devtools/client/inspector/markup/test/browser_markup_toggle_closing_tag_line.js
devtools/client/inspector/markup/test/doc_markup_dragdrop.html
devtools/client/inspector/markup/test/head.js
devtools/client/inspector/markup/test/helper_shadowdom.js
devtools/client/inspector/rules/test/browser.ini
devtools/client/inspector/rules/test/browser_rules_shadowdom_slot_rules.js
devtools/client/inspector/test/browser.ini
devtools/client/inspector/test/browser_inspector_breadcrumbs_shadowdom.js
devtools/client/inspector/test/head.js
devtools/client/inspector/test/shared-head.js
--- a/devtools/client/inspector/markup/markup.js
+++ b/devtools/client/inspector/markup/markup.js
@@ -475,16 +475,23 @@ MarkupView.prototype = {
       }, NEW_SELECTION_HIGHLIGHTER_TIMER);
     });
     this._briefBoxModelPromise.resolve = _resolve;
 
     return promise.all([onShown, this._briefBoxModelPromise]);
   },
 
   /**
+   * Used by tests
+   */
+  getSelectedContainer: function() {
+    return this._selectedContainer;
+  },
+
+  /**
    * Get the MarkupContainer object for a given node, or undefined if
    * none exists.
    *
    * @param  {NodeFront} nodeFront
    *         The node to get the container for.
    * @param  {Boolean} slotted
    *         true to get the slotted version of the container.
    * @return {MarkupContainer} The container for the provided node.
--- a/devtools/client/inspector/markup/test/browser.ini
+++ b/devtools/client/inspector/markup/test/browser.ini
@@ -46,16 +46,17 @@ support-files =
   events_bundle.js.map
   events_original.js
   head.js
   helper_attributes_test_runner.js
   helper_diff.js
   helper_events_test_runner.js
   helper_markup_accessibility_navigation.js
   helper_outerhtml_test_runner.js
+  helper_shadowdom.js
   helper_style_attr_test_runner.js
   lib_babel_6.21.0_min.js
   lib_jquery_1.0.js
   lib_jquery_1.1.js
   lib_jquery_1.2_min.js
   lib_jquery_1.3_min.js
   lib_jquery_1.4_min.js
   lib_jquery_1.6_min.js
@@ -160,16 +161,32 @@ skip-if = (os == 'linux' && bits == 32 &
 [browser_markup_node_names_namespaced.js]
 [browser_markup_node_not_displayed_01.js]
 [browser_markup_node_not_displayed_02.js]
 [browser_markup_pagesize_01.js]
 [browser_markup_pagesize_02.js]
 [browser_markup_remove_xul_attributes.js]
 skip-if = e10s # Bug 1036409 - The last selected node isn't reselected
 [browser_markup_search_01.js]
+[browser_markup_shadowdom.js]
+skip-if = !stylo # shadow DOM is only enabled with stylo.
+[browser_markup_shadowdom_clickreveal.js]
+skip-if = !stylo # shadow DOM is only enabled with stylo.
+[browser_markup_shadowdom_delete.js]
+skip-if = !stylo # shadow DOM is only enabled with stylo.
+[browser_markup_shadowdom_maxchildren.js]
+skip-if = !stylo # shadow DOM is only enabled with stylo.
+[browser_markup_shadowdom_mutations_shadow.js]
+skip-if = !stylo # shadow DOM is only enabled with stylo.
+[browser_markup_shadowdom_navigation.js]
+skip-if = !stylo # shadow DOM is only enabled with stylo.
+[browser_markup_shadowdom_noslot.js]
+skip-if = !stylo # shadow DOM is only enabled with stylo.
+[browser_markup_shadowdom_slotupdate.js]
+skip-if = !stylo # shadow DOM is only enabled with stylo.
 [browser_markup_tag_delete_whitespace_node.js]
 [browser_markup_tag_edit_01.js]
 [browser_markup_tag_edit_02.js]
 [browser_markup_tag_edit_03.js]
 [browser_markup_tag_edit_04-backspace.js]
 [browser_markup_tag_edit_04-delete.js]
 [browser_markup_tag_edit_05.js]
 [browser_markup_tag_edit_06.js]
--- a/devtools/client/inspector/markup/test/browser_markup_dragdrop_invalidNodes.js
+++ b/devtools/client/inspector/markup/test/browser_markup_dragdrop_invalidNodes.js
@@ -1,21 +1,22 @@
 /* vim: set ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
  http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
-// Check that pseudo-elements and anonymous nodes are not draggable.
+// Check that pseudo-elements, anonymous nodes and slotted nodes are not draggable.
 
 const TEST_URL = URL_ROOT + "doc_markup_dragdrop.html";
-const PREF = "devtools.inspector.showAllAnonymousContent";
 
 add_task(async function() {
-  Services.prefs.setBoolPref(PREF, true);
+  await pushPref("devtools.inspector.showAllAnonymousContent", true);
+  await pushPref("dom.webcomponents.shadowdom.enabled", true);
+  await pushPref("dom.webcomponents.customelements.enabled", true);
 
   let {inspector} = await openInspectorForURL(TEST_URL);
 
   info("Expanding nodes below #test");
   let parentFront = await getNodeFront("#test", inspector);
   await inspector.markup.expandNode(parentFront);
   await waitForMultipleChildrenUpdates(inspector);
 
@@ -40,9 +41,27 @@ add_task(async function() {
   let anonymousDiv = inputContainer.elt.children[1].firstChild.container;
   inputContainer.elt.scrollIntoView(true);
   await selectNode(anonymousDiv.node, inspector);
 
   info("Simulate dragging the anonymous node");
   await simulateNodeDrag(inspector, anonymousDiv);
 
   ok(!anonymousDiv.isDragging, "anonymous node isn't dragging");
+
+  info("Expanding all nodes below test-component");
+  let testComponentFront = await getNodeFront("test-component", inspector);
+  await inspector.markup.expandAll(testComponentFront);
+  await waitForMultipleChildrenUpdates(inspector);
+
+  info("Getting a slotted node and selecting it");
+  // Directly use the markup getContainer API in order to retrieve the slotted container
+  // for a given node front.
+  let slotted1Front = await getNodeFront(".slotted1", inspector);
+  let slottedContainer = inspector.markup.getContainer(slotted1Front, true);
+  slottedContainer.elt.scrollIntoView(true);
+  await selectNode(slotted1Front, inspector, "no-reason", true);
+
+  info("Simulate dragging the slotted node");
+  await simulateNodeDrag(inspector, slottedContainer);
+
+  ok(!slottedContainer.isDragging, "slotted node isn't dragging");
 });
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_shadowdom.js
@@ -0,0 +1,204 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* import-globals-from helper_shadowdom.js */
+
+"use strict";
+
+loadHelperScript("helper_shadowdom.js");
+
+// Test a few static pages using webcomponents and check that they are displayed as
+// expected in the markup view.
+
+add_task(async function() {
+  await enableWebComponents();
+
+  // Test that expanding a shadow host shows a shadow root node and direct host children.
+  // Test that expanding a shadow root shows the shadow dom.
+  // Test that slotted elements are visible in the shadow dom.
+
+  const TEST_URL = `data:text/html;charset=utf-8,
+    <test-component>
+      <div slot="slot1">slotted-1<div>inner</div></div>
+      <div slot="slot2">slotted-2<div>inner</div></div>
+      <div class="no-slot-class">no-slot-text<div>inner</div></div>
+    </test-component>
+
+    <script>
+      'use strict';
+      customElements.define('test-component', class extends HTMLElement {
+        constructor() {
+          super();
+          let shadowRoot = this.attachShadow({mode: 'open'});
+          shadowRoot.innerHTML = \`
+              <slot name="slot1"></slot>
+              <slot name="slot2"></slot>
+              <slot></slot>
+            \`;
+        }
+      });
+    </script>`;
+
+  const EXPECTED_TREE = `
+    test-component
+      #shadow-root
+        name="slot1"
+          div!slotted
+        name="slot2"
+          div!slotted
+        slot
+          div!slotted
+      slot="slot1"
+        slotted-1
+        inner
+      slot="slot2"
+        slotted-2
+        inner
+      class="no-slot-class"
+        no-slot-text
+        inner`;
+
+  let {inspector} = await openInspectorForURL(TEST_URL);
+  await checkTreeFromRootSelector(EXPECTED_TREE, "test-component", inspector);
+});
+
+add_task(async function() {
+  await enableWebComponents();
+
+  // Test that components without any direct children still display a shadow root node, if
+  // a shadow root is attached to the host.
+
+  const TEST_URL = `data:text/html;charset=utf-8,
+    <test-component></test-component>
+    <script>
+      "use strict";
+      customElements.define("test-component", class extends HTMLElement {
+        constructor() {
+          super();
+          let shadowRoot = this.attachShadow({mode: "open"});
+          shadowRoot.innerHTML = "<slot><div>fallback-content</div></slot>";
+        }
+      });
+    </script>`;
+
+  const EXPECTED_TREE = `
+    test-component
+      #shadow-root
+        slot
+          fallback-content`;
+
+  let {inspector} = await openInspectorForURL(TEST_URL);
+  await checkTreeFromRootSelector(EXPECTED_TREE, "test-component", inspector);
+});
+
+add_task(async function() {
+  await enableWebComponents();
+
+  // Test that the markup view is correctly displayed for non-trivial shadow DOM nesting.
+
+  const TEST_URL = `data:text/html;charset=utf-8,
+    <test-component >
+      <div slot="slot1">slot1-1</div>
+      <third-component slot="slot2"></third-component>
+    </test-component>
+
+    <script>
+    (function() {
+      'use strict';
+
+      function defineComponent(name, html) {
+        customElements.define(name, class extends HTMLElement {
+          constructor() {
+            super();
+            let shadowRoot = this.attachShadow({mode: 'open'});
+            shadowRoot.innerHTML = html;
+          }
+        });
+      }
+
+      defineComponent('test-component', \`
+        <div id="test-container">
+          <slot name="slot1"></slot>
+          <slot name="slot2"></slot>
+          <other-component><div slot="other1">other1-content</div></other-component>
+        </div>\`);
+      defineComponent('other-component',
+        '<div id="other-container"><slot id="other1" name="other1"></slot></div>');
+      defineComponent('third-component', '<div>Third component</div>');
+    })();
+    </script>`;
+
+  const EXPECTED_TREE = `
+    test-component
+      #shadow-root
+        test-container
+          slot
+            div!slotted
+          slot
+            third-component!slotted
+          other-component
+            #shadow-root
+              div
+                slot
+                  div!slotted
+            div
+      div
+      third-component
+        #shadow-root
+          div`;
+
+  let {inspector} = await openInspectorForURL(TEST_URL);
+  await checkTreeFromRootSelector(EXPECTED_TREE, "test-component", inspector);
+});
+
+add_task(async function() {
+  await enableWebComponents();
+
+  // Test that ::before and ::after pseudo elements are correctly displayed in host
+  // components and in slot elements.
+
+  const TEST_URL = `data:text/html;charset=utf-8,
+    <style>
+      test-component::before { content: "before-host" }
+      test-component::after { content: "after-host" }
+    </style>
+
+    <test-component>
+      <div class="light-dom"></div>
+    </test-component>
+
+    <script>
+      "use strict";
+      customElements.define("test-component", class extends HTMLElement {
+        constructor() {
+          super();
+          let shadowRoot = this.attachShadow({mode: "open"});
+          shadowRoot.innerHTML = \`
+            <style>
+              slot { display: block } /* avoid whitespace nodes */
+              slot::before { content: "before-slot" }
+              slot::after { content: "after-slot" }
+            </style>
+            <slot>default content</slot>
+          \`;
+        }
+      });
+    </script>`;
+
+  const EXPECTED_TREE = `
+    test-component
+      #shadow-root
+        style
+          slot { display: block }
+        slot
+          ::before
+          div!slotted
+          ::after
+      ::before
+      class="light-dom"
+      ::after`;
+
+  let {inspector} = await openInspectorForURL(TEST_URL);
+  await checkTreeFromRootSelector(EXPECTED_TREE, "test-component", inspector);
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_shadowdom_clickreveal.js
@@ -0,0 +1,90 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* import-globals-from helper_shadowdom.js */
+
+"use strict";
+
+loadHelperScript("helper_shadowdom.js");
+
+// Test that the corresponding non-slotted node container gets selected when clicking on
+// the reveal link for a slotted node.
+
+const TEST_URL = `data:text/html;charset=utf-8,
+  <test-component>
+    <div slot="slot1" id="el1">slot1-1</div>
+    <div slot="slot1" id="el2">slot1-2</div>
+  </test-component>
+
+  <script>
+    'use strict';
+    customElements.define('test-component', class extends HTMLElement {
+      constructor() {
+        super();
+        let shadowRoot = this.attachShadow({mode: 'open'});
+        shadowRoot.innerHTML = '<slot name="slot1"></slot>';
+      }
+    });
+  </script>`;
+
+add_task(async function() {
+  await enableWebComponents();
+
+  let {inspector} = await openInspectorForURL(TEST_URL);
+  let {markup} = inspector;
+
+  info("Find and expand the test-component shadow DOM host.");
+  let hostFront = await getNodeFront("test-component", inspector);
+  let hostContainer = markup.getContainer(hostFront);
+  await expandContainer(inspector, hostContainer);
+
+  info("Expand the shadow root");
+  let shadowRootContainer = hostContainer.getChildContainers()[0];
+  await expandContainer(inspector, shadowRootContainer);
+
+  info("Expand the slot");
+  let slotContainer = shadowRootContainer.getChildContainers()[0];
+  await expandContainer(inspector, slotContainer);
+
+  let slotChildContainers = slotContainer.getChildContainers();
+  is(slotChildContainers.length, 2, "Expecting 2 slotted children");
+
+  await checkRevealLink(inspector, slotChildContainers[0].node);
+  is(inspector.selection.nodeFront.id, "el1", "The right node was selected");
+  is(hostContainer.getChildContainers()[1].node, inspector.selection.nodeFront);
+
+  await checkRevealLink(inspector, slotChildContainers[1].node);
+  is(inspector.selection.nodeFront.id, "el2", "The right node was selected");
+  is(hostContainer.getChildContainers()[2].node, inspector.selection.nodeFront);
+});
+
+async function checkRevealLink(inspector, node) {
+  let slottedContainer = inspector.markup.getContainer(node, true);
+  info("Select the slotted container for the element");
+  await selectNode(node, inspector, "no-reason", true);
+  ok(inspector.selection.isSlotted(), "The selection is the slotted version");
+  ok(inspector.markup.getSelectedContainer().isSlotted(),
+    "The selected container is slotted");
+
+  info("Click on the reveal link and wait for the new node to be selected");
+  await clickOnRevealLink(inspector, slottedContainer);
+  let selectedFront = inspector.selection.nodeFront;
+  is(selectedFront, node, "The same node front is still selected");
+  ok(!inspector.selection.isSlotted(), "The selection is not the slotted version");
+  ok(!inspector.markup.getSelectedContainer().isSlotted(),
+    "The selected container is not slotted");
+}
+
+async function clickOnRevealLink(inspector, container) {
+  let onSelection = inspector.selection.once("new-node-front");
+  let revealLink = container.elt.querySelector(".reveal-link");
+  let tagline = revealLink.closest(".tag-line");
+  let win = inspector.markup.doc.defaultView;
+
+  // First send a mouseover on the tagline to force the link to be displayed.
+  EventUtils.synthesizeMouseAtCenter(tagline, {type: "mouseover"}, win);
+  EventUtils.synthesizeMouseAtCenter(revealLink, {}, win);
+
+  await onSelection;
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_shadowdom_delete.js
@@ -0,0 +1,92 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* import-globals-from helper_shadowdom.js */
+
+"use strict";
+
+loadHelperScript("helper_shadowdom.js");
+
+// Test that slot elements are correctly updated when slotted elements are being removed
+// from the DOM.
+
+const TEST_URL = `data:text/html;charset=utf-8,
+  <test-component>
+    <div slot="slot1" id="el1">slot1-1</div>
+    <div slot="slot1" id="el2">slot1-2</div>
+  </test-component>
+
+  <script>
+    'use strict';
+    customElements.define('test-component', class extends HTMLElement {
+      constructor() {
+        super();
+        let shadowRoot = this.attachShadow({mode: 'open'});
+        shadowRoot.innerHTML = '<slot name="slot1"><div>default</div></slot>';
+      }
+    });
+  </script>`;
+
+add_task(async function() {
+  await enableWebComponents();
+
+  let {inspector} = await openInspectorForURL(TEST_URL);
+
+  // <test-component> is a shadow host.
+  info("Find and expand the test-component shadow DOM host.");
+  let hostFront = await getNodeFront("test-component", inspector);
+  await inspector.markup.expandNode(hostFront);
+  await waitForMultipleChildrenUpdates(inspector);
+
+  info("Test that expanding a shadow host shows shadow root and direct host children.");
+  let {markup} = inspector;
+  let hostContainer = markup.getContainer(hostFront);
+  let childContainers = hostContainer.getChildContainers();
+
+  is(childContainers.length, 3, "Expecting 3 children: shadowroot, 2 host children");
+  checkText(childContainers[0], "#shadow-root");
+  checkText(childContainers[1], "div");
+  checkText(childContainers[2], "div");
+
+  info("Expand the shadow root");
+  let shadowRootContainer = childContainers[0];
+  await expandContainer(inspector, shadowRootContainer);
+
+  let shadowChildContainers = shadowRootContainer.getChildContainers();
+  is(shadowChildContainers.length, 1, "Expecting 1 child slot");
+  checkText(shadowChildContainers[0], "slot");
+
+  info("Expand the slot");
+  let slotContainer = shadowChildContainers[0];
+  await expandContainer(inspector, slotContainer);
+
+  let slotChildContainers = slotContainer.getChildContainers();
+  is(slotChildContainers.length, 2, "Expecting 2 slotted children");
+  slotChildContainers.forEach(container => checkSlotted(container));
+
+  await deleteNode(inspector, "#el1");
+  slotChildContainers = slotContainer.getChildContainers();
+  is(slotChildContainers.length, 1, "Expecting 1 slotted child");
+  checkSlotted(slotChildContainers[0]);
+
+  await deleteNode(inspector, "#el2");
+  slotChildContainers = slotContainer.getChildContainers();
+  // After deleting the last host direct child we expect the slot to show the default
+  // content <div>default</div>
+  is(slotChildContainers.length, 1, "Expecting 1 child");
+  ok(!slotChildContainers[0].isSlotted(), "Container is a not slotted container");
+});
+
+async function deleteNode(inspector, selector) {
+  info("Select node " + selector + " and make sure it is focused");
+  await selectNode(selector, inspector);
+  await clickContainer(selector, inspector);
+
+  info("Delete the node");
+  let mutated = inspector.once("markupmutation");
+  let updated = inspector.once("inspector-updated");
+  EventUtils.sendKey("delete", inspector.panelWin);
+  await mutated;
+  await updated;
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_shadowdom_maxchildren.js
@@ -0,0 +1,103 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* import-globals-from helper_shadowdom.js */
+
+"use strict";
+
+loadHelperScript("helper_shadowdom.js");
+
+// Test that the markup view properly displays the "more nodes" button both for host
+// elements and for slot elements.
+
+const TEST_URL = `data:text/html;charset=utf-8,
+<test-component>
+  <div>node 1</div><div>node 2</div><div>node 3</div>
+  <div>node 4</div><div>node 5</div><div>node 6</div>
+</test-component>
+
+<script>
+  "use strict";
+  customElements.define("test-component", class extends HTMLElement {
+    constructor() {
+      super();
+      let shadowRoot = this.attachShadow({mode: "open"});
+      shadowRoot.innerHTML = "<slot>some default content</slot>";
+    }
+    connectedCallback() {}
+    disconnectedCallback() {}
+  });
+</script>`;
+
+add_task(async function() {
+  await enableWebComponents();
+  await pushPref("devtools.markup.pagesize", 5);
+
+  let {inspector} = await openInspectorForURL(TEST_URL);
+
+  // <test-component> is a shadow host.
+  info("Find and expand the test-component shadow DOM host.");
+  let hostFront = await getNodeFront("test-component", inspector);
+  await inspector.markup.expandNode(hostFront);
+  await waitForMultipleChildrenUpdates(inspector);
+
+  info("Test that expanding a shadow host shows shadow root and direct host children.");
+  let {markup} = inspector;
+  let hostContainer = markup.getContainer(hostFront);
+  let childContainers = hostContainer.getChildContainers();
+
+  is(childContainers.length, 6, "Expecting 6 children: shadowroot, 5 host children");
+  checkText(childContainers[0], "#shadow-root");
+  for (let i = 1; i < 6; i++) {
+    checkText(childContainers[i], "div");
+    checkText(childContainers[i], "node " + i);
+  }
+
+  info("Click on the more nodes button under the host element");
+  let moreNodesLink = hostContainer.elt.querySelector(".more-nodes");
+  ok(!!moreNodesLink, "A 'more nodes' button is displayed in the host container");
+  moreNodesLink.querySelector("button").click();
+  await inspector.markup._waitForChildren();
+
+  childContainers = hostContainer.getChildContainers();
+  is(childContainers.length, 7, "Expecting one additional host child");
+  checkText(childContainers[6], "div");
+  checkText(childContainers[6], "node 6");
+
+  info("Expand the shadow root");
+  let shadowRootContainer = childContainers[0];
+  let shadowRootFront = shadowRootContainer.node;
+  await inspector.markup.expandNode(shadowRootFront);
+  await waitForMultipleChildrenUpdates(inspector);
+
+  let shadowChildContainers = shadowRootContainer.getChildContainers();
+  is(shadowChildContainers.length, 1, "Expecting 1 slot child");
+  checkText(shadowChildContainers[0], "slot");
+
+  info("Expand the slot");
+  let slotContainer = shadowChildContainers[0];
+  let slotFront = slotContainer.node;
+  await inspector.markup.expandNode(slotFront);
+  await waitForMultipleChildrenUpdates(inspector);
+
+  let slotChildContainers = slotContainer.getChildContainers();
+  is(slotChildContainers.length, 5, "Expecting 5 slotted children");
+  for (let slotChildContainer of slotChildContainers) {
+    checkText(slotChildContainer, "div");
+    ok(slotChildContainer.elt.querySelector(".reveal-link"),
+      "Slotted container has a reveal link element");
+  }
+
+  info("Click on the more nodes button under the slot element");
+  moreNodesLink = slotContainer.elt.querySelector(".more-nodes");
+  ok(!!moreNodesLink, "A 'more nodes' button is displayed in the host container");
+  EventUtils.sendMouseEvent({type: "click"}, moreNodesLink.querySelector("button"));
+  await inspector.markup._waitForChildren();
+
+  slotChildContainers = slotContainer.getChildContainers();
+  is(slotChildContainers.length, 6, "Expecting one additional slotted element");
+  checkText(slotChildContainers[5], "div");
+  ok(slotChildContainers[5].elt.querySelector(".reveal-link"),
+    "Slotted container has a reveal link element");
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_shadowdom_mutations_shadow.js
@@ -0,0 +1,84 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* import-globals-from helper_shadowdom.js */
+
+"use strict";
+
+loadHelperScript("helper_shadowdom.js");
+
+// Test that the markup view is correctly updated when elements under a shadow root are
+// deleted or updated.
+
+const TEST_URL = `data:text/html;charset=utf-8,
+  <test-component>
+    <div slot="slot1" id="el1">slot1-1</div>
+    <div slot="slot1" id="el2">slot1-2</div>
+  </test-component>
+
+  <script>
+    'use strict';
+    customElements.define('test-component', class extends HTMLElement {
+      constructor() {
+        super();
+        let shadowRoot = this.attachShadow({mode: 'open'});
+        shadowRoot.innerHTML = \`<div id="slot1-container">
+                                   <slot name="slot1"></slot>
+                                 </div>
+                                 <div id="another-div"></div>\`;
+      }
+    });
+  </script>`;
+
+add_task(async function() {
+  await enableWebComponents();
+
+  let {inspector} = await openInspectorForURL(TEST_URL);
+
+  const tree = `
+    test-component
+      #shadow-root
+        slot1-container
+          slot
+            div!slotted
+            div!slotted
+        another-div
+      div
+      div`;
+  await checkTreeFromRootSelector(tree, "test-component", inspector);
+
+  info("Delete a shadow dom element and check the updated markup view");
+  let mutated = waitForMutation(inspector, "childList");
+  ContentTask.spawn(gBrowser.selectedBrowser, {}, function() {
+    let shadowRoot = content.document.querySelector("test-component").shadowRoot;
+    let slotContainer = shadowRoot.getElementById("slot1-container");
+    slotContainer.remove();
+  });
+  await mutated;
+
+  let treeAfterDelete = `
+    test-component
+      #shadow-root
+        another-div
+      div
+      div`;
+  await checkTreeFromRootSelector(treeAfterDelete, "test-component", inspector);
+
+  mutated = inspector.once("markupmutation");
+  ContentTask.spawn(gBrowser.selectedBrowser, {}, function() {
+    let shadowRoot = content.document.querySelector("test-component").shadowRoot;
+    let shadowDiv = shadowRoot.getElementById("another-div");
+    shadowDiv.setAttribute("random-attribute", "1");
+  });
+  await mutated;
+
+  info("Add an attribute on a shadow dom element and check the updated markup view");
+  let treeAfterAttrChange = `
+    test-component
+      #shadow-root
+        random-attribute
+      div
+      div`;
+  await checkTreeFromRootSelector(treeAfterAttrChange, "test-component", inspector);
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_shadowdom_navigation.js
@@ -0,0 +1,87 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the markup-view navigation works correctly with shadow dom slotted nodes.
+// Each slotted nodes has two containers representing the same node front in the markup
+// view, we need to make sure that navigating to the slotted version selects the slotted
+// container, and navigating to the non-slotted element selects the non-slotted container.
+
+const TEST_URL = `data:text/html;charset=utf-8,
+  <test-component class="test-component">
+    <div slot="slot1" class="slotted1"><div class="slot1-child">slot1-1</div></div>
+    <div slot="slot1" class="slotted2">slot1-2</div>
+  </test-component>
+
+  <script>
+    'use strict';
+    customElements.define('test-component', class extends HTMLElement {
+      constructor() {
+        super();
+        let shadowRoot = this.attachShadow({mode: 'open'});
+        shadowRoot.innerHTML = '<slot class="slot1" name="slot1"></slot>';
+      }
+    });
+  </script>`;
+
+const TEST_DATA = [
+  ["KEY_PageUp", "html"],
+  ["KEY_ArrowDown", "head"],
+  ["KEY_ArrowDown", "body"],
+  ["KEY_ArrowDown", "test-component"],
+  ["KEY_ArrowRight", "test-component"],
+  ["KEY_ArrowDown", "shadow-root"],
+  ["KEY_ArrowRight", "shadow-root"],
+  ["KEY_ArrowDown", "slot1"],
+  ["KEY_ArrowRight", "slot1"],
+  ["KEY_ArrowDown", "div", "slotted1"],
+  ["KEY_ArrowDown", "div", "slotted2"],
+  ["KEY_ArrowDown", "slotted1"],
+  ["KEY_ArrowRight", "slotted1"],
+  ["KEY_ArrowDown", "slot1-child"],
+  ["KEY_ArrowDown", "slotted2"],
+];
+
+add_task(async function() {
+  await pushPref("dom.webcomponents.shadowdom.enabled", true);
+  await pushPref("dom.webcomponents.customelements.enabled", true);
+
+  let {inspector} = await openInspectorForURL(TEST_URL);
+
+  info("Making sure the markup-view frame is focused");
+  inspector.markup._frame.focus();
+
+  info("Starting to iterate through the test data");
+  for (let [key, expected, slottedClassName] of TEST_DATA) {
+    info("Testing step: " + key + " to navigate to " + expected);
+    EventUtils.synthesizeKey(key);
+
+    info("Making sure markup-view children get updated");
+    await waitForChildrenUpdated(inspector);
+
+    info("Checking the right node is selected");
+    checkSelectedNode(key, expected, slottedClassName, inspector);
+  }
+
+  // Same as in browser_markup_navigation.js, use a single catch-call event listener.
+  await inspector.once("inspector-updated");
+});
+
+function checkSelectedNode(key, expected, slottedClassName, inspector) {
+  let selectedContainer = inspector.markup.getSelectedContainer();
+  let slotted = !!slottedClassName;
+
+  is(selectedContainer.isSlotted(), slotted,
+    `Selected container is ${slotted ? "slotted" : "not slotted"} as expected`);
+  is(inspector.selection.isSlotted(), slotted,
+    `Inspector selection is also ${slotted ? "slotted" : "not slotted"}`);
+  ok(selectedContainer.elt.textContent.includes(expected),
+    "Found expected content: " + expected + " in container after pressing " + key);
+
+  if (slotted) {
+    is(selectedContainer.node.className, slottedClassName,
+      "Slotted has the expected classname " + slottedClassName);
+  }
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_shadowdom_noslot.js
@@ -0,0 +1,112 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* import-globals-from helper_shadowdom.js */
+
+"use strict";
+
+loadHelperScript("helper_shadowdom.js");
+
+// Test that the markup view is correctly displayed when a component has children but no
+// slots are available under the shadow root.
+
+const TEST_URL = `data:text/html;charset=utf-8,
+  <style>
+    .has-before::before { content: "before-content" }
+  </style>
+
+  <div class="root">
+    <no-slot-component>
+      <div class="not-nested">light</div>
+      <div class="nested">
+        <div class="has-before"></div>
+        <div>dummy for Bug 1441863</div>
+      </div>
+    </no-slot-component>
+    <slot-component>
+      <div class="not-nested">light</div>
+      <div class="nested">
+        <div class="has-before"></div>
+      </div>
+    </slot-component>
+  </div>
+
+  <script>
+    'use strict';
+    customElements.define('no-slot-component', class extends HTMLElement {
+      constructor() {
+        super();
+        let shadowRoot = this.attachShadow({mode: 'open'});
+        shadowRoot.innerHTML = '<div class="no-slot-div"></div>';
+      }
+    });
+    customElements.define('slot-component', class extends HTMLElement {
+      constructor() {
+        super();
+        let shadowRoot = this.attachShadow({mode: 'open'});
+        shadowRoot.innerHTML = '<slot></slot>';
+      }
+    });
+  </script>`;
+
+add_task(async function() {
+  await enableWebComponents();
+
+  let {inspector} = await openInspectorForURL(TEST_URL);
+
+  // We expect that host children are correctly displayed when no slots are defined.
+  let beforeTree = `
+  class="root"
+    no-slot-component
+      #shadow-root
+        no-slot-div
+      class="not-nested"
+      class="nested"
+        class="has-before"
+        dummy for Bug 1441863
+    slot-component
+      #shadow-root
+        slot
+          div!slotted
+          div!slotted
+      class="not-nested"
+      class="nested"
+        class="has-before"
+          ::before`;
+  await checkTreeFromRootSelector(beforeTree, ".root", inspector);
+
+  info("Move the non-slotted element with class has-before and check the pseudo appears");
+  let mutated = waitForNMutations(inspector, "childList", 2);
+  let pseudoMutated = waitForMutation(inspector, "nativeAnonymousChildList");
+  ContentTask.spawn(gBrowser.selectedBrowser, {}, function() {
+    let root = content.document.querySelector(".root");
+    let hasBeforeEl = content.document.querySelector("no-slot-component .has-before");
+    root.appendChild(hasBeforeEl);
+  });
+  await mutated;
+  await pseudoMutated;
+
+  // As the non-slotted has-before is moved into the tree, the before pseudo is expected
+  // to appear.
+  let afterTree = `
+    class="root"
+      no-slot-component
+        #shadow-root
+          no-slot-div
+        class="not-nested"
+        class="nested"
+          dummy for Bug 1441863
+      slot-component
+        #shadow-root
+          slot
+            div!slotted
+            div!slotted
+        class="not-nested"
+        class="nested"
+          class="has-before"
+            ::before
+      class="has-before"
+        ::before`;
+  await checkTreeFromRootSelector(afterTree, ".root", inspector);
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_shadowdom_slotupdate.js
@@ -0,0 +1,75 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* import-globals-from helper_shadowdom.js */
+
+"use strict";
+
+loadHelperScript("helper_shadowdom.js");
+
+// Test that slotted elements are correctly updated when the slot attribute is modified
+// on already slotted elements.
+
+const TEST_URL = `data:text/html;charset=utf-8,
+  <test-component>
+    <div slot="slot1">slot1-1</div>
+    <div slot="slot1">slot1-2</div>
+    <div slot="slot2" id="to-update">slot2-1</div>
+    <div slot="slot2">slot2-2</div>
+  </test-component>
+
+  <script>
+    'use strict';
+    customElements.define('test-component', class extends HTMLElement {
+      constructor() {
+        super();
+        let shadowRoot = this.attachShadow({mode: 'open'});
+        shadowRoot.innerHTML = '<slot name="slot1"></slot><slot name="slot2"></slot>';
+      }
+    });
+  </script>`;
+
+add_task(async function() {
+  await enableWebComponents();
+
+  let {inspector} = await openInspectorForURL(TEST_URL);
+
+  const tree = `
+    test-component
+      #shadow-root
+        name="slot1"
+          div!slotted
+          div!slotted
+        name="slot2"
+          div!slotted
+          div!slotted
+      slot1-1
+      slot1-2
+      slot2-1
+      slot2-2`;
+  await checkTreeFromRootSelector(tree, "test-component", inspector);
+
+  info("Listening for the markupmutation event");
+  let mutated = inspector.once("markupmutation");
+  ContentTask.spawn(gBrowser.selectedBrowser, {}, function() {
+    content.document.getElementById("to-update").setAttribute("slot", "slot1");
+  });
+  await mutated;
+
+  // After mutation we expect slot1 to have one more slotted node, and slot2 one less.
+  const mutatedTree = `
+    test-component
+      #shadow-root
+        name="slot1"
+          div!slotted
+          div!slotted
+          div!slotted
+        name="slot2"
+          div!slotted
+      slot1-1
+      slot1-2
+      slot2-1
+      slot2-2`;
+  await checkTreeFromRootSelector(mutatedTree, "test-component", inspector);
+});
--- a/devtools/client/inspector/markup/test/browser_markup_toggle_closing_tag_line.js
+++ b/devtools/client/inspector/markup/test/browser_markup_toggle_closing_tag_line.js
@@ -12,34 +12,34 @@ const TEST_URL = `data:text/html;charset
 <div class="outer-div"><span>test</span></div>
 <iframe src="data:text/html;charset=utf8,<div>test</div>"></iframe>`;
 
 add_task(async function() {
   let {inspector} = await openInspectorForURL(TEST_URL);
 
   info("Getting the container for .outer-div parent element");
   let container = await getContainerForSelector(".outer-div", inspector);
-  await expandContainer(inspector, container);
+  await expandContainerByClick(inspector, container);
 
   let closeTagLine = container.closeTagLine;
   ok(closeTagLine && closeTagLine.textContent.includes("div"),
      "DIV has a close tag-line with the correct content");
 
   info("Expand the iframe element");
   container = await getContainerForSelector("iframe", inspector);
-  await expandContainer(inspector, container);
+  await expandContainerByClick(inspector, container);
   ok(container.expanded, "iframe is expanded");
   closeTagLine = container.closeTagLine;
   ok(closeTagLine && closeTagLine.textContent.includes("iframe"),
      "IFRAME has a close tag-line with the correct content");
 
   info("Retrieve the nodefront for the #document root inside the iframe");
   let iframe = await getNodeFront("iframe", inspector);
   let {nodes} = await inspector.walker.children(iframe);
   let documentFront = nodes[0];
   ok(documentFront.displayName === "#document", "First child of IFRAME is #document");
 
   info("Expand the iframe's #document node element");
   container = getContainerForNodeFront(documentFront, inspector);
-  await expandContainer(inspector, container);
+  await expandContainerByClick(inspector, container);
   ok(container.expanded, "#document is expanded");
   ok(!container.closeTagLine, "readonly (#document) node has no close tag-line");
 });
--- a/devtools/client/inspector/markup/test/doc_markup_dragdrop.html
+++ b/devtools/client/inspector/markup/test/doc_markup_dragdrop.html
@@ -14,10 +14,26 @@ https://bugzilla.mozilla.org/show_bug.cg
 </head>
 <body>
   <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=858038">Mozilla Bug 858038</a>
   <p id="display"></p>
   <div id="content" style="display: none">
   </div>
   <input id="anonymousParent" /><span id="before">Before<!-- Force not-inline --></span>
   <pre id="test"><span id="firstChild">First</span><span id="middleChild">Middle</span><span id="lastChild">Last</span></pre>  <span id="after">After</span>
+
+  <test-component class="test-component">
+    <div slot="slot1" class="slotted1">slot1-1</div>
+    <div slot="slot1" class="slotted2">slot1-2</div>
+  </test-component>
+
+  <script>
+    "use strict";
+    customElements.define("test-component", class extends HTMLElement {
+      constructor() {
+        super();
+        let shadowRoot = this.attachShadow({mode: "open"});
+        shadowRoot.innerHTML = '<slot class="slot1" name="slot1"></slot>';
+      }
+    });
+  </script>
 </body>
 </html>
--- a/devtools/client/inspector/markup/test/head.js
+++ b/devtools/client/inspector/markup/test/head.js
@@ -614,21 +614,8 @@ async function checkDeleteAndSelection(i
   let node = await getNodeFront(selector, inspector);
   ok(!node, "The node can't be found in the page anymore");
 
   info("Undo the deletion to restore the original markup");
   await undoChange(inspector);
   node = await getNodeFront(selector, inspector);
   ok(node, "The node is back");
 }
-
-/**
- * Expand the provided markup container by clicking on the expand arrow and waiting for
- * inspector and children to update.
- */
-async function expandContainer(inspector, container) {
-  let onChildren = waitForChildrenUpdated(inspector);
-  let onUpdated = inspector.once("inspector-updated");
-  EventUtils.synthesizeMouseAtCenter(container.expander, {},
-    inspector.markup.doc.defaultView);
-  await onChildren;
-  await onUpdated;
-}
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/markup/test/helper_shadowdom.js
@@ -0,0 +1,142 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+
+/* import-globals-from head.js */
+
+"use strict";
+
+async function checkTreeFromRootSelector(tree, selector, inspector) {
+  let {markup} = inspector;
+
+  info(`Find and expand the shadow DOM host matching selector ${selector}.`);
+  let rootFront = await getNodeFront(selector, inspector);
+  let rootContainer = markup.getContainer(rootFront);
+
+  let parsedTree = parseTree(tree);
+  let treeRoot = parsedTree.children[0];
+  await checkNode(treeRoot, rootContainer, inspector);
+}
+
+async function checkNode(treeNode, container, inspector) {
+  let {node, children, path} = treeNode;
+  info("Checking [" + path + "]");
+  info("Checking node: " + node);
+
+  let slotted = node.includes("!slotted");
+  if (slotted) {
+    checkSlotted(container, node.replace("!slotted", ""));
+  } else {
+    checkText(container, node);
+  }
+
+  if (!children.length) {
+    ok(!container.canExpand, "Container for [" + path + "] has no children");
+    return;
+  }
+
+  // Expand the container if not already done.
+  if (!container.expanded) {
+    await expandContainer(inspector, container);
+  }
+
+  let containers = container.getChildContainers();
+  is(containers.length, children.length,
+     "Node [" + path + "] has the expected number of children");
+  for (let i = 0; i < children.length; i++) {
+    await checkNode(children[i], containers[i], inspector);
+  }
+}
+
+/**
+ * Helper designed to parse a tree represented as:
+ * root
+ *   child1
+ *     subchild1
+ *     subchild2
+ *   child2
+ *     subchild3!slotted
+ *
+ * Lines represent a simplified view of the markup, where the trimmed line is supposed to
+ * be included in the text content of the actual markupview container.
+ * This method returns an object that can be passed to checkNode() to verify the current
+ * markup view displays the expected structure.
+ */
+function parseTree(inputString) {
+  let tree = {
+    level: 0,
+    children: []
+  };
+  let lines = inputString.split("\n");
+  lines = lines.filter(l => l.trim());
+
+  let currentNode = tree;
+  for (let line of lines) {
+    let nodeString = line.trim();
+    let level = line.split("  ").length;
+
+    let parent;
+    if (level > currentNode.level) {
+      parent = currentNode;
+    } else {
+      parent = currentNode.parent;
+      for (let i = 0; i < currentNode.level - level; i++) {
+        parent = parent.parent;
+      }
+    }
+
+    let node = {
+      node: nodeString,
+      children: [],
+      parent,
+      level,
+      path: parent.path + " " + nodeString
+    };
+
+    parent.children.push(node);
+    currentNode = node;
+  }
+
+  return tree;
+}
+
+function checkSlotted(container, expectedType = "div") {
+  checkText(container, expectedType);
+  ok(container.isSlotted(), "Container is a slotted container");
+  ok(container.elt.querySelector(".reveal-link"),
+     "Slotted container has a reveal link element");
+}
+
+function checkText(container, expectedText) {
+  let textContent = container.elt.textContent;
+  ok(textContent.includes(expectedText), "Container has expected text: " + expectedText);
+}
+
+function waitForMutation(inspector, type) {
+  return waitForNMutations(inspector, type, 1);
+}
+
+function waitForNMutations(inspector, type, count) {
+  info(`Expecting ${count} markupmutation of type ${type}`);
+  let receivedMutations = 0;
+  return new Promise(resolve => {
+    inspector.on("markupmutation", function onMutation(mutations) {
+      let validMutations = mutations.filter(m => m.type === type).length;
+      receivedMutations = receivedMutations + validMutations;
+      if (receivedMutations == count) {
+        inspector.off("markupmutation", onMutation);
+        resolve();
+      }
+    });
+  });
+}
+
+/**
+ * Temporarily flip all the preferences needed to enable web components.
+ */
+async function enableWebComponents() {
+  await pushPref("dom.webcomponents.shadowdom.enabled", true);
+  await pushPref("dom.webcomponents.customelements.enabled", true);
+}
--- a/devtools/client/inspector/rules/test/browser.ini
+++ b/devtools/client/inspector/rules/test/browser.ini
@@ -241,16 +241,18 @@ subsuite = clipboard
 skip-if = (os == 'linux' && bits == 32 && debug) # bug 1328915, disable linux32 debug devtools for timeouts
 [browser_rules_selector-highlighter-on-navigate.js]
 [browser_rules_selector-highlighter_01.js]
 [browser_rules_selector-highlighter_02.js]
 [browser_rules_selector-highlighter_03.js]
 [browser_rules_selector-highlighter_04.js]
 [browser_rules_selector-highlighter_05.js]
 [browser_rules_selector_highlight.js]
+[browser_rules_shadowdom_slot_rules.js]
+skip-if = !stylo # shadow DOM is only enabled with stylo.
 [browser_rules_shapes-toggle_01.js]
 [browser_rules_shapes-toggle_02.js]
 [browser_rules_shapes-toggle_03.js]
 [browser_rules_shapes-toggle_04.js]
 [browser_rules_shapes-toggle_05.js]
 [browser_rules_shapes-toggle_06.js]
 [browser_rules_shapes-toggle_07.js]
 [browser_rules_shorthand-overridden-lists.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_shadowdom_slot_rules.js
@@ -0,0 +1,83 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that when selecting a slot element, the rule view displays the rules for the
+// corresponding element.
+
+const TEST_URL = `data:text/html;charset=utf-8,` + encodeURIComponent(`
+  <html>
+  <head>
+  <style>
+    #el1 { color: red }
+    #el2 { color: blue }
+  </style>
+  </head>
+  <body>
+  <test-component>
+    <div slot="slot1" id="el1">slot1-1</div>
+    <div slot="slot1" id="el2">slot1-2</div>
+  </test-component>
+
+  <script>
+    'use strict';
+    customElements.define('test-component', class extends HTMLElement {
+      constructor() {
+        super();
+        let shadowRoot = this.attachShadow({mode: 'open'});
+        shadowRoot.innerHTML = '<slot name="slot1"></slot>';
+      }
+    });
+  </script>
+  </body>
+  </html>
+`);
+
+add_task(async function() {
+  await pushPref("dom.webcomponents.shadowdom.enabled", true);
+  await pushPref("dom.webcomponents.customelements.enabled", true);
+
+  let {inspector} = await openInspectorForURL(TEST_URL);
+  let {markup} = inspector;
+  let ruleview = inspector.getPanel("ruleview").view;
+
+  // <test-component> is a shadow host.
+  info("Find and expand the test-component shadow DOM host.");
+  let hostFront = await getNodeFront("test-component", inspector);
+
+  await markup.expandNode(hostFront);
+  await waitForMultipleChildrenUpdates(inspector);
+
+  info("Test that expanding a shadow host shows shadow root and one host child.");
+  let hostContainer = markup.getContainer(hostFront);
+
+  info("Expand the shadow root");
+  let childContainers = hostContainer.getChildContainers();
+  let shadowRootContainer = childContainers[0];
+  await expandContainer(inspector, shadowRootContainer);
+
+  info("Expand the slot");
+  let shadowChildContainers = shadowRootContainer.getChildContainers();
+  let slotContainer = shadowChildContainers[0];
+  await expandContainer(inspector, slotContainer);
+
+  let slotChildContainers = slotContainer.getChildContainers();
+  is(slotChildContainers.length, 2, "Expecting 2 slotted children");
+
+  info("Select slotted node and check that the rule view displays correct content");
+  await selectNode(slotChildContainers[0].node, inspector);
+  checkRule(ruleview, "#el1", "color", "red");
+
+  info("Select another slotted node and check the rule view");
+  await selectNode(slotChildContainers[1].node, inspector);
+  checkRule(ruleview, "#el2", "color", "blue");
+});
+
+function checkRule(ruleview, selector, name, expectedValue) {
+  let rule = getRuleViewRule(ruleview, selector);
+  ok(rule, "ruleview shows the expected rule for slotted " + selector);
+  let value = getRuleViewPropertyValue(ruleview, selector, name);
+  is(value, expectedValue, "ruleview shows the expected value for slotted " + selector);
+}
--- a/devtools/client/inspector/test/browser.ini
+++ b/devtools/client/inspector/test/browser.ini
@@ -54,16 +54,18 @@ support-files =
 [browser_inspector_addSidebarTab.js]
 [browser_inspector_breadcrumbs.js]
 [browser_inspector_breadcrumbs_highlight_hover.js]
 [browser_inspector_breadcrumbs_keybinding.js]
 [browser_inspector_breadcrumbs_keyboard_trap.js]
 skip-if = os == "mac" # Full keyboard navigation on OSX only works if Full Keyboard Access setting is set to All Control in System Keyboard Preferences
 [browser_inspector_breadcrumbs_mutations.js]
 [browser_inspector_breadcrumbs_namespaced.js]
+[browser_inspector_breadcrumbs_shadowdom.js]
+skip-if = !stylo # shadow DOM is only enabled with stylo.
 [browser_inspector_breadcrumbs_visibility.js]
 [browser_inspector_delete-selected-node-01.js]
 [browser_inspector_delete-selected-node-02.js]
 [browser_inspector_delete-selected-node-03.js]
 [browser_inspector_destroy-after-navigation.js]
 [browser_inspector_destroy-before-ready.js]
 [browser_inspector_expand-collapse.js]
 [browser_inspector_gcli-inspect-command.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_breadcrumbs_shadowdom.js
@@ -0,0 +1,86 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that the breadcrumbs widget refreshes correctly when there are markup
+// mutations, even if the currently selected node is a slotted node in the shadow DOM.
+
+const TEST_URL = `data:text/html;charset=utf-8,
+  <test-component>
+    <div slot="slot1" id="el1">content</div>
+  </test-component>
+
+  <script>
+    'use strict';
+    customElements.define('test-component', class extends HTMLElement {
+      constructor() {
+        super();
+        let shadowRoot = this.attachShadow({mode: 'open'});
+        shadowRoot.innerHTML = '<slot class="slot-class" name="slot1"></slot>';
+      }
+    });
+  </script>`;
+
+add_task(async function() {
+  await pushPref("dom.webcomponents.shadowdom.enabled", true);
+  await pushPref("dom.webcomponents.customelements.enabled", true);
+
+  let {inspector} = await openInspectorForURL(TEST_URL);
+  let {markup} = inspector;
+  let breadcrumbs = inspector.panelDoc.getElementById("inspector-breadcrumbs");
+
+  info("Find and expand the test-component shadow DOM host.");
+  let hostFront = await getNodeFront("test-component", inspector);
+  let hostContainer = markup.getContainer(hostFront);
+  await expandContainer(inspector, hostContainer);
+
+  info("Expand the shadow root");
+  let shadowRootContainer = hostContainer.getChildContainers()[0];
+  await expandContainer(inspector, shadowRootContainer);
+
+  let slotContainer = shadowRootContainer.getChildContainers()[0];
+
+  info("Select the slot node and wait for the breadcrumbs update");
+  let slotNodeFront = slotContainer.node;
+  let onBreadcrumbsUpdated = inspector.once("breadcrumbs-updated");
+  inspector.selection.setNodeFront(slotNodeFront);
+  await onBreadcrumbsUpdated;
+
+  checkBreadcrumbsContent(breadcrumbs,
+    ["html", "body", "test-component", "slot.slot-class"]);
+
+  info("Expand the slot");
+  await expandContainer(inspector, slotContainer);
+
+  let slotChildContainers = slotContainer.getChildContainers();
+  is(slotChildContainers.length, 1, "Expecting 1 slotted child");
+
+  info("Select the slotted node and wait for the breadcrumbs update");
+  let slottedNodeFront = slotChildContainers[0].node;
+  onBreadcrumbsUpdated = inspector.once("breadcrumbs-updated");
+  inspector.selection.setNodeFront(slottedNodeFront);
+  await onBreadcrumbsUpdated;
+
+  checkBreadcrumbsContent(breadcrumbs, ["html", "body", "test-component", "div#el1"]);
+
+  info("Update the classname of the real element and wait for the breadcrumbs update");
+  onBreadcrumbsUpdated = inspector.once("breadcrumbs-updated");
+  await ContentTask.spawn(gBrowser.selectedBrowser, {}, function() {
+    content.document.getElementById("el1").setAttribute("class", "test");
+  });
+  await onBreadcrumbsUpdated;
+
+  checkBreadcrumbsContent(breadcrumbs,
+    ["html", "body", "test-component", "div#el1.test"]);
+});
+
+function checkBreadcrumbsContent(breadcrumbs, selectors) {
+  info("Check the output of the breadcrumbs widget");
+  let container = breadcrumbs.querySelector(".html-arrowscrollbox-inner");
+  is(container.childNodes.length, selectors.length, "Correct number of buttons");
+  for (let i = 0; i < container.childNodes.length; i++) {
+    is(container.childNodes[i].textContent, selectors[i],
+      "Text content for button " + i + " is correct");
+  }
+}
--- a/devtools/client/inspector/test/head.js
+++ b/devtools/client/inspector/test/head.js
@@ -833,8 +833,31 @@ async function toggleShapesHighlighter(v
     shapesToggle.click();
     await onHighlighterShown;
   } else {
     let onHighlighterHidden = highlighters.once("shapes-highlighter-hidden");
     shapesToggle.click();
     await onHighlighterHidden;
   }
 }
+
+/**
+ * Expand the provided markup container programatically and  wait for all children to
+ * update.
+ */
+async function expandContainer(inspector, container) {
+  await inspector.markup.expandNode(container.node);
+  await waitForMultipleChildrenUpdates(inspector);
+}
+
+/**
+ * Expand the provided markup container by clicking on the expand arrow and waiting for
+ * inspector and children to update. Similar to expandContainer helper, but this method
+ * uses a click rather than programatically calling expandNode().
+ */
+async function expandContainerByClick(inspector, container) {
+  let onChildren = waitForChildrenUpdated(inspector);
+  let onUpdated = inspector.once("inspector-updated");
+  EventUtils.synthesizeMouseAtCenter(container.expander, {},
+    inspector.markup.doc.defaultView);
+  await onChildren;
+  await onUpdated;
+}
--- a/devtools/client/inspector/test/shared-head.js
+++ b/devtools/client/inspector/test/shared-head.js
@@ -196,21 +196,21 @@ function getNodeFront(selector, {walker}
  * selector
  * @param {String|NodeFront} selector
  * @param {InspectorPanel} inspector The instance of InspectorPanel currently
  * loaded in the toolbox
  * @param {String} reason Defaults to "test" which instructs the inspector not
  * to highlight the node upon selection
  * @return {Promise} Resolves when the inspector is updated with the new node
  */
-var selectNode = async function(selector, inspector, reason = "test") {
+var selectNode = async function(selector, inspector, reason = "test", isSlotted) {
   info("Selecting the node for '" + selector + "'");
   let nodeFront = await getNodeFront(selector, inspector);
   let updated = inspector.once("inspector-updated");
-  inspector.selection.setNodeFront(nodeFront, { reason });
+  inspector.selection.setNodeFront(nodeFront, { reason, isSlotted });
   await updated;
 };
 
 /**
  * Create a throttling function that can be manually "flushed". This is to replace the
  * use of the `debounce` function from `devtools/client/inspector/shared/utils.js`, which
  * has a setTimeout that can cause intermittents.
  * @return {Function} This function has the same function signature as debounce, but