Bug 1053898 - Integration tests for DevTools shadow dom draft
authorJulian Descottes <jdescottes@mozilla.com>
Tue, 13 Feb 2018 20:03:39 +0100
changeset 760366 7c338e4ebdc305d4554a2dd94e3a68ca94bfab38
parent 760365 5e8f0be46107a7cba57b3be1c552ac5793a0f3ac
child 760367 3d4a3721a851bbe5a83e64c79a39fca6b47bb582
push id100612
push userjdescottes@mozilla.com
push dateTue, 27 Feb 2018 12:40:30 +0000
bugs1053898
milestone60.0a1
Bug 1053898 - Integration tests for DevTools shadow dom MozReview-Commit-ID: 7C56R5ZMQ4B
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_emptycomponent.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_nested.js
devtools/client/inspector/markup/test/browser_markup_shadowdom_pseudo.js
devtools/client/inspector/markup/test/browser_markup_shadowdom_slotupdate.js
devtools/client/inspector/markup/test/doc_markup_dragdrop.html
devtools/client/inspector/markup/test/doc_markup_shadowdom.html
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/shared-head.js
--- a/devtools/client/inspector/markup/test/browser.ini
+++ b/devtools/client/inspector/markup/test/browser.ini
@@ -28,16 +28,17 @@ support-files =
   doc_markup_image_and_canvas_2.html
   doc_markup_links.html
   doc_markup_mutation.html
   doc_markup_navigation.html
   doc_markup_not_displayed.html
   doc_markup_pagesize_01.html
   doc_markup_pagesize_02.html
   doc_markup_search.html
+  doc_markup_shadowdom.html
   doc_markup_svg_attributes.html
   doc_markup_toggle.html
   doc_markup_tooltip.png
   doc_markup_void_elements.html
   doc_markup_void_elements.xhtml
   doc_markup_whitespace.html
   doc_markup_xul.xul
   doc_markup_update-on-navigtion_1.html
@@ -46,16 +47,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
@@ -161,16 +163,26 @@ 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]
+[browser_markup_shadowdom_clickreveal.js]
+[browser_markup_shadowdom_delete.js]
+[browser_markup_shadowdom_emptycomponent.js]
+[browser_markup_shadowdom_maxchildren.js]
+[browser_markup_shadowdom_mutations_shadow.js]
+[browser_markup_shadowdom_navigation.js]
+[browser_markup_shadowdom_nested.js]
+[browser_markup_shadowdom_pseudo.js]
+[browser_markup_shadowdom_slotupdate.js]
 [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,48 +1,66 @@
 /* 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(function* () {
-  Services.prefs.setBoolPref(PREF, true);
+add_task(async function () {
+  await pushPref("devtools.inspector.showAllAnonymousContent", true);
+  await pushPref("dom.webcomponents.shadowdom.enabled", true);
 
-  let {inspector} = yield openInspectorForURL(TEST_URL);
+  let {inspector} = await openInspectorForURL(TEST_URL);
 
   info("Expanding nodes below #test");
-  let parentFront = yield getNodeFront("#test", inspector);
-  yield inspector.markup.expandNode(parentFront);
-  yield waitForMultipleChildrenUpdates(inspector);
+  let parentFront = await getNodeFront("#test", inspector);
+  await inspector.markup.expandNode(parentFront);
+  await waitForMultipleChildrenUpdates(inspector);
 
   info("Getting the ::before pseudo element and selecting it");
-  let parentContainer = yield getContainerForNodeFront(parentFront, inspector);
+  let parentContainer = getContainerForNodeFront(parentFront, inspector);
   let beforePseudo = parentContainer.elt.children[1].firstChild.container;
   parentContainer.elt.scrollIntoView(true);
-  yield selectNode(beforePseudo.node, inspector);
+  await selectNode(beforePseudo.node, inspector);
 
   info("Simulate dragging the ::before pseudo element");
-  yield simulateNodeDrag(inspector, beforePseudo);
+  await simulateNodeDrag(inspector, beforePseudo);
 
   ok(!beforePseudo.isDragging, "::before pseudo element isn't dragging");
 
   info("Expanding nodes below #anonymousParent");
-  let inputFront = yield getNodeFront("#anonymousParent", inspector);
-  yield inspector.markup.expandNode(inputFront);
-  yield waitForMultipleChildrenUpdates(inspector);
+  let inputFront = await getNodeFront("#anonymousParent", inspector);
+  await inspector.markup.expandNode(inputFront);
+  await waitForMultipleChildrenUpdates(inspector);
 
   info("Getting the anonymous node and selecting it");
-  let inputContainer = yield getContainerForNodeFront(inputFront, inspector);
+  let inputContainer = getContainerForNodeFront(inputFront, inspector);
   let anonymousDiv = inputContainer.elt.children[1].firstChild.container;
   inputContainer.elt.scrollIntoView(true);
-  yield selectNode(anonymousDiv.node, inspector);
+  await selectNode(anonymousDiv.node, inspector);
 
   info("Simulate dragging the anonymous node");
-  yield simulateNodeDrag(inspector, anonymousDiv);
+  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,61 @@
+/* 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 expanding a shadow host shows a shadow root node and the light dom.
+// 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`;
+
+add_task(async function () {
+  await pushPref("dom.webcomponents.shadowdom.enabled", true);
+
+  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 light DOM 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 pushPref("dom.webcomponents.shadowdom.enabled", true);
+
+  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("Follow the other 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"></slot>';
+      }
+    });
+  </script>`;
+
+add_task(async function () {
+  await pushPref("dom.webcomponents.shadowdom.enabled", true);
+
+  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 a shadow root node and the light dom.");
+  let {markup} = inspector;
+  let hostContainer = markup.getContainer(hostFront);
+  let childContainers = hostContainer.getChildContainers();
+
+  is(childContainers.length, 3, "Expecting 3 children: shadowroot, 2 light dom divs");
+  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();
+  is(slotChildContainers.length, 0, "Expecting 0 children");
+
+  // TODO: Check that the markup view falls back to default content from the slot when
+  // https://bugzilla.mozilla.org/show_bug.cgi?id=1438210 is fixed.
+});
+
+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_emptycomponent.js
@@ -0,0 +1,38 @@
+/* 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 components without any light DOM 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`;
+
+add_task(async function () {
+  await pushPref("dom.webcomponents.shadowdom.enabled", true);
+
+  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_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 pushPref("dom.webcomponents.shadowdom.enabled", true);
+  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 a shadow root node and the light dom.");
+  let {markup} = inspector;
+  let hostContainer = markup.getContainer(hostFront);
+  let childContainers = hostContainer.getChildContainers();
+
+  is(childContainers.length, 6, "Expecting 6 children: shadowroot and 5 light dom divs");
+  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");
+  EventUtils.sendMouseEvent({type: "click"}, moreNodesLink.querySelector("button"));
+  await inspector.markup._waitForChildren();
+
+  childContainers = hostContainer.getChildContainers();
+  is(childContainers.length, 7, "Expecting one additional light dom div");
+  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 hsot 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 pushPref("dom.webcomponents.shadowdom.enabled", true);
+
+  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 = onMutationOfType(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,86 @@
+/* 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 correclty 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 light DOM element selects the light DOM element.
+
+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);
+
+  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_nested.js
@@ -0,0 +1,69 @@
+/* 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 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`;
+
+add_task(async function () {
+  await pushPref("dom.webcomponents.shadowdom.enabled", true);
+
+  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_pseudo.js
@@ -0,0 +1,60 @@
+/* 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 ::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`;
+
+add_task(async function () {
+  await pushPref("dom.webcomponents.shadowdom.enabled", true);
+
+  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_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 pushPref("dom.webcomponents.shadowdom.enabled", true);
+
+  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/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>
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/markup/test/doc_markup_shadowdom.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<html>
+<body>
+
+<test-component>
+  <div slot="slot1">slotted-1<div>inner</div></div>
+  <div slot="slot2">slotted-2<div>inner</div></div>
+  <div>no-slot-defined<div>inner</div></div>
+</test-component>
+
+<script>
+  "use strict";
+  (function () {
+    customElements.define("test-component", class extends HTMLElement {
+      constructor() {
+        super(); // always call super() first in the ctor.
+
+        // Create shadow DOM for the component.
+        let shadowRoot = this.attachShadow({mode: "open"});
+        shadowRoot.innerHTML = `
+          <slot name="slot1"></slot>
+          <slot name="slot2"></slot>
+          <slot></slot>
+        `;
+      }
+      connectedCallback() {}
+      disconnectedCallback() {}
+    });
+  })();
+</script>
+
+</body>
+</html>
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/markup/test/helper_shadowdom.js
@@ -0,0 +1,132 @@
+/* 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 test-component shadow DOM host.");
+  let rootFront = await getNodeFront("test-component", 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);
+}
+
+async function expandContainer(inspector, container) {
+  await inspector.markup.expandNode(container.node);
+  await waitForMultipleChildrenUpdates(inspector);
+}
+
+function onMutationOfType(inspector, type) {
+  info("Listening for a " + type + " markupmutation event");
+  return new Promise(resolve => {
+    inspector.on("markupmutation", function onMutation(event, mutations) {
+      if (mutations.some(m => m.type === type)) {
+        inspector.off("markupmutation", onMutation);
+        resolve();
+      }
+    });
+  });
+}
--- a/devtools/client/inspector/rules/test/browser.ini
+++ b/devtools/client/inspector/rules/test/browser.ini
@@ -241,16 +241,17 @@ 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]
 [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,88 @@
+/* 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);
+
+  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 a shadow root node and the light dom.");
+  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);
+}
+
+async function expandContainer(inspector, container) {
+  await inspector.markup.expandNode(container.node);
+  await waitForMultipleChildrenUpdates(inspector);
+}
+
--- a/devtools/client/inspector/test/browser.ini
+++ b/devtools/client/inspector/test/browser.ini
@@ -48,16 +48,17 @@ support-files =
   !/devtools/client/shared/test/test-actor.js
   !/devtools/client/shared/test/test-actor-registry.js
 
 [browser_inspector_addNode_01.js]
 [browser_inspector_addNode_02.js]
 [browser_inspector_addNode_03.js]
 [browser_inspector_addSidebarTab.js]
 [browser_inspector_breadcrumbs.js]
+[browser_inspector_breadcrumbs_shadowdom.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_visibility.js]
 [browser_inspector_delete-selected-node-01.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_breadcrumbs_shadowdom.js
@@ -0,0 +1,91 @@
+/* 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 (and that it doesn't refresh when those mutations don't change its
+// output).
+
+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);
+
+  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");
+  }
+}
+
+async function expandContainer(inspector, container) {
+  await inspector.markup.expandNode(container.node);
+  await waitForMultipleChildrenUpdates(inspector);
+}
--- a/devtools/client/inspector/test/shared-head.js
+++ b/devtools/client/inspector/test/shared-head.js
@@ -183,21 +183,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 = Task.async(function* (selector, inspector, reason = "test") {
+var selectNode = Task.async(function* (selector, inspector, reason = "test", slotted) {
   info("Selecting the node for '" + selector + "'");
   let nodeFront = yield getNodeFront(selector, inspector);
   let updated = inspector.once("inspector-updated");
-  inspector.selection.setNodeFront(nodeFront, reason);
+  inspector.selection.setNodeFront(nodeFront, reason, slotted);
   yield 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