Bug 1449959 - Listen to shadowrootattached events to update markup view;r=bgrins,ochameau
MozReview-Commit-ID: JcYGg8lKupl
--- 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;
}
}