Bug 1449333 - Display closed shadow roots in the inspector;r=bgrins
MozReview-Commit-ID: GIA5A4kOYFX
--- a/devtools/client/inspector/markup/test/browser_markup_shadowdom.js
+++ b/devtools/client/inspector/markup/test/browser_markup_shadowdom.js
@@ -3,229 +3,215 @@
http://creativecommons.org/publicdomain/zero/1.0/ */
/* import-globals-from helper_shadowdom.js */
"use strict";
loadHelperScript("helper_shadowdom.js");
+requestLongerTimeout(2);
+
// Test a few static pages using webcomponents and check that they are displayed as
// expected in the markup view.
-add_task(async function() {
- await enableWebComponents();
-
- // Test that expanding a shadow host shows a shadow root node and direct host children.
- // Test that expanding a shadow root shows the shadow dom.
- // Test that slotted elements are visible in the shadow dom.
-
- const TEST_URL = `data:text/html;charset=utf-8,
- <test-component>
- <div slot="slot1">slotted-1<div>inner</div></div>
- <div slot="slot2">slotted-2<div>inner</div></div>
- <div class="no-slot-class">no-slot-text<div>inner</div></div>
- </test-component>
-
- <script>
- 'use strict';
- customElements.define('test-component', class extends HTMLElement {
- constructor() {
- super();
- let shadowRoot = this.attachShadow({mode: 'open'});
- shadowRoot.innerHTML = \`
- <slot name="slot1"></slot>
- <slot name="slot2"></slot>
- <slot></slot>
- \`;
- }
- });
- </script>`;
-
- const EXPECTED_TREE = `
- test-component
- #shadow-root
- name="slot1"
- div!slotted
- name="slot2"
- div!slotted
- slot
- div!slotted
- slot="slot1"
- slotted-1
- inner
- slot="slot2"
- slotted-2
- inner
- class="no-slot-class"
- no-slot-text
- inner`;
+const TEST_DATA = [
+ {
+ // Test that expanding a shadow host shows a shadow root node and direct children.
+ // Test that expanding a shadow root shows the shadow dom.
+ // Test that slotted elements are visible in the shadow dom.
+ title: "generic shadow dom 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>
- const {inspector} = await openInspectorForURL(TEST_URL);
- await checkTreeFromRootSelector(EXPECTED_TREE, "test-component", inspector);
-});
-
-add_task(async function() {
- await enableWebComponents();
-
- // Test that components without any direct children still display a shadow root node, if
- // a shadow root is attached to the host.
-
- const TEST_URL = `data:text/html;charset=utf-8,
- <test-component></test-component>
- <script>
- "use strict";
- customElements.define("test-component", class extends HTMLElement {
- constructor() {
- super();
- let shadowRoot = this.attachShadow({mode: "open"});
- shadowRoot.innerHTML = "<slot><div>fallback-content</div></slot>";
- }
- });
- </script>`;
-
- const EXPECTED_TREE = `
- test-component
- #shadow-root
- slot
- fallback-content`;
-
- const {inspector} = await openInspectorForURL(TEST_URL);
- await checkTreeFromRootSelector(EXPECTED_TREE, "test-component", inspector);
-});
-
-add_task(async function() {
- await enableWebComponents();
-
- // Test that the markup view is correctly displayed for non-trivial shadow DOM nesting.
-
- const TEST_URL = `data:text/html;charset=utf-8,
- <test-component >
- <div slot="slot1">slot1-1</div>
- <third-component slot="slot2"></third-component>
- </test-component>
-
- <script>
- (function() {
- 'use strict';
-
- function defineComponent(name, html) {
- customElements.define(name, class extends HTMLElement {
+ <script>
+ 'use strict';
+ customElements.define('test-component', class extends HTMLElement {
constructor() {
super();
- let shadowRoot = this.attachShadow({mode: 'open'});
- shadowRoot.innerHTML = html;
+ let shadowRoot = this.attachShadow({mode: "#MODE#"});
+ shadowRoot.innerHTML = \`
+ <slot name="slot1"></slot>
+ <slot name="slot2"></slot>
+ <slot></slot>
+ \`;
+ }
+ });
+ </script>`,
+ 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`
+
+ }, {
+ // Test that components without any direct children still display a shadow root node,
+ // if a shadow root is attached to the host.
+ title: "shadow root without direct children",
+ 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: "#MODE#"});
+ shadowRoot.innerHTML = "<slot><div>fallback-content</div></slot>";
}
});
- }
+ </script>`,
+ tree: `
+ test-component
+ #shadow-root
+ slot
+ fallback-content`
+
+ }, {
+ // Test that markup view is correctly displayed for non-trivial shadow DOM nesting.
+ title: "nested components",
+ 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';
- 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>`;
+ function defineComponent(name, html) {
+ customElements.define(name, class extends HTMLElement {
+ constructor() {
+ super();
+ let shadowRoot = this.attachShadow({mode: "#MODE#"});
+ shadowRoot.innerHTML = html;
+ }
+ });
+ }
- const EXPECTED_TREE = `
- test-component
- #shadow-root
- test-container
- slot
- div!slotted
- slot
- third-component!slotted
- other-component
- #shadow-root
+ 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>`,
+ tree: `
+ test-component
+ #shadow-root
+ test-container
+ slot
+ div!slotted
+ slot
+ third-component!slotted
+ other-component
+ #shadow-root
+ div
+ slot
+ div!slotted
div
- slot
- div!slotted
- div
- div
- third-component
- #shadow-root
- div`;
-
- const {inspector} = await openInspectorForURL(TEST_URL);
- await checkTreeFromRootSelector(EXPECTED_TREE, "test-component", inspector);
-});
+ div
+ third-component
+ #shadow-root
+ div`
-add_task(async function() {
- await enableWebComponents();
-
- // Test that ::before and ::after pseudo elements are correctly displayed in host
- // components and in slot elements.
-
- const TEST_URL = `data:text/html;charset=utf-8,
- <style>
- test-component::before { content: "before-host" }
- test-component::after { content: "after-host" }
- </style>
-
- <test-component>
- <div class="light-dom"></div>
- </test-component>
+ }, {
+ // Test that ::before and ::after pseudo elements are correctly displayed in host
+ // components and in slot elements.
+ title: "pseudo elements",
+ url: `data:text/html;charset=utf-8,
+ <style>
+ test-component::before { content: "before-host" }
+ test-component::after { content: "after-host" }
+ </style>
- <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>`;
+ <test-component>
+ <div class="light-dom"></div>
+ </test-component>
- const EXPECTED_TREE = `
- test-component
- #shadow-root
- style
- slot { display: block }
- slot
- ::before
- div!slotted
- ::after
- ::before
- class="light-dom"
- ::after`;
+ <script>
+ "use strict";
+ customElements.define("test-component", class extends HTMLElement {
+ constructor() {
+ super();
+ let shadowRoot = this.attachShadow({mode: "#MODE#"});
+ 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>`,
+ tree: `
+ test-component
+ #shadow-root
+ style
+ slot { display: block }
+ slot
+ ::before
+ div!slotted
+ ::after
+ ::before
+ class="light-dom"
+ ::after`
- const {inspector} = await openInspectorForURL(TEST_URL);
- await checkTreeFromRootSelector(EXPECTED_TREE, "test-component", inspector);
-});
-
-add_task(async function() {
- await enableWebComponents();
-
- // Test empty web components are still displayed correctly.
-
- const TEST_URL = `data:text/html;charset=utf-8,
- <test-component></test-component>
+ }, {
+ // Test empty web components are still displayed correctly.
+ title: "empty components",
+ 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 = "";
- }
- });
- </script>`;
+ <script>
+ "use strict";
+ customElements.define("test-component", class extends HTMLElement {
+ constructor() {
+ super();
+ let shadowRoot = this.attachShadow({mode: "#MODE#"});
+ shadowRoot.innerHTML = "";
+ }
+ });
+ </script>`,
+ tree: `
+ test-component
+ #shadow-root`
+ }
+];
- const EXPECTED_TREE = `
- test-component
- #shadow-root`;
-
- const {inspector} = await openInspectorForURL(TEST_URL);
- await checkTreeFromRootSelector(EXPECTED_TREE, "test-component", inspector);
-});
+for (const {url, tree, title} of TEST_DATA) {
+ // Test each configuration in both open and closed modes
+ add_task(async function() {
+ info(`Testing: [${title}] in OPEN mode`);
+ await enableWebComponents();
+ const {inspector} = await openInspectorForURL(url.replace("#MODE#", "open"));
+ await checkTreeFromRootSelector(tree, "test-component", inspector);
+ });
+ add_task(async function() {
+ info(`Testing: [${title}] in CLOSED mode`);
+ await enableWebComponents();
+ const {inspector} = await openInspectorForURL(url.replace("#MODE#", "closed"));
+ await checkTreeFromRootSelector(tree, "test-component", inspector);
+ });
+}
--- a/devtools/client/inspector/markup/test/helper_shadowdom.js
+++ b/devtools/client/inspector/markup/test/helper_shadowdom.js
@@ -82,22 +82,24 @@ function parseTree(inputString) {
parent = currentNode;
} else {
parent = currentNode.parent;
for (let i = 0; i < currentNode.level - level; i++) {
parent = parent.parent;
}
}
+ const path = (parent.path ? parent.path + " " : "") + nodeString;
+
const node = {
node: nodeString,
children: [],
parent,
level,
- path: parent.path + " " + nodeString
+ path
};
parent.children.push(node);
currentNode = node;
}
return tree;
}
--- a/devtools/server/actors/inspector/node.js
+++ b/devtools/server/actors/inspector/node.js
@@ -179,28 +179,28 @@ const NodeActor = protocol.ActorClassWit
},
get isShadowRoot() {
const isFragment = this.rawNode.nodeType === Node.DOCUMENT_FRAGMENT_NODE;
return isFragment && !!this.rawNode.host;
},
get isShadowHost() {
- const shadowRoot = this.rawNode.shadowRoot;
+ const shadowRoot = this.rawNode.openOrClosedShadowRoot;
return shadowRoot && shadowRoot.nodeType === Node.DOCUMENT_FRAGMENT_NODE;
},
get isDirectShadowHostChild() {
// Pseudo elements are always part of the anonymous tree.
if (this.isBeforePseudoElement || this.isAfterPseudoElement) {
return false;
}
const parentNode = this.rawNode.parentNode;
- return parentNode && !!parentNode.shadowRoot;
+ return parentNode && !!parentNode.openOrClosedShadowRoot;
},
// Estimate the number of children that the walker will return without making
// a call to children() if possible.
get numChildren() {
// For pseudo elements, childNodes.length returns 1, but the walker
// will return 0.
if (this.isBeforePseudoElement || this.isAfterPseudoElement) {
--- a/devtools/server/actors/inspector/walker.js
+++ b/devtools/server/actors/inspector/walker.js
@@ -622,18 +622,18 @@ var WalkerActor = protocol.ActorClassWit
// make it easier.
const getFilteredWalker = documentWalkerNode => {
const { 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.
const skipTo = SKIP_TO_SIBLING;
- const useAnonymousWalker = !(isShadowRoot || isShadowHost || isUnslottedHostChild);
- if (!useAnonymousWalker) {
+ const useNonAnonymousWalker = isShadowRoot || isShadowHost || isUnslottedHostChild;
+ if (useNonAnonymousWalker) {
// Do not use an anonymous walker for :
// - shadow roots: if the host element has an ::after pseudo element, a walker on
// the last child of the shadow root will jump to the ::after element, which is
// not a child of the shadow root.
// TODO: For this case, should rather use an anonymous walker with a new
// dedicated filter.
// - shadow hosts: anonymous children of host elements make up the shadow dom,
// while we want to return the direct children of the shadow host.
@@ -710,17 +710,17 @@ var WalkerActor = protocol.ActorClassWit
const hasBefore = first && this._ref(first).isBeforePseudoElement;
const lastChildWalker = this.getDocumentWalker(node.rawNode);
const last = lastChildWalker.lastChild();
const hasAfter = last && this._ref(last).isAfterPseudoElement;
nodes = [
// #shadow-root
- this._ref(node.rawNode.shadowRoot),
+ this._ref(node.rawNode.openOrClosedShadowRoot),
// ::before
...(hasBefore ? [this._ref(first)] : []),
// shadow host direct children
...nodes,
// ::after
...(hasAfter ? [this._ref(last)] : []),
];
}
--- a/devtools/shared/layout/utils.js
+++ b/devtools/shared/layout/utils.js
@@ -553,17 +553,17 @@ exports.isNativeAnonymous = isNativeAnon
*/
function isXBLAnonymous(node) {
const parent = getBindingParent(node);
if (!parent) {
return false;
}
// Shadow nodes also show up in getAnonymousNodes, so return false.
- if (parent.shadowRoot && parent.shadowRoot.contains(node)) {
+ if (parent.openOrClosedShadowRoot && parent.openOrClosedShadowRoot.contains(node)) {
return false;
}
const anonNodes = [...node.ownerDocument.getAnonymousNodes(parent) || []];
return anonNodes.indexOf(node) > -1;
}
exports.isXBLAnonymous = isXBLAnonymous;
@@ -577,17 +577,17 @@ exports.isXBLAnonymous = isXBLAnonymous;
function isShadowAnonymous(node) {
const parent = getBindingParent(node);
if (!parent) {
return false;
}
// If there is a shadowRoot and this is part of it then this
// is not native anonymous
- return parent.shadowRoot && parent.shadowRoot.contains(node);
+ return parent.openOrClosedShadowRoot && parent.openOrClosedShadowRoot.contains(node);
}
exports.isShadowAnonymous = isShadowAnonymous;
/**
* Get the current zoom factor applied to the container window of a given node.
* Container windows are used as a weakmap key to store the corresponding
* nsIDOMWindowUtils instance to avoid querying it every time.
*