Bug 1449959 - Listen to shadowrootattached events to update markup view;r=bgrins,ochameau
authorJulian Descottes <jdescottes@mozilla.com>
Mon, 25 Jun 2018 12:22:26 +0200
changeset 814161 3928fa2e88ee1ed812cfe7a1f14d01e7e4b81201
parent 814160 2fafbc23c259ba61732ed064492271ec95248771
child 814162 af93628a6d33be7d1c601d5546a674e82589814c
push id115123
push userjdescottes@mozilla.com
push dateWed, 04 Jul 2018 17:42:29 +0000
reviewersbgrins, ochameau
bugs1449959
milestone63.0a1
Bug 1449959 - Listen to shadowrootattached events to update markup view;r=bgrins,ochameau MozReview-Commit-ID: JcYGg8lKupl
devtools/client/inspector/markup/markup.js
devtools/client/inspector/markup/test/browser.ini
devtools/client/inspector/markup/test/browser_markup_shadowdom_dynamic.js
devtools/server/actors/inspector/walker.js
devtools/shared/fronts/inspector.js
--- a/devtools/client/inspector/markup/markup.js
+++ b/devtools/client/inspector/markup/markup.js
@@ -1089,17 +1089,17 @@ MarkupView.prototype = {
         // we're not viewing.
         continue;
       }
 
       if (type === "attributes" || type === "characterData"
         || type === "events" || type === "pseudoClassLock") {
         container.update();
       } else if (type === "childList" || type === "nativeAnonymousChildList"
-        || type === "slotchange") {
+        || type === "slotchange" || type === "shadowRootAttached") {
         container.childrenDirty = true;
         // Update the children to take care of changes in the markup view DOM
         // and update container (and its subtree) DOM tree depth level for
         // accessibility where necessary.
         this._updateChildren(container, {flash: true}).then(() =>
           container.updateLevel());
       } else if (type === "inlineTextChild") {
         container.childrenDirty = true;
--- a/devtools/client/inspector/markup/test/browser.ini
+++ b/devtools/client/inspector/markup/test/browser.ini
@@ -163,16 +163,17 @@ skip-if = verify
 [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_clickreveal_scroll.js]
 [browser_markup_shadowdom_delete.js]
+[browser_markup_shadowdom_dynamic.js]
 [browser_markup_shadowdom_maxchildren.js]
 [browser_markup_shadowdom_mutations_shadow.js]
 [browser_markup_shadowdom_navigation.js]
 [browser_markup_shadowdom_noslot.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]
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_shadowdom_dynamic.js
@@ -0,0 +1,154 @@
+/* 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 inspector is correctly updated when shadow roots are attached to
+// components after displaying them in the markup view.
+
+const TEST_URL = `data:text/html;charset=utf-8,` + encodeURIComponent(`
+  <div id="root">
+    <test-component>
+      <div slot="slot1" id="el1">slot1-1</div>
+      <div slot="slot1" id="el2">slot1-2</div>
+    </test-component>
+    <inline-component>inline text</inline-component>
+  </div>
+
+  <script>
+    'use strict';
+    window.attachTestComponent = function () {
+      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>
+                                   <other-component>
+                                     <div slot="slot2">slot2-1</div>
+                                   </other-component>\`;
+        }
+      });
+    }
+
+    window.attachOtherComponent = function () {
+      customElements.define('other-component', class extends HTMLElement {
+        constructor() {
+          super();
+          let shadowRoot = this.attachShadow({mode: 'open'});
+          shadowRoot.innerHTML = \`<div id="slot2-container">
+                                     <slot name="slot2"></slot>
+                                     <div>some-other-node</div>
+                                   </div>\`;
+        }
+      });
+    }
+
+    window.attachInlineComponent = function () {
+      customElements.define('inline-component', class extends HTMLElement {
+        constructor() {
+          super();
+          let shadowRoot = this.attachShadow({mode: 'open'});
+          shadowRoot.innerHTML = \`<div id="inline-component-content">
+                                     <div>some-inline-content</div>
+                                   </div>\`;
+        }
+      });
+    }
+  </script>`);
+
+add_task(async function() {
+  await enableWebComponents();
+
+  const {inspector} = await openInspectorForURL(TEST_URL);
+
+  const tree = `
+    div
+      test-component
+        slot1-1
+        slot1-2
+      inline text`;
+  await assertMarkupViewAsTree(tree, "#root", inspector);
+
+  info("Attach a shadow root to test-component");
+  let mutated = waitForMutation(inspector, "shadowRootAttached");
+  ContentTask.spawn(gBrowser.selectedBrowser, {}, function() {
+    content.wrappedJSObject.attachTestComponent();
+  });
+  await mutated;
+
+  const treeAfterTestAttach = `
+    div
+      test-component
+        #shadow-root
+          slot1-container
+            slot
+              div!slotted
+              div!slotted
+          other-component
+            slot2-1
+        slot1-1
+        slot1-2
+      inline text`;
+  await assertMarkupViewAsTree(treeAfterTestAttach, "#root", inspector);
+
+  info("Attach a shadow root to other-component, nested in test-component");
+  mutated = waitForMutation(inspector, "shadowRootAttached");
+  ContentTask.spawn(gBrowser.selectedBrowser, {}, function() {
+    content.wrappedJSObject.attachOtherComponent();
+  });
+  await mutated;
+
+  const treeAfterOtherAttach = `
+    div
+      test-component
+        #shadow-root
+          slot1-container
+            slot
+              div!slotted
+              div!slotted
+          other-component
+            #shadow-root
+              slot2-container
+                slot
+                  div!slotted
+                some-other-node
+            slot2-1
+        slot1-1
+        slot1-2
+      inline text`;
+  await assertMarkupViewAsTree(treeAfterOtherAttach, "#root", inspector);
+
+  info("Attach a shadow root to inline-component, check the inline text child.");
+  mutated = waitForMutation(inspector, "shadowRootAttached");
+  ContentTask.spawn(gBrowser.selectedBrowser, {}, function() {
+    content.wrappedJSObject.attachInlineComponent();
+  });
+  await mutated;
+
+  const treeAfterInlineAttach = `
+    div
+      test-component
+        #shadow-root
+          slot1-container
+            slot
+              div!slotted
+              div!slotted
+          other-component
+            #shadow-root
+              slot2-container
+                slot
+                  div!slotted
+                some-other-node
+            slot2-1
+        slot1-1
+        slot1-2
+      inline-component
+        #shadow-root
+          inline-component-content
+            some-inline-content
+        inline text`;
+  await assertMarkupViewAsTree(treeAfterInlineAttach, "#root", inspector);
+});
--- a/devtools/server/actors/inspector/walker.js
+++ b/devtools/server/actors/inspector/walker.js
@@ -132,24 +132,32 @@ var WalkerActor = protocol.ActorClassWit
 
     // The client can tell the walker that it is interested in a node
     // even when it is orphaned with the `retainNode` method.  This
     // list contains orphaned nodes that were so retained.
     this._retainedOrphans = new Set();
 
     this.onMutations = this.onMutations.bind(this);
     this.onSlotchange = this.onSlotchange.bind(this);
+    this.onShadowrootattached = this.onShadowrootattached.bind(this);
     this.onFrameLoad = this.onFrameLoad.bind(this);
     this.onFrameUnload = this.onFrameUnload.bind(this);
     this._throttledEmitNewMutations = throttle(this._emitNewMutations.bind(this),
       MUTATIONS_THROTTLING_DELAY);
 
     targetActor.on("will-navigate", this.onFrameUnload);
     targetActor.on("window-ready", this.onFrameLoad);
 
+    // Keep a reference to the chromeEventHandler for the current targetActor, to make
+    // sure we will be able to remove the listener during the WalkerActor destroy().
+    this.chromeEventHandler = targetActor.chromeEventHandler;
+    // shadowrootattached is a chrome-only event.
+    this.chromeEventHandler.addEventListener("shadowrootattached",
+      this.onShadowrootattached);
+
     // Ensure that the root document node actor is ready and
     // managed.
     this.rootNode = this.document();
 
     this.layoutChangeObserver = getLayoutChangesObserver(this.targetActor);
     this._onReflows = this._onReflows.bind(this);
     this.layoutChangeObserver.on("reflows", this._onReflows);
     this._onResize = this._onResize.bind(this);
@@ -228,16 +236,18 @@ var WalkerActor = protocol.ActorClassWit
       this.rootNode = null;
       this.layoutHelpers = null;
       this._orphaned = null;
       this._retainedOrphans = null;
       this._refMap = null;
 
       this.targetActor.off("will-navigate", this.onFrameUnload);
       this.targetActor.off("window-ready", this.onFrameLoad);
+      this.chromeEventHandler.removeEventListener("shadowrootattached",
+        this.onShadowrootattached);
 
       this.onFrameLoad = null;
       this.onFrameUnload = null;
 
       this.walkerSearch.destroy();
 
       this.layoutChangeObserver.off("reflows", this._onReflows);
       this.layoutChangeObserver.off("resize", this._onResize);
@@ -246,16 +256,17 @@ var WalkerActor = protocol.ActorClassWit
 
       eventListenerService.removeListenerChangeListener(
         this._onEventListenerChange);
 
       this.onMutations = null;
 
       this.layoutActor = null;
       this.targetActor = null;
+      this.chromeEventHandler = null;
 
       this.emit("destroyed");
     } catch (e) {
       console.error(e);
     }
   },
 
   release: function() {},
@@ -1730,16 +1741,29 @@ var WalkerActor = protocol.ActorClassWit
     }
 
     this.queueMutation({
       type: "slotchange",
       target: targetActor.actorID
     });
   },
 
+  onShadowrootattached: function(event) {
+    const actor = this.getNode(event.target);
+    if (!actor) {
+      return;
+    }
+
+    const mutation = {
+      type: "shadowRootAttached",
+      target: actor.actorID,
+    };
+    this.queueMutation(mutation);
+  },
+
   onFrameLoad: function({ window, isTopLevel }) {
     const { readyState } = window.document;
     if (readyState != "interactive" && readyState != "complete") {
       window.addEventListener("DOMContentLoaded",
         this.onFrameLoad.bind(this, { window, isTopLevel }),
         { once: true });
       return;
     }
--- a/devtools/shared/fronts/inspector.js
+++ b/devtools/shared/fronts/inspector.js
@@ -376,32 +376,35 @@ const WalkerFront = FrontClassWithSpec(w
 
           // We try to give fronts instead of actorIDs, but these fronts need
           // to be destroyed now.
           emittedMutation.target = targetFront.actorID;
           emittedMutation.targetParent = targetFront.parentNode();
 
           // Release the document node and all of its children, even retained.
           this._releaseFront(targetFront, true);
+        } else if (change.type === "shadowRootAttached") {
+          targetFront._form.isShadowHost = true;
         } else if (change.type === "unretained") {
           // Retained orphans were force-released without the intervention of
           // client (probably a navigated frame).
           for (const released of change.nodes) {
             const releasedFront = this.get(released);
             this._retainedOrphans.delete(released);
             this._releaseFront(releasedFront, true);
           }
         } else {
           targetFront.updateMutation(change);
         }
 
         // Update the inlineTextChild property of the target for a selected list of
         // mutation types.
         if (change.type === "inlineTextChild" ||
             change.type === "childList" ||
+            change.type === "shadowRootAttached" ||
             change.type === "nativeAnonymousChildList") {
           if (change.inlineTextChild) {
             targetFront.inlineTextChild =
               types.getType("domnode").read(change.inlineTextChild, this);
           } else {
             targetFront.inlineTextChild = undefined;
           }
         }