豆豆友情提示:这是一个非官方 GitHub 代理镜像,主要用于网络测试或访问加速。请勿在此进行登录、注册或处理任何敏感信息。进行这些操作请务必访问官方网站 github.com。 Raw 内容也通过此代理提供。
Skip to content

Commit 7d49b08

Browse files
jackfranklinDevtools-frontend LUCI CQ
authored andcommitted
ComputedStylesWidget is given styles rather than fetching them.
This CL updates the ComputedStylesWidget so that it does not fetch data from the model directly. Instead it is provided the latest set of styles. This opens the path for using this widget in AI Assistance where we want to use it to snapshot styles on a node at a point in time. Bug: 477241796 Change-Id: If14820a45b90a7251916211696b0c1ca24b963e7 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/7572408 Auto-Submit: Jack Franklin <jacktfranklin@chromium.org> Commit-Queue: Jack Franklin <jacktfranklin@chromium.org> Reviewed-by: Philip Pfaffe <pfaffe@chromium.org>
1 parent 748db09 commit 7d49b08

File tree

7 files changed

+107
-73
lines changed

7 files changed

+107
-73
lines changed

front_end/models/computed_style/ComputedStyleModel.test.ts

Lines changed: 24 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -103,51 +103,47 @@ describeWithMockConnection('ComputedStyleModel', () => {
103103
sinon.assert.callCount(computedStyleListener, 0);
104104
});
105105

