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.
- package/.agents/skills/foundation-test-migration/SKILL.md +171 -0
- package/.agents/skills/verification/SKILL.md +2 -11
- package/front_end/core/common/Base64.ts +12 -2
- package/front_end/core/i18n/i18nImpl.ts +8 -4
- package/front_end/core/root/Runtime.ts +28 -9
- package/front_end/entrypoints/device_mode_emulation_frame/device_mode_emulation_frame.ts +1 -1
- package/front_end/entrypoints/shell/shell.ts +1 -1
- package/front_end/models/ai_assistance/agents/PerformanceAgent.ts +2 -1
- package/front_end/panels/ai_assistance/components/ChatMessage.ts +85 -44
- package/front_end/panels/console/SymbolizedErrorWidget.ts +69 -0
- package/front_end/panels/console/console.ts +3 -0
- package/front_end/panels/emulation/DeviceModeToolbar.ts +152 -119
- package/front_end/panels/sources/WatchExpressionsSidebarPane.ts +12 -4
- package/front_end/panels/timeline/components/liveMetricsView.css +2 -2
- package/front_end/third_party/chromium/README.chromium +1 -1
- package/front_end/{core → ui}/dom_extension/DOMExtension.ts +1 -1
- package/front_end/ui/legacy/Widget.ts +1 -1
- package/front_end/ui/visual_logging/KnownContextValues.ts +2 -0
- package/package.json +1 -1
- /package/front_end/{core → ui}/dom_extension/dom_extension.ts +0 -0
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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,
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
79
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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 '../../
|
|
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 '../../
|
|
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
|
-
|
|
927
|
-
|
|
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
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
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
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
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
|
};
|