commit 4147d8ed8b77dd5fb0b35f4ca05b6beeca2f6801
parent e44fab07fd3d713e6495fbc2e49d262801e29704
Author: Nicolas Chevobbe <nchevobbe@mozilla.com>
Date: Tue, 2 Dec 2025 11:35:00 +0000
Bug 1940930 - [devtools] Fix add new rule for shadow-root elements. r=devtools-reviewers,jdescottes
We only needed to retrieve the shadow root when the element is in the shadow DOM
A test is added to cover adding a rule and a declaration on shadow DOM elements
from the rules view.
Differential Revision: https://phabricator.services.mozilla.com/D273222
Diffstat:
6 files changed, 172 insertions(+), 12 deletions(-)
diff --git a/devtools/client/inspector/rules/test/browser_part1.toml b/devtools/client/inspector/rules/test/browser_part1.toml
@@ -62,6 +62,8 @@ support-files = [
["browser_rules_add-rule-pseudo-class.js"]
+["browser_rules_add-rule-shadow-dom.js"]
+
["browser_rules_add-rule-then-property-edit-selector.js"]
["browser_rules_add-rule-with-menu.js"]
diff --git a/devtools/client/inspector/rules/test/browser_rules_add-rule-and-property.js b/devtools/client/inspector/rules/test/browser_rules_add-rule-and-property.js
@@ -28,4 +28,16 @@ add_task(async function () {
const prop = textProps[textProps.length - 1];
is(prop.name, "font-weight", "The last property name is font-weight");
is(prop.value, "bold", "The last property value is bold");
+
+ info(
+ "Add another rule to make sure we reuse the stylesheet we created the first time we added a rule"
+ );
+ await addNewRuleAndDismissEditor(inspector, view, "#testid", 1);
+
+ const styleSheetsCount = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ () => content.document.styleSheets.length
+ );
+ is(styleSheetsCount, 1, "Only one stylesheet was created in the document");
});
diff --git a/devtools/client/inspector/rules/test/browser_rules_add-rule-shadow-dom.js b/devtools/client/inspector/rules/test/browser_rules_add-rule-shadow-dom.js
@@ -0,0 +1,135 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests adding a rule on elements in shadow DOM
+
+const TEST_URL =
+ `data:text/html;charset=utf-8,` +
+ encodeURIComponent(`
+ <html>
+ <body>
+ <test-component>
+ <div slot="my-slot">a slot</div>
+ </test-component>
+
+ <script>
+ 'use strict';
+ customElements.define('test-component', class extends HTMLElement {
+ constructor() {
+ super();
+ let shadowRoot = this.attachShadow({mode: 'open'});
+ shadowRoot.innerHTML = '<h1>hello</h1><slot name="my-slot"></slot>';
+ }
+ });
+ </script>
+ </body>
+ </html>
+`);
+
+add_task(async function () {
+ await addTab(TEST_URL);
+ const { inspector, view } = await openRuleView();
+ const { markup } = inspector;
+
+ // <test-component> is a shadow host.
+ info("Find and expand the test-component shadow DOM host.");
+ const 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."
+ );
+ const hostContainer = markup.getContainer(hostFront);
+
+ info("Expand the shadow root");
+ const childContainers = hostContainer.getChildContainers();
+ const shadowRootContainer = childContainers[0];
+ await expandContainer(inspector, shadowRootContainer);
+
+ const [h1Container, slotContainer] = shadowRootContainer.getChildContainers();
+
+ info("Add a rule on the h1 node in the shadow DOM");
+ await selectNode(h1Container.node, inspector);
+
+ // Add the rule
+ await addNewRuleAndDismissEditor(inspector, view, "h1", 1);
+ // and a property
+ await addProperty(view, 1, "color", "red");
+
+ await checkRuleViewContent(view, [
+ {
+ selector: "element",
+ declarations: [],
+ },
+ {
+ selector: "h1",
+ declarations: [{ name: "color", value: "red", dirty: true }],
+ },
+ ]);
+ let computedColor = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ () =>
+ content.getComputedStyle(
+ content.document
+ .querySelector("test-component")
+ .shadowRoot.querySelector("h1")
+ ).color
+ );
+ is(
+ computedColor,
+ "rgb(255, 0, 0)",
+ "The declaration was properly assigned to the shadow DOM h1"
+ );
+
+ info("Add a rule on the slot node in the shadow DOM");
+ await selectNode(slotContainer.node, inspector);
+
+ // Add the rule
+ await addNewRuleAndDismissEditor(inspector, view, "slot", 1);
+ // and a property
+ await addProperty(view, 1, "color", "blue");
+
+ await checkRuleViewContent(view, [
+ {
+ selector: "element",
+ declarations: [],
+ },
+ {
+ selector: "slot",
+ declarations: [{ name: "color", value: "blue", dirty: true }],
+ },
+ ]);
+ computedColor = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ () =>
+ content.getComputedStyle(
+ content.document
+ .querySelector("test-component")
+ .shadowRoot.querySelector("slot")
+ ).color
+ );
+ is(
+ computedColor,
+ "rgb(0, 0, 255)",
+ "The declaration was properly assigned to the shadow DOM h1"
+ );
+
+ const shadowRootStyleSheetsCount = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ () =>
+ content.document.querySelector("test-component").shadowRoot.styleSheets
+ .length
+ );
+ is(
+ shadowRootStyleSheetsCount,
+ 1,
+ "Only one stylesheet was created in the shadow root"
+ );
+});
diff --git a/devtools/server/actors/page-style.js b/devtools/server/actors/page-style.js
@@ -94,8 +94,8 @@ class PageStyleActor extends Actor {
// Latest node queried for its applied styles.
this.selectedElement = null;
- // Maps document elements to style elements, used to add new rules.
- this.styleElements = new WeakMap();
+ // Maps root node (document|ShadowRoot) to stylesheets, which are used to add new rules.
+ this.styleSheetsByRootNode = new WeakMap();
this.onFrameUnload = this.onFrameUnload.bind(this);
@@ -122,7 +122,7 @@ class PageStyleActor extends Actor {
this.refMap = null;
this.selectedElement = null;
this.cssLogic = null;
- this.styleElements = null;
+ this.styleSheetsByRootNode = null;
this._observedRules = [];
}
@@ -1219,7 +1219,7 @@ class PageStyleActor extends Actor {
* On page navigation, tidy up remaining objects.
*/
onFrameUnload() {
- this.styleElements = new WeakMap();
+ this.styleSheetsByRootNode = new WeakMap();
}
_onStylesheetUpdated({ resourceId, updateKind, updates = {} }) {
@@ -1270,14 +1270,19 @@ class PageStyleActor extends Actor {
async addNewRule(node, pseudoClasses) {
let sheet = null;
const doc = node.rawNode.ownerDocument;
+ const rootNode = node.rawNode.getRootNode();
+
if (
- this.styleElements.has(doc) &&
- this.styleElements.get(doc).ownerNode?.isConnected
+ this.styleSheetsByRootNode.has(rootNode) &&
+ this.styleSheetsByRootNode.get(rootNode).ownerNode?.isConnected
) {
- sheet = this.styleElements.get(doc);
+ sheet = this.styleSheetsByRootNode.get(rootNode);
} else {
- sheet = await this.styleSheetsManager.addStyleSheet(doc);
- this.styleElements.set(doc, sheet);
+ sheet = await this.styleSheetsManager.addStyleSheet(
+ doc,
+ node.rawNode.containingShadowRoot || doc.documentElement
+ );
+ this.styleSheetsByRootNode.set(rootNode, sheet);
}
const cssRules = sheet.cssRules;
diff --git a/devtools/server/actors/style-sheets.js b/devtools/server/actors/style-sheets.js
@@ -83,7 +83,12 @@ class StyleSheetsActor extends Actor {
*/
async addStyleSheet(text, fileName = null) {
const styleSheetsManager = this._getStyleSheetsManager();
- await styleSheetsManager.addStyleSheet(this.document, text, fileName);
+ await styleSheetsManager.addStyleSheet(
+ this.document,
+ this.document.documentElement,
+ text,
+ fileName
+ );
}
_getStyleSheetsManager() {
diff --git a/devtools/server/actors/utils/stylesheets-manager.js b/devtools/server/actors/utils/stylesheets-manager.js
@@ -270,13 +270,14 @@ class StyleSheetsManager extends EventEmitter {
*
* @param {Document} document
* Document that the new style sheet belong to.
+ * @param {Element} parent
+ * The element into which we'll append the <style> element
* @param {string} text
* Content of style sheet.
* @param {string} fileName
* If the stylesheet adding is from file, `fileName` indicates the path.
*/
- async addStyleSheet(document, text, fileName) {
- const parent = document.documentElement;
+ async addStyleSheet(document, parent, text, fileName) {
const style = document.createElementNS(
"http://www.w3.org/1999/xhtml",
"style"