commit d7679d7acc1bca859a296e21655686c90ec1874b
parent 5b66be067d5c6aa2bfa2adbf50b8fe763e826291
Author: Nicolas Chevobbe <nchevobbe@mozilla.com>
Date: Fri, 14 Nov 2025 10:29:13 +0000
Bug 1947747 - [devtools] Handle ::view-transition elements in the Rules view. r=devtools-reviewers,ochameau.
We had to handle the label on the inherited section for view transition pseudo element,
and fix StyleRuleActor#currentlySelectedElementComputedStyle so inactiveCSS will
perform as expected on view transition pseudo element nodes.
A test is added to check that the rules are properly displayed when a view transition
pseudo element is selected in the markup view, and that the inherited properties
are visible and that the expected declarations are overridden (acts as a client
test for Bug 1997145)
Differential Revision: https://phabricator.services.mozilla.com/D271009
Diffstat:
3 files changed, 222 insertions(+), 16 deletions(-)
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
@@ -241,4 +241,164 @@ add_task(async function () {
declarations: [{ name: "color", value: "#333" }],
},
]);
+
+ info("Check rules on ::view-transition");
+ const htmlNodeFront = await getNodeFront("html", inspector);
+
+ const onMarkupMutation = inspector.once("markupmutation");
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => {
+ const document = content.document;
+ const transition = document.startViewTransition(() => {
+ document.querySelector(".transition").append("updated");
+ });
+ await transition.ready;
+ await transition.updateCallbackDone;
+ });
+ await onMarkupMutation;
+
+ const htmlChildren = await inspector.markup.walker.children(htmlNodeFront);
+ const viewTransitionNodeFront = htmlChildren.nodes[2];
+
+ is(
+ viewTransitionNodeFront.getAttribute("type"),
+ ":view-transition",
+ "Got expected ::view-transition node front"
+ );
+
+ await selectNode(viewTransitionNodeFront, inspector);
+
+ checkRuleViewContent(view, [
+ {
+ selector: `::view-transition`,
+ declarations: [{ name: "color", value: `lime` }],
+ },
+ {
+ header: "Inherited from html",
+ },
+ {
+ selector: `html:active-view-transition`,
+ inherited: true,
+ declarations: [{ name: "color", value: "peachpuff", overridden: true }],
+ },
+ {
+ selector: `*`,
+ inherited: true,
+ declarations: [{ name: "cursor", value: "default" }],
+ },
+ ]);
+
+ const viewTransitionChildren = await inspector.markup.walker.children(
+ viewTransitionNodeFront
+ );
+ const viewTransitionGroupNodeFront = viewTransitionChildren.nodes[0];
+ is(
+ viewTransitionGroupNodeFront.getAttribute("type"),
+ ":view-transition-group",
+ "Got expected ::view-transition-group node front"
+ );
+
+ const viewTransitionGroupChildren = await inspector.markup.walker.children(
+ viewTransitionGroupNodeFront
+ );
+ const viewTransitionImagePairNodeFront = viewTransitionGroupChildren.nodes[0];
+ is(
+ viewTransitionImagePairNodeFront.getAttribute("type"),
+ ":view-transition-image-pair",
+ "Got expected ::view-transition-image-pair node front"
+ );
+
+ const viewTransitionImagePairChildren =
+ await inspector.markup.walker.children(viewTransitionImagePairNodeFront);
+ const [viewTransitionOldNodeFront, viewTransitionNewNodeFront] =
+ viewTransitionImagePairChildren.nodes;
+ is(
+ viewTransitionOldNodeFront.getAttribute("type"),
+ ":view-transition-old",
+ "Got expected ::view-transition-old node front"
+ );
+ is(
+ viewTransitionNewNodeFront.getAttribute("type"),
+ ":view-transition-new",
+ "Got expected ::view-transition-new node front"
+ );
+
+ info("Check rules on ::view-transition-old");
+ await selectNode(viewTransitionOldNodeFront, inspector);
+ checkRuleViewContent(view, [
+ {
+ selector: `::view-transition-old(root), ::view-transition-new(root)`,
+ declarations: [
+ { name: "animation-duration", value: `1000s` },
+ { name: "top", value: `1em` },
+ { name: "gap", value: `10px`, inactiveCSS: true },
+ ],
+ },
+ {
+ header: "Inherited from ::view-transition",
+ },
+ {
+ selector: `::view-transition`,
+ inherited: true,
+ declarations: [{ name: "color", value: `lime` }],
+ },
+ {
+ header: "Inherited from html",
+ },
+ {
+ selector: `html:active-view-transition`,
+ inherited: true,
+ declarations: [{ name: "color", value: "peachpuff", overridden: true }],
+ },
+ {
+ selector: `*`,
+ inherited: true,
+ declarations: [{ name: "cursor", value: "default" }],
+ },
+ ]);
+
+ info("Check rules on ::view-transition-new");
+ await selectNode(viewTransitionNewNodeFront, inspector);
+ checkRuleViewContent(view, [
+ {
+ selector: `::view-transition-new(root)`,
+ declarations: [
+ { name: "animation-duration", value: `3600s` },
+ { name: "color", value: `thistle` },
+ ],
+ },
+ {
+ selector: `::view-transition-old(root), ::view-transition-new(root)`,
+ declarations: [
+ { name: "animation-duration", value: `1000s`, overridden: true },
+ {
+ name: "top",
+ value: `1em`,
+ // This shouldn't be inactive. See Bug 1998357
+ inactiveCSS: true,
+ },
+ { name: "gap", value: `10px`, inactiveCSS: true },
+ ],
+ },
+ {
+ header: "Inherited from ::view-transition",
+ },
+ {
+ selector: `::view-transition`,
+ inherited: true,
+ declarations: [{ name: "color", value: `lime`, overridden: true }],
+ },
+ {
+ header: "Inherited from html",
+ },
+ {
+ selector: `html:active-view-transition`,
+ inherited: true,
+ declarations: [{ name: "color", value: "peachpuff", overridden: true }],
+ },
+ {
+ selector: `*`,
+ inherited: true,
+ declarations: [{ name: "cursor", value: "default" }],
+ },
+ ]);
});
diff --git a/devtools/client/inspector/rules/test/doc_pseudoelement.html b/devtools/client/inspector/rules/test/doc_pseudoelement.html
@@ -166,6 +166,29 @@ details[open]::details-content {
border: 4px solid darkmagenta;
}
+html:active-view-transition {
+ color: peachpuff;
+}
+
+::view-transition {
+ color: lime;
+}
+
+/* Use very long animation-duration so the view-transition pseudo elements are available
+ during the whole test */
+::view-transition-old(root),
+::view-transition-new(root) {
+ animation-duration: 1000s;
+ top: 1em;
+ gap: 10px;
+}
+
+::view-transition-new(root) {
+ /* This should override the previous rule declaration when ::view-transition-new(root) is selected */
+ animation-duration: 3600s;
+ color: thistle;
+}
+
</style>
</head>
<body>
@@ -214,6 +237,8 @@ details[open]::details-content {
<p>In details</p>
</details>
+ <aside class="transition">Transition</section>
+
<script>
"use strict";
// This is the only way to have the ::backdrop style to be applied
diff --git a/devtools/server/actors/style-rule.js b/devtools/server/actors/style-rule.js
@@ -29,6 +29,12 @@ loader.lazyRequireGetter(
);
loader.lazyRequireGetter(
this,
+ "getNodeDisplayName",
+ "resource://devtools/server/actors/inspector/utils.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
"SharedCssLogic",
"resource://devtools/shared/inspector/css-logic.js"
);
@@ -292,21 +298,45 @@ class StyleRuleActor extends Actor {
}
/**
+ * Returns true if the pseudo element anonymous node (e.g. ::before, ::marker, …) is selected.
+ * Returns false if a non pseudo element node is selected and we're looking into its pseudo
+ * elements rules (i.e. this is for the "Pseudo-elements" section in the Rules view")
+ */
+ get isPseudoElementAnonymousNodeSelected() {
+ if (!this._pseudoElement) {
+ return false;
+ }
+
+ // `this._pseudoElement` is the returned value by getNodeDisplayName, i.e that does
+ // differ from this.pageStyle.selectedElement.implementedPseudoElement (e.g. for
+ // view transition element, it will be `::view-transition-group(root)`, while
+ // implementedPseudoElement will be `::view-transition-group`).
+ return (
+ this._pseudoElement === getNodeDisplayName(this.pageStyle.selectedElement)
+ );
+ }
+
+ /**
* StyleRuleActor is spawned once per CSS Rule, but will be refreshed based on the
* currently selected DOM Element, which is updated when PageStyleActor.getApplied
* is called.
*/
get currentlySelectedElement() {
let { selectedElement } = this.pageStyle;
- if (!this._pseudoElement) {
+ // If we're not handling a pseudo element, or if the pseudo element node
+ // (e.g. ::before, ::marker, …) is the one selected in the markup view, we can
+ // directly return selected element.
+ if (!this._pseudoElement || this.isPseudoElementAnonymousNodeSelected) {
return selectedElement;
}
- // Otherwise, we can be in one of two cases:
- // - we are selecting a pseudo element, and that pseudo element is referenced
- // by `selectedElement`
- // - we are selecting the pseudo element "parent", we need to walk down the tree
- // from `selectedElemnt` to find the pseudo element.
+ // Otherwise we are selecting the pseudo element "parent" (binding), and we need to
+ // walk down the tree from `selectedElement` to find the pseudo element.
+
+ // FIXME: ::view-transition pseudo elements don't have a _moz_generated_content_ prefixed
+ // nodename, but have specific type and name attribute.
+ // At the moment this isn't causing any issues because we don't display the view
+ // transition rules in the pseudo element section, but this should be fixed in Bug 1998345.
const pseudo = this._pseudoElement.replaceAll(":", "");
const nodeName = `_moz_generated_content_${pseudo}`;
@@ -334,20 +364,11 @@ class StyleRuleActor extends Actor {
const { selectedElement } = this.pageStyle;
- // We can be in one of two cases:
- // - we are selecting a pseudo element, and that pseudo element is referenced
- // by `selectedElement`
- // - we are selecting the pseudo element "parent".
- // implementPseudoElement returns the pseudo-element string if this element represents
- // a pseudo-element, or null otherwise. See https://searchfox.org/mozilla-central/rev/1b90936792b2c71ef931cb1b8d6baff9d825592e/dom/webidl/Element.webidl#102-107
- const isPseudoElementParentSelected =
- selectedElement.implementedPseudoElement !== this._pseudoElement;
-
return selectedElement.ownerGlobal.getComputedStyle(
selectedElement,
// If we are selecting the pseudo element parent, we need to pass the pseudo element
// to getComputedStyle to actually get the computed style of the pseudo element.
- isPseudoElementParentSelected ? this._pseudoElement : null
+ !this.isPseudoElementAnonymousNodeSelected ? this._pseudoElement : null
);
}