blob: e714c14b055fb242aa1ca74e9686811bf14bfd9e [file] [log] [blame] [view]
Danil Somsikovd884ae72025-02-12 07:56:131# Chromium DevTools UI Engineering
2
3## Objective and scope
4
5This document defines how to build Chromium DevTools UI. It aims at improving consistency, maintainability and extensibility of the current code
6
7**Consistency** here means to have one way to do one thing, in the context of this doc to have a single reusable component per repeated task.
8
9**Maintainability** is addressed here through the [separation of concerns](https://en.wikipedia.org/wiki/Separation_of_concerns) while avoiding unnecessary indirection.
10
11**Extensibility** requires the ease of understanding and imitation, i.e. being able to take an existing code, understand and use it as an example to solve another problem.
12
13Additionally, all the changes need to be applicable to the existing code **point-wise** instead of requiring extensive rewriting.
14
15## Reusable web components
16
17Common UI primitives need to be implemented as web components. Examples include buttons, checkboxs, or data grids. Web components should not be used solely for encapsulation, we should not have single-use web components.
18
19Not all reusable code should be a web component: in the Application panel we have several similar views showing key-value pairs in a datagrid with a preview sidebar. Making this a truly reusable component will lead to an unjustifiable complexity. Instead we should have extracted a base class or helpers implementing the common functionality.
20
21In implementation we should prefer wrapping existing code under ui/legacy over new implementation. The former is the most feature-rich (including less obvious aspects like a11y) and has stood the test of time.
22
23We should however strive to expose a HTML-native API: e.g. toolbar doesnt need an `items` setter if its child HTML elements could define its content. Even the data grid doesnt need to expose data grid nodes, when `<tr>` and `<td>`s are enough.
24
25## Model-View-Presenter architecture
26
27We should strictly separate business logic, from UI logic and presentation. This means that most of the UI code should be centered around a presenter (subclass of a `UI.Widget`) that gets a view function injected. All the logic that is not related to the DevTools UI (i.e. that would stay the same if we rewrite DevTools as a command-line tool), should belong to the model layer.
28
29The presenter code should make no assumptions about the details of model or view code. It should not care what race conditions CDP exposes or how many layers of `<div class=”wrapper”>` does the markup have. However, for simplicity, the injected view function should have a default implementation inlined with a presenter (see an example below).
30
31In tests, we use a simple stub as a view function, which allows us to test the presenter logic without any DOM manipulation. To test the view function itself we should use screenshot and e2e tests.
32
33## Declarative and orchestrated DOM updates
34
35We should no longer use imperative API to update DOM. Instead we rely on orchestrated rendering of lit-html templates. The view function described above should be a call to lit-html `render`. The view function should be called from `UI.Widget`s `performUpdate` method, which by default is scheduled using `requestAnimationFrame`.
36
37To embed another presenter (`UI.Widget`) in the lit-html template, use `<devtools-widget .widgetConfig=${widgetConfig(<class>, {foo: 1, bar: 2})}`
38
39This will instantiate a `Widget` class with the web component as its `element` and, optionally, will set the properties provided in the second parameter. The widget wont be re-instantiated on the subsequent template renders, but the properties would be updated. For this to work, the widget needs to accept `HTMLElement` as a sole constructor parameter and properties need to be public members or setters.
40
Simon Zündafade152025-05-06 11:13:3841For backwards compatibility, the first argument to `widgetConfig` can also be a factory function: `<devtools-widget .widgetConfig=${widgetConfig(element => new MyWidget(foo, bar, element))}>`. Similar to the class constructor version, `element` is the actual `<devtools-widget>` so the following two invocations of `widgetConfig` are equivalent: `widgetConfig(MyWidget)` and `widgetConfig(element = new MyWidget(element))`.
42
Ergun Erdogmusa9f216e2025-07-15 13:42:3443## Styling
44To prevent style conflicts in widgets without relying on shadow DOM, we use the CSS [`@scope`](https://developer.mozilla.org/en-US/docs/Web/CSS/@scope) at-rule for style encapsulation. This ensures that styles defined for a widget do not leak out and affect other components.
45
46To simplify this process, a helper function, `UI.Widget.widgetScoped`, is provided. This function automatically wraps the given CSS rules in an `@scope to (devtools-widget)` block. The to (devtools-widget) part is crucial, as it establishes a "lower boundary," preventing the styles from cascading into any nested child widgets (which are rendered as <devtools-widget> elements).
47
48```ts
49import {html} from 'lit-html';
50import * as UI from '../../ui/legacy/legacy.js';
51import myWidgetStyles from './myWidget.css.js';
52
53render(html`
54 <style>
55 ${UI.Widget.widgetScoped(myWidgetStyles)}
56 </style>
57 <div class="container">
58 <h3 class="title">My Widget</h3>
59 ...
60 <devtools-widget .widgetConfig=${widgetConfig(NestedWidget)}></devtools-widget>
61 </div>
62`, this.element);
63```
64
65In this example, styles like `.title` will apply within the parent widget but will not apply to any elements inside the nested `<devtools-widget>`.
66
Danil Somsikovd884ae72025-02-12 07:56:1367## Examples
68
69```html
Alex Rudenko8d6d6c22025-02-27 15:36:4070<devtools-widget .widgetConfig=${widgetConfig(ElementsPanel)}>
Wolfgang Beyerc73e1302025-05-14 15:57:1471 <devtools-split-view>
Alex Rudenko8d6d6c22025-02-27 15:36:4072 <devtools-widget slot="main" .widgetConfig=${widgetConfig(ElementsTree)}></devtools-widget>
Mathias Bynensae705ff2025-02-12 09:51:2373 <devtools-tab-pane slot="sidebar">
Alex Rudenko8d6d6c22025-02-27 15:36:4074 <devtools-widget .widgetConfig=${widgetConfig(StylesPane, {element: input.element})}></devtools-widget>
75 <devtools-widget .widgetConfig=${widgetConfig(ComputedPane, {element: input.element})}></devtools-widget>
Mathias Bynensae705ff2025-02-12 09:51:2376 ...
Danil Somsikovd884ae72025-02-12 07:56:1377 </devtools-tab-pane>
Wolfgang Beyerc73e1302025-05-14 15:57:1478 </devtools-split-view>
Danil Somsikovd884ae72025-02-12 07:56:1379</devtools-widget>
80```
81
82```ts
83class StylesPane extends UI.Widget {
84 constructor(element, view = (input, output, target) => {
85 render(html`
Alex Rudenko8d6d6c22025-02-27 15:36:4086 <devtools-widget .widgetConfig=${widgetConfig(MetricsPane, {element: input.element})}>
Mathias Bynensae705ff2025-02-12 09:51:2387 </devtools-widget>
Danil Somsikovd884ae72025-02-12 07:56:1388 <devtools-toolbar>
Mathias Bynensae705ff2025-02-12 09:51:2389 <devtools-filter-input @change=${input.onFilter}></devtools-filter-input>
90 <devtools-checkbox @change=${input.onShowAll}>Show All</devtools-checkbox>
91 <devtools-checkbox @change=${input.onGroup}>Group</devtools-checkbox>
Danil Somsikovd884ae72025-02-12 07:56:1392 </devtools-toolbar>
93 <devtools-tree-outline>
94 ${input.properties.map(p => html`<li>
95 <dt>${p.key}</dt><dd>${renderValue(p.value)}</dd>
96 <ol>${p.subproperties.map(...)}
97 </li>`)}
98 </devtools-tree-outline>`
99 }
100}
101```
102
103[https://source.chromium.org/chromium/chromium/src/+/main:third\_party/devtools-frontend/src/front\_end/panels/protocol\_monitor/ProtocolMonitor.ts;l=197](https://source.chromium.org/chromium/chromium/src/+/main:third_party/devtools-frontend/src/front_end/panels/protocol_monitor/ProtocolMonitor.ts;l=197)
104
105[https://source.chromium.org/chromium/chromium/src/+/main:third\_party/devtools-frontend/src/front\_end/panels/developer\_resources/DeveloperResourcesListView.ts;l=86](https://source.chromium.org/chromium/chromium/src/+/main:third_party/devtools-frontend/src/front_end/panels/developer_resources/DeveloperResourcesListView.ts;l=86)
106
107[https://source.chromium.org/chromium/chromium/src/+/main:third\_party/devtools-frontend/src/front\_end/panels/timeline/TimelineSelectorStatsView.ts;l=113](https://source.chromium.org/chromium/chromium/src/+/main:third_party/devtools-frontend/src/front_end/panels/timeline/TimelineSelectorStatsView.ts;l=113)
Alex Rudenkoc6499cc2025-02-17 09:08:25108
109
110### Unit tests
111
Alex Rudenkoe5c358e2025-02-17 11:31:52112When testing presenters, rely on observable effects such as view updates or model calls.
113
114#### View stubbing
Alex Rudenkoc6499cc2025-02-17 09:08:25115
116```ts
Alex Rudenko6f29fce2025-03-05 16:15:51117// ✅ recommended: stub the view function using createViewFunctionStub.
118import {createViewFunctionStub} from './ViewFunctionHelpers.js';
119const view = createViewFunctionStub(Presenter);
Alex Rudenkoc6499cc2025-02-17 09:08:25120const presenter = new Presenter(view);
121
122// ✅ recommended: expect a view stub call in response to presenter behavior.
Alex Rudenkoc6499cc2025-02-17 09:08:25123present.show();
Alex Rudenko6f29fce2025-03-05 16:15:51124const input = await view.nextInput;
Alex Rudenkoc6499cc2025-02-17 09:08:25125
126// ✅ recommended: expect a view stub call in response to an event from the view.
Alex Rudenko6f29fce2025-03-05 16:15:51127input.onEvent();
128assert.deepStrictEqual(await view.nextInput, {});
Alex Rudenkoc6499cc2025-02-17 09:08:25129
130// ❌ not recommended: Widget.updateComplete only reports a current view update
131// operation status and might create flakiness depending on doSomething() implementation.
132presenter.doSomething();
133await presenter.updateComplete;
134assert.deepStrictEqual(view.lastCall.args[0], {});
135
136// ❌ not recommended: awaiting for the present logic to finish might
137// not account for async or throttled view updates.
138await presenter.doSomething();
Alex Rudenkoe5c358e2025-02-17 11:31:52139// ❌ not recommended: it is easy for such assertions to
140// rely on the data not caused by the action being tested.
141sinon.assert.calledWith(view, sinon.match({ data: 'smth' }));
142```
143
144#### Model stubbing
145
146```ts
147// ✅ recommended: stub models that the presenter relies on.
148// Note there are many good ways to stub/mock models with sinon
149// depending on the use case and existing model code structure.
150const cssModel = sinon.createStubInstance(SDK.CSSModel.CSSModel);
151
152const presenter = new Presenter();
153// ✅ recommended: expect model calls as the result of invoking
Alex Rudenko6f29fce2025-03-05 16:15:51154// presenter's logic.
Alex Rudenkoe5c358e2025-02-17 11:31:52155const modelCall = expectCall(cssModel.headersForSourceURL, {
156 fakeFn: () => {
157 return false,
158 },
159});
160// ✅ recommended: expect view calls to result in output based
161// on the mocked model.
Alex Rudenko6f29fce2025-03-05 16:15:51162const viewCall = view.nextInput;
Alex Rudenkoe5c358e2025-02-17 11:31:52163
164presenter.doSomething();
165
166// ✅ recommended: assert arguments provided to model calls.
167const [url] = await modelCall;
168assert.strictEqual(url, '...');
169
Alex Rudenko6f29fce2025-03-05 16:15:51170assert.deepStrictEqual((await viewCall).headersForSourceURL, [{...}]);
Alex Rudenkoe5c358e2025-02-17 11:31:52171
172// ❌ not recommended: mocking CDP responses to make the models behave in a certain way
Alex Rudenko6f29fce2025-03-05 16:15:51173// while testing a presenter is fragile.
Alex Rudenkoe5c358e2025-02-17 11:31:52174setMockConnectionResponseHandler('CSS.getHeaders', () => ({}));
175const presenter = new Presenter();
176presenter.doSomething();
Simon Zündafade152025-05-06 11:13:38177```