chrome-devtools-frontend 1.0.1621678 → 1.0.1622369

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,171 @@
1
+ ---
2
+ name: foundation-test-migration
3
+ description: Migrating unit tests to foundation unit tests using TestUniverse and devtools_foundation_module. Use when moving tests away from DOM-heavy helpers like describeWithEnvironment or describeWithMockConnection.
4
+ ---
5
+
6
+ # Foundation Test Migration
7
+
8
+ This skill provides guidance on migrating DevTools unit tests to the "foundation" pattern, which is lighter, avoids global singletons, and is compatible with both Node and Browser runtimes (Isomorphic).
9
+
10
+ ## Core Concepts
11
+
12
+ ### devtools_foundation_module (BUILD.gn)
13
+ Use this template for modules that should be platform-agnostic.
14
+ - **Enforcement**: It type-checks the code against both Browser and Node APIs.
15
+ - **Constraint**: Avoid direct DOM access (like `FileReader` or layout metrics) or heavy DevTools dependencies. Use `Universe` to access services.
16
+
17
+ ### TestUniverse
18
+ `TestUniverse` is the preferred way to setup a DevTools-like environment for tests without global singletons.
19
+ - **Lazy**: Dependencies (targetManager, settings, workspace, etc.) are only created when accessed via getters.
20
+ - **Scoped**: Does not install instances as globals (avoids `Common.Settings.Settings.instance()`).
21
+ - **Explicit**: Uses `DevToolsContext` to manage dependencies.
22
+
23
+ ## Migration Guide
24
+
25
+ ### 1. Replace Heavy Helpers
26
+ Avoid `describeWithEnvironment` or `describeWithMockConnection`.
27
+
28
+ Instead, use standard `describe` and initialize environment hooks at the top-level `describe` block. **Crucial:** Without `setupRuntimeHooks`, tests creating SDK models will crash due to uninitialized experiments (e.g. `capture-node-creation-stacks`).
29
+
30
+ ```ts
31
+ import {setupLocaleHooks} from '../../testing/LocaleHelpers.js';
32
+ import {setupSettingsHooks} from '../../testing/SettingsHelpers.js';
33
+ import {setupRuntimeHooks} from '../../testing/RuntimeHelpers.js';
34
+ import {TestUniverse} from '../../testing/TestUniverse.js';
35
+
36
+ describe('MyComponent', () => {
37
+ setupLocaleHooks();
38
+ setupSettingsHooks();
39
+ setupRuntimeHooks();
40
+
41
+ let universe: TestUniverse;
42
+
43
+ beforeEach(() => {
44
+ universe = new TestUniverse();
45
+ });
46
+ });
47
+ ```
48
+
49
+ ### 2. Access Dependencies via Universe
50
+ Instead of using `SDK.TargetManager.TargetManager.instance()`, use `universe.targetManager`. Use `universe.createTarget()` instead of the global `createTarget`.
51
+
52
+ ```ts
53
+ // OLD
54
+ const target = createTarget();
55
+ const targetManager = SDK.TargetManager.TargetManager.instance();
56
+
57
+ // NEW
58
+ const target = universe.createTarget({url: urlString`http://example.com/`});
59
+ const targetManager = universe.targetManager;
60
+ ```
61
+
62
+ #### Mocking CDP Traffic
63
+ If the test used `describeWithMockConnection` and global `setMockConnectionResponseHandler` to stub CDP traffic, you can migrate this by passing a `MockCDPConnection` to `universe.createTarget()`. This allows stubbing out CDP traffic scoped to a specific target tree rather than globally.
64
+
65
+ ```ts
66
+ import {MockCDPConnection} from '../../testing/MockCDPConnection.js';
67
+
68
+ const cdpConnection = new MockCDPConnection([
69
+ {
70
+ method: 'Network.getResponseBody',
71
+ response: () => ({body: 'mocked body', base64Encoded: false}),
72
+ }
73
+ ]);
74
+
75
+ const target = universe.createTarget({connection: cdpConnection});
76
+ ```
77
+
78
+ > [!TIP]
79
+ > **Legacy Target URLs**: `EnvironmentHelpers.createTarget()` defaults to `http://example.com/`. `TestUniverse.createTarget()` defaults to `about:blank`. If your test asserts against specific URLs, remember to pass the URL explicitly.
80
+
81
+ ### 3. Dealing with Legacy Singletons & Helpers
82
+
83
+ For large integration tests, you may encounter code that strictly calls `SomeModule.instance()` or uses complex legacy helpers (like `createWorkspaceProject`).
84
+
85
+ **Do not use `setUpEnvironment()`** as it will create disconnected singletons. Instead, wire the singletons to your `TestUniverse` and stub the globals to bridge legacy helpers:
86
+
87
+ ```ts
88
+ beforeEach(async () => {
89
+ universe = new TestUniverse();
90
+ const {targetManager, workspace, settings} = universe;
91
+
92
+ // 1. Stub globals so legacy helpers use TestUniverse components
93
+ sinon.stub(Workspace.Workspace.WorkspaceImpl, 'instance').returns(workspace);
94
+ sinon.stub(SDK.TargetManager.TargetManager, 'instance').returns(targetManager);
95
+ sinon.stub(Common.Settings.Settings, 'instance').returns(settings);
96
+
97
+ // 2. Initialize interdependent singletons in the correct order
98
+ SDK.NetworkManager.MultitargetNetworkManager.instance({forceNew: true, targetManager});
99
+
100
+ // 3. Now safe to use legacy helpers that rely on the above instances
101
+ await createWorkspaceProject(urlString`file:///path/to/overrides`, [...]);
102
+ });
103
+ ```
104
+
105
+ ## Pitfalls & Troubleshooting
106
+
107
+ ### Strict Equality in Protocol Responses (`assert.deepEqual`)
108
+ Protocol requests/responses dynamically generated by Chrome (like Network conditions) can vary slightly (e.g., adding `connectionType`, `urlPattern`).
109
+ - **Problem**: `assert.deepEqual(rules, [{...}])` will flake if unrequested fields are present.
110
+ - **Solution**: Use `sinon.spy()` for handlers and assert with `sinon.assert.calledOnceWithMatch`:
111
+
112
+ ```ts
113
+ const emulateSpy = sinon.spy();
114
+ connection.setHandler('Network.emulateNetworkConditionsByRule', request => {
115
+ emulateSpy(request);
116
+ return {result: {ruleIds: []}};
117
+ });
118
+
119
+ // Matches only the fields you care about, ignoring extra protocol fields
120
+ sinon.assert.calledOnceWithMatch(emulateSpy, {
121
+ offline: false,
122
+ matchedNetworkConditions: [sinon.match({ downloadThroughput: 1000 })],
123
+ });
124
+ ```
125
+
126
+ ### DOM Globals (`ReferenceError: FileReader is not defined`)
127
+ Foundation tests run in Node.js where `window`, `FileReader`, and certain DOM string encodings don't exist.
128
+ - **Fix**: Use isomorphic equivalents (e.g. `btoa()`, `Uint8Array`).
129
+ - **Last Resort**: Skip the test with `it.skip()` if it explicitly tests browser quirks (e.g. legacy charset decoding).
130
+
131
+ ### Initialization Order Lockups
132
+ If a test times out (5000ms exceeded), it is usually an unhandled promise caused by a missing singleton. Double-check the constructor of the failing manager to see which `instance()` it listens to, and ensure that dependent singleton was created *first*.
133
+
134
+ ## BUILD.gn Changes
135
+
136
+ When a module and its tests are ready, update `BUILD.gn`:
137
+
138
+ 1. Change `devtools_module` to `devtools_foundation_module` for both the module and its unittests.
139
+ 2. Ensure the tests are grouped under a `foundation_unittests` target in the parent `BUILD.gn`.
140
+
141
+ ```gn
142
+ # front_end/my_module/BUILD.gn
143
+
144
+ devtools_foundation_module("my_module") {
145
+ sources = [ "MyModule.ts" ]
146
+ deps = [ "../../core/common:bundle" ]
147
+ }
148
+
149
+ devtools_foundation_module("unittests") {
150
+ testonly = true
151
+ sources = [ "MyModule.test.ts" ]
152
+ deps = [
153
+ ":my_module",
154
+ "../../testing",
155
+ ]
156
+ }
157
+ ```
158
+
159
+ ## Verification
160
+
161
+ Foundation tests must pass in both Node.js and Browser runtimes.
162
+
163
+ ### Run in Node.js
164
+ ```bash
165
+ npm test -- front_end/core/sdk/NetworkManager.test.ts --node-unit-tests
166
+ ```
167
+
168
+ ### Run in Browser
169
+ ```bash
170
+ npm test -- front_end/core/sdk/NetworkManager.test.ts
171
+ ```
@@ -13,25 +13,16 @@ description: MANDATORY: Activate this skill ANY TIME you need to build the proje
13
13
 
14
14
  ## Building & compiling
15
15
 
16
- - Check for TypeScript or dependency issues in the build system by running `autoninja -C out/Default`.
17
-
18
- ## Fast builds
19
-
20
- - If the `out/Fast` or `out/fast-build` directory exists, this means that a build that does not execute TypeScript is available to you which greatly decreases build time.
21
- - To use the fast build for tests, pass the `--target=Fast` (adjust the value based on the name of the directory) argument to `npm run test`.
16
+ - Check for build issues by running `autoninja -C out/Default`.
22
17
 
23
18
  ## Linting
24
19
 
25
20
  - `npm run lint` will execute ESLint and StyleLint. It will report any violations and automatically fix them where possible.
26
21
  - To run the linter on a specific file or directory, you can run `npm run lint -- <PATH>` where `PATH` is a path to a file or directory. This will also automatically fix violations where possible.
27
22
 
28
- ## Presubmit
29
-
30
- - `git cl presubmit -u` will check if the current change is ready for upload. It will also format and lint the change.
31
-
32
23
  ## Best practices
33
24
 
34
25
  - Run tests often to verify your changes.
35
26
  - Prefer using a fast build, if it exists, to keep the feedback loop shorter.
36
- - Periodically compile with TypeScript to check for type errors.
27
+ - Periodically build to check for errors.
37
28
  - Run `git cl presubmit -u` at the end of your code changes.
@@ -36,8 +36,18 @@ export function decode(input: string): Uint8Array<ArrayBuffer> {
36
36
  * Note: if input can be very large (larger than the max string size), callers should
37
37
  * expect this to throw an error.
38
38
  */
39
- export function encode(input: BlobPart): Promise<string> {
40
- return new Promise((resolve, reject) => {
39
+ export async function encode(input: BlobPart): Promise<string> {
40
+ // Node.js environment (for foundation unit tests)
41
+ if (typeof FileReader === 'undefined') {
42
+ const blob = new Blob([input]);
43
+ const arrayBuffer = await blob.arrayBuffer();
44
+ // Use globalThis.Buffer to avoid TypeScript errors if Node types are not included.
45
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
46
+ return (globalThis as any).Buffer.from(arrayBuffer).toString('base64');
47
+ }
48
+
49
+ // Browser environment
50
+ return await new Promise((resolve, reject) => {
41
51
  const reader = new FileReader();
42
52
  reader.onerror = () => reject(new Error('failed to convert to base64: internal error'));
43
53
  reader.onload = () => {
@@ -64,10 +64,14 @@ function getLocaleFetchUrl(locale: Intl.UnicodeBCP47LocaleIdentifier, location:
64
64
  * fetched locally or remotely.
65
65
  */
66
66
  export async function fetchAndRegisterLocaleData(
67
- locale: Intl.UnicodeBCP47LocaleIdentifier, location = self.location.toString()): Promise<void> {
68
- const localeDataTextPromise = fetch(getLocaleFetchUrl(locale, location)).then(result => result.json());
69
- const timeoutPromise =
70
- new Promise<never>((_, reject) => window.setTimeout(() => reject(new Error('timed out fetching locale')), 5000));
67
+ locale: Intl.UnicodeBCP47LocaleIdentifier,
68
+ // Type issue with universal types.
69
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
70
+ location = (globalThis as any).location?.toString() ?? ''): Promise<void> {
71
+ const localeDataTextPromise =
72
+ fetch(getLocaleFetchUrl(locale, location)).then(result => result.json()) as Promise<I18n.I18n.LocalizedMessages>;
73
+ const timeoutPromise = new Promise<never>(
74
+ (_, reject) => globalThis.setTimeout(() => reject(new Error('timed out fetching locale')), 5000));
71
75
  const localeData = await Promise.race([timeoutPromise, localeDataTextPromise]);
72
76
  i18nInstance.registerLocaleData(locale, localeData);
73
77
  }
@@ -12,12 +12,27 @@ let runtimeInstance: Runtime|undefined;
12
12
  let isNode: boolean|undefined;
13
13
  let isTraceAppEntry: boolean|undefined;
14
14
 
15
+ interface Global {
16
+ location?: {
17
+ toString(): string,
18
+ pathname: string,
19
+ search: string,
20
+ };
21
+ navigator?: {
22
+ userAgent: string,
23
+ };
24
+ localStorage?: Storage;
25
+ self?: Global;
26
+ }
27
+
28
+ const globalObject = (globalThis as unknown as Global);
29
+
15
30
  /**
16
31
  * Returns the base URL (similar to `<base>`).
17
32
  * Used to resolve the relative URLs of any additional DevTools files (locale strings, etc) needed.
18
33
  * See: https://cs.chromium.org/remoteBase+f:devtools_window
19
34
  */
20
- export function getRemoteBase(location: string = self.location.toString()): {
35
+ export function getRemoteBase(location: string = globalObject.self?.location?.toString() ?? ''): {
21
36
  base: string,
22
37
  version: string,
23
38
  }|null {
@@ -36,7 +51,7 @@ export function getRemoteBase(location: string = self.location.toString()): {
36
51
  }
37
52
 
38
53
  export function getPathName(): string {
39
- return window.location.pathname;
54
+ return globalObject.location?.pathname ?? '';
40
55
  }
41
56
 
42
57
  export function isNodeEntry(pathname: string): boolean {
@@ -46,7 +61,7 @@ export function isNodeEntry(pathname: string): boolean {
46
61
 
47
62
  export const getChromeVersion = (): string => {
48
63
  const chromeRegex = /(?:^|\W)(?:Chrome|HeadlessChrome)\/(\S+)/;
49
- const chromeMatch = navigator.userAgent.match(chromeRegex);
64
+ const chromeMatch = globalObject.navigator?.userAgent?.match(chromeRegex);
50
65
  if (chromeMatch && chromeMatch.length > 1) {
51
66
  return chromeMatch[1];
52
67
  }
@@ -75,9 +90,8 @@ export class Runtime {
75
90
  static #queryParamsObject: URLSearchParams;
76
91
 
77
92
  static #getSearchParams(): URLSearchParams|null {
78
- // TODO(crbug.com/451502260): Find a more explicit way to support running in Node.js
79
- if (!Runtime.#queryParamsObject && 'location' in globalThis) {
80
- Runtime.#queryParamsObject = new URLSearchParams(location.search);
93
+ if (!Runtime.#queryParamsObject && globalObject.location) {
94
+ Runtime.#queryParamsObject = new URLSearchParams(globalObject.location.search);
81
95
  }
82
96
  return Runtime.#queryParamsObject;
83
97
  }
@@ -297,13 +311,13 @@ export class ExperimentsSupport {
297
311
  }
298
312
  }
299
313
 
300
- /** Manages the 'experiments' dictionary in self.localStorage */
314
+ /** Manages the 'experiments' dictionary in globalThis.localStorage */
301
315
  class ExperimentStorage {
302
316
  readonly #experiments: Record<string, boolean|undefined> = {};
303
317
 
304
318
  constructor() {
305
319
  try {
306
- const storedExperiments = self.localStorage?.getItem('experiments');
320
+ const storedExperiments = globalObject.localStorage?.getItem('experiments');
307
321
  if (storedExperiments) {
308
322
  this.#experiments = JSON.parse(storedExperiments);
309
323
  }
@@ -337,7 +351,7 @@ class ExperimentStorage {
337
351
  }
338
352
 
339
353
  #syncToLocalStorage(): void {
340
- self.localStorage?.setItem('experiments', JSON.stringify(this.#experiments));
354
+ globalObject.localStorage?.setItem('experiments', JSON.stringify(this.#experiments));
341
355
  }
342
356
  }
343
357
 
@@ -480,6 +494,10 @@ export interface HostConfigAiAssistanceAccessibilityAgent {
480
494
  enabled: boolean;
481
495
  }
482
496
 
497
+ export interface HostConfigAiAssistanceStorageAgent {
498
+ enabled: boolean;
499
+ }
500
+
483
501
  export interface HostConfigAiCodeCompletion {
484
502
  modelId: string;
485
503
  temperature: number;
@@ -638,6 +656,7 @@ export type HostConfig = Platform.TypeScriptUtilities.RecursivePartial<{
638
656
  devToolsAiAssistanceFileAgent: HostConfigAiAssistanceFileAgent,
639
657
  devToolsAiAssistancePerformanceAgent: HostConfigAiAssistancePerformanceAgent,
640
658
  devToolsAiAssistanceAccessibilityAgent: HostConfigAiAssistanceAccessibilityAgent,
659
+ devToolsAiAssistanceStorageAgent: HostConfigAiAssistanceStorageAgent,
641
660
  devToolsAiAssistanceV2: HostConfigAiAssistanceV2,
642
661
  devToolsAiCodeCompletion: HostConfigAiCodeCompletion,
643
662
  devToolsAiCodeGeneration: HostConfigAiCodeGeneration,
@@ -2,7 +2,7 @@
2
2
  // Use of this source code is governed by a BSD-style license that can be
3
3
  // found in the LICENSE file.
4
4
 
5
- import '../../core/dom_extension/dom_extension.js';
5
+ import '../../ui/dom_extension/dom_extension.js';
6
6
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
7
7
  // @ts-ignore: tsc 6.0 does not support side-effect imports without a type definition.
8
8
  // We cannot use `@ts-expect-error` here because the import is correctly resolved
@@ -7,7 +7,7 @@
7
7
  // We cannot use `@ts-expect-error` here because the import is correctly resolved
8
8
  // when bundling the application (which doesn't error) and only errors in unbundled builds.
9
9
  import '../../Images/Images.js';
10
- import '../../core/dom_extension/dom_extension.js';
10
+ import '../../ui/dom_extension/dom_extension.js';
11
11
  import '../../panels/sources/sources-meta.js';
12
12
  import '../../panels/profiler/profiler-meta.js';
13
13
  import '../../panels/console/console-meta.js';
@@ -222,10 +222,11 @@ enum ScorePriority {
222
222
  DEFAULT = 1,
223
223
  }
224
224
 
225
- // TODO(crbug.com/503296282): Remove this when we add support for all insights
226
225
  const SUPPORTED_INSIGHT_WIDGETS = new Set<Trace.Insights.Types.InsightKeys>([
227
226
  Trace.Insights.Types.InsightKeys.LCP_BREAKDOWN,
228
227
  Trace.Insights.Types.InsightKeys.RENDER_BLOCKING,
228
+ Trace.Insights.Types.InsightKeys.LCP_DISCOVERY,
229
+ Trace.Insights.Types.InsightKeys.CLS_CULPRITS,
229
230
  ]);
230
231
 
231
232
  export class PerformanceTraceContext extends ConversationContext<AgentFocus> {
@@ -20,6 +20,7 @@ import * as AiAssistanceModel from '../../../models/ai_assistance/ai_assistance.
20
20
  import * as ComputedStyle from '../../../models/computed_style/computed_style.js';
21
21
  import * as Trace from '../../../models/trace/trace.js';
22
22
  import * as PanelsCommon from '../../../panels/common/common.js';
23
+ import * as TraceBounds from '../../../services/trace_bounds/trace_bounds.js';
23
24
  import * as Marked from '../../../third_party/marked/marked.js';
24
25
  import * as Buttons from '../../../ui/components/buttons/buttons.js';
25
26
  import * as Input from '../../../ui/components/input/input.js';
@@ -31,6 +32,7 @@ import * as Lit from '../../../ui/lit/lit.js';
31
32
  import * as VisualLogging from '../../../ui/visual_logging/visual_logging.js';
32
33
  import * as Elements from '../../elements/elements.js';
33
34
  import * as TimelineComponents from '../../timeline/components/components.js';
35
+ import type {BaseInsightComponent} from '../../timeline/components/insights/BaseInsightComponent.js';
34
36
  import * as TimelineInsights from '../../timeline/components/insights/insights.js';
35
37
  import * as Timeline from '../../timeline/timeline.js';
36
38
  import * as TimelineUtils from '../../timeline/utils/utils.js';
@@ -201,6 +203,14 @@ const UIStringsNotTranslate = {
201
203
  * @description Accessible label for the reveal button in the LCP breakdown widget.
202
204
  */
203
205
  revealLcpBreakdown: 'Reveal LCP breakdown',
206
+ /**
207
+ * @description Accessible label for the reveal button in the LCP discovery widget.
208
+ */
209
+ revealLcpDiscovery: 'Reveal LCP discovery',
210
+ /**
211
+ * @description Accessible label for the reveal button in the layout shift culprits widget.
212
+ */
213
+ revealClsCulprits: 'Reveal layout shift culprits',
204
214
  /**
205
215
  * @description Accessible label for the reveal button in the render-blocking requests widget.
206
216
  */
@@ -225,6 +235,14 @@ const UIStringsNotTranslate = {
225
235
  * @description Title for the LCP breakdown widget.
226
236
  */
227
237
  lcpBreakdown: 'LCP breakdown',
238
+ /**
239
+ * @description Title for the LCP discovery widget.
240
+ */
241
+ lcpDiscovery: 'LCP discovery',
242
+ /**
243
+ * @description Title for the layout shift culprits widget.
244
+ */
245
+ clsCulprits: 'Layout shift culprits',
228
246
  /**
229
247
  * @description Title for the render-blocking requests widget.
230
248
  */
@@ -922,55 +940,78 @@ async function makeStylePropertiesWidget(widgetData: StylePropertiesAiWidget): P
922
940
  };
923
941
  }
924
942
 
943
+ const INSIGHT_METADATA: Record<string, {
944
+ component: typeof TimelineInsights.LCPBreakdown.LCPBreakdown | typeof TimelineInsights.RenderBlocking.RenderBlocking |
945
+ typeof TimelineInsights.LCPDiscovery.LCPDiscovery | typeof TimelineInsights.CLSCulprits.CLSCulprits,
946
+ accessibleLabel: string,
947
+ title: string,
948
+ jslog: string,
949
+ }> = {
950
+ [Trace.Insights.Types.InsightKeys.LCP_BREAKDOWN]: {
951
+ component: TimelineInsights.LCPBreakdown.LCPBreakdown,
952
+ accessibleLabel: UIStringsNotTranslate.revealLcpBreakdown,
953
+ title: UIStringsNotTranslate.lcpBreakdown,
954
+ jslog: 'lcp-breakdown-widget',
955
+ },
956
+ [Trace.Insights.Types.InsightKeys.RENDER_BLOCKING]: {
957
+ component: TimelineInsights.RenderBlocking.RenderBlocking,
958
+ accessibleLabel: UIStringsNotTranslate.revealRenderBlockingBreakdown,
959
+ title: UIStringsNotTranslate.renderBlockingBreakdown,
960
+ jslog: 'render-blocking-widget',
961
+ },
962
+ [Trace.Insights.Types.InsightKeys.LCP_DISCOVERY]: {
963
+ component: TimelineInsights.LCPDiscovery.LCPDiscovery,
964
+ accessibleLabel: UIStringsNotTranslate.revealLcpDiscovery,
965
+ title: UIStringsNotTranslate.lcpDiscovery,
966
+ jslog: 'lcp-discovery-widget',
967
+ },
968
+ [Trace.Insights.Types.InsightKeys.CLS_CULPRITS]: {
969
+ component: TimelineInsights.CLSCulprits.CLSCulprits,
970
+ accessibleLabel: UIStringsNotTranslate.revealClsCulprits,
971
+ title: UIStringsNotTranslate.clsCulprits,
972
+ jslog: 'cls-culprits-widget',
973
+ },
974
+ };
975
+
976
+ function renderInsightWidget<T extends Trace.Insights.Types.InsightModel>(
977
+ component: new () => BaseInsightComponent<T>, insight: T, jslog: string, accessibleLabel: string, title: string,
978
+ bounds?: Trace.Types.Timing.TraceWindowMicro): WidgetMakerResponse {
979
+ const renderedWidget = html`<devtools-widget
980
+ class=${jslog}
981
+ ${widget(component, {
982
+ model: insight,
983
+ minimal: true,
984
+ bounds: bounds ?? null,
985
+ })}></devtools-widget>`;
986
+
987
+ return {
988
+ renderedWidget,
989
+ revealable: new TimelineUtils.Helpers.RevealableInsight(insight),
990
+ accessibleRevealLabel: lockedString(accessibleLabel),
991
+ title: lockedString(title),
992
+ jslogContext: jslog,
993
+ };
994
+ }
995
+
925
996
  async function makePerfInsightWidget(widgetData: PerfInsightAiWidget): Promise<WidgetMakerResponse|null> {
926
- switch (widgetData.data.insight) {
927
- case Trace.Insights.Types.InsightKeys.LCP_BREAKDOWN: {
928
- const insight = widgetData.data.insightData;
929
- if (!insight || !Trace.Insights.Models.LCPBreakdown.isLCPBreakdownInsight(insight)) {
930
- return null;
931
- }
932
- // clang-format off
933
- const renderedWidget = html`<devtools-widget
934
- class="lcp-breakdown-widget"
935
- ${widget(TimelineInsights.LCPBreakdown.LCPBreakdown, {
936
- model: insight,
937
- minimal: true,
938
- })}></devtools-widget>`;
939
- // clang-format on
997
+ const insightKey = widgetData.data.insight;
998
+ const insight = widgetData.data.insightData;
940
999
 
941
- return {
942
- renderedWidget,
943
- revealable: new TimelineUtils.Helpers.RevealableInsight(insight),
944
- accessibleRevealLabel: lockedString(UIStringsNotTranslate.revealLcpBreakdown),
945
- title: lockedString(UIStringsNotTranslate.lcpBreakdown),
946
- jslogContext: 'lcp-breakdown',
947
- };
948
- }
949
- case Trace.Insights.Types.InsightKeys.RENDER_BLOCKING: {
950
- const insight = widgetData.data.insightData;
951
- if (!insight || !Trace.Insights.Models.RenderBlocking.isRenderBlockingInsight(insight)) {
952
- return null;
953
- }
954
- // clang-format off
955
- const renderedWidget = html`<devtools-widget
956
- class="render-blocking-widget"
957
- ${widget(TimelineInsights.RenderBlocking.RenderBlocking, {
958
- model: insight,
959
- minimal: true,
960
- })}></devtools-widget>`;
961
- // clang-format on
1000
+ const meta = INSIGHT_METADATA[insightKey];
1001
+ if (!meta) {
1002
+ return null;
1003
+ }
962
1004
 
963
- return {
964
- renderedWidget,
965
- revealable: new TimelineUtils.Helpers.RevealableInsight(insight),
966
- accessibleRevealLabel: lockedString(UIStringsNotTranslate.revealRenderBlockingBreakdown),
967
- title: lockedString(UIStringsNotTranslate.renderBlockingBreakdown),
968
- jslogContext: 'render-blocking-widget',
969
- };
970
- }
971
- default:
1005
+ let bounds;
1006
+ if (insightKey === Trace.Insights.Types.InsightKeys.CLS_CULPRITS) {
1007
+ const traceBounds = TraceBounds.TraceBounds.BoundsManager.instance().state()?.micro.entireTraceBounds;
1008
+ if (!traceBounds) {
972
1009
  return null;
1010
+ }
1011
+ bounds = traceBounds;
973
1012
  }
1013
+
1014
+ return renderInsightWidget(meta.component, insight, meta.jslog, meta.accessibleLabel, meta.title, bounds);
974
1015
  }
975
1016
 
976
1017
  async function makeBottomUpTimelineTreeWidget(widgetData: BottomUpTreeAiWidget): Promise<WidgetMakerResponse|null> {
@@ -0,0 +1,69 @@
1
+ // Copyright 2026 The Chromium Authors
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 Bindings from '../../models/bindings/bindings.js';
6
+ import type * as Workspace from '../../models/workspace/workspace.js';
7
+ import * as UI from '../../ui/legacy/legacy.js';
8
+
9
+ export interface ViewInput {
10
+ error: Bindings.SymbolizedError.SymbolizedError;
11
+ ignoreListManager?: Workspace.IgnoreListManager.IgnoreListManager;
12
+ }
13
+
14
+ const DEFAULT_VIEW = (_input: ViewInput, _output: object, _target: HTMLElement): void => {};
15
+
16
+ export class SymbolizedErrorWidget extends UI.Widget.Widget {
17
+ #error?: Bindings.SymbolizedError.SymbolizedError;
18
+ #view: typeof DEFAULT_VIEW;
19
+ #ignoreListManager?: Workspace.IgnoreListManager.IgnoreListManager;
20
+
21
+ constructor(element?: HTMLElement, view: typeof DEFAULT_VIEW = DEFAULT_VIEW) {
22
+ super(element);
23
+ this.#view = view;
24
+ }
25
+
26
+ set ignoreListManager(ignoreListManager: Workspace.IgnoreListManager.IgnoreListManager) {
27
+ this.#ignoreListManager = ignoreListManager;
28
+ this.requestUpdate();
29
+ }
30
+
31
+ get ignoreListManager(): Workspace.IgnoreListManager.IgnoreListManager|undefined {
32
+ return this.#ignoreListManager;
33
+ }
34
+
35
+ set error(error: Bindings.SymbolizedError.SymbolizedError) {
36
+ this.#error?.removeEventListener(Bindings.SymbolizedError.Events.UPDATED, this.requestUpdate, this);
37
+ this.#error = error;
38
+ if (this.isShowing()) {
39
+ this.#error?.addEventListener(Bindings.SymbolizedError.Events.UPDATED, this.requestUpdate, this);
40
+ }
41
+ this.requestUpdate();
42
+ }
43
+
44
+ get error(): Bindings.SymbolizedError.SymbolizedError|undefined {
45
+ return this.#error;
46
+ }
47
+
48
+ override wasShown(): void {
49
+ super.wasShown();
50
+ this.#error?.addEventListener(Bindings.SymbolizedError.Events.UPDATED, this.requestUpdate, this);
51
+ this.requestUpdate();
52
+ }
53
+
54
+ override willHide(): void {
55
+ super.willHide();
56
+ this.#error?.removeEventListener(Bindings.SymbolizedError.Events.UPDATED, this.requestUpdate, this);
57
+ }
58
+
59
+ override performUpdate(): void {
60
+ if (!this.#error) {
61
+ return;
62
+ }
63
+ const input: ViewInput = {
64
+ error: this.#error,
65
+ ignoreListManager: this.#ignoreListManager,
66
+ };
67
+ this.#view(input, {}, this.contentElement);
68
+ }
69
+ }
@@ -13,6 +13,7 @@ import './ConsoleViewMessage.js';
13
13
  import './ConsolePrompt.js';
14
14
  import './ConsoleView.js';
15
15
  import './ConsolePanel.js';
16
+ import './SymbolizedErrorWidget.js';
16
17
  import './PromptBuilder.js';
17
18
 
18
19
  import * as ConsoleContextSelector from './ConsoleContextSelector.js';
@@ -27,6 +28,7 @@ import * as ConsoleView from './ConsoleView.js';
27
28
  import * as ConsoleViewMessage from './ConsoleViewMessage.js';
28
29
  import * as ConsoleViewport from './ConsoleViewport.js';
29
30
  import * as PromptBuilder from './PromptBuilder.js';
31
+ import * as SymbolizedErrorWidget from './SymbolizedErrorWidget.js';
30
32
 
31
33
  export {
32
34
  ConsoleContextSelector,
@@ -41,4 +43,5 @@ export {
41
43
  ConsoleViewMessage,
42
44
  ConsoleViewport,
43
45
  PromptBuilder,
46
+ SymbolizedErrorWidget,
44
47
  };