106-
it('fetchAllComputedStyleInfo calls the backend and returns the data', async () => {
106+
it('fetchMatchedCascade returns null for matchedStyles if the node does not match', async () => {
107107
const cssModel = domNode1.domModel().cssModel();
108108
assert.isOk(cssModel);
109109
computedStyleModel.node = domNode1;
110110

111-
const mockComputedStyle = new Map([['color', 'red']]);
112-
const getComputedStyleStub = sinon.stub(cssModel, 'getComputedStyle').resolves(mockComputedStyle);
113-
114-
const mockMatchedStyles = await getMatchedStyles({
115-
node: domNode1,
111+
const domNode2 = createNode(target, {nodeId: 2 as Protocol.DOM.NodeId});
112+
const mockMatchedStylesForNode2 = await getMatchedStyles({
113+
node: domNode2,
116114
});
117115
const cachedMatchedCascadeForNodeStub =
118-
sinon.stub(cssModel, 'cachedMatchedCascadeForNode').resolves(mockMatchedStyles);
116+
sinon.stub(cssModel, 'cachedMatchedCascadeForNode').resolves(mockMatchedStylesForNode2);
119117

120-
const result = await computedStyleModel.fetchAllComputedStyleInfo();
121-
122-
sinon.assert.calledOnce(getComputedStyleStub);
118+
const matchedStyles = await computedStyleModel.fetchMatchedCascade();
123119
sinon.assert.calledOnce(cachedMatchedCascadeForNodeStub);
124-
125-
assert.deepEqual(result.computedStyle?.computedStyle, mockComputedStyle);
126-
assert.deepEqual(result.matchedStyles, mockMatchedStyles);
120+
assert.isNull(matchedStyles);
127121
});
128122

129-
it('fetchAllComputedStyleInfo returns null for matchedStyles if the node does not match', async () => {
123+
it('fetchComputedStyle returns null if the node has become outdated', async () => {
130124
const cssModel = domNode1.domModel().cssModel();
131125
assert.isOk(cssModel);
132126
computedStyleModel.node = domNode1;
133127

134-
const mockComputedStyle = new Map([['color', 'red']]);
135-
const getComputedStyleStub =
136-
sinon.stub(cssModel, 'getComputedStyle').resolves(mockComputedStyle as Map<string, string>);
137-
138128
const domNode2 = createNode(target, {nodeId: 2 as Protocol.DOM.NodeId});
139-
const mockMatchedStyles = await getMatchedStyles({
140-
node: domNode2,
129+
// We need to control when this promise resolves, hence using callsFake and
130+
// providing the promise manually.
131+
const computedStylePromise = Promise.withResolvers<Map<string, string>>();
132+
const getComputedStyleStub = sinon.stub(cssModel, 'getComputedStyle').callsFake(() => {
133+
return computedStylePromise.promise;
141134
});
142-
const cachedMatchedCascadeForNodeStub =
143-
sinon.stub(cssModel, 'cachedMatchedCascadeForNode').resolves(mockMatchedStyles);
144-
145-
const result = await computedStyleModel.fetchAllComputedStyleInfo();
146135

136+
// To emulate this scenario we need to:
137+
// 1. Set the node to ID=1, and make the fetchComputedStyle() call.
138+
const stylesPromise = computedStyleModel.fetchComputedStyle();
139+
// 2. Before that resolves, set the node to ID = 2
140+
computedStyleModel.node = domNode2;
141+
// 3. Resolve the getComputedStyle promise, at which point the node check
142+
// will see that the nodes are different.
143+
const mockComputedStyle = new Map([['color', 'red']]);
144+
computedStylePromise.resolve(mockComputedStyle);
145+
const styles = await stylesPromise;
147146
sinon.assert.calledOnce(getComputedStyleStub);
148-
sinon.assert.calledOnce(cachedMatchedCascadeForNodeStub);
149-
150-
assert.deepEqual(result.computedStyle?.computedStyle, mockComputedStyle);
151-
assert.isNull(result.matchedStyles);
147+
assert.isNull(styles);
152148
});
153149
});

front_end/models/computed_style/ComputedStyleModel.ts

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -125,20 +125,33 @@ export class ComputedStyleModel extends Common.ObjectWrapper.ObjectWrapper<Event
125125
}
126126

127127
if (!this.computedStylePromise) {
128-
this.computedStylePromise = cssModel.getComputedStyle(nodeId).then(verifyOutdated.bind(this, elementNode));
128+
this.computedStylePromise = cssModel.getComputedStyle(nodeId).then(style => {
129+
return this.#validateNodeStyles(elementNode, style);
130+
});
129131
}
130132

131133
return await this.computedStylePromise;
134+
}
132135

133-
function verifyOutdated(
134-
this: ComputedStyleModel, elementNode: SDK.DOMModel.DOMNode, style: Map<string, string>|null): ComputedStyle|
135-
null {
136-
return elementNode === this.elementNode() && style ? new ComputedStyle(elementNode, style) :
137-
null as ComputedStyle | null;
136+
/**
137+
* Once we fetch the node's CSS styles, we validate them to ensure that the
138+
* active Node didn't change between initiating the request to fetch the
139+
* styles and the request returning. If it did, we discard these styles as
140+
* outdated.
141+
*/
142+
#validateNodeStyles(node: SDK.DOMModel.DOMNode, styles: Map<string, string>|null): ComputedStyle|null {
143+
if (node === this.elementNode() && styles) {
144+
return new ComputedStyle(node, styles);
138145
}
146+
return null;
139147
}
140148

141-
private async fetchMatchedCascade(): Promise<SDK.CSSMatchedStyles.CSSMatchedStyles|null> {
149+
/**
150+
* Fetches the CSS cascade for the node, including matched rules, inherited
151+
* styles, and pseudo-elements.
152+
* This allows determining which properties are active or overridden.
153+
*/
154+
async fetchMatchedCascade(): Promise<SDK.CSSMatchedStyles.CSSMatchedStyles|null> {
142155
const node = this.node;
143156
if (!node || !this.cssModel()) {
144157
return null;
@@ -155,14 +168,6 @@ export class ComputedStyleModel extends Common.ObjectWrapper.ObjectWrapper<Event
155168
}
156169
return matchedStyles.node() === this.node ? matchedStyles : null;
157170
}
158-
159-
async fetchAllComputedStyleInfo(): Promise<{
160-
computedStyle: ComputedStyle | null,
161-
matchedStyles: SDK.CSSMatchedStyles.CSSMatchedStyles|null,
162-
}> {
163-
const [computedStyle, matchedStyles] = await Promise.all([this.fetchComputedStyle(), this.fetchMatchedCascade()]);
164-
return {computedStyle, matchedStyles};
165-
}
166171
}
167172

168173
export const enum Events {

front_end/panels/elements/ComputedStyleWidget.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ describeWithMockConnection('ComputedStyleWidget', () => {
7272
const computedStyleWidget = new Elements.ComputedStyleWidget.ComputedStyleWidget(computedStyleModel);
7373
renderElementIntoDOM(computedStyleWidget);
7474

75+
computedStyleWidget.nodeStyle = {node, computedStyle: new Map([['color', 'red']])};
76+
computedStyleWidget.matchedStyles = cssMatchedStyles;
77+
7578
return computedStyleWidget;
7679
}
7780

front_end/panels/elements/ComputedStyleWidget.ts

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ import * as Common from '../../core/common/common.js';
3838
import * as i18n from '../../core/i18n/i18n.js';
3939
import * as Platform from '../../core/platform/platform.js';
4040
import * as SDK from '../../core/sdk/sdk.js';
41-
import * as ComputedStyleModule from '../../models/computed_style/computed_style.js';
41+
import type * as ComputedStyleModule from '../../models/computed_style/computed_style.js';
4242
import * as TreeOutline from '../../ui/components/tree_outline/tree_outline.js';
4343
import * as InlineEditor from '../../ui/legacy/components/inline_editor/inline_editor.js';
4444
import * as Components from '../../ui/legacy/components/utils/utils.js';
@@ -296,17 +296,17 @@ export const DEFAULT_VIEW: View = (input, _output, target) => {
296296
>${i18nString(UIStrings.group)}</devtools-checkbox>
297297
</devtools-toolbar>
298298
</div>
299-
<div class="computed-style-tree-outline-container">
300-
${input.computedStylesTree}
301-
</div>
299+
${input.computedStylesTree}
302300
${!input.hasMatches ? html`<div class="gray-info-message">${i18nString(UIStrings.noMatchingProperty)}</div>` : ''}
303301
<devtools-widget .widgetConfig=${UI.Widget.widgetConfig(PlatformFontsWidget, { sharedModel: input.computedStyleModel})}></devtools-widget>
304302
`, target);
305303
// clang-format on
306304
};
307305

308306
export class ComputedStyleWidget extends UI.Widget.VBox {
309-
private computedStyleModel: ComputedStyleModule.ComputedStyleModel.ComputedStyleModel;
307+
#computedStyleModel: ComputedStyleModule.ComputedStyleModel.ComputedStyleModel;
308+
#nodeStyle: ComputedStyleModule.ComputedStyleModel.ComputedStyle|null = null;
309+
#matchedStyles: SDK.CSSMatchedStyles.CSSMatchedStyles|null = null;
310310
private readonly showInheritedComputedStylePropertiesSetting: Common.Settings.Setting<boolean>;
311311
private readonly groupComputedStylesSetting: Common.Settings.Setting<boolean>;
312312
private filterRegex: RegExp|null;
@@ -324,11 +324,7 @@ export class ComputedStyleWidget extends UI.Widget.VBox {
324324

325325
this.contentElement.classList.add('styles-sidebar-computed-style-widget');
326326

327-
this.computedStyleModel = computedStyleModel;
328-
this.computedStyleModel.addEventListener(
329-
ComputedStyleModule.ComputedStyleModel.Events.CSS_MODEL_CHANGED, this.requestUpdate, this);
330-
this.computedStyleModel.addEventListener(
331-
ComputedStyleModule.ComputedStyleModel.Events.COMPUTED_STYLE_CHANGED, this.requestUpdate, this);
327+
this.#computedStyleModel = computedStyleModel;
332328

333329
this.showInheritedComputedStylePropertiesSetting =
334330
Common.Settings.Settings.instance().createSetting('show-inherited-computed-style-properties', false);
@@ -340,16 +336,14 @@ export class ComputedStyleWidget extends UI.Widget.VBox {
340336
});
341337

342338
this.filterRegex = null;
343-
344339
this.linkifier = new Components.Linkifier.Linkifier(maxLinkLength);
345-
346340
this.imagePreviewPopover = new ImagePreviewPopover(this.contentElement, event => {
347341
const link = event.composedPath()[0];
348342
if (link instanceof Element) {
349343
return link;
350344
}
351345
return null;
352-
}, () => this.computedStyleModel.node);
346+
}, () => this.#computedStyleModel.node);
353347

354348
this.#updateView({hasMatches: true});
355349
}
@@ -377,7 +371,7 @@ export class ComputedStyleWidget extends UI.Widget.VBox {
377371
{
378372
computedStylesTree: this.#computedStylesTree,
379373
hasMatches,
380-
computedStyleModel: this.computedStyleModel,
374+
computedStyleModel: this.#computedStyleModel,
381375
showInheritedComputedStylePropertiesSetting: this.showInheritedComputedStylePropertiesSetting,
382376
groupComputedStylesSetting: this.groupComputedStylesSetting,
383377
onFilterChanged: this.onFilterChanged.bind(this),
@@ -386,8 +380,27 @@ export class ComputedStyleWidget extends UI.Widget.VBox {
386380
null, this.contentElement);
387381
}
388382

383+
get nodeStyle(): ComputedStyleModule.ComputedStyleModel.ComputedStyle|null {
384+
return this.#nodeStyle;
385+
}
386+
387+
set nodeStyle(nodeStyle: ComputedStyleModule.ComputedStyleModel.ComputedStyle|null) {
388+
this.#nodeStyle = nodeStyle;
389+
this.requestUpdate();
390+
}
391+
392+
get matchedStyles(): SDK.CSSMatchedStyles.CSSMatchedStyles|null {
393+
return this.#matchedStyles;
394+
}
395+
396+
set matchedStyles(matchedStyles: SDK.CSSMatchedStyles.CSSMatchedStyles|null) {
397+
this.#matchedStyles = matchedStyles;
398+
this.requestUpdate();
399+
}
400+
389401
override async performUpdate(): Promise<void> {
390-
const {computedStyle: nodeStyles, matchedStyles} = await this.computedStyleModel.fetchAllComputedStyleInfo();
402+
const nodeStyles = this.#nodeStyle;
403+
const matchedStyles = this.#matchedStyles;
391404
if (!nodeStyles || !matchedStyles) {
392405
this.#updateView({hasMatches: false});
393406
return;
@@ -405,7 +418,7 @@ export class ComputedStyleWidget extends UI.Widget.VBox {
405418
matchedStyles: SDK.CSSMatchedStyles.CSSMatchedStyles): Promise<void> {
406419
this.imagePreviewPopover.hide();
407420
this.linkifier.reset();
408-
const cssModel = this.computedStyleModel.cssModel();
421+
const cssModel = this.#computedStyleModel.cssModel();
409422
if (!cssModel) {
410423
return;
411424
}
@@ -448,7 +461,7 @@ export class ComputedStyleWidget extends UI.Widget.VBox {
448461
matchedStyles: SDK.CSSMatchedStyles.CSSMatchedStyles|null): Promise<void> {
449462
this.imagePreviewPopover.hide();
450463
this.linkifier.reset();
451-
const cssModel = this.computedStyleModel.cssModel();
464+
const cssModel = this.#computedStyleModel.cssModel();
452465
if (!nodeStyle || !matchedStyles || !cssModel) {
453466
this.#updateView({hasMatches: false});
454467
return;

front_end/panels/elements/ElementsPanel.test.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import type * as Protocol from '../../generated/protocol.js';
99
import * as ComputedStyle from '../../models/computed_style/computed_style.js';
1010
import {raf, renderElementIntoDOM} from '../../testing/DOMHelpers.js';
1111
import {createTarget, stubNoopSettings, updateHostConfig} from '../../testing/EnvironmentHelpers.js';
12-
import {expectCall} from '../../testing/ExpectStubCall.js';
12+
import {expectCall, expectCalled} from '../../testing/ExpectStubCall.js';
1313
import {
1414
describeWithMockConnection,
1515
dispatchEvent,
@@ -213,7 +213,7 @@ describeWithMockConnection('ElementsPanel', () => {
213213
panel.detach();
214214
});
215215

216-
describe('tracking Computed styles', () => {
216+
describe('tracking and updating Computed styles', () => {
217217
const StylesSidebarPane = Elements.StylesSidebarPane.StylesSidebarPane;
218218
const ComputedStyleModel = ComputedStyle.ComputedStyleModel.ComputedStyleModel;
219219
const ComputedStyleWidget = Elements.ComputedStyleWidget.ComputedStyleWidget;
@@ -222,12 +222,16 @@ describeWithMockConnection('ElementsPanel', () => {
222222
get: sinon.SinonSpy,
223223
set: sinon.SinonSpy,
224224
};
225+
let computedStyleFetchStylesSpy: sinon.SinonStub;
226+
let computedStyleFetchCascadeSpy: sinon.SinonStub;
225227
let panel: Elements.ElementsPanel.ElementsPanel;
226228
let node: SDK.DOMModel.DOMNode;
227229
let cssModel: sinon.SinonStubbedInstance<SDK.CSSModel.CSSModel>;
228230

229231
beforeEach(() => {
230232
computedStyleNodeSpy = sinon.spy(ComputedStyleModel.prototype, 'node', ['get', 'set']);
233+
computedStyleFetchStylesSpy = sinon.stub(ComputedStyleModel.prototype, 'fetchComputedStyle').resolves(null);
234+
computedStyleFetchCascadeSpy = sinon.stub(ComputedStyleModel.prototype, 'fetchMatchedCascade').resolves(null);
231235
Common.Debouncer.enableTestOverride();
232236
panel = Elements.ElementsPanel.ElementsPanel.instance({forceNew: true});
233237

@@ -256,6 +260,12 @@ describeWithMockConnection('ElementsPanel', () => {
256260
sinon.assert.calledOnceWithExactly(computedStyleNodeSpy.set, node);
257261
});
258262

263+
it('fetches the styles from the computed style model when the dom node changes', async () => {
264+
UI.Context.Context.instance().setFlavor(SDK.DOMModel.DOMNode, node);
265+
await expectCalled(computedStyleFetchStylesSpy);
266+
await expectCalled(computedStyleFetchCascadeSpy);
267+
});
268+
259269
it('enables tracking when a ComputedStyleWidget is created', async () => {
260270
UI.Context.Context.instance().setFlavor(SDK.DOMModel.DOMNode, node);
261271
const computedStylesWidget = sinon.createStubInstance(ComputedStyleWidget);

front_end/panels/elements/ElementsPanel.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ export class ElementsPanel extends UI.Panel.Panel implements UI.SearchableView.S
212212
private readonly accessibilityTreeView: AccessibilityTreeView|undefined;
213213
private breadcrumbs: ElementsComponents.ElementsBreadcrumbs.ElementsBreadcrumbs;
214214
stylesWidget: StylesSidebarPane;
215-
private readonly computedStyleWidget: ComputedStyleWidget;
215+
readonly #computedStyleWidget: ComputedStyleWidget;
216216
private readonly metricsWidget: MetricsSidebarPane;
217217
private searchResults!: Array<{
218218
domModel: SDK.DOMModel.DOMModel,
@@ -315,7 +315,12 @@ export class ElementsPanel extends UI.Panel.Panel implements UI.SearchableView.S
315315
ComputedStyleWidget, this.evaluateTrackingComputedStyleUpdatesForNode, this);
316316

317317
this.stylesWidget = new StylesSidebarPane(this.#computedStyleModel);
318-
this.computedStyleWidget = new ComputedStyleWidget(this.#computedStyleModel);
318+
this.#computedStyleWidget = new ComputedStyleWidget(this.#computedStyleModel);
319+
this.#computedStyleModel.addEventListener(
320+
ComputedStyle.ComputedStyleModel.Events.COMPUTED_STYLE_CHANGED, this.#updateComputedStyles, this);
321+
this.#computedStyleModel.addEventListener(
322+
ComputedStyle.ComputedStyleModel.Events.CSS_MODEL_CHANGED, this.#updateComputedStyles, this);
323+
319324
this.metricsWidget = new MetricsSidebarPane(this.#computedStyleModel);
320325

321326
Common.Settings.Settings.instance()
@@ -374,6 +379,13 @@ export class ElementsPanel extends UI.Panel.Panel implements UI.SearchableView.S
374379
shouldTrackComputedStyleUpdates ? selectedNode.id : undefined);
375380
}, 100);
376381

382+
async #updateComputedStyles(): Promise<void> {
383+
const computedStyle = await this.#computedStyleModel.fetchComputedStyle();
384+
const matchedCascade = await this.#computedStyleModel.fetchMatchedCascade();
385+
this.#computedStyleWidget.nodeStyle = computedStyle;
386+
this.#computedStyleWidget.matchedStyles = matchedCascade;
387+
}
388+
377389
private handleElementExpanded(): void {
378390
if (Annotations.AnnotationRepository.annotationsEnabled()) {
379391
void PanelCommon.AnnotationManager.instance().resolveAnnotationsOfType(Annotations.AnnotationType.ELEMENT_NODE);
@@ -1007,7 +1019,7 @@ export class ElementsPanel extends UI.Panel.Panel implements UI.SearchableView.S
10071019
const computedStylePanesWrapper = new UI.Widget.VBox();
10081020
computedStylePanesWrapper.element.classList.add('style-panes-wrapper');
10091021
computedStylePanesWrapper.element.setAttribute('jslog', `${VisualLogging.pane('computed').track({resize: true})}`);
1010-
this.computedStyleWidget.show(computedStylePanesWrapper.element);
1022+
this.#computedStyleWidget.show(computedStylePanesWrapper.element);
10111023

10121024
const stylesSplitWidget = new UI.SplitWidget.SplitWidget(
10131025
true /* isVertical */, true /* secondIsSidebar */, 'elements.styles.sidebar.width', 100);
@@ -1024,7 +1036,7 @@ export class ElementsPanel extends UI.Panel.Panel implements UI.SearchableView.S
10241036
});
10251037

10261038
const showMetricsWidgetInComputedPane = (): void => {
1027-
this.metricsWidget.show(computedStylePanesWrapper.element, this.computedStyleWidget.element);
1039+
this.metricsWidget.show(computedStylePanesWrapper.element, this.#computedStyleWidget.element);
10281040
this.stylesWidget.removeEventListener(StylesSidebarPaneEvents.STYLES_UPDATE_COMPLETED, toggleMetricsWidget);
10291041
};
10301042

@@ -1147,7 +1159,7 @@ export class ElementsPanel extends UI.Panel.Panel implements UI.SearchableView.S
11471159
}
11481160

11491161
getComputedStyleWidget(): ComputedStyleWidget {
1150-
return this.computedStyleWidget;
1162+
return this.#computedStyleWidget;
11511163
}
11521164

11531165
private setupStyleTracking(cssModel: SDK.CSSModel.CSSModel): void {

front_end/panels/elements/computedStyleWidget.css

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,7 @@
1414
width: 100%;
1515
}
1616

17-
.styles-sidebar-computed-style-widget {
18-
min-height: auto;
19-
}
20-
21-
.computed-style-tree-outline-container {
22-
flex-grow: 1;
17+
devtools-tree-outline {
2318
flex-shrink: 0;
2419
}
2520

0 commit comments

Comments
 (0)