Bug 1449333 - Display closed shadow roots in the inspector;r=bgrins draft
authorJulian Descottes <jdescottes@mozilla.com>
Fri, 15 Jun 2018 15:35:28 -0700
changeset 810086 03766e229910776476910a90ba4cebe97648c767
parent 809159 9390a911ee68708cf873677fae1e1711041fa5cd
child 810772 af386c52748c55d518445c0175ac129c101a63da
push id113889
push userjdescottes@mozilla.com
push dateMon, 25 Jun 2018 08:48:39 +0000
reviewersbgrins
bugs1449333
milestone62.0a1
Bug 1449333 - Display closed shadow roots in the inspector;r=bgrins MozReview-Commit-ID: GIA5A4kOYFX
devtools/client/inspector/markup/test/browser_markup_shadowdom.js
devtools/client/inspector/markup/test/helper_shadowdom.js
devtools/server/actors/inspector/node.js
devtools/server/actors/inspector/walker.js
devtools/shared/layout/utils.js
--- 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.
  *