tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 0c75044fa77d949dfbcf097a945c7cbf743416c0
parent 770dda45ef43282d4a60863836cbcebd758f951e
Author: Nicolas Chevobbe <nchevobbe@mozilla.com>
Date:   Wed,  3 Dec 2025 08:11:56 +0000

Bug 1895196 - [devtools] Add anchor badge in markup view for node with valid anchor-name. r=devtools-reviewers,devtools-backward-compat-reviewers,ochameau.

Differential Revision: https://phabricator.services.mozilla.com/D274552

Diffstat:
Mdevtools/client/fronts/node.js | 4++++
Mdevtools/client/inspector/markup/markup.js | 5+++++
Mdevtools/client/inspector/markup/test/browser.toml | 2++
Adevtools/client/inspector/markup/test/browser_markup_anchor_badge.js | 153+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdevtools/client/inspector/markup/views/element-editor.js | 25+++++++++++++++++++++++++
Mdevtools/server/actors/inspector/node.js | 18++++++++++++++++++
Mdevtools/server/actors/inspector/walker.js | 11+++++++++++
Mdevtools/shared/specs/walker.js | 4++++
8 files changed, 222 insertions(+), 0 deletions(-)

diff --git a/devtools/client/fronts/node.js b/devtools/client/fronts/node.js @@ -494,6 +494,10 @@ class NodeFront extends FrontClassWithSpec(nodeSpec) { return this._form.containerType; } + get anchorName() { + return this._form.anchorName; + } + get isTreeDisplayed() { let parent = this; while (parent) { diff --git a/devtools/client/inspector/markup/markup.js b/devtools/client/inspector/markup/markup.js @@ -379,6 +379,7 @@ class MarkupView extends EventEmitter { this._initShortcuts(); this._walkerEventListener = new WalkerEventListener(this.inspector, { + "anchor-name-change": this._onWalkerNodeStatesChanged, "container-type-change": this._onWalkerNodeStatesChanged, "display-change": this._onWalkerNodeStatesChanged, "scrollable-change": this._onWalkerNodeStatesChanged, @@ -1058,6 +1059,10 @@ class MarkupView extends EventEmitter { // TODO: use resource api listeners? if (nodeFront) { nodeFront.walkerFront.on( + "anchor-name-change", + this._onWalkerNodeStatesChanged + ); + nodeFront.walkerFront.on( "container-type-change", this._onWalkerNodeStatesChanged ); diff --git a/devtools/client/inspector/markup/test/browser.toml b/devtools/client/inspector/markup/test/browser.toml @@ -65,6 +65,8 @@ run-if = [ ["browser_markup_accessibility_semantics.js"] +["browser_markup_anchor_badge.js"] + ["browser_markup_anonymous_01.js"] ["browser_markup_anonymous_03.js"] diff --git a/devtools/client/inspector/markup/test/browser_markup_anchor_badge.js b/devtools/client/inspector/markup/test/browser_markup_anchor_badge.js @@ -0,0 +1,153 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that the anchor badge is displayed on element with expected anchor-name values. + +const TEST_URI = ` + <style type="text/css"> + #not-an-anchor { + anchor-name: none; + } + + #anchor { + anchor-name: --my-anchor; + } + + #anchor-with-multiple-names { + anchor-name: --my-other-anchor, --anchor-alias; + } + + .anchored { + position: fixed; + left: anchor(right); + position-anchor: --my-anchor; + width: 20px; + height: 20px; + background-color: gold; + } + </style> + <span id="anchor">--my-anchor</span> + <span id="anchor-with-multiple-names">--my-other-anchor --anchor-alias</span> + <span id="not-an-anchor">not an anchor</span> + <div class="anchored">A</div> + <div class="anchored" style="position-anchor: --my-other-anchor">B</div> + <div class="anchored" style="position-anchor: --updated-anchor-name">C</div> +`; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + const { inspector } = await openLayoutView(); + + let badge = await getAnchorBadgeForSelector("#anchor", inspector); + ok(!!badge, "anchor badge is displayed for element with valid anchor name"); + is(badge.textContent, "anchor", "badge has expected text"); + is(badge.title, "anchor-name: --my-anchor", "badge has expected title"); + + badge = await getAnchorBadgeForSelector( + "#anchor-with-multiple-names", + inspector + ); + ok( + !!badge, + "anchor badge is displayed for element with multiple anchor name" + ); + is(badge.textContent, "anchor", "badge has expected text"); + is( + badge.title, + "anchor-name: --my-other-anchor, --anchor-alias", + "badge has expected title" + ); + + info( + "Change the element anchorName value to see if the badge title is updated" + ); + const oldTitle = badge.title; + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () { + content.document.getElementById( + "anchor-with-multiple-names" + ).style.anchorName = "--updated-anchor-name"; + }); + await waitFor(() => badge.title !== oldTitle); + + badge = await getAnchorBadgeForSelector( + "#anchor-with-multiple-names", + inspector + ); + ok(!!badge, "anchor badge is still displayed after changing the anchor name"); + is( + badge.textContent, + "anchor", + "badge has expected text after changing the anchor name" + ); + is( + badge.title, + "anchor-name: --updated-anchor-name", + "badge has expected title after changing the anchor name" + ); + + info("Set the element anchorName to none to see if the badge gets hidden"); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () { + content.document.getElementById( + "anchor-with-multiple-names" + ).style.anchorName = "none"; + }); + await waitFor( + async () => + (await getAnchorBadgeForSelector( + "#anchor-with-multiple-names", + inspector + )) === null, + "wait for badge to be hidden", + // interval + 500, + // max tries + 10 + ); + ok(true, "The badge was hidden when setting anchorName to none"); + + info( + "Change the element anchorName value back to a dashed ident to see if the badge is shown again" + ); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () { + content.document.getElementById( + "anchor-with-multiple-names" + ).style.anchorName = "--my-other-anchor"; + }); + badge = await waitFor( + async () => + await getAnchorBadgeForSelector("#anchor-with-multiple-names", inspector), + "wait for badge to be visible", + // interval + 500, + // max tries + 10 + ); + + ok( + !!badge, + "anchor badge is displayed again after setting a valid anchor name" + ); + is( + badge.textContent, + "anchor", + "badge has expected text after setting a valid anchor name" + ); + is( + badge.title, + "anchor-name: --my-other-anchor", + "badge has expected title after setting a valid anchor name" + ); + + badge = await getAnchorBadgeForSelector("#not-an-anchor", inspector); + ok( + !badge, + "anchor badge is not displayed for element with anchor-name: none" + ); +}); + +async function getAnchorBadgeForSelector(selector, inspector) { + const container = await getContainerForSelector(selector, inspector); + return container.elt.querySelector(".inspector-badge[data-anchor]"); +} diff --git a/devtools/client/inspector/markup/views/element-editor.js b/devtools/client/inspector/markup/views/element-editor.js @@ -340,6 +340,7 @@ ElementEditor.prototype = { this.updateCustomBadge(); this.updateScrollableBadge(); this.updateContainerBadge(); + this.updateAnchorBadge(); this.updateTextEditor(); this.updateUnavailableChildren(); this.updateOverflowBadge(); @@ -550,6 +551,30 @@ ElementEditor.prototype = { this.markup.emit("badge-added-event"); }, + updateAnchorBadge() { + const showAnchorBadge = this.node.anchorName?.includes?.("--"); + + if (this._anchorBadge && !showAnchorBadge) { + this._anchorBadge.remove(); + this._anchorBadge = null; + } else if (showAnchorBadge && !this._anchorBadge) { + this._createAnchorBadge(); + } + + if (this._anchorBadge) { + this._anchorBadge.title = `anchor-name: ${this.node.anchorName}`; + } + }, + + _createAnchorBadge() { + this._anchorBadge = this.doc.createElement("div"); + this._anchorBadge.classList.add("inspector-badge"); + this._anchorBadge.dataset.anchor = "true"; + + this._anchorBadge.append(this.doc.createTextNode("anchor")); + this.elt.insertBefore(this._anchorBadge, this._containerBadge); + }, + /** * If node causes overflow, toggle its overflow highlight if its scrollable ancestor's * scrollable badge is active/inactive. diff --git a/devtools/server/actors/inspector/node.js b/devtools/server/actors/inspector/node.js @@ -102,6 +102,7 @@ class NodeActor extends Actor { this.wasDisplayed = this.isDisplayed; this.wasScrollable = wasScrollable; this.currentContainerType = this.containerType; + this.currentAnchorName = this.anchorName; if (wasScrollable) { this.walker.updateOverflowCausingElements( @@ -199,6 +200,7 @@ class NodeActor extends Actor { isTopLevelDocument: this.isTopLevelDocument, causesOverflow: this.walker.overflowCausingElementsMap.has(this.rawNode), containerType: this.containerType, + anchorName: this.anchorName, // doctype attributes name: this.rawNode.name, @@ -392,6 +394,22 @@ class NodeActor extends Actor { } /** + * Returns the computed anchorName style property value of the node. + */ + get anchorName() { + // non-element nodes can't be anchors + if ( + isNodeDead(this) || + this.rawNode.nodeType !== Node.ELEMENT_NODE || + !this.computedStyle + ) { + return null; + } + + return this.computedStyle.anchorName; + } + + /** * Check whether the node currently has scrollbars and is scrollable. */ get isScrollable() { diff --git a/devtools/server/actors/inspector/walker.js b/devtools/server/actors/inspector/walker.js @@ -544,6 +544,7 @@ class WalkerActor extends Actor { const containerTypeChanges = []; const displayTypeChanges = []; const scrollableStateChanges = []; + const anchorNameChanges = []; const currentOverflowCausingElementsMap = new Map(); @@ -584,6 +585,12 @@ class WalkerActor extends Actor { containerTypeChanges.push(actor); actor.currentContainerType = containerType; } + + const anchorName = actor.anchorName; + if (anchorName !== actor.currentAnchorName) { + anchorNameChanges.push(actor); + actor.currentAnchorName = anchorName; + } } // Get the NodeActor for each node in the symmetric difference of @@ -615,6 +622,10 @@ class WalkerActor extends Actor { if (containerTypeChanges.length) { this.emit("container-type-change", containerTypeChanges); } + + if (anchorNameChanges.length) { + this.emit("anchor-name-change", anchorNameChanges); + } } /** diff --git a/devtools/shared/specs/walker.js b/devtools/shared/specs/walker.js @@ -91,6 +91,10 @@ const walkerSpec = generateActorSpec({ type: "container-type-change", nodes: Arg(0, "array:domnode"), }, + "anchor-name-change": { + type: "anchor-name-change", + nodes: Arg(0, "array:domnode"), + }, // The walker actor emits a useful "resize" event to its front to let // clients know when the browser window gets resized. This may be useful // for refreshing a DOM node's styles for example, since those may depend on