Bug 1449968 - Add support for Inspect Element in Shadow DOM;r=bgrins,emilio
MozReview-Commit-ID: HhwyDMyZe1k
--- a/browser/modules/ContextMenu.jsm
+++ b/browser/modules/ContextMenu.jsm
@@ -11,17 +11,17 @@ var EXPORTED_SYMBOLS = ["ContextMenu"];
ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyGlobalGetters(this, ["URL"]);
XPCOMUtils.defineLazyModuleGetters(this, {
E10SUtils: "resource://gre/modules/E10SUtils.jsm",
BrowserUtils: "resource://gre/modules/BrowserUtils.jsm",
- findCssSelector: "resource://gre/modules/css-selector.js",
+ findAllCssSelectors: "resource://gre/modules/css-selector.js",
SpellCheckHelper: "resource://gre/modules/InlineSpellChecker.jsm",
LoginManagerContent: "resource://gre/modules/LoginManagerContent.jsm",
WebNavigationFrames: "resource://gre/modules/WebNavigationFrames.jsm",
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
InlineSpellCheckerContent: "resource://gre/modules/InlineSpellCheckerContent.jsm",
});
XPCOMUtils.defineLazyGetter(this, "PageMenuChild", () => {
@@ -448,44 +448,16 @@ class ContextMenu {
if (!request) {
return true;
}
return false;
}
- /**
- * Retrieve the array of CSS selectors corresponding to the provided node.
- *
- * The selectors are ordered starting with the root document and ending with the deepest
- * nested frame. Additional items are used if the node is inside a frame, each
- * representing the CSS selector for finding the frame element in its parent document.
- *
- * This format is expected by DevTools in order to handle the Inspect Node context menu
- * item.
- *
- * @param {aNode}
- * The node for which the CSS selectors should be computed
- * @return {Array}
- * An array of CSS selectors to find the target node. Several selectors can be
- * needed if the element is nested in frames and not directly in the root
- * document. The selectors are ordered starting with the root document and
- * ending with the deepest nested frame.
- */
- _getNodeSelectors(aNode) {
- let selectors = [];
- while (aNode) {
- selectors.unshift(findCssSelector(aNode));
- aNode = aNode.ownerGlobal.frameElement;
- }
-
- return selectors;
- }
-
handleEvent(aEvent) {
let defaultPrevented = aEvent.defaultPrevented;
if (!Services.prefs.getBoolPref("dom.event.contextmenu.enabled")) {
let plugin = null;
try {
plugin = aEvent.composedTarget.QueryInterface(Ci.nsIObjectLoadingContent);
@@ -552,17 +524,17 @@ class ContextMenu {
contentDisposition = props.get("content-disposition", Ci.nsISupportsCString).data;
} catch (e) {}
} catch (e) {}
}
let selectionInfo = BrowserUtils.getSelectionDetails(this.content);
let loadContext = this.global.docShell.QueryInterface(Ci.nsILoadContext);
let userContextId = loadContext.originAttributes.userContextId;
- let popupNodeSelectors = this._getNodeSelectors(aEvent.composedTarget);
+ let popupNodeSelectors = findAllCssSelectors(aEvent.composedTarget);
this._setContext(aEvent);
let context = this.context;
this.target = context.target;
let spellInfo = null;
let editFlags = null;
let principal = null;
--- a/devtools/client/framework/devtools.js
+++ b/devtools/client/framework/devtools.js
@@ -645,18 +645,25 @@ DevTools.prototype = {
async function querySelectors(nodeFront) {
const selector = nodeSelectors.shift();
if (!selector) {
return nodeFront;
}
nodeFront = await walker.querySelector(nodeFront, selector);
if (nodeSelectors.length > 0) {
const { nodes } = await walker.children(nodeFront);
- // This is the NodeFront for the document node inside the iframe
- nodeFront = nodes[0];
+ // If there are remaining selectors to process, they will target a document or a
+ // document-fragment under the current node. Whether the element is a frame or
+ // a web component, it can only contain one document/document-fragment, so just
+ // select the first one available.
+ nodeFront = nodes.find(node => {
+ const { nodeType } = node;
+ return nodeType === Node.DOCUMENT_FRAGMENT_NODE ||
+ nodeType === Node.DOCUMENT_NODE;
+ });
}
return querySelectors(nodeFront);
}
const nodeFront = await walker.getRootNode();
return querySelectors(nodeFront);
},
/**
--- a/devtools/client/inspector/markup/test/browser.ini
+++ b/devtools/client/inspector/markup/test/browser.ini
@@ -168,18 +168,18 @@ skip-if = e10s # Bug 1036409 - The last
[browser_markup_shadowdom_clickreveal.js]
[browser_markup_shadowdom_clickreveal_scroll.js]
[browser_markup_shadowdom_delete.js]
[browser_markup_shadowdom_dynamic.js]
[browser_markup_shadowdom_hover.js]
[browser_markup_shadowdom_maxchildren.js]
[browser_markup_shadowdom_mutations_shadow.js]
[browser_markup_shadowdom_navigation.js]
+[browser_markup_shadowdom_nested_pick_inspect.js]
[browser_markup_shadowdom_noslot.js]
-[browser_markup_shadowdom_pick_nested.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]
rename from devtools/client/inspector/markup/test/browser_markup_shadowdom_pick_nested.js
rename to devtools/client/inspector/markup/test/browser_markup_shadowdom_nested_pick_inspect.js
--- a/devtools/client/inspector/markup/test/browser_markup_shadowdom_pick_nested.js
+++ b/devtools/client/inspector/markup/test/browser_markup_shadowdom_nested_pick_inspect.js
@@ -1,16 +1,19 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
+/* globals getTestActorWithoutToolbox */
"use strict";
-// Test that using the element picker on nodes inside a shadow root expands the markup
-// view as expected and that the correct markup container is selected.
+// Test that the markup view is correctly expanded when inspecting an element nested
+// in several shadow roots:
+// - when using the context-menu "Inspect element"
+// - when using the element picker
const TEST_URL = `data:text/html;charset=utf-8,` + encodeURIComponent(`
<test-outer></test-outer>
<script>
(function() {
'use strict';
function defineComponent(name, html) {
@@ -33,31 +36,47 @@ const TEST_URL = `data:text/html;charset
<div>
<div>
<slot></slot>
</div>
</div>
</div>\`);
defineComponent('test-image',
- \`<div style="display:block; height: 200px; width: 200px; background:red"></div>\`);
+ \`<div style="display:block; height: 200px; width: 100%; background:red"></div>\`);
})();
</script>`);
add_task(async function() {
await enableWebComponents();
- const { inspector, toolbox, testActor } = await openInspectorForURL(TEST_URL);
+ const { inspector, toolbox, tab, testActor } = await openInspectorForURL(TEST_URL);
+
+ info("Waiting for element picker to become active");
+ await startPicker(toolbox);
+ info("Click and pick the pick-target");
+ await pickElement(inspector, testActor, "test-outer", 10, 10);
+ info("Check that the markup view is displayed as expected");
+ await assertMarkupView(inspector);
+
+ info("Close DevTools before testing Inspect Element");
+ await gDevTools.closeToolbox(inspector.target);
info("Waiting for element picker to become active.");
- await startPicker(toolbox);
+ const newTestActor = await getTestActorWithoutToolbox(tab);
+ info("Click on Inspect Element for our test-image <div>");
+ // Note: we click on test-outer, because we can't find the <div> using a simple
+ // querySelector. However the click is simulated in the middle of the <test-outer>
+ // component, and will always hit the test <div> which takes all the space.
+ const newInspector = await clickOnInspectMenuItem(newTestActor, "test-outer");
+ info("Check again that the markup view is displayed as expected");
+ await assertMarkupView(newInspector);
+});
- info("Click and pick the pick-target");
- await pickElement(inspector, testActor, "test-outer", 10, 10);
-
+async function assertMarkupView(inspector) {
const outerFront = await getNodeFront("test-outer", inspector);
const outerContainer = inspector.markup.getContainer(outerFront);
assertContainer(outerContainer, {expanded: true, text: "test-outer", children: 1});
const outerShadowContainer = outerContainer.getChildContainers()[0];
assertContainer(outerShadowContainer,
{expanded: true, text: "#shadow-root", children: 1});
@@ -71,17 +90,17 @@ add_task(async function() {
const imageShadowContainer = imageContainer.getChildContainers()[0];
assertContainer(imageShadowContainer,
{expanded: true, text: "#shadow-root", children: 1});
const redDivContainer = imageShadowContainer.getChildContainers()[0];
assertContainer(redDivContainer, {expanded: false, text: "div"});
is(redDivContainer.selected, true, "Div element is selected as expected");
-});
+}
/**
* Check if the provided markup container is expanded, has the expected text and the
* expected number of children.
*/
function assertContainer(container, {expanded, text, children}) {
is(container.expanded, expanded, "Container is expanded");
assertContainerHasText(container, text);
--- a/devtools/server/actors/inspector/walker.js
+++ b/devtools/server/actors/inspector/walker.js
@@ -734,16 +734,23 @@ var WalkerActor = protocol.ActorClassWit
if (options.center) {
start = options.center.rawNode;
} else if (options.start) {
start = options.start.rawNode;
} else {
start = firstChild;
}
+ // A shadow root is not included in the children returned by the walker, so we can
+ // not use it as start node. However it will be displayed as the first node, so
+ // we use firstChild as a fallback.
+ if (isShadowRoot(start)) {
+ start = firstChild;
+ }
+
// Start by reading backward from the starting point if we're centering...
const backwardWalker = getFilteredWalker(start);
if (backwardWalker.currentNode != firstChild && options.center) {
backwardWalker.previousSibling();
const backwardCount = Math.floor(maxNodes / 2);
const backwardNodes = this._readBackward(backwardWalker, backwardCount);
nodes = backwardNodes;
}
--- a/toolkit/modules/css-selector.js
+++ b/toolkit/modules/css-selector.js
@@ -1,72 +1,117 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80: */
/* 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";
-var EXPORTED_SYMBOLS = ["findCssSelector"];
+var EXPORTED_SYMBOLS = ["findAllCssSelectors", "findCssSelector"];
/**
* 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,
* it will return the bound element
*
*/
function getRootBindingParent(node) {
- let parent;
let doc = node.ownerDocument;
if (!doc) {
return node;
}
+
+ if (getShadowRoot(node)) {
+ // If the node is under a shadow root, the shadow host is the "binding
+ // parent" but we can create a CSS selector between the shadow root and the
+ // node, so return immediately.
+ return node;
+ }
+
+ let parent;
while ((parent = doc.getBindingParent(node))) {
node = parent;
}
return node;
}
/**
+ * Return the node's parent shadow root if the node in shadow DOM, null
+ * otherwise.
+ */
+function getShadowRoot(node) {
+ let doc = node.ownerDocument;
+ if (!doc) {
+ return null;
+ }
+
+ const parent = doc.getBindingParent(node);
+ const shadowRoot = parent && parent.openOrClosedShadowRoot;
+ if (shadowRoot) {
+ return shadowRoot;
+ }
+
+ return null;
+}
+
+/**
* Find the position of [element] in [nodeList].
* @returns an index of the match, or -1 if there is no match
*/
function positionInNodeList(element, nodeList) {
for (let i = 0; i < nodeList.length; i++) {
if (element === nodeList[i]) {
return i;
}
}
return -1;
}
/**
+ * Retrieve the document or shadow-root containing the provided node. This will
+ * be the topmost element from which the node can be retrieved using
+ * querySelectorAll and consequently where to start creating a css selector.
+ */
+function getDocumentOrShadowRoot(node) {
+ const shadowRoot = getShadowRoot(node);
+ if (shadowRoot) {
+ // If the node is under a shadow root, return the corresponding
+ // document-fragment.
+ return shadowRoot;
+ }
+
+ // Otherwise return the ownerDocument.
+ return node.ownerDocument;
+}
+
+/**
* 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;
- if (!document || !document.contains(ele)) {
+
+ let containingDocOrShadow = getDocumentOrShadowRoot(ele);
+ if (!containingDocOrShadow || !containingDocOrShadow.contains(ele)) {
// findCssSelector received element not inside document.
return "";
}
let cssEscape = ele.ownerGlobal.CSS.escape;
// document.querySelectorAll("#id") returns multiple if elements share an ID
if (ele.id &&
- document.querySelectorAll("#" + cssEscape(ele.id)).length === 1) {
+ containingDocOrShadow.querySelectorAll("#" + cssEscape(ele.id)).length === 1) {
return "#" + cssEscape(ele.id);
}
// Inherently unique by tag name
let tagName = ele.localName;
if (tagName === "html") {
return "html";
}
@@ -74,42 +119,82 @@ const findCssSelector = function(ele) {
return "head";
}
if (tagName === "body") {
return "body";
}
// We might be able to find a unique class name
let selector, index, matches;
- if (ele.classList.length > 0) {
- for (let i = 0; i < ele.classList.length; i++) {
- // Is this className unique by itself?
- selector = "." + cssEscape(ele.classList.item(i));
- matches = document.querySelectorAll(selector);
- if (matches.length === 1) {
- return selector;
- }
- // Maybe it's unique with a tag name?
- selector = cssEscape(tagName) + selector;
- matches = document.querySelectorAll(selector);
- if (matches.length === 1) {
- return selector;
- }
- // Maybe it's unique using a tag name and nth-child
- index = positionInNodeList(ele, ele.parentNode.children) + 1;
- selector = selector + ":nth-child(" + index + ")";
- matches = document.querySelectorAll(selector);
- if (matches.length === 1) {
- return selector;
- }
+ for (let i = 0; i < ele.classList.length; i++) {
+ // Is this className unique by itself?
+ selector = "." + cssEscape(ele.classList.item(i));
+ matches = containingDocOrShadow.querySelectorAll(selector);
+ if (matches.length === 1) {
+ return selector;
+ }
+ // Maybe it's unique with a tag name?
+ selector = cssEscape(tagName) + selector;
+ matches = containingDocOrShadow.querySelectorAll(selector);
+ if (matches.length === 1) {
+ return selector;
+ }
+ // Maybe it's unique using a tag name and nth-child
+ index = positionInNodeList(ele, ele.parentNode.children) + 1;
+ selector = selector + ":nth-child(" + index + ")";
+ matches = containingDocOrShadow.querySelectorAll(selector);
+ if (matches.length === 1) {
+ return selector;
}
}
- // 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 + ")";
+ // Not unique enough yet.
+ index = positionInNodeList(ele, ele.parentNode.children) + 1;
+ selector = cssEscape(tagName) + ":nth-child(" + index + ")";
+ if (ele.parentNode !== containingDocOrShadow) {
+ selector = findCssSelector(ele.parentNode) + " > " + selector;
+ }
+ return selector;
+};
+
+/**
+ * If the element is in a frame or under a shadowRoot, return the corresponding
+ * element.
+ */
+function getSelectorParent(node) {
+ const shadowRoot = getShadowRoot(node);
+ if (shadowRoot) {
+ // The element is in a shadowRoot, return the host component.
+ return shadowRoot.host;
}
- return selector;
+ // Otherwise return the parent frameElement.
+ return node.ownerGlobal.frameElement;
+}
+
+/**
+ * Retrieve the array of CSS selectors corresponding to the provided node.
+ *
+ * The selectors are ordered starting with the root document and ending with the deepest
+ * nested frame. Additional items are used if the node is inside a frame or a shadow root,
+ * each representing the CSS selector for finding the frame or root element in its parent
+ * document.
+ *
+ * This format is expected by DevTools in order to handle the Inspect Node context menu
+ * item.
+ *
+ * @param {node}
+ * The node for which the CSS selectors should be computed
+ * @return {Array}
+ * An array of CSS selectors to find the target node. Several selectors can be
+ * needed if the element is nested in frames and not directly in the root
+ * document. The selectors are ordered starting with the root document and
+ * ending with the deepest nested frame or shadow root.
+ */
+const findAllCssSelectors = function(node) {
+ let selectors = [];
+ while (node) {
+ selectors.unshift(findCssSelector(node));
+ node = getSelectorParent(node);
+ }
+
+ return selectors;
};