Skip to content

Commit 3c51433

Browse files
ergunshDevtools-frontend LUCI CQ
authored andcommitted
[Elements] Move tracking computed style updates for node to model
also: * Enables tracking computed style updates when Styles tab is shown and DevToolsAnimationStylesInStyles tab flag is enabled. * Separated CSS_MODEL_CHANGED and COMPUTED_STYLE_CHANGED events and only emits COMPUTED_STYLE_CHANGED event when the computed styles are changed. This way we don't trigger re-render in Styles tab yet and keep the re-rendering behavior for MetricsWidget and ComputedStylesWidget. Bug: 349566291 Change-Id: I52c6e9fdc3dae8a7bae0c8c98be90a43186f48d7 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6088054 Auto-Submit: Ergün Erdoğmuş <[email protected]> Reviewed-by: Changhao Han <[email protected]> Commit-Queue: Changhao Han <[email protected]>
1 parent 88564e7 commit 3c51433

File tree

8 files changed

+231
-88
lines changed

8 files changed

+231
-88
lines changed

front_end/panels/elements/BUILD.gn

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ ts_library("unittests") {
155155
"AccessibilityTreeView.test.ts",
156156
"CSSRuleValidator.test.ts",
157157
"ClassesPaneWidget.test.ts",
158-
"ComputedStyleWidget.test.ts",
158+
"ComputedStyleModel.test.ts",
159159
"DOMLinkifier.test.ts",
160160
"ElementStatePaneWidget.test.ts",
161161
"ElementsPanel.test.ts",
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
// Copyright 2023 The Chromium Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import * as SDK from '../../core/sdk/sdk.js';
6+
import type * as Protocol from '../../generated/protocol.js';
7+
import {createTarget, getGetHostConfigStub, stubNoopSettings} from '../../testing/EnvironmentHelpers.js';
8+
import {describeWithMockConnection} from '../../testing/MockConnection.js';
9+
import * as UI from '../../ui/legacy/legacy.js';
10+
11+
import * as Elements from './elements.js';
12+
13+
function createNode(target: SDK.Target.Target, {nodeId}: {nodeId: Protocol.DOM.NodeId}): SDK.DOMModel.DOMNode {
14+
const domModel = target.model(SDK.DOMModel.DOMModel);
15+
assert.exists(domModel);
16+
17+
return SDK.DOMModel.DOMNode.create(domModel, null, false, {
18+
nodeId,
19+
backendNodeId: 2 as Protocol.DOM.BackendNodeId,
20+
nodeType: Node.ELEMENT_NODE,
21+
nodeName: 'div',
22+
localName: 'div',
23+
nodeValue: '',
24+
});
25+
}
26+
27+
describeWithMockConnection('ComputedStyleModel', () => {
28+
let target: SDK.Target.Target;
29+
let computedStyleModel: Elements.ComputedStyleModel.ComputedStyleModel;
30+
let trackComputedStyleUpdatesForNodeSpy: sinon.SinonSpy;
31+
32+
async function waitForTrackComputedStyleUpdatesForNodeCall(): Promise<void> {
33+
if (trackComputedStyleUpdatesForNodeSpy.getCalls().length > 0) {
34+
return;
35+
}
36+
37+
await new Promise<void>(resolve => {
38+
requestAnimationFrame(async () => {
39+
await waitForTrackComputedStyleUpdatesForNodeCall();
40+
resolve();
41+
});
42+
});
43+
}
44+
45+
beforeEach(() => {
46+
stubNoopSettings();
47+
target = createTarget();
48+
const cssModel = target.model(SDK.CSSModel.CSSModel);
49+
50+
UI.Context.Context.instance().setFlavor(Elements.StylesSidebarPane.StylesSidebarPane, null);
51+
UI.Context.Context.instance().setFlavor(Elements.ComputedStyleWidget.ComputedStyleWidget, null);
52+
UI.Context.Context.instance().setFlavor(SDK.DOMModel.DOMNode, null);
53+
54+
sinon.stub(Elements.ComputedStyleModel.ComputedStyleModel.prototype, 'cssModel').returns(cssModel);
55+
trackComputedStyleUpdatesForNodeSpy =
56+
sinon.spy(SDK.CSSModel.CSSModel.prototype, 'trackComputedStyleUpdatesForNode');
57+
computedStyleModel = new Elements.ComputedStyleModel.ComputedStyleModel();
58+
});
59+
60+
afterEach(() => {
61+
computedStyleModel.dispose();
62+
trackComputedStyleUpdatesForNodeSpy.restore();
63+
});
64+
65+
it('should track computed style updates when computed widget is shown', async () => {
66+
UI.Context.Context.instance().setFlavor(
67+
SDK.DOMModel.DOMNode, createNode(target, {nodeId: 1 as Protocol.DOM.NodeId}));
68+
UI.Context.Context.instance().setFlavor(
69+
Elements.ComputedStyleWidget.ComputedStyleWidget,
70+
sinon.createStubInstance(Elements.ComputedStyleWidget.ComputedStyleWidget));
71+
72+
await waitForTrackComputedStyleUpdatesForNodeCall();
73+
74+
sinon.assert.calledWith(trackComputedStyleUpdatesForNodeSpy, 1);
75+
});
76+
77+
it('should track computed style updates when styles tab is shown and DevToolsAnimationStylesInStylesTab is enabled',
78+
async () => {
79+
const hostConfigStub = getGetHostConfigStub({
80+
devToolsAnimationStylesInStylesTab: {
81+
enabled: true,
82+
},
83+
});
84+
UI.Context.Context.instance().setFlavor(
85+
SDK.DOMModel.DOMNode, createNode(target, {nodeId: 1 as Protocol.DOM.NodeId}));
86+
UI.Context.Context.instance().setFlavor(
87+
Elements.StylesSidebarPane.StylesSidebarPane,
88+
sinon.createStubInstance(Elements.StylesSidebarPane.StylesSidebarPane));
89+
90+
await waitForTrackComputedStyleUpdatesForNodeCall();
91+
92+
sinon.assert.calledWith(trackComputedStyleUpdatesForNodeSpy, 1);
93+
hostConfigStub.restore();
94+
});
95+
96+
it('should track computed style updates when the node is changed', async () => {
97+
UI.Context.Context.instance().setFlavor(
98+
SDK.DOMModel.DOMNode, createNode(target, {nodeId: 1 as Protocol.DOM.NodeId}));
99+
UI.Context.Context.instance().setFlavor(
100+
Elements.ComputedStyleWidget.ComputedStyleWidget,
101+
sinon.createStubInstance(Elements.ComputedStyleWidget.ComputedStyleWidget));
102+
103+
await waitForTrackComputedStyleUpdatesForNodeCall();
104+
sinon.assert.calledWith(trackComputedStyleUpdatesForNodeSpy, 1);
105+
trackComputedStyleUpdatesForNodeSpy.resetHistory();
106+
107+
UI.Context.Context.instance().setFlavor(
108+
SDK.DOMModel.DOMNode, createNode(target, {nodeId: 2 as Protocol.DOM.NodeId}));
109+
await waitForTrackComputedStyleUpdatesForNodeCall();
110+
111+
sinon.assert.calledWith(trackComputedStyleUpdatesForNodeSpy, 2);
112+
});
113+
114+
it('should stop tracking when computed widget is hidden', async () => {
115+
UI.Context.Context.instance().setFlavor(
116+
SDK.DOMModel.DOMNode, createNode(target, {nodeId: 1 as Protocol.DOM.NodeId}));
117+
UI.Context.Context.instance().setFlavor(
118+
Elements.ComputedStyleWidget.ComputedStyleWidget,
119+
sinon.createStubInstance(Elements.ComputedStyleWidget.ComputedStyleWidget));
120+
121+
await waitForTrackComputedStyleUpdatesForNodeCall();
122+
sinon.assert.calledWith(trackComputedStyleUpdatesForNodeSpy, 1);
123+
trackComputedStyleUpdatesForNodeSpy.resetHistory();
124+
125+
UI.Context.Context.instance().setFlavor(Elements.ComputedStyleWidget.ComputedStyleWidget, null);
126+
await waitForTrackComputedStyleUpdatesForNodeCall();
127+
128+
sinon.assert.calledWith(trackComputedStyleUpdatesForNodeSpy, undefined);
129+
});
130+
131+
it('should stop tracking when computed widget is hidden and styles tab is shown but the flag is not enabled',
132+
async () => {
133+
const hostConfigStub = getGetHostConfigStub({
134+
devToolsAnimationStylesInStylesTab: {
135+
enabled: false,
136+
},
137+
});
138+
UI.Context.Context.instance().setFlavor(
139+
SDK.DOMModel.DOMNode, createNode(target, {nodeId: 1 as Protocol.DOM.NodeId}));
140+
UI.Context.Context.instance().setFlavor(
141+
Elements.ComputedStyleWidget.ComputedStyleWidget,
142+
sinon.createStubInstance(Elements.ComputedStyleWidget.ComputedStyleWidget));
143+
144+
await waitForTrackComputedStyleUpdatesForNodeCall();
145+
sinon.assert.calledWith(trackComputedStyleUpdatesForNodeSpy, 1);
146+
trackComputedStyleUpdatesForNodeSpy.resetHistory();
147+
148+
UI.Context.Context.instance().setFlavor(Elements.ComputedStyleWidget.ComputedStyleWidget, null);
149+
UI.Context.Context.instance().setFlavor(
150+
Elements.StylesSidebarPane.StylesSidebarPane,
151+
sinon.createStubInstance(Elements.StylesSidebarPane.StylesSidebarPane));
152+
await waitForTrackComputedStyleUpdatesForNodeCall();
153+
154+
sinon.assert.calledWith(trackComputedStyleUpdatesForNodeSpy, undefined);
155+
hostConfigStub.restore();
156+
});
157+
});

front_end/panels/elements/ComputedStyleModel.ts

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,36 @@ import * as Common from '../../core/common/common.js';
66
import * as SDK from '../../core/sdk/sdk.js';
77
import * as UI from '../../ui/legacy/legacy.js';
88

9+
import {ComputedStyleWidget} from './ComputedStyleWidget.js';
10+
import {StylesSidebarPane} from './StylesSidebarPane.js';
11+
912
export class ComputedStyleModel extends Common.ObjectWrapper.ObjectWrapper<EventTypes> {
1013
private nodeInternal: SDK.DOMModel.DOMNode|null;
1114
private cssModelInternal: SDK.CSSModel.CSSModel|null;
1215
private eventListeners: Common.EventTarget.EventDescriptor[];
1316
private frameResizedTimer?: number;
1417
private computedStylePromise?: Promise<ComputedStyle|null>;
18+
private currentTrackedNodeId?: number;
19+
1520
constructor() {
1621
super();
1722
this.cssModelInternal = null;
1823
this.eventListeners = [];
1924
this.nodeInternal = UI.Context.Context.instance().flavor(SDK.DOMModel.DOMNode);
25+
2026
UI.Context.Context.instance().addFlavorChangeListener(SDK.DOMModel.DOMNode, this.onNodeChanged, this);
27+
UI.Context.Context.instance().addFlavorChangeListener(
28+
StylesSidebarPane, this.evaluateTrackingComputedStyleUpdatesForNode, this);
29+
UI.Context.Context.instance().addFlavorChangeListener(
30+
ComputedStyleWidget, this.evaluateTrackingComputedStyleUpdatesForNode, this);
31+
}
32+
33+
dispose(): void {
34+
UI.Context.Context.instance().removeFlavorChangeListener(SDK.DOMModel.DOMNode, this.onNodeChanged, this);
35+
UI.Context.Context.instance().removeFlavorChangeListener(
36+
StylesSidebarPane, this.evaluateTrackingComputedStyleUpdatesForNode, this);
37+
UI.Context.Context.instance().removeFlavorChangeListener(
38+
ComputedStyleWidget, this.evaluateTrackingComputedStyleUpdatesForNode, this);
2139
}
2240

2341
node(): SDK.DOMModel.DOMNode|null {
@@ -28,10 +46,47 @@ export class ComputedStyleModel extends Common.ObjectWrapper.ObjectWrapper<Event
2846
return this.cssModelInternal && this.cssModelInternal.isEnabled() ? this.cssModelInternal : null;
2947
}
3048

49+
// This is a debounced method because the user might be navigated from Styles tab to Computed Style tab and vice versa.
50+
// For that case, we want to only run this function once.
51+
private evaluateTrackingComputedStyleUpdatesForNode = Common.Debouncer.debounce((): void => {
52+
if (!this.nodeInternal) {
53+
// There isn't a node selected now, so let's stop tracking computed style updates for the previously tracked node.
54+
if (this.currentTrackedNodeId) {
55+
void this.cssModel()?.trackComputedStyleUpdatesForNode(undefined);
56+
this.currentTrackedNodeId = undefined;
57+
}
58+
return;
59+
}
60+
61+
const hostConfig = Common.Settings.Settings.instance().getHostConfig();
62+
const isComputedStyleWidgetVisible = Boolean(UI.Context.Context.instance().flavor(ComputedStyleWidget));
63+
const isStylesTabVisible = Boolean(UI.Context.Context.instance().flavor(StylesSidebarPane));
64+
const shouldTrackComputedStyleUpdates =
65+
isComputedStyleWidgetVisible || (isStylesTabVisible && hostConfig.devToolsAnimationStylesInStylesTab?.enabled);
66+
// There is a selected node but not the computed style widget nor the styles tab is visible.
67+
// If there is a previously tracked node let's stop tracking computed style updates for that node.
68+
if (!shouldTrackComputedStyleUpdates) {
69+
if (this.currentTrackedNodeId) {
70+
void this.cssModel()?.trackComputedStyleUpdatesForNode(undefined);
71+
this.currentTrackedNodeId = undefined;
72+
}
73+
return;
74+
}
75+
76+
// Either computed style widget or styles tab is visible
77+
// if the currently tracked node id is not the same as the selected node
78+
// let's start tracking the currently selected node.
79+
if (this.currentTrackedNodeId !== this.nodeInternal.id) {
80+
void this.cssModel()?.trackComputedStyleUpdatesForNode(this.nodeInternal.id);
81+
this.currentTrackedNodeId = this.nodeInternal.id;
82+
}
83+
}, 100);
84+
3185
private onNodeChanged(event: Common.EventTarget.EventTargetEvent<SDK.DOMModel.DOMNode|null>): void {
3286
this.nodeInternal = event.data;
3387
this.updateModel(this.nodeInternal ? this.nodeInternal.domModel().cssModel() : null);
3488
this.onCSSModelChanged(null);
89+
this.evaluateTrackingComputedStyleUpdatesForNode();
3590
}
3691

3792
private updateModel(cssModel: SDK.CSSModel.CSSModel|null): void {
@@ -51,7 +106,7 @@ export class ComputedStyleModel extends Common.ObjectWrapper.ObjectWrapper<Event
51106
cssModel.addEventListener(SDK.CSSModel.Events.MediaQueryResultChanged, this.onCSSModelChanged, this),
52107
cssModel.addEventListener(SDK.CSSModel.Events.PseudoStateForced, this.onCSSModelChanged, this),
53108
cssModel.addEventListener(SDK.CSSModel.Events.ModelWasEnabled, this.onCSSModelChanged, this),
54-
cssModel.addEventListener(SDK.CSSModel.Events.ComputedStyleUpdated, this.onCSSModelChanged, this),
109+
cssModel.addEventListener(SDK.CSSModel.Events.ComputedStyleUpdated, this.onComputedStyleChanged, this),
55110
domModel.addEventListener(SDK.DOMModel.Events.DOMMutated, this.onDOMModelChanged, this),
56111
resourceTreeModel.addEventListener(SDK.ResourceTreeModel.Events.FrameResized, this.onFrameResized, this),
57112
];
@@ -60,13 +115,19 @@ export class ComputedStyleModel extends Common.ObjectWrapper.ObjectWrapper<Event
60115

61116
private onCSSModelChanged(event: Common.EventTarget.EventTargetEvent<CSSModelChangedEvent>|null): void {
62117
delete this.computedStylePromise;
118+
this.dispatchEventToListeners(Events.CSS_MODEL_CHANGED, event?.data ?? null);
119+
}
120+
121+
private onComputedStyleChanged(event: Common.EventTarget.EventTargetEvent<SDK.CSSModel.ComputedStyleUpdatedEvent>|
122+
null): void {
123+
delete this.computedStylePromise;
63124
// If the event contains `nodeId` and that's not the same as this node's id
64125
// we don't emit the COMPUTED_STYLE_CHANGED event.
65126
if (event?.data && 'nodeId' in event.data && event.data.nodeId !== this.nodeInternal?.id) {
66127
return;
67128
}
68129

69-
this.dispatchEventToListeners(Events.CSS_MODEL_CHANGED, event?.data ?? null);
130+
this.dispatchEventToListeners(Events.COMPUTED_STYLE_CHANGED);
70131
}
71132

72133
private onDOMModelChanged(event: Common.EventTarget.EventTargetEvent<SDK.DOMModel.DOMNode>): void {
@@ -129,13 +190,15 @@ export class ComputedStyleModel extends Common.ObjectWrapper.ObjectWrapper<Event
129190

130191
export const enum Events {
131192
CSS_MODEL_CHANGED = 'CSSModelChanged',
193+
COMPUTED_STYLE_CHANGED = 'ComputedStyleChanged',
132194
}
133195

134196
export type CSSModelChangedEvent = SDK.CSSStyleSheetHeader.CSSStyleSheetHeader|SDK.CSSModel.StyleSheetChangedEvent|
135-
SDK.CSSModel.PseudoStateForcedEvent|SDK.CSSModel.ComputedStyleUpdatedEvent|null|void;
197+
SDK.CSSModel.PseudoStateForcedEvent|null|void;
136198

137199
export type EventTypes = {
138200
[Events.CSS_MODEL_CHANGED]: CSSModelChangedEvent,
201+
[Events.COMPUTED_STYLE_CHANGED]: void,
139202
};
140203

141204
export class ComputedStyle {

front_end/panels/elements/ComputedStyleWidget.test.ts

Lines changed: 0 additions & 74 deletions
This file was deleted.

0 commit comments

Comments
 (0)