tor-browser

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

commit 375743d56b88db26ae735f2835c8809380b3178a
parent 054316229cb666e286a8c9746623e2e44509b084
Author: Nicolas Chevobbe <nchevobbe@mozilla.com>
Date:   Tue, 16 Dec 2025 18:40:31 +0000

Bug 2005189 - [devtools] Display @custom-media rules in Style Editor at-rules sidebar. r=devtools-reviewers,ochameau

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

Diffstat:
Mdevtools/client/styleeditor/StyleEditorUI.sys.mjs | 17+++++++++++++++++
Mdevtools/client/styleeditor/test/browser_styleeditor_at_rules_sidebar.js | 127+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Mdevtools/client/styleeditor/test/media-rules.html | 20++++++++++++++++++++
Mdevtools/server/actors/utils/stylesheets-manager.js | 25+++++++++++++++++++++++++
Mlayout/inspector/InspectorUtils.cpp | 2+-
5 files changed, 184 insertions(+), 7 deletions(-)

diff --git a/devtools/client/styleeditor/StyleEditorUI.sys.mjs b/devtools/client/styleeditor/StyleEditorUI.sys.mjs @@ -1446,6 +1446,23 @@ export class StyleEditorUI extends EventEmitter { type.append( this.#panelDoc.createTextNode(`${rule.positionTryName}\u00A0`) ); + } else if (rule.type === "custom-media") { + const parts = []; + const { customMediaName, customMediaQuery } = rule; + for (let i = 0, len = customMediaQuery.length; i < len; i++) { + const media = customMediaQuery[i]; + const queryEl = this.#panelDoc.createElementNS(HTML_NS, "span"); + queryEl.textContent = media.text; + if (!media.matches) { + queryEl.classList.add("media-condition-unmatched"); + } + parts.push(queryEl); + if (len > 1 && i !== len - 1) { + parts.push(", "); + } + } + + type.append(`${customMediaName} `, ...parts); } const cond = this.#panelDoc.createElementNS(HTML_NS, "span"); diff --git a/devtools/client/styleeditor/test/browser_styleeditor_at_rules_sidebar.js b/devtools/client/styleeditor/test/browser_styleeditor_at_rules_sidebar.js @@ -42,6 +42,8 @@ add_task(async function () { await pushPref("layout.css.properties-and-values.enabled", true); // Enable anchor positioning await pushPref("layout.css.anchor-positioning.enabled", true); + // Enable @custom-media + await pushPref("layout.css.custom-media.enabled", true); const { ui } = await openStyleEditorForURL(TESTCASE_URI); @@ -91,7 +93,7 @@ async function testInlineAtRulesEditor(ui, editor) { is(sidebar.hidden, false, "sidebar is showing on editor with @media"); const entries = sidebar.querySelectorAll(".at-rule-label"); - is(entries.length, 8, "8 at-rules displayed in sidebar"); + is(entries.length, 14, "14 at-rules displayed in sidebar"); await testRule({ ui, @@ -166,6 +168,70 @@ async function testInlineAtRulesEditor(ui, editor) { type: "position-try", positionTryName: "--pt-custom-bottom", }); + + await testRule({ + ui, + editor, + rule: entries[8], + line: 42, + type: "custom-media", + customMediaName: "--mobile-breakpoint", + customMediaQuery: [ + { text: "(width < 320px) and (height < 1420px)" }, + { text: ", " }, + { text: "not print" }, + ], + }); + + await testRule({ + ui, + editor, + rule: entries[9], + line: 43, + type: "custom-media", + customMediaName: "--enabled", + customMediaQuery: [{ text: "true" }], + }); + + await testRule({ + ui, + editor, + rule: entries[10], + line: 44, + type: "custom-media", + customMediaName: "--disabled", + customMediaQuery: [{ text: "false", matches: false }], + }); + + await testRule({ + ui, + editor, + rule: entries[11], + line: 49, + type: "media", + conditionText: "(--mobile-breakpoint)", + matches: false, + }); + + await testRule({ + ui, + editor, + rule: entries[12], + line: 53, + type: "media", + conditionText: "(--enabled)", + matches: false, + }); + + await testRule({ + ui, + editor, + rule: entries[13], + line: 57, + type: "media", + conditionText: "(--disabled)", + matches: false, + }); } async function testMediaEditor(ui, editor) { @@ -298,6 +364,12 @@ async function testMediaRuleAdded(ui, editor) { * @param {string} options.layerName: Optional name of the @layer * @param {string} options.positionTryName: Name of the @position-try if type is "position-try" * @param {string} options.propertyName: Name of the @property if type is "property" + * @param {string} options.customMediaName: Name of the @custom-media if type is "custom-media" + * @param {Array<object>} options.customMediaQuery: query parts of the @custom-media if type is "custom-media" + * @param {string} options.customMediaQuery[].text: the query string of the part of the @custom-media + * if type is "custom-media" + * @param {boolean} options.customMediaQuery[].matches: whether or not this part is style as matching, + * if type is "custom-media". Defaults to true. * @param {number} options.line: Line of the rule * @param {string} options.type: The type of the rule (container, layer, media, support, property ). * Defaults to "media". @@ -311,6 +383,8 @@ async function testRule({ layerName, positionTryName, propertyName, + customMediaName, + customMediaQuery, line, type = "media", }) { @@ -323,11 +397,52 @@ async function testRule({ } else if (type === "position-try") { name = positionTryName; } - is( - atTypeEl.textContent, - `@${type}\u00A0${name ? `${name}\u00A0` : ""}`, - "label for at-rule type is correct" - ); + + if (type === "custom-media") { + const atTypeChilNodes = Array.from(atTypeEl.childNodes); + is( + atTypeChilNodes.shift().textContent, + `@custom-media\u00A0`, + "label for @custom-media is correct" + ); + is( + atTypeChilNodes.shift().textContent, + `${customMediaName} `, + "name for @custom-media is correct" + ); + is( + atTypeChilNodes.length, + customMediaQuery.length, + `Got expected number of children of @custom-media (got ${JSON.stringify(atTypeChilNodes.map(n => n.textContent))})` + ); + for (let i = 0; i < atTypeChilNodes.length; i++) { + const node = atTypeChilNodes[i]; + is( + node.textContent, + customMediaQuery[i].text, + `Got expected text for part #${i} of @custom-media` + ); + if (customMediaQuery[i].matches ?? true) { + ok( + // handle TextNode + !node.classList || + !node.classList.contains("media-condition-unmatched"), + `Text for part #${i} of @custom-media ("${node.textContent}") does not have unmatching class` + ); + } else { + ok( + node.classList.contains("media-condition-unmatched"), + `Text for part #${i} of @custom-media ("${node.textContent}") has expected unmatching class` + ); + } + } + } else { + is( + atTypeEl.textContent, + `@${type}\u00A0${name ? `${name}\u00A0` : ""}`, + "label for at-rule type is correct" + ); + } const cond = rule.querySelector(".at-rule-condition"); is( diff --git a/devtools/client/styleeditor/test/media-rules.html b/devtools/client/styleeditor/test/media-rules.html @@ -51,6 +51,26 @@ bottom: unset; margin-top: 10px; } + + @custom-media --mobile-breakpoint (width < 320px) and (height < 1420px), not print; + @custom-media --enabled true; + @custom-media --disabled false; + + div { + color: chocolate; + + @media (--mobile-breakpoint) { + color: peachpuff; + } + + @media (--enabled) { + color: green; + } + + @media (--disabled) { + color: red; + } + } </style> </head> <body> diff --git a/devtools/server/actors/utils/stylesheets-manager.js b/devtools/server/actors/utils/stylesheets-manager.js @@ -728,6 +728,31 @@ class StyleSheetsManager extends EventEmitter { line: InspectorUtils.getRelativeRuleLine(rule), column: InspectorUtils.getRuleColumn(rule), }); + } else if (className === "CSSCustomMediaRule") { + const customMediaQuery = []; + if (typeof rule.query === "boolean") { + customMediaQuery.push({ + text: rule.query.toString(), + matches: rule.query === true, + }); + } else { + // if query is not a boolean, it's a MediaList + for (let i = 0, len = rule.query.length; i < len; i++) { + customMediaQuery.push({ + text: rule.query[i], + // For now always consider the media query as matching. + // This should be changed as part of Bug 2006379 + matches: true, + }); + } + } + atRules.push({ + type: "custom-media", + customMediaName: rule.name, + customMediaQuery, + line: InspectorUtils.getRelativeRuleLine(rule), + column: InspectorUtils.getRuleColumn(rule), + }); } } return { diff --git a/layout/inspector/InspectorUtils.cpp b/layout/inspector/InspectorUtils.cpp @@ -568,6 +568,7 @@ static uint32_t CollectAtRules(ServoCSSRuleList& aRuleList, // so the DevTools team gets notified and can decide if it should be // displayed. switch (rule->Type()) { + case StyleCssRuleType::CustomMedia: case StyleCssRuleType::Media: case StyleCssRuleType::Supports: case StyleCssRuleType::LayerBlock: @@ -577,7 +578,6 @@ static uint32_t CollectAtRules(ServoCSSRuleList& aRuleList, (void)aResult.AppendElement(OwningNonNull(*rule), fallible); break; } - case StyleCssRuleType::CustomMedia: case StyleCssRuleType::Style: case StyleCssRuleType::Import: case StyleCssRuleType::Document: