[css value tracing] evaluate percentages in longhands

This adds support for evaluating <percentage> units in css value
tracing, but only for longhands. For shorthands, we need additional reasoning about which longhand the unit pertains to in order to understand whether it's relative to a width or a height.

Bug: 401213719
Change-Id: I823ef9d52bb12ab40e7bcabce770733ad36a51b7
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6376104
Reviewed-by: Eric Leese <leese@chromium.org>
Auto-Submit: Philip Pfaffe <pfaffe@chromium.org>
Commit-Queue: Eric Leese <leese@chromium.org>
This commit is contained in:
Philip Pfaffe
2025-03-25 03:36:09 -07:00
committed by Devtools-frontend LUCI CQ
parent 0832829c9c
commit a0c4c4a06c
5 changed files with 72 additions and 31 deletions
+11 -3
View File
@@ -43,6 +43,7 @@ import * as Root from '../root/root.js';
import {CSSFontFace} from './CSSFontFace.js';
import {CSSMatchedStyles} from './CSSMatchedStyles.js';
import {CSSMedia} from './CSSMedia.js';
import {cssMetadata} from './CSSMetadata.js';
import {CSSStyleRule} from './CSSRule.js';
import {CSSStyleDeclaration, Type} from './CSSStyleDeclaration.js';
import {CSSStyleSheetHeader} from './CSSStyleSheetHeader.js';
@@ -132,9 +133,16 @@ export class CSSModel extends SDKModel<EventTypes> {
return this.#colorScheme;
}
async resolveValues(nodeId: Protocol.DOM.NodeId, ...values: string[]): Promise<string[]|null> {
const response = await this.agent.invoke_resolveValues({values, nodeId});
return response.getError() ? null : response.results;
async resolveValues(propertyName: string|undefined, nodeId: Protocol.DOM.NodeId, ...values: string[]):
Promise<string[]|null> {
if (propertyName && cssMetadata().getLonghands(propertyName)?.length) {
return null;
}
const response = await this.agent.invoke_resolveValues({values, nodeId, propertyName});
if (response.getError()) {
return null;
}
return response.results;
}
headersForSourceURL(sourceURL: Platform.DevToolsPath.UrlString): CSSStyleSheetHeader[] {
@@ -684,10 +684,10 @@ export class LengthMatch implements Match {
export class LengthMatcher extends matcherBase(LengthMatch) {
// clang-format on
static readonly LENGTH_UNITS = new Set([
'em', 'ex', 'ch', 'cap', 'ic', 'lh', 'rem', 'rex', 'rch', 'rlh', 'ric', 'rcap', 'pt',
'pc', 'in', 'cm', 'mm', 'Q', 'vw', 'vh', 'vi', 'vb', 'vmin', 'vmax', 'dvw', 'dvh',
'dvi', 'dvb', 'dvmin', 'dvmax', 'svw', 'svh', 'svi', 'svb', 'svmin', 'svmax', 'lvw', 'lvh', 'lvi',
'lvb', 'lvmin', 'lvmax', 'cqw', 'cqh', 'cqi', 'cqb', 'cqmin', 'cqmax', 'cqem', 'cqlh', 'cqex', 'cqch',
'em', 'ex', 'ch', 'cap', 'ic', 'lh', 'rem', 'rex', 'rch', 'rlh', 'ric', 'rcap', 'pt', 'pc',
'in', 'cm', 'mm', 'Q', 'vw', 'vh', 'vi', 'vb', 'vmin', 'vmax', 'dvw', 'dvh', 'dvi', 'dvb',
'dvmin', 'dvmax', 'svw', 'svh', 'svi', 'svb', 'svmin', 'svmax', 'lvw', 'lvh', 'lvi', 'lvb', 'lvmin', 'lvmax',
'cqw', 'cqh', 'cqi', 'cqb', 'cqmin', 'cqmax', 'cqem', 'cqlh', 'cqex', 'cqch', '%'
]);
override matches(node: CodeMirror.SyntaxNode, matching: BottomUpTreeMatching): LengthMatch|null {
if (node.name !== 'NumberLiteral') {
@@ -73,7 +73,7 @@ async function showTrace(
view.showTrace(
property, null, matchedStyles, new Map(),
Elements.StylePropertyTreeElement.getPropertyRenderers(
property.ownerStyle, treeElement.parentPane(), matchedStyles, treeElement,
property.name, property.ownerStyle, treeElement.parentPane(), matchedStyles, treeElement,
treeElement.getComputedStyles() ?? new Map()));
return await viewFunction.nextInput;
}
@@ -286,7 +286,7 @@ describeWithMockConnection('StylePropertyTreeElement', () => {
const {valueElement} = Elements.PropertyRenderer.Renderer.renderValueElement(
property, matchedResult,
Elements.StylePropertyTreeElement.getPropertyRenderers(
matchedStyles.nodeStyles()[0], stylesSidebarPane, matchedStyles, null, new Map()),
property.name, matchedStyles.nodeStyles()[0], stylesSidebarPane, matchedStyles, null, new Map()),
context);
const colorSwatch = valueElement.querySelector('devtools-color-swatch');
@@ -1655,7 +1655,7 @@ describeWithMockConnection('StylePropertyTreeElement', () => {
const addPopoverPromise = Promise.withResolvers<void>();
sinon.stub(Elements.StylePropertyTreeElement.LengthRenderer.prototype, 'popOverAttachedForTest')
.callsFake(() => addPopoverPromise.resolve());
const stylePropertyTreeElement = getTreeElement('margin', '5px 2em');
const stylePropertyTreeElement = getTreeElement('property', '5px 2em');
setMockConnectionResponseHandler('CSS.getComputedStyleForNode', () => ({computedStyle: {}}));
await stylePropertyTreeElement.onpopulate();
@@ -1664,6 +1664,17 @@ describeWithMockConnection('StylePropertyTreeElement', () => {
const popover = stylePropertyTreeElement.valueElement?.querySelector('devtools-tooltip');
assert.strictEqual(popover?.innerText, '15px');
});
it('passes the property name to evaluations', async () => {
const cssModel = stylesSidebarPane.cssModel();
assert.exists(cssModel);
const resolveValuesStub = sinon.stub(cssModel, 'resolveValues').resolves([]);
const stylePropertyTreeElement = getTreeElement('left', '2%');
stylePropertyTreeElement.updateTitle();
assert.isTrue(resolveValuesStub.calledOnce);
assert.strictEqual(resolveValuesStub.args[0][0], 'left');
});
});
describe('MathFunctionRenderer', () => {
@@ -1715,13 +1726,29 @@ describeWithMockConnection('StylePropertyTreeElement', () => {
view.showTrace(
property, null, matchedStyles, new Map(),
Elements.StylePropertyTreeElement.getPropertyRenderers(
property.ownerStyle, stylesSidebarPane, matchedStyles, null, new Map()));
property.name, property.ownerStyle, stylesSidebarPane, matchedStyles, null, new Map()));
assert.isTrue(evaluationSpy.calledOnce);
const originalText = evaluationSpy.args[0][0].textContent;
await evaluationSpy.returnValues[0];
assert.strictEqual(originalText, evaluationSpy.args[0][0].textContent);
});
it('shows the original text during tracing when evaluation fails', async () => {
const cssModel = stylesSidebarPane.cssModel();
assert.exists(cssModel);
const resolveValuesStub = sinon.stub(cssModel, 'resolveValues').resolves([]);
const property = addProperty('width', 'calc(1 + 1)');
const view = new Elements.CSSValueTraceView.CSSValueTraceView(undefined, () => {});
view.showTrace(
property, null, matchedStyles, new Map(),
Elements.StylePropertyTreeElement.getPropertyRenderers(
property.name, property.ownerStyle, stylesSidebarPane, matchedStyles, null, new Map()));
assert.isTrue(resolveValuesStub.calledOnce);
assert.strictEqual(resolveValuesStub.args[0][0], 'width');
});
});
describe('AutoBaseRenderer', () => {
@@ -256,7 +256,7 @@ function getTracingTooltip(
?.getWidget()
?.showTrace(
property, text, matchedStyles, computedStyles,
getPropertyRenderers(
getPropertyRenderers(property.name,
property.ownerStyle, stylesPane, matchedStyles, null,
computedStyles));
}
@@ -308,7 +308,8 @@ export class VariableRenderer extends rendererBase(SDK.CSSPropertyParserMatchers
const {nodes, cssControls} = Renderer.renderValueNodes(
{name: declaration.name, value: declaration.value ?? ''},
substitution.cachedParsedValue(declaration.declaration, this.#matchedStyles, this.#computedStyles),
getPropertyRenderers(declaration.style, this.#stylesPane, this.#matchedStyles, null, this.#computedStyles),
getPropertyRenderers(
declaration.name, declaration.style, this.#stylesPane, this.#matchedStyles, null, this.#computedStyles),
substitution);
cssControls.forEach((value, key) => value.forEach(control => context.addControl(key, control)));
return nodes;
@@ -697,7 +698,7 @@ export class ColorMixRenderer extends rendererBase(SDK.CSSPropertyParserMatchers
context.addControl('color', swatch);
const nodeId = this.#pane.node()?.id;
if (nodeId !== undefined) {
void this.#pane.cssModel()?.resolveValues(nodeId, colorMixText).then(results => {
void this.#pane.cssModel()?.resolveValues(undefined, nodeId, colorMixText).then(results => {
if (results) {
const color = Common.Color.parse(results[0]);
if (color) {
@@ -1291,11 +1292,13 @@ export class GridTemplateRenderer extends rendererBase(SDK.CSSPropertyParserMatc
// clang-format off
export class LengthRenderer extends rendererBase(SDK.CSSPropertyParserMatchers.LengthMatch) {
readonly #stylesPane: StylesSidebarPane;
// clang-format on
constructor(stylesPane: StylesSidebarPane) {
readonly #stylesPane: StylesSidebarPane;
readonly #propertyName: string;
constructor(stylesPane: StylesSidebarPane, propertyName: string) {
super();
this.#stylesPane = stylesPane;
this.#propertyName = propertyName;
}
override render(match: SDK.CSSPropertyParserMatchers.LengthMatch, context: RenderingContext): Node[] {
@@ -1318,7 +1321,7 @@ export class LengthRenderer extends rendererBase(SDK.CSSPropertyParserMatchers.L
return;
}
const pixelValue = await this.#stylesPane.cssModel()?.resolveValues(nodeId, value);
const pixelValue = await this.#stylesPane.cssModel()?.resolveValues(this.#propertyName, nodeId, value);
if (pixelValue) {
valueElement.textContent = pixelValue[0];
@@ -1331,7 +1334,7 @@ export class LengthRenderer extends rendererBase(SDK.CSSPropertyParserMatchers.L
return;
}
const pixelValue = await this.#stylesPane.cssModel()?.resolveValues(nodeId, value);
const pixelValue = await this.#stylesPane.cssModel()?.resolveValues(this.#propertyName, nodeId, value);
if (!pixelValue) {
return;
}
@@ -1351,17 +1354,19 @@ export class LengthRenderer extends rendererBase(SDK.CSSPropertyParserMatchers.L
// clang-format off
export class MathFunctionRenderer extends rendererBase(SDK.CSSPropertyParserMatchers.MathFunctionMatch) {
readonly #stylesPane: StylesSidebarPane;
#matchedStyles: SDK.CSSMatchedStyles.CSSMatchedStyles;
#computedStyles: Map<string, string>;
// clang-format on
readonly #stylesPane: StylesSidebarPane;
#matchedStyles: SDK.CSSMatchedStyles.CSSMatchedStyles;
#computedStyles: Map<string, string>;
#propertyName: string;
constructor(
stylesPane: StylesSidebarPane, matchedStyles: SDK.CSSMatchedStyles.CSSMatchedStyles,
computedStyles: Map<string, string>) {
computedStyles: Map<string, string>, propertyName: string) {
super();
this.#matchedStyles = matchedStyles;
this.#computedStyles = computedStyles;
this.#stylesPane = stylesPane;
this.#propertyName = propertyName;
}
override render(match: SDK.CSSPropertyParserMatchers.MathFunctionMatch, context: RenderingContext): Node[] {
@@ -1397,7 +1402,7 @@ export class MathFunctionRenderer extends rendererBase(SDK.CSSPropertyParserMatc
if (nodeId === undefined) {
return;
}
const evaled = await this.#stylesPane.cssModel()?.resolveValues(nodeId, value);
const evaled = await this.#stylesPane.cssModel()?.resolveValues(this.#propertyName, nodeId, value);
if (!evaled?.[0] || evaled[0] === value) {
return;
}
@@ -1413,7 +1418,7 @@ export class MathFunctionRenderer extends rendererBase(SDK.CSSPropertyParserMatc
// and compare the function result to the values of all its arguments. Evaluating the arguments eliminates nested
// function calls and normalizes all units to px.
values.unshift(functionText);
const evaledArgs = await this.#stylesPane.cssModel()?.resolveValues(nodeId, ...values);
const evaledArgs = await this.#stylesPane.cssModel()?.resolveValues(this.#propertyName, nodeId, ...values);
if (!evaledArgs) {
return;
}
@@ -1555,7 +1560,7 @@ export class PositionTryRenderer extends rendererBase(SDK.CSSPropertyParserMatch
}
export function getPropertyRenderers(
style: SDK.CSSStyleDeclaration.CSSStyleDeclaration, stylesPane: StylesSidebarPane,
propertyName: string, style: SDK.CSSStyleDeclaration.CSSStyleDeclaration, stylesPane: StylesSidebarPane,
matchedStyles: SDK.CSSMatchedStyles.CSSMatchedStyles, treeElement: StylePropertyTreeElement|null,
computedStyles: Map<string, string>): Array<MatchRenderer<SDK.CSSPropertyParser.Match>> {
return [
@@ -1576,8 +1581,8 @@ export function getPropertyRenderers(
new PositionAnchorRenderer(stylesPane),
new FlexGridRenderer(stylesPane, treeElement),
new PositionTryRenderer(matchedStyles),
new LengthRenderer(stylesPane),
new MathFunctionRenderer(stylesPane, matchedStyles, computedStyles),
new LengthRenderer(stylesPane, propertyName),
new MathFunctionRenderer(stylesPane, matchedStyles, computedStyles, propertyName),
new AutoBaseRenderer(computedStyles),
new BinOpRenderer(),
];
@@ -1999,10 +2004,11 @@ export class StylePropertyTreeElement extends UI.TreeOutline.TreeElement {
this.expandElement.setAttribute('jslog', `${VisualLogging.expand().track({click: true})}`);
}
const renderers = this.property.parsedOk ? getPropertyRenderers(
this.style, this.parentPaneInternal, this.matchedStylesInternal,
this, this.getComputedStyles() ?? new Map()) :
[];
const renderers = this.property.parsedOk ?
getPropertyRenderers(
this.name, this.style, this.parentPaneInternal, this.matchedStylesInternal, this,
this.getComputedStyles() ?? new Map()) :
[];
if (Root.Runtime.experiments.isEnabled('font-editor') && this.property.parsedOk) {
renderers.push(new FontRenderer(this));