Bug 1053898 - WIP: support and display shadow nodes draft
authorJulian Descottes <jdescottes@mozilla.com>
Tue, 06 Feb 2018 23:15:29 +0100
changeset 753088 160b0fe0a2d5dfb1691b4115fc9b6ba61eef1e18
parent 753087 775eedd248d4c155efdb23c3fb126c6ec0828406
push id98478
push userjdescottes@mozilla.com
push dateFri, 09 Feb 2018 16:32:07 +0000
bugs1053898
milestone60.0a1
Bug 1053898 - WIP: support and display shadow nodes MozReview-Commit-ID: FdrP78zczzG
devtools/client/inspector/inspector.js
devtools/client/inspector/markup/markup.js
devtools/client/inspector/markup/views/moz.build
devtools/client/inspector/markup/views/shadow-node-container.js
devtools/client/inspector/markup/views/shadow-node-editor.js
devtools/client/inspector/markup/views/slotted-node-container.js
devtools/client/inspector/markup/views/slotted-node-editor.js
devtools/server/actors/highlighters/selector.js
devtools/server/actors/inspector/document-walker.js
devtools/server/actors/inspector/moz.build
devtools/server/actors/inspector/node-actor.js
devtools/server/actors/inspector/shadow-node-actor.js
devtools/server/actors/inspector/slotted-node-actor.js
devtools/server/actors/inspector/walker-actor.js
devtools/shared/fronts/node.js
devtools/shared/specs/node.js
toolkit/modules/css-selector.js
--- a/devtools/client/inspector/inspector.js
+++ b/devtools/client/inspector/inspector.js
@@ -1148,40 +1148,40 @@ Inspector.prototype = {
    */
   onNewSelection: function (event, value, reason) {
     if (reason === "selection-destroy") {
       return;
     }
 
     // Wait for all the known tools to finish updating and then let the
     // client know.
-    let selection = this.selection.nodeFront;
+    let selectionNodeFront = this.selection.nodeFront;
 
     // Update the state of the add button in the toolbar depending on the
     // current selection.
     let btn = this.panelDoc.querySelector("#inspector-element-add-button");
     if (this.canAddHTMLChild()) {
       btn.removeAttribute("disabled");
     } else {
       btn.setAttribute("disabled", "true");
     }
 
     // On any new selection made by the user, store the unique css selector
     // of the selected node so it can be restored after reload of the same page
     if (this.canGetUniqueSelector &&
         this.selection.isElementNode()) {
-      selection.getUniqueSelector().then(selector => {
+      selectionNodeFront.getUniqueSelector().then(selector => {
         this.selectionCssSelector = selector;
       }, this._handleRejectionIfNotDestroyed);
     }
 
     let selfUpdate = this.updating("inspector-panel");
     executeSoon(() => {
       try {
-        selfUpdate(selection);
+        selfUpdate(selectionNodeFront);
       } catch (ex) {
         console.error(ex);
       }
     });
   },
 
   /**
    * Delay the "inspector-updated" notification while a tool
--- a/devtools/client/inspector/markup/markup.js
+++ b/devtools/client/inspector/markup/markup.js
@@ -13,17 +13,17 @@ const EventEmitter = require("devtools/s
 const {LocalizationHelper} = require("devtools/shared/l10n");
 const {PluralForm} = require("devtools/shared/plural-form");
 const AutocompletePopup = require("devtools/client/shared/autocomplete-popup");
 const KeyShortcuts = require("devtools/client/shared/key-shortcuts");
 const {scrollIntoViewIfNeeded} = require("devtools/client/shared/scroll");
 const {UndoStack} = require("devtools/client/shared/undo");
 const {HTMLTooltip} = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
 const {PrefObserver} = require("devtools/client/shared/prefs");
-const ShadowNodeContainer = require("devtools/client/inspector/markup/views/shadow-node-container");
+const SlottedNodeContainer = require("devtools/client/inspector/markup/views/slotted-node-container");
 const MarkupElementContainer = require("devtools/client/inspector/markup/views/element-container");
 const MarkupReadOnlyContainer = require("devtools/client/inspector/markup/views/read-only-container");
 const MarkupTextContainer = require("devtools/client/inspector/markup/views/text-container");
 const RootContainer = require("devtools/client/inspector/markup/views/root-container");
 
 const INSPECTOR_L10N =
       new LocalizationHelper("devtools/client/locales/inspector.properties");
 
@@ -957,23 +957,23 @@ MarkupView.prototype = {
       return null;
     }
 
     if (this._containers.has(node)) {
       return this.getContainer(node);
     }
 
     let container;
-    let {nodeType, isPseudoElement, isShadowNode} = node;
+    let {nodeType, isPseudoElement, isSlottedNode} = node;
     if (node === this.walker.rootNode) {
       container = new RootContainer(this, node);
       this._elt.appendChild(container.elt);
       this._rootNode = node;
-    } else if (isShadowNode) {
-      container = new ShadowNodeContainer(this, node, this.inspector);
+    } else if (isSlottedNode) {
+      container = new SlottedNodeContainer(this, node, this.inspector);
     } else if (nodeType == nodeConstants.ELEMENT_NODE && !isPseudoElement) {
       container = new MarkupElementContainer(this, node, this.inspector);
     } else if (nodeType == nodeConstants.COMMENT_NODE ||
                nodeType == nodeConstants.TEXT_NODE) {
       container = new MarkupTextContainer(this, node, this.inspector);
     } else {
       container = new MarkupReadOnlyContainer(this, node, this.inspector);
     }
--- a/devtools/client/inspector/markup/views/moz.build
+++ b/devtools/client/inspector/markup/views/moz.build
@@ -7,13 +7,13 @@
 DevToolsModules(
     'element-container.js',
     'element-editor.js',
     'html-editor.js',
     'markup-container.js',
     'read-only-container.js',
     'read-only-editor.js',
     'root-container.js',
-    'shadow-node-container.js',
-    'shadow-node-editor.js',
+    'slotted-node-container.js',
+    'slotted-node-editor.js',
     'text-container.js',
     'text-editor.js',
 )
rename from devtools/client/inspector/markup/views/shadow-node-container.js
rename to devtools/client/inspector/markup/views/slotted-node-container.js
--- a/devtools/client/inspector/markup/views/shadow-node-container.js
+++ b/devtools/client/inspector/markup/views/slotted-node-container.js
@@ -1,38 +1,38 @@
 /* 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/. */
 
 "use strict";
 
-const ShadowNodeEditor = require("devtools/client/inspector/markup/views/shadow-node-editor");
+const SlottedNodeEditor = require("devtools/client/inspector/markup/views/slotted-node-editor");
 const MarkupContainer = require("devtools/client/inspector/markup/views/markup-container");
 const {extend} = require("devtools/shared/extend");
 
 /**
  * An implementation of MarkupContainer for Pseudo Elements,
  * Doctype nodes, or any other type generic node that doesn't
  * fit for other editors.
  * Does not allow any editing, just viewing / selecting.
  *
  * @param  {MarkupView} markupView
  *         The markup view that owns this container.
  * @param  {NodeFront} node
  *         The node to display.
  */
-function ShadowNodeContainer(markupView, node) {
+function SlottedNodeContainer(markupView, node) {
   MarkupContainer.prototype.initialize.call(this, markupView, node,
     "readonlycontainer");
 
-  this.editor = new ShadowNodeEditor(this, node);
+  this.editor = new SlottedNodeEditor(this, node);
   this.tagLine.appendChild(this.editor.elt);
 }
 
-ShadowNodeContainer.prototype = extend(MarkupContainer.prototype, {
+SlottedNodeContainer.prototype = extend(MarkupContainer.prototype, {
   _onMouseDown: function (event) {
     if (event.target.classList.contains("reveal-link")) {
       event.stopPropagation();
       event.preventDefault();
       return;
     }
     MarkupContainer.prototype._onMouseDown.call(this, event);
   },
@@ -42,9 +42,9 @@ ShadowNodeContainer.prototype = extend(M
       let actorID = this.node._form.nodeActor;
       let walkerFront = this.markup.inspector.walker;
       let nodeFront = await walkerFront.getNodeFromActor(actorID, []);
       this.markup.inspector.selection.setNodeFront(nodeFront);
     }
   }
 });
 
-module.exports = ShadowNodeContainer;
+module.exports = SlottedNodeContainer;
rename from devtools/client/inspector/markup/views/shadow-node-editor.js
rename to devtools/client/inspector/markup/views/slotted-node-editor.js
--- a/devtools/server/actors/highlighters/selector.js
+++ b/devtools/server/actors/highlighters/selector.js
@@ -16,16 +16,26 @@ const MAX_HIGHLIGHTED_ELEMENTS = 100;
  * document of the provided context node and then uses the BoxModelHighlighter
  * to highlight the matching nodes
  */
 function SelectorHighlighter(highlighterEnv) {
   this.highlighterEnv = highlighterEnv;
   this._highlighters = [];
 }
 
+function getShadowRoot(node) {
+  let parent = node;
+  while ((parent = parent.parentNode)) {
+    if (parent.nodeType === 11 && parent.mode && parent.host) {
+      return parent;
+    }
+  }
+  return null;
+}
+
 SelectorHighlighter.prototype = {
   typeName: "SelectorHighlighter",
 
   /**
    * Show BoxModelHighlighter on each node that matches that provided selector.
    * @param {DOMNode} node A context node that is used to get the document on
    * which querySelectorAll should be executed. This node will NOT be
    * highlighted.
@@ -35,19 +45,23 @@ SelectorHighlighter.prototype = {
    */
   show: function (node, options = {}) {
     this.hide();
 
     if (!isNodeValid(node) || !options.selector) {
       return false;
     }
 
+    // If the node is in a shadow dom tree, return the shadow root for this element.
+    let shadowRoot = getShadowRoot(node);
+    let document = shadowRoot || node.ownerDocument;
+
     let nodes = [];
     try {
-      nodes = [...node.ownerDocument.querySelectorAll(options.selector)];
+      nodes = [...document.querySelectorAll(options.selector)];
     } catch (e) {
       // It's fine if the provided selector is invalid, nodes will be an empty
       // array.
     }
 
     delete options.selector;
 
     let i = 0;
--- a/devtools/server/actors/inspector/document-walker.js
+++ b/devtools/server/actors/inspector/document-walker.js
@@ -49,16 +49,17 @@ function DocumentWalker(node, rootWin,
 
   this.walker = Cc["@mozilla.org/inspector/deep-tree-walker;1"]
     .createInstance(Ci.inIDeepTreeWalker);
   this.walker.showAnonymousContent = showAnonymousContent;
   this.walker.showSubDocuments = true;
   this.walker.showDocumentsAsNodes = true;
   this.walker.init(rootWin.document, whatToShow);
   this.filter = filter;
+  this.skipTo = skipTo;
 
   // Make sure that the walker knows about the initial node (which could
   // be skipped due to a filter).
   this.walker.currentNode = this.getStartingNode(node, skipTo);
 }
 
 DocumentWalker.prototype = {
 
@@ -67,16 +68,20 @@ DocumentWalker.prototype = {
   },
   get currentNode() {
     return this.walker.currentNode;
   },
   set currentNode(val) {
     this.walker.currentNode = val;
   },
 
+  setStartingNode: function (node) {
+    this.walker.currentNode = this.getStartingNode(node, this.skipTo);
+  },
+
   parentNode: function () {
     return this.walker.parentNode();
   },
 
   nextNode: function () {
     let node = this.walker.currentNode;
     if (!node) {
       return null;
--- a/devtools/server/actors/inspector/moz.build
+++ b/devtools/server/actors/inspector/moz.build
@@ -3,16 +3,16 @@
 # 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/.
 
 DevToolsModules(
   'document-walker.js',
   'inspector-actor.js',
   'node-actor.js',
-  'shadow-node-actor.js',
+  'slotted-node-actor.js',
   'utils.js',
   'walker-actor.js',
   'walker-search.js',
 )
 
 with Files('**'):
     BUG_COMPONENT = ('Firefox', 'Developer Tools: Inspector')
--- a/devtools/server/actors/inspector/node-actor.js
+++ b/devtools/server/actors/inspector/node-actor.js
@@ -108,17 +108,17 @@ const NodeActor = protocol.ActorClassWit
       publicId: this.rawNode.publicId,
       systemId: this.rawNode.systemId,
 
       attrs: this.writeAttrs(),
       isBeforePseudoElement: this.isBeforePseudoElement,
       isAfterPseudoElement: this.isAfterPseudoElement,
       isAnonymous: isAnonymous(this.rawNode),
       isShadowRoot: !!this.rawNode.host,
-      isShadowNode: false,
+      isSlottedNode: false,
       isNativeAnonymous: isNativeAnonymous(this.rawNode),
       isXBLAnonymous: isXBLAnonymous(this.rawNode),
       isShadowAnonymous: isShadowAnonymous(this.rawNode),
       pseudoClassLocks: this.writePseudoClassLocks(),
 
       isDisplayed: this.isDisplayed,
       isInHTMLDocument: this.rawNode.ownerDocument &&
         this.rawNode.ownerDocument.contentType === "text/html",
rename from devtools/server/actors/inspector/shadow-node-actor.js
rename to devtools/server/actors/inspector/slotted-node-actor.js
--- a/devtools/server/actors/inspector/shadow-node-actor.js
+++ b/devtools/server/actors/inspector/slotted-node-actor.js
@@ -1,28 +1,28 @@
 /* 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/. */
 
 "use strict";
 
 const protocol = require("devtools/shared/protocol");
-const {shadowNodeSpec} = require("devtools/shared/specs/node");
+const {slottedNodeSpec} = require("devtools/shared/specs/node");
 
 /**
  * Server side of the node actor.
  */
-const ShadowNodeActor = protocol.ActorClassWithSpec(shadowNodeSpec, {
+const SlottedNodeActor = protocol.ActorClassWithSpec(slottedNodeSpec, {
   initialize: function (nodeActor) {
     protocol.Actor.prototype.initialize.call(this, null);
     this.nodeActor = nodeActor;
   },
 
   toString: function () {
-    return "[ShadowNodeActor " + this.actorID + " for NodeActor" +
+    return "[SlottedNodeActor " + this.actorID + " for NodeActor" +
             this.nodeActor.actorID + "]";
   },
 
   get conn() {
     return this.nodeActor.conn;
   },
 
   isDocumentElement: function () {
@@ -39,17 +39,17 @@ const ShadowNodeActor = protocol.ActorCl
       return this.actorID;
     }
 
     let form = this.nodeActor.form();
     form.actor = this.actorID;
     form.nodeActor = this.nodeActor.actorID;
     form.numChildren = 0;
     form.inlineTextChild = undefined;
-    form.isShadowNode = true;
+    form.isSlottedNode = true;
 
     return form;
   },
 
   watchDocument: function () {
     // no-op
   },
 
@@ -160,9 +160,9 @@ const ShadowNodeActor = protocol.ActorCl
    * rgba(r, g, b, a). Defaults to rgba(255, 255, 255, 1) if no
    * background color is found.
    */
   getClosestBackgroundColor: function () {
     return this.nodeActor.getClosestBackgroundColor();
   }
 });
 
-exports.ShadowNodeActor = ShadowNodeActor;
+exports.SlottedNodeActor = SlottedNodeActor;
--- a/devtools/server/actors/inspector/walker-actor.js
+++ b/devtools/server/actors/inspector/walker-actor.js
@@ -20,17 +20,17 @@ loader.lazyRequireGetter(this, "throttle
 loader.lazyRequireGetter(this, "allAnonymousContentTreeWalkerFilter", "devtools/server/actors/inspector/utils", true);
 loader.lazyRequireGetter(this, "isNodeDead", "devtools/server/actors/inspector/utils", true);
 loader.lazyRequireGetter(this, "nodeDocument", "devtools/server/actors/inspector/utils", true);
 loader.lazyRequireGetter(this, "standardTreeWalkerFilter", "devtools/server/actors/inspector/utils", true);
 
 loader.lazyRequireGetter(this, "DocumentWalker", "devtools/server/actors/inspector/document-walker", true);
 loader.lazyRequireGetter(this, "SKIP_TO_SIBLING", "devtools/server/actors/inspector/document-walker", true);
 loader.lazyRequireGetter(this, "NodeActor", "devtools/server/actors/inspector/node-actor", true);
-loader.lazyRequireGetter(this, "ShadowNodeActor", "devtools/server/actors/inspector/shadow-node-actor", true);
+loader.lazyRequireGetter(this, "SlottedNodeActor", "devtools/server/actors/inspector/slotted-node-actor", true);
 loader.lazyRequireGetter(this, "NodeListActor", "devtools/server/actors/inspector/node-actor", true);
 loader.lazyRequireGetter(this, "WalkerSearch", "devtools/server/actors/inspector/walker-search", true);
 loader.lazyRequireGetter(this, "LayoutActor", "devtools/server/actors/layout", true);
 loader.lazyRequireGetter(this, "getLayoutChangesObserver", "devtools/server/actors/reflow", true);
 loader.lazyRequireGetter(this, "releaseLayoutChangesObserver", "devtools/server/actors/reflow", true);
 
 loader.lazyServiceGetter(this, "eventListenerService",
   "@mozilla.org/eventlistenerservice;1", "nsIEventListenerService");
@@ -118,17 +118,17 @@ var WalkerActor = protocol.ActorClassWit
    *    The server connection.
    */
   initialize: function (conn, tabActor, options) {
     protocol.Actor.prototype.initialize.call(this, conn);
     this.tabActor = tabActor;
     this.rootWin = tabActor.window;
     this.rootDoc = this.rootWin.document;
     this._refMap = new Map();
-    this._shadowRefMap = new Map();
+    this._slottedRefMap = new Map();
     this._pendingMutations = [];
     this._activePseudoClassLocks = new Set();
     this.showAllAnonymousContent = options.showAllAnonymousContent;
 
     this.walkerSearch = new WalkerSearch(this);
 
     // Nodes which have been removed from the client's known
     // ownership tree are considered "orphaned", and stored in
@@ -221,17 +221,17 @@ var WalkerActor = protocol.ActorClassWit
       this._hoveredNode = null;
       this.rootWin = null;
       this.rootDoc = null;
       this.rootNode = null;
       this.layoutHelpers = null;
       this._orphaned = null;
       this._retainedOrphans = null;
       this._refMap = null;
-      this._shadowRefMap = null;
+      this._slottedRefMap = null;
 
       this.tabActor.off("will-navigate", this.onFrameUnload);
       this.tabActor.off("window-ready", this.onFrameLoad);
 
       this.onFrameLoad = null;
       this.onFrameUnload = null;
 
       this.walkerSearch.destroy();
@@ -259,74 +259,83 @@ var WalkerActor = protocol.ActorClassWit
 
   unmanage: function (actor) {
     if (actor instanceof NodeActor) {
       if (this._activePseudoClassLocks &&
           this._activePseudoClassLocks.has(actor)) {
         this.clearPseudoClassLocks(actor);
       }
       this._refMap.delete(actor.rawNode);
-      this._shadowRefMap.delete(actor.rawNode);
+      this._slottedRefMap.delete(actor.rawNode);
     }
     protocol.Actor.prototype.unmanage.call(this, actor);
   },
 
   /**
    * Determine if the walker has come across this DOM node before.
    * @param {DOMNode} rawNode
    * @return {Boolean}
    */
-  hasNode: function (rawNode, shadow) {
-    if (shadow) {
-      return this._shadowRefMap.has(rawNode);
-    }
+  hasNode: function (rawNode) {
     return this._refMap.has(rawNode);
   },
 
+  hasSlottedNode: function (rawNode) {
+    return this._slottedRefMap.has(rawNode);
+  },
+
   /**
    * If the walker has come across this DOM node before, then get the
    * corresponding node actor.
    * @param {DOMNode} rawNode
    * @return {NodeActor}
    */
-  getNode: function (rawNode, shadow = false) {
-    if (shadow) {
-      return this._shadowRefMap.get(rawNode);
-    }
+  getNode: function (rawNode) {
     return this._refMap.get(rawNode);
   },
 
-  _ref: function (node, shadow) {
-    let actor = this.getNode(node, shadow);
+  getSlottedNode: function (rawNode) {
+    return this._slottedRefMap.get(rawNode);
+  },
+
+  _ref: function (node) {
+    let actor = this.getNode(node);
     if (actor) {
       return actor;
     }
 
-    if (shadow) {
-      actor = new ShadowNodeActor(node);
-    } else {
-      actor = new NodeActor(this, node);
-    }
+    actor = new NodeActor(this, node);
 
     // Add the node actor as a child of this walker actor, assigning
     // it an actorID.
     this.manage(actor);
-
-    if (shadow) {
-      this._shadowRefMap.set(node, actor);
-    } else {
-      this._refMap.set(node, actor);
-    }
+    this._refMap.set(node, actor);
 
     if (node.nodeType === Ci.nsIDOMNode.DOCUMENT_NODE) {
       actor.watchDocument(this.onMutations);
     }
     return actor;
   },
 
+  _slottedRef: function (node) {
+    let actor = this.getSlottedNode(node);
+    if (actor) {
+      return actor;
+    }
+
+    let nodeActor = this._ref(node);
+    actor = new SlottedNodeActor(nodeActor);
+
+    // Add the node actor as a child of this walker actor, assigning
+    // it an actorID.
+    this.manage(actor);
+    this._slottedRefMap.set(node, actor);
+    return actor;
+  },
+
   _onReflows: function (reflows) {
     // Going through the nodes the walker knows about, see which ones have
     // had their display changed and send a display-change event if any
     let changes = [];
     for (let [node, actor] of this._refMap) {
       if (Cu.isDeadWrapper(node)) {
         continue;
       }
@@ -463,23 +472,25 @@ var WalkerActor = protocol.ActorClassWit
 
       parents.push(this._ref(cur));
     }
     return parents;
   },
 
   parentNode: function (node) {
     try {
-      let walker;
-      if (node.rawNode && node.rawNode.parentNode &&
-          node.rawNode.parentNode.shadowRoot && !node.nodeActor) {
-        walker = this.getDocumentWalker(node.rawNode, {showAnonymousContent: false});
-      } else {
-        walker = this.getDocumentWalker(node.rawNode);
-      }
+      let parentNode = node.rawNode && node.rawNode.parentNode && node.rawNode.parentNode;
+      let isInLightDOM =
+        !node.nodeActor // not a shadow node actor
+        && !node.isBeforePseudoElement && !node.isAfterPseudoElement // not after/before
+        && parentNode && parentNode.shadowRoot; // parentNode is a shadow host;
+
+      let walker = this.getDocumentWalker(node.rawNode, {
+        showAnonymousContent: !isInLightDOM
+      });
       let parent = walker.parentNode();
       if (parent) {
         return this._ref(parent);
       }
     } catch (e) {
       dump("CAUGHT ERROR\n");
     }
     return null;
@@ -639,34 +650,30 @@ var WalkerActor = protocol.ActorClassWit
       throw Error("Can't specify both 'center' and 'start' options.");
     }
     let maxNodes = options.maxNodes || -1;
     if (maxNodes == -1) {
       maxNodes = Number.MAX_VALUE;
     }
 
     let isShadowHost = !!node.rawNode.shadowRoot;
+    let isShadowRoot = !!node.rawNode.host;
 
-    // We're going to create a few document walkers with the same filter,
-    // make it easier.
-    let getFilteredWalker = (documentWalkerNode, showAnonymousContent) => {
-      let { whatToShow } = options;
-      // Use SKIP_TO_SIBLING to force the walker to use a sibling of the provided node
-      // in case this one is incompatible with the walker's filter function.
-      return this.getDocumentWalker(documentWalkerNode, {
-        whatToShow,
-        skipTo: SKIP_TO_SIBLING,
-        showAnonymousContent: showAnonymousContent || !isShadowHost
-      });
-    };
+    let rawNode = node.rawNode;
+    let walker = this.getDocumentWalker(rawNode, {
+      whatToShow: options.whatToShow,
+      skipTo: SKIP_TO_SIBLING,
+      showAnonymousContent: !isShadowHost && !isShadowRoot
+    });
 
     // Need to know the first and last child.
-    let rawNode = node.rawNode;
-    let firstChild = getFilteredWalker(rawNode).firstChild();
-    let lastChild = getFilteredWalker(rawNode).lastChild();
+    let firstChild = walker.firstChild();
+
+    walker.setStartingNode(rawNode);
+    let lastChild = walker.lastChild();
 
     if (!firstChild) {
       // No children, we're done.
       return { hasFirst: true, hasLast: true, nodes: [] };
     }
 
     let start;
     if (options.center) {
@@ -675,68 +682,108 @@ var WalkerActor = protocol.ActorClassWit
       start = options.start.rawNode;
     } else {
       start = firstChild;
     }
 
     let nodes = [];
 
     // Start by reading backward from the starting point if we're centering...
-    let backwardWalker = getFilteredWalker(start);
-    if (backwardWalker.currentNode != firstChild && options.center) {
-      backwardWalker.previousSibling();
+    walker.setStartingNode(start);
+
+    let lastBackwardNode;
+    if (walker.currentNode != firstChild && options.center) {
+      walker.previousSibling();
       let backwardCount = Math.floor(maxNodes / 2);
-      let backwardNodes = this._readBackward(backwardWalker, backwardCount);
+      let backwardNodes = this._readBackward(walker, backwardCount);
       nodes = backwardNodes;
+      lastBackwardNode = walker.currentNode;
     }
 
     // Then read forward by any slack left in the max children...
-    let forwardWalker = getFilteredWalker(start);
+    walker.setStartingNode(start);
     let forwardCount = maxNodes - nodes.length;
-    nodes = nodes.concat(this._readForward(forwardWalker, forwardCount));
+    nodes = nodes.concat(this._readForward(walker, forwardCount));
 
     // If there's any room left, it means we've run all the way to the end.
     // If we're centering, check if there are more items to read at the front.
     let remaining = maxNodes - nodes.length;
     if (options.center && remaining > 0 && nodes[0].rawNode != firstChild) {
-      let firstNodes = this._readBackward(backwardWalker, remaining);
+      walker.setStartingNode = lastBackwardNode;
+      let firstNodes = this._readBackward(walker, remaining);
 
       // Then put it all back together.
       nodes = firstNodes.concat(nodes);
     }
 
-    nodes = nodes.map(n => {
-      if (n.rawNode.parentNode === node.rawNode) {
-        return n;
-      }
-      return this._ref(n, true);
-    });
+    // Identify slotted elements if they are listed as children of a slot (not when they
+    // are listed as part of the light DOM). Convert their nodeActors to
+    // slottedNodeActors.
+    if (node.rawNode.nodeName === "SLOT") {
+      nodes = this._convertNodesToSlottedNodes(node, nodes);
+    }
 
     if (nodes.length === 0) {
       return { hasFirst: true, hasLast: true, nodes: [] };
     }
 
+    // Compare first/last with expected nodes before modifying the nodes array in case
+    // this is a shadow host.
     let hasFirst = nodes[0].rawNode == firstChild;
     let hasLast = nodes[nodes.length - 1].rawNode == lastChild;
 
     if (isShadowHost) {
-      let before = this._ref(getFilteredWalker(rawNode, true).firstChild());
-      let after = this._ref(getFilteredWalker(rawNode, true).lastChild());
-      if (before.isBeforePseudoElement) {
-        nodes = [before, ...nodes];
-      }
-      if (after.isAfterPseudoElement) {
-        nodes.push(after);
-      }
-      nodes = [this._ref(node.rawNode.shadowRoot), ...nodes];
+      // We need a dedicated anonymous walker to fetch before / after pseudos.
+      let {before, after} = this._getBeforeAfterElements(rawNode);
+      nodes = [
+        // #shadow-root
+        this._ref(node.rawNode.shadowRoot),
+        // ::before
+        ...(before ? [before] : []),
+        // light dom nodes
+        ...nodes,
+        // ::after
+        ...(after ? [after] : []),
+      ];
     }
 
     return {hasFirst, hasLast, nodes};
   },
 
+  _getBeforeAfterElements: function (node) {
+    let anonymousWalker = this.getDocumentWalker(node, {
+      showAnonymousContent: true
+    });
+    let before = this._ref(anonymousWalker.firstChild());
+
+    anonymousWalker.setStartingNode(node);
+    let after = this._ref(anonymousWalker.lastChild());
+
+    return {
+      before: before.isBeforePseudoElement ? before : undefined,
+      after: after.isAfterPseudoElement ? after : undefined,
+    };
+  },
+
+  _convertNodesToSlottedNodes: function (parentActor, nodes) {
+    return nodes.map(nodeActor => {
+      if (nodeActor.rawNode.nodeType !== Ci.nsIDOMNode.ELEMENT_NODE) {
+        return nodeActor;
+      }
+
+      // An unused slot will list its default children (if any) and those should not be
+      // converted to slotted node actors.
+      if (nodeActor.rawNode.parentNode === parentActor.rawNode) {
+        return nodeActor;
+      }
+
+      return this._slottedRef(nodeActor.rawNode);
+    });
+  },
+
   /**
    * Return siblings of the given node.  By default this method will return
    * all siblings of the node, but there are options that can restrict this
    * to a more manageable subset.
    *
    * If `start` or `center` are not specified, this method will center on the
    * node whose siblings are requested.
    *
--- a/devtools/shared/fronts/node.js
+++ b/devtools/shared/fronts/node.js
@@ -270,18 +270,18 @@ const NodeFront = FrontClassWithSpec(nod
   },
   get hasEventListeners() {
     return this._form.hasEventListeners;
   },
 
   get isShadowRoot() {
     return this._form.isShadowRoot;
   },
-  get isShadowNode() {
-    return this._form.isShadowNode;
+  get isSlottedNode() {
+    return this._form.isSlottedNode;
   },
   get isBeforePseudoElement() {
     return this._form.isBeforePseudoElement;
   },
   get isAfterPseudoElement() {
     return this._form.isAfterPseudoElement;
   },
   get isPseudoElement() {
--- a/devtools/shared/specs/node.js
+++ b/devtools/shared/specs/node.js
@@ -123,18 +123,18 @@ const nodeSpec = generateActorSpec({
         value: RetVal("string")
       }
     },
   }
 });
 
 exports.nodeSpec = nodeSpec;
 
-const shadowNodeSpec = generateActorSpec({
-  typeName: "shadownode",
+const slottedNodeSpec = generateActorSpec({
+  typeName: "slottednode",
 
   methods: {
     getNodeValue: {
       request: {},
       response: {
         value: RetVal("longstring")
       }
     },
@@ -188,9 +188,9 @@ const shadowNodeSpec = generateActorSpec
       request: {},
       response: {
         value: RetVal("string")
       }
     },
   }
 });
 
-exports.shadowNodeSpec = shadowNodeSpec;
+exports.slottedNodeSpec = slottedNodeSpec;
--- a/toolkit/modules/css-selector.js
+++ b/toolkit/modules/css-selector.js
@@ -3,16 +3,26 @@
 /* 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/. */
 
 "use strict";
 
 this.EXPORTED_SYMBOLS = ["findCssSelector"];
 
+function getShadowRoot(node) {
+  let parent = node;
+  while ((parent = parent.parentNode)) {
+    if (parent.nodeType === 11 && parent.mode && parent.host) {
+      return parent;
+    }
+  }
+  return null;
+}
+
 /**
  * Traverse getBindingParent until arriving upon the bound element
  * responsible for the generation of the specified node.
  * See https://developer.mozilla.org/en-US/docs/XBL/XBL_1.0_Reference/DOM_Interfaces#getBindingParent.
  *
  * @param {DOMNode} node
  * @return {DOMNode}
  *         If node is not anonymous, this will return node. Otherwise,
@@ -20,16 +30,17 @@ this.EXPORTED_SYMBOLS = ["findCssSelecto
  *
  */
 function getRootBindingParent(node) {
   let parent;
   let doc = node.ownerDocument;
   if (!doc) {
     return node;
   }
+
   while ((parent = doc.getBindingParent(node))) {
     node = parent;
   }
   return node;
 }
 
 /**
  * Find the position of [element] in [nodeList].
@@ -45,18 +56,26 @@ function positionInNodeList(element, nod
 }
 
 /**
  * Find a unique CSS selector for a given element
  * @returns a string such that ele.ownerDocument.querySelector(reply) === ele
  * and ele.ownerDocument.querySelectorAll(reply).length === 1
  */
 const findCssSelector = function(ele) {
-  ele = getRootBindingParent(ele);
-  let document = ele.ownerDocument;
+  let shadowRoot = getShadowRoot(ele);
+
+  let document;
+  if (shadowRoot) {
+    document = shadowRoot;
+  } else {
+    ele = getRootBindingParent(ele);
+    document = ele.ownerDocument;
+  }
+
   if (!document || !document.contains(ele)) {
     throw new Error("findCssSelector received element not inside document");
   }
 
   let cssEscape = ele.ownerGlobal.CSS.escape;
 
   // document.querySelectorAll("#id") returns multiple if elements share an ID
   if (ele.id &&
@@ -103,12 +122,14 @@ const findCssSelector = function(ele) {
   }
 
   // Not unique enough yet.  As long as it's not a child of the document,
   // continue recursing up until it is unique enough.
   if (ele.parentNode !== document) {
     index = positionInNodeList(ele, ele.parentNode.children) + 1;
     selector = findCssSelector(ele.parentNode) + " > " +
       cssEscape(tagName) + ":nth-child(" + index + ")";
+  } else if (!!shadowRoot) {
+    selector = cssEscape(tagName) + ":nth-child(" + index + ")";
   }
 
   return selector;
 };