commit 4ae808c8e166d0e148123a092a6a7b0745c994d4
parent 4a56cbd3c8b83555f96487b38f3202dfa77eab6e
Author: Nicolas Chevobbe <nchevobbe@mozilla.com>
Date: Fri, 21 Nov 2025 06:55:49 +0000
Bug 2000219 - [devtools] Properly detect unmatched selectors in Pseudo elements section. r=devtools-reviewers,bomsy.
Differential Revision: https://phabricator.services.mozilla.com/D273181
Diffstat:
6 files changed, 103 insertions(+), 71 deletions(-)
diff --git a/devtools/client/inspector/rules/test/browser_rules_pseudo-element_01.js b/devtools/client/inspector/rules/test/browser_rules_pseudo-element_01.js
@@ -610,29 +610,6 @@ async function assertPseudoElementRulesNumbers(
);
}
- // If we do have pseudo element rules displayed, ensure we don't mark their selectors
- // as matched or unmatched
- if (
- rules.elementRules.length &&
- view._elementStyle.rules.length !== rules.elementRules.length
- ) {
- const pseudoElementContainer = view.styleWindow.document.getElementById(
- "pseudo-elements-container"
- );
- const selectors = Array.from(
- pseudoElementContainer.querySelectorAll(".ruleview-selector")
- );
- ok(selectors.length, "We do have selectors for pseudo element rules");
- ok(
- selectors.every(
- selectorEl =>
- !selectorEl.classList.contains("matched") &&
- !selectorEl.classList.contains("unmatched")
- ),
- "Pseudo element selectors are not marked as matched nor unmatched"
- );
- }
-
return rules;
}
diff --git a/devtools/client/inspector/rules/test/browser_rules_pseudo-element_02.js b/devtools/client/inspector/rules/test/browser_rules_pseudo-element_02.js
@@ -3,7 +3,7 @@
"use strict";
-// Test that pseudoelements are displayed correctly in the markup view.
+// Test that pseudo elements rules are displayed correctly in Rules view.
const TEST_URI = URL_ROOT + "doc_pseudoelement.html";
@@ -18,11 +18,7 @@ add_task(async function () {
info("Check rules on #topleft::before node");
const beforeElement = children.nodes[0];
- is(
- beforeElement.tagName,
- "_moz_generated_content_before",
- "tag name is correct"
- );
+ is(beforeElement.displayName, "::before", "display name is correct");
await selectNode(beforeElement, inspector);
checkRuleViewContent(view, [
{
@@ -65,12 +61,8 @@ add_task(async function () {
]);
info("Check rules on #topleft::after node");
- const afterElement = children.nodes[children.nodes.length - 1];
- is(
- afterElement.tagName,
- "_moz_generated_content_after",
- "tag name is correct"
- );
+ const afterElement = children.nodes.at(-1);
+ is(afterElement.displayName, "::after", "display name is correct");
await selectNode(afterElement, inspector);
checkRuleViewContent(view, [
{
@@ -114,9 +106,9 @@ add_task(async function () {
const listChildren = await inspector.markup.walker.children(listNode);
const listAfterNode = listChildren.nodes.at(-1);
is(
- listAfterNode.tagName,
- "_moz_generated_content_after",
- "tag name is correct for #list::after"
+ listAfterNode.displayName,
+ "::after",
+ "display name is correct for #list::after"
);
const listAfterChildren =
await inspector.markup.walker.children(listAfterNode);
@@ -127,9 +119,9 @@ add_task(async function () {
);
const listAfterMarkerNode = listAfterChildren.nodes[0];
is(
- listAfterMarkerNode.tagName,
- "_moz_generated_content_marker",
- "tag name is correct for #list::after::marker"
+ listAfterMarkerNode.displayName,
+ "::marker",
+ "display name is correct for #list::after::marker"
);
info("Check rules on #list-item::marker node");
await selectNode(listAfterMarkerNode, inspector);
@@ -170,11 +162,7 @@ add_task(async function () {
info("Check rules on #list-item::marker node");
const markerElement = listItemChildren.nodes[0];
- is(
- markerElement.tagName,
- "_moz_generated_content_marker",
- "tag name is correct"
- );
+ is(markerElement.displayName, "::marker", "display name is correct");
await selectNode(markerElement, inspector);
checkRuleViewContent(view, [
{
@@ -204,11 +192,7 @@ add_task(async function () {
info("Check rules on #list-item::before node");
const listBeforeElement = listItemChildren.nodes[1];
- is(
- listBeforeElement.tagName,
- "_moz_generated_content_before",
- "tag name is correct"
- );
+ is(listBeforeElement.displayName, "::before", "display name is correct");
await selectNode(listBeforeElement, inspector);
checkRuleViewContent(view, [
{
@@ -242,6 +226,37 @@ add_task(async function () {
},
]);
+ info("Check unmatched selector parts in Pseudo element section");
+ await selectNode("#with-unmatched-selector", inspector);
+ checkRuleViewContent(view, [
+ {
+ header: `Pseudo-elements`,
+ },
+ {
+ selector: `#with-unmatched-selector::before, ~~unknown::before~~, #with-unmatched-selector::after, ~~anotherunknown~~`,
+ declarations: [{ name: "content", value: `"unmatched pseudo"` }],
+ },
+ {
+ header: `This Element`,
+ },
+ {
+ selector: `element`,
+ declarations: [],
+ },
+ {
+ selector: `*`,
+ declarations: [{ name: "cursor", value: "default" }],
+ },
+ {
+ header: "Inherited from body",
+ },
+ {
+ selector: `body`,
+ inherited: true,
+ declarations: [{ name: "color", value: "#333" }],
+ },
+ ]);
+
info("Check rules on ::view-transition");
const htmlNodeFront = await getNodeFront("html", inspector);
diff --git a/devtools/client/inspector/rules/test/doc_pseudoelement.html b/devtools/client/inspector/rules/test/doc_pseudoelement.html
@@ -189,6 +189,13 @@ html:active-view-transition {
color: thistle;
}
+#with-unmatched-selector::before,
+unknown::before,
+#with-unmatched-selector::after,
+anotherunknown {
+ content: "unmatched pseudo";
+}
+
</style>
</head>
<body>
@@ -210,6 +217,8 @@ html:active-view-transition {
<p>Bottom Left<br />Position</p>
</div>
+ <div id="with-unmatched-selector">Some unmatched pseudo-element selector part</div>
+
<ol id="list">
<li id="list-item" class="box">List element</li>
</ol>
diff --git a/devtools/client/inspector/rules/views/rule-editor.js b/devtools/client/inspector/rules/views/rule-editor.js
@@ -934,16 +934,11 @@ RuleEditor.prototype = {
});
}
- let containerClass = "ruleview-selector ";
-
- // Only add matched/unmatched class when the rule does have some matched
- // selectors. We don't always have some (e.g. rules for pseudo elements)
-
- if (this.rule.matchedSelectorIndexes.length) {
- containerClass += this.rule.matchedSelectorIndexes.includes(selectorIndex)
+ const containerClass =
+ "ruleview-selector " +
+ (this.rule.matchedSelectorIndexes.includes(selectorIndex)
? "matched"
- : "unmatched";
- }
+ : "unmatched");
let selectorContainerTitle;
if (
diff --git a/devtools/server/actors/page-style.js b/devtools/server/actors/page-style.js
@@ -179,7 +179,11 @@ class PageStyleActor extends Actor {
*/
_styleRef(item, pseudoElement, userAdded = false) {
if (this.refMap.has(item)) {
- return this.refMap.get(item);
+ const styleRuleActor = this.refMap.get(item);
+ if (pseudoElement) {
+ styleRuleActor.addPseudo(pseudoElement);
+ }
+ return styleRuleActor;
}
const actor = new StyleRuleActor({
pageStyle: this,
@@ -1042,22 +1046,38 @@ class PageStyleActor extends Actor {
? entry.inherited.rawNode
: node.rawNode;
+ const pseudos = [];
const { bindingElement, pseudo } =
CssLogic.getBindingElementAndPseudo(element);
+ if (pseudo) {
+ pseudos.push(pseudo);
+ } else if (entry.rule.pseudoElements.size) {
+ // if `node` is not a pseudo element but the rule applies to some pseudo elements,
+ // we need to pass those to CSSStyleRule#selectorMatchesElement
+ pseudos.push(...entry.rule.pseudoElements);
+ } else {
+ // If the rule doesn't apply to any pseudo, set a null item so we'll still do
+ // the proper check below
+ pseudos.push(null);
+ }
+
const relevantLinkVisited = CssLogic.hasVisitedState(bindingElement);
entry.matchedSelectorIndexes = [];
-
const len = domRule.selectorCount;
for (let i = 0; i < len; i++) {
- if (
- domRule.selectorMatchesElement(
- i,
- bindingElement,
- pseudo,
- relevantLinkVisited
- )
- ) {
- entry.matchedSelectorIndexes.push(i);
+ for (const pseudoElementName of pseudos) {
+ if (
+ domRule.selectorMatchesElement(
+ i,
+ bindingElement,
+ pseudoElementName,
+ relevantLinkVisited
+ )
+ ) {
+ entry.matchedSelectorIndexes.push(i);
+ // if we matched the selector for one pseudo, no need to check the other ones
+ break;
+ }
}
}
}
diff --git a/devtools/server/actors/style-rule.js b/devtools/server/actors/style-rule.js
@@ -94,7 +94,11 @@ class StyleRuleActor extends Actor {
this.pageStyle = pageStyle;
this.rawStyle = item.style;
this._userAdded = userAdded;
+ this._pseudoElements = new Set();
this._pseudoElement = pseudoElement;
+ if (pseudoElement) {
+ this._pseudoElements.add(pseudoElement);
+ }
this._parentSheet = null;
// Parsed CSS declarations from this.form().declarations used to check CSS property
// names and values before tracking changes. Using cached values instead of accessing
@@ -149,6 +153,10 @@ class StyleRuleActor extends Actor {
this.rawNode = null;
this.rawRule = null;
this._declarations = null;
+ if (this._pseudoElements) {
+ this._pseudoElements.clear();
+ this._pseudoElements = null;
+ }
}
// Objects returned by this actor are owned by the PageStyleActor
@@ -372,6 +380,14 @@ class StyleRuleActor extends Actor {
);
}
+ get pseudoElements() {
+ return this._pseudoElements;
+ }
+
+ addPseudo(pseudoElement) {
+ this._pseudoElements.add(pseudoElement);
+ }
+
getDocument(sheet) {
if (!sheet.associatedDocument) {
throw new Error(