commit a431a9498a3d4d00fedfce72b5f0f70cb8a13b4b
parent 9716e9415c5b2456252b0a9562d88781deff8fef
Author: Nicolas Chevobbe <nchevobbe@mozilla.com>
Date: Thu, 13 Nov 2025 15:43:40 +0000
Bug 1152343 - [devtools] Fix computed panel matching selectors for pseudo element nodes. r=devtools-reviewers,jdescottes.
When the currently selected element is a pseudo element (::after, ::before, …),
the call to selectorMatchesElement would always return false as it doesn't handle
anonymous element.
To fix this, we retrieve the eventual bindingElement and pseudo of the selected
element, that we pass together to selectorMatchesElement.
Some test cases are added to make sure this works properly now.
We also move `checkMatchedSelectorForProperty` to head.js so we can easily
reuse it in different tests.
Differential Revision: https://phabricator.services.mozilla.com/D272458
Diffstat:
5 files changed, 163 insertions(+), 71 deletions(-)
diff --git a/devtools/client/inspector/computed/test/browser_computed_matched-selectors-order.js b/devtools/client/inspector/computed/test/browser_computed_matched-selectors-order.js
@@ -900,55 +900,3 @@ async function checkBackgroundColorMatchedSelectors(
content.document.getElementById(`style-${id}`)?.remove();
});
}
-
-async function checkMatchedSelectorForProperty(
- view,
- { property, expectedComputedValue, expectedMatchedSelectors }
-) {
- const propertyView = getPropertyView(view, property);
- ok(propertyView, `found PropertyView for "${property}"`);
- const { valueNode } = propertyView;
- is(
- valueNode.textContent,
- expectedComputedValue,
- `Expected displayed computed value for "${property}"`
- );
-
- is(propertyView.hasMatchedSelectors, true, "hasMatchedSelectors is true");
-
- info("Expanding the matched selectors");
- propertyView.matchedExpanded = true;
- await propertyView.refreshMatchedSelectors();
-
- const selectorsEl =
- propertyView.matchedSelectorsContainer.querySelectorAll(".rule-text");
- is(
- selectorsEl.length,
- expectedMatchedSelectors.length,
- "Expected number of selectors are displayed"
- );
-
- selectorsEl.forEach((selectorEl, index) => {
- is(
- selectorEl.querySelector(".fix-get-selection").innerText,
- expectedMatchedSelectors[index].selector,
- `Selector #${index} is the expected one`
- );
- is(
- selectorEl.querySelector(".computed-other-property-value").innerText,
- expectedMatchedSelectors[index].value,
- `Selector #${index} ("${expectedMatchedSelectors[index].selector}") has the expected "${property}"`
- );
- const classToMatch = index === 0 ? "bestmatch" : "matched";
- const expectedMatch = expectedMatchedSelectors[index].match ?? true;
- is(
- selectorEl.classList.contains(classToMatch),
- expectedMatch,
- `Selector #${index} ("${expectedMatchedSelectors[index].selector}") element does ${expectedMatch ? "" : "not "}have a matching class`
- );
- });
-}
-
-function getPropertyView(computedView, name) {
- return computedView.propertyViews.find(view => view.name === name);
-}
diff --git a/devtools/client/inspector/computed/test/browser_computed_pseudo-element.js b/devtools/client/inspector/computed/test/browser_computed_pseudo-element.js
@@ -24,15 +24,90 @@ async function testTopLeft(inspector, view) {
const beforeElement = children.nodes[0];
await selectNode(beforeElement, inspector);
- let top = getComputedViewPropertyValue(view, "top");
- is(top, "0px", "The computed view shows the correct top");
- let left = getComputedViewPropertyValue(view, "left");
- is(left, "0px", "The computed view shows the correct left");
- const afterElement = children.nodes[children.nodes.length - 1];
+ info("check `top` property on #topleft::before");
+ await checkMatchedSelectorForProperty(view, {
+ property: "top",
+ expectedComputedValue: "0px",
+ expectedMatchedSelectors: [
+ {
+ selector: ".topleft::before",
+ value: "0px",
+ },
+ {
+ selector: ":where(.topleft)::before",
+ value: "10px",
+ },
+ ],
+ });
+
+ info("check `left` property on #topleft::before");
+ await checkMatchedSelectorForProperty(view, {
+ property: "left",
+ expectedComputedValue: "0px",
+ expectedMatchedSelectors: [
+ {
+ selector: ".topleft::before",
+ value: "0px",
+ },
+ {
+ selector: ":where(.topleft)::before",
+ value: "20px",
+ },
+ ],
+ });
+
+ info("check `color` property on #topleft::before");
+ await checkMatchedSelectorForProperty(view, {
+ property: "color",
+ expectedComputedValue: "rgb(0, 255, 0)",
+ expectedMatchedSelectors: [
+ {
+ selector: ":where(.topleft)::before",
+ value: "lime",
+ },
+ {
+ selector: ".topleft",
+ value: "blue",
+ match: false,
+ },
+ {
+ selector: "body",
+ value: "rgb(51, 51, 51)",
+ match: false,
+ },
+ {
+ selector: ":root",
+ value: "canvastext",
+ match: false,
+ },
+ ],
+ });
+
+ const afterElement = children.nodes.at(-1);
await selectNode(afterElement, inspector);
- top = getComputedViewPropertyValue(view, "top");
- is(top, "96px", "The computed view shows the correct top");
- left = getComputedViewPropertyValue(view, "left");
- is(left, "96px", "The computed view shows the correct left");
+
+ info("check `top` property on #topleft::after");
+ await checkMatchedSelectorForProperty(view, {
+ property: "top",
+ expectedComputedValue: "96px",
+ expectedMatchedSelectors: [
+ {
+ selector: ".box::after",
+ value: "50%",
+ },
+ ],
+ });
+
+ info("check `left` property on #topleft::after");
+ await checkMatchedSelectorForProperty(view, {
+ property: "left",
+ expectedComputedValue: "96px",
+ expectedMatchedSelectors: [
+ {
+ selector: ".box::after",
+ value: "50%",
+ },
+ ],
+ });
}
diff --git a/devtools/client/inspector/computed/test/doc_pseudoelement.html b/devtools/client/inspector/computed/test/doc_pseudoelement.html
@@ -73,11 +73,18 @@ p:first-letter {
margin-left: -16px;
}
+.topleft {
+ color: blue;
+}
.topleft:before {
top:0;
left:0;
}
-
+:where(.topleft)::before {
+ top:10px;
+ left:20px;
+ color: lime;
+}
.topleft:first-line {
color: orange;
}
diff --git a/devtools/client/inspector/computed/test/head.js b/devtools/client/inspector/computed/test/head.js
@@ -67,14 +67,7 @@ function getComputedViewProperty(view, name) {
* @return {PropertyView}
*/
function getComputedViewPropertyView(view, name) {
- let propView;
- for (const propertyView of view.propertyViews) {
- if (propertyView.propertyInfo.name === name) {
- propView = propertyView;
- break;
- }
- }
- return propView;
+ return view.propertyViews.find(propertyView => propertyView.name === name);
}
/**
@@ -277,3 +270,70 @@ function failClipboardCheck(expectedPattern) {
info("Actual: " + escape(actual));
info("Expected: " + escape(expectedPattern));
}
+
+/**
+ * Check that the given property has the expected value and the expected matched selectors
+ *
+ * @param {CssComputedView} view
+ * The instance of the computed view panel
+ * @param {Object} options
+ * @param {string} options.property
+ * The property name to check
+ * @param {string} options.expectedComputedValue
+ * The expected value displayed for the property
+ * @param {Object[]} options.expectedMatchedSelectors
+ * An array of objects describing the expected matched selectors
+ * @param {string} options.expectedMatchedSelectors[].selector
+ * The selector that should be displayed at this index
+ * @param {string} options.expectedMatchedSelectors[].value
+ * The value that should be displayed at this index
+ * @param {boolean} options.expectedMatchedSelectors[].match
+ * Whether the selector should match the currently selected element. Defaults to true.
+ */
+async function checkMatchedSelectorForProperty(
+ view,
+ { property, expectedComputedValue, expectedMatchedSelectors }
+) {
+ const propertyView = getComputedViewPropertyView(view, property);
+ ok(propertyView, `found PropertyView for "${property}"`);
+ const { valueNode } = propertyView;
+ is(
+ valueNode.textContent,
+ expectedComputedValue,
+ `Expected displayed computed value for "${property}"`
+ );
+
+ is(propertyView.hasMatchedSelectors, true, "hasMatchedSelectors is true");
+
+ info("Expanding the matched selectors");
+ propertyView.matchedExpanded = true;
+ await propertyView.refreshMatchedSelectors();
+
+ const selectorsEl =
+ propertyView.matchedSelectorsContainer.querySelectorAll(".rule-text");
+ is(
+ selectorsEl.length,
+ expectedMatchedSelectors.length,
+ "Expected number of selectors are displayed"
+ );
+
+ selectorsEl.forEach((selectorEl, index) => {
+ is(
+ selectorEl.querySelector(".fix-get-selection").innerText,
+ expectedMatchedSelectors[index].selector,
+ `Selector #${index} is the expected one`
+ );
+ is(
+ selectorEl.querySelector(".computed-other-property-value").innerText,
+ expectedMatchedSelectors[index].value,
+ `Selector #${index} ("${expectedMatchedSelectors[index].selector}") has the expected "${property}"`
+ );
+ const classToMatch = index === 0 ? "bestmatch" : "matched";
+ const expectedMatch = expectedMatchedSelectors[index].match ?? true;
+ is(
+ selectorEl.classList.contains(classToMatch),
+ expectedMatch,
+ `Selector #${index} ("${expectedMatchedSelectors[index].selector}") element does ${expectedMatch ? "" : "not "}have a matching class`
+ );
+ });
+}
diff --git a/devtools/server/actors/inspector/css-logic.js b/devtools/server/actors/inspector/css-logic.js
@@ -486,7 +486,9 @@ class CssLogic {
selectorMatchesElement(domRule, idx) {
let element = this.viewedElement;
do {
- if (domRule.selectorMatchesElement(idx, element)) {
+ const { bindingElement, pseudo } =
+ CssLogic.getBindingElementAndPseudo(element);
+ if (domRule.selectorMatchesElement(idx, bindingElement, pseudo)) {
return true;
}