--- a/devtools/client/inspector/markup/markup.js
+++ b/devtools/client/inspector/markup/markup.js
@@ -475,16 +475,23 @@ MarkupView.prototype = {
}, NEW_SELECTION_HIGHLIGHTER_TIMER);
});
this._briefBoxModelPromise.resolve = _resolve;
return promise.all([onShown, this._briefBoxModelPromise]);
},
/**
+ * Used by tests
+ */
+ getSelectedContainer: function() {
+ return this._selectedContainer;
+ },
+
+ /**
* Get the MarkupContainer object for a given node, or undefined if
* none exists.
*
* @param {NodeFront} nodeFront
* The node to get the container for.
* @param {Boolean} slotted
* true to get the slotted version of the container.
* @return {MarkupContainer} The container for the provided node.
--- a/devtools/client/inspector/markup/test/browser.ini
+++ b/devtools/client/inspector/markup/test/browser.ini
@@ -46,16 +46,17 @@ support-files =
events_bundle.js.map
events_original.js
head.js
helper_attributes_test_runner.js
helper_diff.js
helper_events_test_runner.js
helper_markup_accessibility_navigation.js
helper_outerhtml_test_runner.js
+ helper_shadowdom.js
helper_style_attr_test_runner.js
lib_babel_6.21.0_min.js
lib_jquery_1.0.js
lib_jquery_1.1.js
lib_jquery_1.2_min.js
lib_jquery_1.3_min.js
lib_jquery_1.4_min.js
lib_jquery_1.6_min.js
@@ -160,16 +161,32 @@ skip-if = (os == 'linux' && bits == 32 &
[browser_markup_node_names_namespaced.js]
[browser_markup_node_not_displayed_01.js]
[browser_markup_node_not_displayed_02.js]
[browser_markup_pagesize_01.js]
[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]
+skip-if = !stylo # shadow DOM is only enabled with stylo.
+[browser_markup_shadowdom_clickreveal.js]
+skip-if = !stylo # shadow DOM is only enabled with stylo.
+[browser_markup_shadowdom_delete.js]
+skip-if = !stylo # shadow DOM is only enabled with stylo.
+[browser_markup_shadowdom_maxchildren.js]
+skip-if = !stylo # shadow DOM is only enabled with stylo.
+[browser_markup_shadowdom_mutations_shadow.js]
+skip-if = !stylo # shadow DOM is only enabled with stylo.
+[browser_markup_shadowdom_navigation.js]
+skip-if = !stylo # shadow DOM is only enabled with stylo.
+[browser_markup_shadowdom_noslot.js]
+skip-if = !stylo # shadow DOM is only enabled with stylo.
+[browser_markup_shadowdom_slotupdate.js]
+skip-if = !stylo # shadow DOM is only enabled with stylo.
[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]
[browser_markup_tag_edit_06.js]
--- a/devtools/client/inspector/markup/test/browser_markup_dragdrop_invalidNodes.js
+++ b/devtools/client/inspector/markup/test/browser_markup_dragdrop_invalidNodes.js
@@ -1,21 +1,22 @@
/* 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";
-// Check that pseudo-elements and anonymous nodes are not draggable.
+// Check that pseudo-elements, anonymous nodes and slotted nodes are not draggable.
const TEST_URL = URL_ROOT + "doc_markup_dragdrop.html";
-const PREF = "devtools.inspector.showAllAnonymousContent";
add_task(async function() {
- Services.prefs.setBoolPref(PREF, true);
+ await pushPref("devtools.inspector.showAllAnonymousContent", true);
+ await pushPref("dom.webcomponents.shadowdom.enabled", true);
+ await pushPref("dom.webcomponents.customelements.enabled", true);
let {inspector} = await openInspectorForURL(TEST_URL);
info("Expanding nodes below #test");
let parentFront = await getNodeFront("#test", inspector);
await inspector.markup.expandNode(parentFront);
await waitForMultipleChildrenUpdates(inspector);
@@ -40,9 +41,27 @@ add_task(async function() {
let anonymousDiv = inputContainer.elt.children[1].firstChild.container;
inputContainer.elt.scrollIntoView(true);
await selectNode(anonymousDiv.node, inspector);
info("Simulate dragging the anonymous node");
await simulateNodeDrag(inspector, anonymousDiv);
ok(!anonymousDiv.isDragging, "anonymous node isn't dragging");
+
+ info("Expanding all nodes below test-component");
+ let testComponentFront = await getNodeFront("test-component", inspector);
+ await inspector.markup.expandAll(testComponentFront);
+ await waitForMultipleChildrenUpdates(inspector);
+
+ info("Getting a slotted node and selecting it");
+ // Directly use the markup getContainer API in order to retrieve the slotted container
+ // for a given node front.
+ let slotted1Front = await getNodeFront(".slotted1", inspector);
+ let slottedContainer = inspector.markup.getContainer(slotted1Front, true);
+ slottedContainer.elt.scrollIntoView(true);
+ await selectNode(slotted1Front, inspector, "no-reason", true);
+
+ info("Simulate dragging the slotted node");
+ await simulateNodeDrag(inspector, slottedContainer);
+
+ ok(!slottedContainer.isDragging, "slotted node isn't dragging");
});
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_shadowdom.js
@@ -0,0 +1,204 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* import-globals-from helper_shadowdom.js */
+
+"use strict";
+
+loadHelperScript("helper_shadowdom.js");
+
+// 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`;
+
+ let {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`;
+
+ let {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 {
+ constructor() {
+ super();
+ let shadowRoot = this.attachShadow({mode: 'open'});
+ shadowRoot.innerHTML = html;
+ }
+ });
+ }
+
+ 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>`;
+
+ const EXPECTED_TREE = `
+ test-component
+ #shadow-root
+ test-container
+ slot
+ div!slotted
+ slot
+ third-component!slotted
+ other-component
+ #shadow-root
+ div
+ slot
+ div!slotted
+ div
+ div
+ third-component
+ #shadow-root
+ div`;
+
+ let {inspector} = await openInspectorForURL(TEST_URL);
+ await checkTreeFromRootSelector(EXPECTED_TREE, "test-component", inspector);
+});
+
+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>
+
+ <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>`;
+
+ const EXPECTED_TREE = `
+ test-component
+ #shadow-root
+ style
+ slot { display: block }
+ slot
+ ::before
+ div!slotted
+ ::after
+ ::before
+ class="light-dom"
+ ::after`;
+
+ let {inspector} = await openInspectorForURL(TEST_URL);
+ await checkTreeFromRootSelector(EXPECTED_TREE, "test-component", inspector);
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_shadowdom_clickreveal.js
@@ -0,0 +1,90 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* import-globals-from helper_shadowdom.js */
+
+"use strict";
+
+loadHelperScript("helper_shadowdom.js");
+
+// Test that the corresponding non-slotted node container gets selected when clicking on
+// the reveal link for a slotted node.
+
+const TEST_URL = `data:text/html;charset=utf-8,
+ <test-component>
+ <div slot="slot1" id="el1">slot1-1</div>
+ <div slot="slot1" id="el2">slot1-2</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>';
+ }
+ });
+ </script>`;
+
+add_task(async function() {
+ await enableWebComponents();
+
+ let {inspector} = await openInspectorForURL(TEST_URL);
+ let {markup} = inspector;
+
+ info("Find and expand the test-component shadow DOM host.");
+ let hostFront = await getNodeFront("test-component", inspector);
+ let hostContainer = markup.getContainer(hostFront);
+ await expandContainer(inspector, hostContainer);
+
+ info("Expand the shadow root");
+ let shadowRootContainer = hostContainer.getChildContainers()[0];
+ await expandContainer(inspector, shadowRootContainer);
+
+ info("Expand the slot");
+ let slotContainer = shadowRootContainer.getChildContainers()[0];
+ await expandContainer(inspector, slotContainer);
+
+ let slotChildContainers = slotContainer.getChildContainers();
+ is(slotChildContainers.length, 2, "Expecting 2 slotted children");
+
+ await checkRevealLink(inspector, slotChildContainers[0].node);
+ is(inspector.selection.nodeFront.id, "el1", "The right node was selected");
+ is(hostContainer.getChildContainers()[1].node, inspector.selection.nodeFront);
+
+ await checkRevealLink(inspector, slotChildContainers[1].node);
+ is(inspector.selection.nodeFront.id, "el2", "The right node was selected");
+ is(hostContainer.getChildContainers()[2].node, inspector.selection.nodeFront);
+});
+
+async function checkRevealLink(inspector, node) {
+ let slottedContainer = inspector.markup.getContainer(node, true);
+ info("Select the slotted container for the element");
+ await selectNode(node, inspector, "no-reason", true);
+ ok(inspector.selection.isSlotted(), "The selection is the slotted version");
+ ok(inspector.markup.getSelectedContainer().isSlotted(),
+ "The selected container is slotted");
+
+ info("Click on the reveal link and wait for the new node to be selected");
+ await clickOnRevealLink(inspector, slottedContainer);
+ let selectedFront = inspector.selection.nodeFront;
+ is(selectedFront, node, "The same node front is still selected");
+ ok(!inspector.selection.isSlotted(), "The selection is not the slotted version");
+ ok(!inspector.markup.getSelectedContainer().isSlotted(),
+ "The selected container is not slotted");
+}
+
+async function clickOnRevealLink(inspector, container) {
+ let onSelection = inspector.selection.once("new-node-front");
+ let revealLink = container.elt.querySelector(".reveal-link");
+ let tagline = revealLink.closest(".tag-line");
+ let win = inspector.markup.doc.defaultView;
+
+ // First send a mouseover on the tagline to force the link to be displayed.
+ EventUtils.synthesizeMouseAtCenter(tagline, {type: "mouseover"}, win);
+ EventUtils.synthesizeMouseAtCenter(revealLink, {}, win);
+
+ await onSelection;
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_shadowdom_delete.js
@@ -0,0 +1,92 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* import-globals-from helper_shadowdom.js */
+
+"use strict";
+
+loadHelperScript("helper_shadowdom.js");
+
+// Test that slot elements are correctly updated when slotted elements are being removed
+// from the DOM.
+
+const TEST_URL = `data:text/html;charset=utf-8,
+ <test-component>
+ <div slot="slot1" id="el1">slot1-1</div>
+ <div slot="slot1" id="el2">slot1-2</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"><div>default</div></slot>';
+ }
+ });
+ </script>`;
+
+add_task(async function() {
+ await enableWebComponents();
+
+ let {inspector} = await openInspectorForURL(TEST_URL);
+
+ // <test-component> is a shadow host.
+ info("Find and expand the test-component shadow DOM host.");
+ let hostFront = await getNodeFront("test-component", inspector);
+ await inspector.markup.expandNode(hostFront);
+ await waitForMultipleChildrenUpdates(inspector);
+
+ info("Test that expanding a shadow host shows shadow root and direct host children.");
+ let {markup} = inspector;
+ let hostContainer = markup.getContainer(hostFront);
+ let childContainers = hostContainer.getChildContainers();
+
+ is(childContainers.length, 3, "Expecting 3 children: shadowroot, 2 host children");
+ checkText(childContainers[0], "#shadow-root");
+ checkText(childContainers[1], "div");
+ checkText(childContainers[2], "div");
+
+ info("Expand the shadow root");
+ let shadowRootContainer = childContainers[0];
+ await expandContainer(inspector, shadowRootContainer);
+
+ let shadowChildContainers = shadowRootContainer.getChildContainers();
+ is(shadowChildContainers.length, 1, "Expecting 1 child slot");
+ checkText(shadowChildContainers[0], "slot");
+
+ info("Expand the slot");
+ let slotContainer = shadowChildContainers[0];
+ await expandContainer(inspector, slotContainer);
+
+ let slotChildContainers = slotContainer.getChildContainers();
+ is(slotChildContainers.length, 2, "Expecting 2 slotted children");
+ slotChildContainers.forEach(container => checkSlotted(container));
+
+ await deleteNode(inspector, "#el1");
+ slotChildContainers = slotContainer.getChildContainers();
+ is(slotChildContainers.length, 1, "Expecting 1 slotted child");
+ checkSlotted(slotChildContainers[0]);
+
+ await deleteNode(inspector, "#el2");
+ slotChildContainers = slotContainer.getChildContainers();
+ // After deleting the last host direct child we expect the slot to show the default
+ // content <div>default</div>
+ is(slotChildContainers.length, 1, "Expecting 1 child");
+ ok(!slotChildContainers[0].isSlotted(), "Container is a not slotted container");
+});
+
+async function deleteNode(inspector, selector) {
+ info("Select node " + selector + " and make sure it is focused");
+ await selectNode(selector, inspector);
+ await clickContainer(selector, inspector);
+
+ info("Delete the node");
+ let mutated = inspector.once("markupmutation");
+ let updated = inspector.once("inspector-updated");
+ EventUtils.sendKey("delete", inspector.panelWin);
+ await mutated;
+ await updated;
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_shadowdom_maxchildren.js
@@ -0,0 +1,103 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* import-globals-from helper_shadowdom.js */
+
+"use strict";
+
+loadHelperScript("helper_shadowdom.js");
+
+// Test that the markup view properly displays the "more nodes" button both for host
+// elements and for slot elements.
+
+const TEST_URL = `data:text/html;charset=utf-8,
+<test-component>
+ <div>node 1</div><div>node 2</div><div>node 3</div>
+ <div>node 4</div><div>node 5</div><div>node 6</div>
+</test-component>
+
+<script>
+ "use strict";
+ customElements.define("test-component", class extends HTMLElement {
+ constructor() {
+ super();
+ let shadowRoot = this.attachShadow({mode: "open"});
+ shadowRoot.innerHTML = "<slot>some default content</slot>";
+ }
+ connectedCallback() {}
+ disconnectedCallback() {}
+ });
+</script>`;
+
+add_task(async function() {
+ await enableWebComponents();
+ await pushPref("devtools.markup.pagesize", 5);
+
+ let {inspector} = await openInspectorForURL(TEST_URL);
+
+ // <test-component> is a shadow host.
+ info("Find and expand the test-component shadow DOM host.");
+ let hostFront = await getNodeFront("test-component", inspector);
+ await inspector.markup.expandNode(hostFront);
+ await waitForMultipleChildrenUpdates(inspector);
+
+ info("Test that expanding a shadow host shows shadow root and direct host children.");
+ let {markup} = inspector;
+ let hostContainer = markup.getContainer(hostFront);
+ let childContainers = hostContainer.getChildContainers();
+
+ is(childContainers.length, 6, "Expecting 6 children: shadowroot, 5 host children");
+ checkText(childContainers[0], "#shadow-root");
+ for (let i = 1; i < 6; i++) {
+ checkText(childContainers[i], "div");
+ checkText(childContainers[i], "node " + i);
+ }
+
+ info("Click on the more nodes button under the host element");
+ let moreNodesLink = hostContainer.elt.querySelector(".more-nodes");
+ ok(!!moreNodesLink, "A 'more nodes' button is displayed in the host container");
+ moreNodesLink.querySelector("button").click();
+ await inspector.markup._waitForChildren();
+
+ childContainers = hostContainer.getChildContainers();
+ is(childContainers.length, 7, "Expecting one additional host child");
+ checkText(childContainers[6], "div");
+ checkText(childContainers[6], "node 6");
+
+ info("Expand the shadow root");
+ let shadowRootContainer = childContainers[0];
+ let shadowRootFront = shadowRootContainer.node;
+ await inspector.markup.expandNode(shadowRootFront);
+ await waitForMultipleChildrenUpdates(inspector);
+
+ let shadowChildContainers = shadowRootContainer.getChildContainers();
+ is(shadowChildContainers.length, 1, "Expecting 1 slot child");
+ checkText(shadowChildContainers[0], "slot");
+
+ info("Expand the slot");
+ let slotContainer = shadowChildContainers[0];
+ let slotFront = slotContainer.node;
+ await inspector.markup.expandNode(slotFront);
+ await waitForMultipleChildrenUpdates(inspector);
+
+ let slotChildContainers = slotContainer.getChildContainers();
+ is(slotChildContainers.length, 5, "Expecting 5 slotted children");
+ for (let slotChildContainer of slotChildContainers) {
+ checkText(slotChildContainer, "div");
+ ok(slotChildContainer.elt.querySelector(".reveal-link"),
+ "Slotted container has a reveal link element");
+ }
+
+ info("Click on the more nodes button under the slot element");
+ moreNodesLink = slotContainer.elt.querySelector(".more-nodes");
+ ok(!!moreNodesLink, "A 'more nodes' button is displayed in the host container");
+ EventUtils.sendMouseEvent({type: "click"}, moreNodesLink.querySelector("button"));
+ await inspector.markup._waitForChildren();
+
+ slotChildContainers = slotContainer.getChildContainers();
+ is(slotChildContainers.length, 6, "Expecting one additional slotted element");
+ checkText(slotChildContainers[5], "div");
+ ok(slotChildContainers[5].elt.querySelector(".reveal-link"),
+ "Slotted container has a reveal link element");
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_shadowdom_mutations_shadow.js
@@ -0,0 +1,84 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* import-globals-from helper_shadowdom.js */
+
+"use strict";
+
+loadHelperScript("helper_shadowdom.js");
+
+// Test that the markup view is correctly updated when elements under a shadow root are
+// deleted or updated.
+
+const TEST_URL = `data:text/html;charset=utf-8,
+ <test-component>
+ <div slot="slot1" id="el1">slot1-1</div>
+ <div slot="slot1" id="el2">slot1-2</div>
+ </test-component>
+
+ <script>
+ 'use strict';
+ 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>
+ <div id="another-div"></div>\`;
+ }
+ });
+ </script>`;
+
+add_task(async function() {
+ await enableWebComponents();
+
+ let {inspector} = await openInspectorForURL(TEST_URL);
+
+ const tree = `
+ test-component
+ #shadow-root
+ slot1-container
+ slot
+ div!slotted
+ div!slotted
+ another-div
+ div
+ div`;
+ await checkTreeFromRootSelector(tree, "test-component", inspector);
+
+ info("Delete a shadow dom element and check the updated markup view");
+ let mutated = waitForMutation(inspector, "childList");
+ ContentTask.spawn(gBrowser.selectedBrowser, {}, function() {
+ let shadowRoot = content.document.querySelector("test-component").shadowRoot;
+ let slotContainer = shadowRoot.getElementById("slot1-container");
+ slotContainer.remove();
+ });
+ await mutated;
+
+ let treeAfterDelete = `
+ test-component
+ #shadow-root
+ another-div
+ div
+ div`;
+ await checkTreeFromRootSelector(treeAfterDelete, "test-component", inspector);
+
+ mutated = inspector.once("markupmutation");
+ ContentTask.spawn(gBrowser.selectedBrowser, {}, function() {
+ let shadowRoot = content.document.querySelector("test-component").shadowRoot;
+ let shadowDiv = shadowRoot.getElementById("another-div");
+ shadowDiv.setAttribute("random-attribute", "1");
+ });
+ await mutated;
+
+ info("Add an attribute on a shadow dom element and check the updated markup view");
+ let treeAfterAttrChange = `
+ test-component
+ #shadow-root
+ random-attribute
+ div
+ div`;
+ await checkTreeFromRootSelector(treeAfterAttrChange, "test-component", inspector);
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_shadowdom_navigation.js
@@ -0,0 +1,87 @@
+/* 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 markup-view navigation works correctly with shadow dom slotted nodes.
+// Each slotted nodes has two containers representing the same node front in the markup
+// view, we need to make sure that navigating to the slotted version selects the slotted
+// container, and navigating to the non-slotted element selects the non-slotted container.
+
+const TEST_URL = `data:text/html;charset=utf-8,
+ <test-component class="test-component">
+ <div slot="slot1" class="slotted1"><div class="slot1-child">slot1-1</div></div>
+ <div slot="slot1" class="slotted2">slot1-2</div>
+ </test-component>
+
+ <script>
+ 'use strict';
+ customElements.define('test-component', class extends HTMLElement {
+ constructor() {
+ super();
+ let shadowRoot = this.attachShadow({mode: 'open'});
+ shadowRoot.innerHTML = '<slot class="slot1" name="slot1"></slot>';
+ }
+ });
+ </script>`;
+
+const TEST_DATA = [
+ ["KEY_PageUp", "html"],
+ ["KEY_ArrowDown", "head"],
+ ["KEY_ArrowDown", "body"],
+ ["KEY_ArrowDown", "test-component"],
+ ["KEY_ArrowRight", "test-component"],
+ ["KEY_ArrowDown", "shadow-root"],
+ ["KEY_ArrowRight", "shadow-root"],
+ ["KEY_ArrowDown", "slot1"],
+ ["KEY_ArrowRight", "slot1"],
+ ["KEY_ArrowDown", "div", "slotted1"],
+ ["KEY_ArrowDown", "div", "slotted2"],
+ ["KEY_ArrowDown", "slotted1"],
+ ["KEY_ArrowRight", "slotted1"],
+ ["KEY_ArrowDown", "slot1-child"],
+ ["KEY_ArrowDown", "slotted2"],
+];
+
+add_task(async function() {
+ await pushPref("dom.webcomponents.shadowdom.enabled", true);
+ await pushPref("dom.webcomponents.customelements.enabled", true);
+
+ let {inspector} = await openInspectorForURL(TEST_URL);
+
+ info("Making sure the markup-view frame is focused");
+ inspector.markup._frame.focus();
+
+ info("Starting to iterate through the test data");
+ for (let [key, expected, slottedClassName] of TEST_DATA) {
+ info("Testing step: " + key + " to navigate to " + expected);
+ EventUtils.synthesizeKey(key);
+
+ info("Making sure markup-view children get updated");
+ await waitForChildrenUpdated(inspector);
+
+ info("Checking the right node is selected");
+ checkSelectedNode(key, expected, slottedClassName, inspector);
+ }
+
+ // Same as in browser_markup_navigation.js, use a single catch-call event listener.
+ await inspector.once("inspector-updated");
+});
+
+function checkSelectedNode(key, expected, slottedClassName, inspector) {
+ let selectedContainer = inspector.markup.getSelectedContainer();
+ let slotted = !!slottedClassName;
+
+ is(selectedContainer.isSlotted(), slotted,
+ `Selected container is ${slotted ? "slotted" : "not slotted"} as expected`);
+ is(inspector.selection.isSlotted(), slotted,
+ `Inspector selection is also ${slotted ? "slotted" : "not slotted"}`);
+ ok(selectedContainer.elt.textContent.includes(expected),
+ "Found expected content: " + expected + " in container after pressing " + key);
+
+ if (slotted) {
+ is(selectedContainer.node.className, slottedClassName,
+ "Slotted has the expected classname " + slottedClassName);
+ }
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_shadowdom_noslot.js
@@ -0,0 +1,112 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* import-globals-from helper_shadowdom.js */
+
+"use strict";
+
+loadHelperScript("helper_shadowdom.js");
+
+// Test that the markup view is correctly displayed when a component has children but no
+// slots are available under the shadow root.
+
+const TEST_URL = `data:text/html;charset=utf-8,
+ <style>
+ .has-before::before { content: "before-content" }
+ </style>
+
+ <div class="root">
+ <no-slot-component>
+ <div class="not-nested">light</div>
+ <div class="nested">
+ <div class="has-before"></div>
+ <div>dummy for Bug 1441863</div>
+ </div>
+ </no-slot-component>
+ <slot-component>
+ <div class="not-nested">light</div>
+ <div class="nested">
+ <div class="has-before"></div>
+ </div>
+ </slot-component>
+ </div>
+
+ <script>
+ 'use strict';
+ customElements.define('no-slot-component', class extends HTMLElement {
+ constructor() {
+ super();
+ let shadowRoot = this.attachShadow({mode: 'open'});
+ shadowRoot.innerHTML = '<div class="no-slot-div"></div>';
+ }
+ });
+ customElements.define('slot-component', class extends HTMLElement {
+ constructor() {
+ super();
+ let shadowRoot = this.attachShadow({mode: 'open'});
+ shadowRoot.innerHTML = '<slot></slot>';
+ }
+ });
+ </script>`;
+
+add_task(async function() {
+ await enableWebComponents();
+
+ let {inspector} = await openInspectorForURL(TEST_URL);
+
+ // We expect that host children are correctly displayed when no slots are defined.
+ let beforeTree = `
+ class="root"
+ no-slot-component
+ #shadow-root
+ no-slot-div
+ class="not-nested"
+ class="nested"
+ class="has-before"
+ dummy for Bug 1441863
+ slot-component
+ #shadow-root
+ slot
+ div!slotted
+ div!slotted
+ class="not-nested"
+ class="nested"
+ class="has-before"
+ ::before`;
+ await checkTreeFromRootSelector(beforeTree, ".root", inspector);
+
+ info("Move the non-slotted element with class has-before and check the pseudo appears");
+ let mutated = waitForNMutations(inspector, "childList", 2);
+ let pseudoMutated = waitForMutation(inspector, "nativeAnonymousChildList");
+ ContentTask.spawn(gBrowser.selectedBrowser, {}, function() {
+ let root = content.document.querySelector(".root");
+ let hasBeforeEl = content.document.querySelector("no-slot-component .has-before");
+ root.appendChild(hasBeforeEl);
+ });
+ await mutated;
+ await pseudoMutated;
+
+ // As the non-slotted has-before is moved into the tree, the before pseudo is expected
+ // to appear.
+ let afterTree = `
+ class="root"
+ no-slot-component
+ #shadow-root
+ no-slot-div
+ class="not-nested"
+ class="nested"
+ dummy for Bug 1441863
+ slot-component
+ #shadow-root
+ slot
+ div!slotted
+ div!slotted
+ class="not-nested"
+ class="nested"
+ class="has-before"
+ ::before
+ class="has-before"
+ ::before`;
+ await checkTreeFromRootSelector(afterTree, ".root", inspector);
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_shadowdom_slotupdate.js
@@ -0,0 +1,75 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* import-globals-from helper_shadowdom.js */
+
+"use strict";
+
+loadHelperScript("helper_shadowdom.js");
+
+// Test that slotted elements are correctly updated when the slot attribute is modified
+// on already slotted elements.
+
+const TEST_URL = `data:text/html;charset=utf-8,
+ <test-component>
+ <div slot="slot1">slot1-1</div>
+ <div slot="slot1">slot1-2</div>
+ <div slot="slot2" id="to-update">slot2-1</div>
+ <div slot="slot2">slot2-2</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>';
+ }
+ });
+ </script>`;
+
+add_task(async function() {
+ await enableWebComponents();
+
+ let {inspector} = await openInspectorForURL(TEST_URL);
+
+ const tree = `
+ test-component
+ #shadow-root
+ name="slot1"
+ div!slotted
+ div!slotted
+ name="slot2"
+ div!slotted
+ div!slotted
+ slot1-1
+ slot1-2
+ slot2-1
+ slot2-2`;
+ await checkTreeFromRootSelector(tree, "test-component", inspector);
+
+ info("Listening for the markupmutation event");
+ let mutated = inspector.once("markupmutation");
+ ContentTask.spawn(gBrowser.selectedBrowser, {}, function() {
+ content.document.getElementById("to-update").setAttribute("slot", "slot1");
+ });
+ await mutated;
+
+ // After mutation we expect slot1 to have one more slotted node, and slot2 one less.
+ const mutatedTree = `
+ test-component
+ #shadow-root
+ name="slot1"
+ div!slotted
+ div!slotted
+ div!slotted
+ name="slot2"
+ div!slotted
+ slot1-1
+ slot1-2
+ slot2-1
+ slot2-2`;
+ await checkTreeFromRootSelector(mutatedTree, "test-component", inspector);
+});
--- a/devtools/client/inspector/markup/test/browser_markup_toggle_closing_tag_line.js
+++ b/devtools/client/inspector/markup/test/browser_markup_toggle_closing_tag_line.js
@@ -12,34 +12,34 @@ const TEST_URL = `data:text/html;charset
<div class="outer-div"><span>test</span></div>
<iframe src="data:text/html;charset=utf8,<div>test</div>"></iframe>`;
add_task(async function() {
let {inspector} = await openInspectorForURL(TEST_URL);
info("Getting the container for .outer-div parent element");
let container = await getContainerForSelector(".outer-div", inspector);
- await expandContainer(inspector, container);
+ await expandContainerByClick(inspector, container);
let closeTagLine = container.closeTagLine;
ok(closeTagLine && closeTagLine.textContent.includes("div"),
"DIV has a close tag-line with the correct content");
info("Expand the iframe element");
container = await getContainerForSelector("iframe", inspector);
- await expandContainer(inspector, container);
+ await expandContainerByClick(inspector, container);
ok(container.expanded, "iframe is expanded");
closeTagLine = container.closeTagLine;
ok(closeTagLine && closeTagLine.textContent.includes("iframe"),
"IFRAME has a close tag-line with the correct content");
info("Retrieve the nodefront for the #document root inside the iframe");
let iframe = await getNodeFront("iframe", inspector);
let {nodes} = await inspector.walker.children(iframe);
let documentFront = nodes[0];
ok(documentFront.displayName === "#document", "First child of IFRAME is #document");
info("Expand the iframe's #document node element");
container = getContainerForNodeFront(documentFront, inspector);
- await expandContainer(inspector, container);
+ await expandContainerByClick(inspector, container);
ok(container.expanded, "#document is expanded");
ok(!container.closeTagLine, "readonly (#document) node has no close tag-line");
});
--- a/devtools/client/inspector/markup/test/doc_markup_dragdrop.html
+++ b/devtools/client/inspector/markup/test/doc_markup_dragdrop.html
@@ -14,10 +14,26 @@ https://bugzilla.mozilla.org/show_bug.cg
</head>
<body>
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=858038">Mozilla Bug 858038</a>
<p id="display"></p>
<div id="content" style="display: none">
</div>
<input id="anonymousParent" /><span id="before">Before<!-- Force not-inline --></span>
<pre id="test"><span id="firstChild">First</span><span id="middleChild">Middle</span><span id="lastChild">Last</span></pre> <span id="after">After</span>
+
+ <test-component class="test-component">
+ <div slot="slot1" class="slotted1">slot1-1</div>
+ <div slot="slot1" class="slotted2">slot1-2</div>
+ </test-component>
+
+ <script>
+ "use strict";
+ customElements.define("test-component", class extends HTMLElement {
+ constructor() {
+ super();
+ let shadowRoot = this.attachShadow({mode: "open"});
+ shadowRoot.innerHTML = '<slot class="slot1" name="slot1"></slot>';
+ }
+ });
+ </script>
</body>
</html>
--- a/devtools/client/inspector/markup/test/head.js
+++ b/devtools/client/inspector/markup/test/head.js
@@ -614,21 +614,8 @@ async function checkDeleteAndSelection(i
let node = await getNodeFront(selector, inspector);
ok(!node, "The node can't be found in the page anymore");
info("Undo the deletion to restore the original markup");
await undoChange(inspector);
node = await getNodeFront(selector, inspector);
ok(node, "The node is back");
}
-
-/**
- * Expand the provided markup container by clicking on the expand arrow and waiting for
- * inspector and children to update.
- */
-async function expandContainer(inspector, container) {
- let onChildren = waitForChildrenUpdated(inspector);
- let onUpdated = inspector.once("inspector-updated");
- EventUtils.synthesizeMouseAtCenter(container.expander, {},
- inspector.markup.doc.defaultView);
- await onChildren;
- await onUpdated;
-}
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/markup/test/helper_shadowdom.js
@@ -0,0 +1,142 @@
+/* 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/. */
+
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+
+/* import-globals-from head.js */
+
+"use strict";
+
+async function checkTreeFromRootSelector(tree, selector, inspector) {
+ let {markup} = inspector;
+
+ info(`Find and expand the shadow DOM host matching selector ${selector}.`);
+ let rootFront = await getNodeFront(selector, inspector);
+ let rootContainer = markup.getContainer(rootFront);
+
+ let parsedTree = parseTree(tree);
+ let treeRoot = parsedTree.children[0];
+ await checkNode(treeRoot, rootContainer, inspector);
+}
+
+async function checkNode(treeNode, container, inspector) {
+ let {node, children, path} = treeNode;
+ info("Checking [" + path + "]");
+ info("Checking node: " + node);
+
+ let slotted = node.includes("!slotted");
+ if (slotted) {
+ checkSlotted(container, node.replace("!slotted", ""));
+ } else {
+ checkText(container, node);
+ }
+
+ if (!children.length) {
+ ok(!container.canExpand, "Container for [" + path + "] has no children");
+ return;
+ }
+
+ // Expand the container if not already done.
+ if (!container.expanded) {
+ await expandContainer(inspector, container);
+ }
+
+ let containers = container.getChildContainers();
+ is(containers.length, children.length,
+ "Node [" + path + "] has the expected number of children");
+ for (let i = 0; i < children.length; i++) {
+ await checkNode(children[i], containers[i], inspector);
+ }
+}
+
+/**
+ * Helper designed to parse a tree represented as:
+ * root
+ * child1
+ * subchild1
+ * subchild2
+ * child2
+ * subchild3!slotted
+ *
+ * Lines represent a simplified view of the markup, where the trimmed line is supposed to
+ * be included in the text content of the actual markupview container.
+ * This method returns an object that can be passed to checkNode() to verify the current
+ * markup view displays the expected structure.
+ */
+function parseTree(inputString) {
+ let tree = {
+ level: 0,
+ children: []
+ };
+ let lines = inputString.split("\n");
+ lines = lines.filter(l => l.trim());
+
+ let currentNode = tree;
+ for (let line of lines) {
+ let nodeString = line.trim();
+ let level = line.split(" ").length;
+
+ let parent;
+ if (level > currentNode.level) {
+ parent = currentNode;
+ } else {
+ parent = currentNode.parent;
+ for (let i = 0; i < currentNode.level - level; i++) {
+ parent = parent.parent;
+ }
+ }
+
+ let node = {
+ node: nodeString,
+ children: [],
+ parent,
+ level,
+ path: parent.path + " " + nodeString
+ };
+
+ parent.children.push(node);
+ currentNode = node;
+ }
+
+ return tree;
+}
+
+function checkSlotted(container, expectedType = "div") {
+ checkText(container, expectedType);
+ ok(container.isSlotted(), "Container is a slotted container");
+ ok(container.elt.querySelector(".reveal-link"),
+ "Slotted container has a reveal link element");
+}
+
+function checkText(container, expectedText) {
+ let textContent = container.elt.textContent;
+ ok(textContent.includes(expectedText), "Container has expected text: " + expectedText);
+}
+
+function waitForMutation(inspector, type) {
+ return waitForNMutations(inspector, type, 1);
+}
+
+function waitForNMutations(inspector, type, count) {
+ info(`Expecting ${count} markupmutation of type ${type}`);
+ let receivedMutations = 0;
+ return new Promise(resolve => {
+ inspector.on("markupmutation", function onMutation(mutations) {
+ let validMutations = mutations.filter(m => m.type === type).length;
+ receivedMutations = receivedMutations + validMutations;
+ if (receivedMutations == count) {
+ inspector.off("markupmutation", onMutation);
+ resolve();
+ }
+ });
+ });
+}
+
+/**
+ * Temporarily flip all the preferences needed to enable web components.
+ */
+async function enableWebComponents() {
+ await pushPref("dom.webcomponents.shadowdom.enabled", true);
+ await pushPref("dom.webcomponents.customelements.enabled", true);
+}
--- a/devtools/client/inspector/rules/test/browser.ini
+++ b/devtools/client/inspector/rules/test/browser.ini
@@ -241,16 +241,18 @@ subsuite = clipboard
skip-if = (os == 'linux' && bits == 32 && debug) # bug 1328915, disable linux32 debug devtools for timeouts
[browser_rules_selector-highlighter-on-navigate.js]
[browser_rules_selector-highlighter_01.js]
[browser_rules_selector-highlighter_02.js]
[browser_rules_selector-highlighter_03.js]
[browser_rules_selector-highlighter_04.js]
[browser_rules_selector-highlighter_05.js]
[browser_rules_selector_highlight.js]
+[browser_rules_shadowdom_slot_rules.js]
+skip-if = !stylo # shadow DOM is only enabled with stylo.
[browser_rules_shapes-toggle_01.js]
[browser_rules_shapes-toggle_02.js]
[browser_rules_shapes-toggle_03.js]
[browser_rules_shapes-toggle_04.js]
[browser_rules_shapes-toggle_05.js]
[browser_rules_shapes-toggle_06.js]
[browser_rules_shapes-toggle_07.js]
[browser_rules_shorthand-overridden-lists.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_shadowdom_slot_rules.js
@@ -0,0 +1,83 @@
+/* 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 when selecting a slot element, the rule view displays the rules for the
+// corresponding element.
+
+const TEST_URL = `data:text/html;charset=utf-8,` + encodeURIComponent(`
+ <html>
+ <head>
+ <style>
+ #el1 { color: red }
+ #el2 { color: blue }
+ </style>
+ </head>
+ <body>
+ <test-component>
+ <div slot="slot1" id="el1">slot1-1</div>
+ <div slot="slot1" id="el2">slot1-2</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>';
+ }
+ });
+ </script>
+ </body>
+ </html>
+`);
+
+add_task(async function() {
+ await pushPref("dom.webcomponents.shadowdom.enabled", true);
+ await pushPref("dom.webcomponents.customelements.enabled", true);
+
+ let {inspector} = await openInspectorForURL(TEST_URL);
+ let {markup} = inspector;
+ let ruleview = inspector.getPanel("ruleview").view;
+
+ // <test-component> is a shadow host.
+ info("Find and expand the test-component shadow DOM host.");
+ let hostFront = await getNodeFront("test-component", inspector);
+
+ await markup.expandNode(hostFront);
+ await waitForMultipleChildrenUpdates(inspector);
+
+ info("Test that expanding a shadow host shows shadow root and one host child.");
+ let hostContainer = markup.getContainer(hostFront);
+
+ info("Expand the shadow root");
+ let childContainers = hostContainer.getChildContainers();
+ let shadowRootContainer = childContainers[0];
+ await expandContainer(inspector, shadowRootContainer);
+
+ info("Expand the slot");
+ let shadowChildContainers = shadowRootContainer.getChildContainers();
+ let slotContainer = shadowChildContainers[0];
+ await expandContainer(inspector, slotContainer);
+
+ let slotChildContainers = slotContainer.getChildContainers();
+ is(slotChildContainers.length, 2, "Expecting 2 slotted children");
+
+ info("Select slotted node and check that the rule view displays correct content");
+ await selectNode(slotChildContainers[0].node, inspector);
+ checkRule(ruleview, "#el1", "color", "red");
+
+ info("Select another slotted node and check the rule view");
+ await selectNode(slotChildContainers[1].node, inspector);
+ checkRule(ruleview, "#el2", "color", "blue");
+});
+
+function checkRule(ruleview, selector, name, expectedValue) {
+ let rule = getRuleViewRule(ruleview, selector);
+ ok(rule, "ruleview shows the expected rule for slotted " + selector);
+ let value = getRuleViewPropertyValue(ruleview, selector, name);
+ is(value, expectedValue, "ruleview shows the expected value for slotted " + selector);
+}
--- a/devtools/client/inspector/test/browser.ini
+++ b/devtools/client/inspector/test/browser.ini
@@ -54,16 +54,18 @@ support-files =
[browser_inspector_addSidebarTab.js]
[browser_inspector_breadcrumbs.js]
[browser_inspector_breadcrumbs_highlight_hover.js]
[browser_inspector_breadcrumbs_keybinding.js]
[browser_inspector_breadcrumbs_keyboard_trap.js]
skip-if = os == "mac" # Full keyboard navigation on OSX only works if Full Keyboard Access setting is set to All Control in System Keyboard Preferences
[browser_inspector_breadcrumbs_mutations.js]
[browser_inspector_breadcrumbs_namespaced.js]
+[browser_inspector_breadcrumbs_shadowdom.js]
+skip-if = !stylo # shadow DOM is only enabled with stylo.
[browser_inspector_breadcrumbs_visibility.js]
[browser_inspector_delete-selected-node-01.js]
[browser_inspector_delete-selected-node-02.js]
[browser_inspector_delete-selected-node-03.js]
[browser_inspector_destroy-after-navigation.js]
[browser_inspector_destroy-before-ready.js]
[browser_inspector_expand-collapse.js]
[browser_inspector_gcli-inspect-command.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_breadcrumbs_shadowdom.js
@@ -0,0 +1,86 @@
+/* vim: set ft=javascript 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 breadcrumbs widget refreshes correctly when there are markup
+// mutations, even if the currently selected node is a slotted node in the shadow DOM.
+
+const TEST_URL = `data:text/html;charset=utf-8,
+ <test-component>
+ <div slot="slot1" id="el1">content</div>
+ </test-component>
+
+ <script>
+ 'use strict';
+ customElements.define('test-component', class extends HTMLElement {
+ constructor() {
+ super();
+ let shadowRoot = this.attachShadow({mode: 'open'});
+ shadowRoot.innerHTML = '<slot class="slot-class" name="slot1"></slot>';
+ }
+ });
+ </script>`;
+
+add_task(async function() {
+ await pushPref("dom.webcomponents.shadowdom.enabled", true);
+ await pushPref("dom.webcomponents.customelements.enabled", true);
+
+ let {inspector} = await openInspectorForURL(TEST_URL);
+ let {markup} = inspector;
+ let breadcrumbs = inspector.panelDoc.getElementById("inspector-breadcrumbs");
+
+ info("Find and expand the test-component shadow DOM host.");
+ let hostFront = await getNodeFront("test-component", inspector);
+ let hostContainer = markup.getContainer(hostFront);
+ await expandContainer(inspector, hostContainer);
+
+ info("Expand the shadow root");
+ let shadowRootContainer = hostContainer.getChildContainers()[0];
+ await expandContainer(inspector, shadowRootContainer);
+
+ let slotContainer = shadowRootContainer.getChildContainers()[0];
+
+ info("Select the slot node and wait for the breadcrumbs update");
+ let slotNodeFront = slotContainer.node;
+ let onBreadcrumbsUpdated = inspector.once("breadcrumbs-updated");
+ inspector.selection.setNodeFront(slotNodeFront);
+ await onBreadcrumbsUpdated;
+
+ checkBreadcrumbsContent(breadcrumbs,
+ ["html", "body", "test-component", "slot.slot-class"]);
+
+ info("Expand the slot");
+ await expandContainer(inspector, slotContainer);
+
+ let slotChildContainers = slotContainer.getChildContainers();
+ is(slotChildContainers.length, 1, "Expecting 1 slotted child");
+
+ info("Select the slotted node and wait for the breadcrumbs update");
+ let slottedNodeFront = slotChildContainers[0].node;
+ onBreadcrumbsUpdated = inspector.once("breadcrumbs-updated");
+ inspector.selection.setNodeFront(slottedNodeFront);
+ await onBreadcrumbsUpdated;
+
+ checkBreadcrumbsContent(breadcrumbs, ["html", "body", "test-component", "div#el1"]);
+
+ info("Update the classname of the real element and wait for the breadcrumbs update");
+ onBreadcrumbsUpdated = inspector.once("breadcrumbs-updated");
+ await ContentTask.spawn(gBrowser.selectedBrowser, {}, function() {
+ content.document.getElementById("el1").setAttribute("class", "test");
+ });
+ await onBreadcrumbsUpdated;
+
+ checkBreadcrumbsContent(breadcrumbs,
+ ["html", "body", "test-component", "div#el1.test"]);
+});
+
+function checkBreadcrumbsContent(breadcrumbs, selectors) {
+ info("Check the output of the breadcrumbs widget");
+ let container = breadcrumbs.querySelector(".html-arrowscrollbox-inner");
+ is(container.childNodes.length, selectors.length, "Correct number of buttons");
+ for (let i = 0; i < container.childNodes.length; i++) {
+ is(container.childNodes[i].textContent, selectors[i],
+ "Text content for button " + i + " is correct");
+ }
+}
--- a/devtools/client/inspector/test/head.js
+++ b/devtools/client/inspector/test/head.js
@@ -833,8 +833,31 @@ async function toggleShapesHighlighter(v
shapesToggle.click();
await onHighlighterShown;
} else {
let onHighlighterHidden = highlighters.once("shapes-highlighter-hidden");
shapesToggle.click();
await onHighlighterHidden;
}
}
+
+/**
+ * Expand the provided markup container programatically and wait for all children to
+ * update.
+ */
+async function expandContainer(inspector, container) {
+ await inspector.markup.expandNode(container.node);
+ await waitForMultipleChildrenUpdates(inspector);
+}
+
+/**
+ * Expand the provided markup container by clicking on the expand arrow and waiting for
+ * inspector and children to update. Similar to expandContainer helper, but this method
+ * uses a click rather than programatically calling expandNode().
+ */
+async function expandContainerByClick(inspector, container) {
+ let onChildren = waitForChildrenUpdated(inspector);
+ let onUpdated = inspector.once("inspector-updated");
+ EventUtils.synthesizeMouseAtCenter(container.expander, {},
+ inspector.markup.doc.defaultView);
+ await onChildren;
+ await onUpdated;
+}
--- a/devtools/client/inspector/test/shared-head.js
+++ b/devtools/client/inspector/test/shared-head.js
@@ -196,21 +196,21 @@ function getNodeFront(selector, {walker}
* selector
* @param {String|NodeFront} selector
* @param {InspectorPanel} inspector The instance of InspectorPanel currently
* loaded in the toolbox
* @param {String} reason Defaults to "test" which instructs the inspector not
* to highlight the node upon selection
* @return {Promise} Resolves when the inspector is updated with the new node
*/
-var selectNode = async function(selector, inspector, reason = "test") {
+var selectNode = async function(selector, inspector, reason = "test", isSlotted) {
info("Selecting the node for '" + selector + "'");
let nodeFront = await getNodeFront(selector, inspector);
let updated = inspector.once("inspector-updated");
- inspector.selection.setNodeFront(nodeFront, { reason });
+ inspector.selection.setNodeFront(nodeFront, { reason, isSlotted });
await updated;
};
/**
* Create a throttling function that can be manually "flushed". This is to replace the
* use of the `debounce` function from `devtools/client/inspector/shared/utils.js`, which
* has a setTimeout that can cause intermittents.
* @return {Function} This function has the same function signature as debounce, but