chrome-devtools-frontend 1.0.1621064 → 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.
Files changed (41) hide show
  1. package/.agents/skills/foundation-test-migration/SKILL.md +171 -0
  2. package/.agents/skills/verification/SKILL.md +2 -11
  3. package/front_end/core/common/Base64.ts +12 -2
  4. package/front_end/core/i18n/i18nImpl.ts +8 -4
  5. package/front_end/core/root/Runtime.ts +28 -9
  6. package/front_end/entrypoints/device_mode_emulation_frame/device_mode_emulation_frame.ts +1 -1
  7. package/front_end/entrypoints/shell/shell.ts +1 -1
  8. package/front_end/generated/InspectorBackendCommands.ts +1 -1
  9. package/front_end/generated/protocol.ts +1 -1
  10. package/front_end/models/ai_assistance/agents/AiAgent.ts +1 -1
  11. package/front_end/models/ai_assistance/agents/PerformanceAgent.ts +32 -20
  12. package/front_end/models/bindings/DebuggerWorkspaceBinding.ts +5 -1
  13. package/front_end/models/bindings/SymbolizedError.ts +18 -1
  14. package/front_end/models/issues_manager/GenericIssue.ts +50 -1
  15. package/front_end/models/issues_manager/descriptions/genericFormModelContextMissingToolDescription.md +5 -0
  16. package/front_end/models/issues_manager/descriptions/genericFormModelContextMissingToolName.md +5 -0
  17. package/front_end/models/issues_manager/descriptions/genericFormModelContextParameterMissingName.md +5 -0
  18. package/front_end/models/issues_manager/descriptions/genericFormModelContextParameterMissingTitleAndDescription.md +5 -0
  19. package/front_end/models/issues_manager/descriptions/genericFormModelContextRequiredParameterMissingName.md +5 -0
  20. package/front_end/models/javascript_metadata/NativeFunctions.js +8 -0
  21. package/front_end/models/trace/insights/Common.ts +4 -0
  22. package/front_end/models/trace/insights/types.ts +1 -1
  23. package/front_end/models/web_mcp/WebMCPModel.ts +11 -1
  24. package/front_end/panels/ai_assistance/components/ChatMessage.ts +94 -23
  25. package/front_end/panels/application/WebMCPView.ts +16 -7
  26. package/front_end/panels/console/SymbolizedErrorWidget.ts +69 -0
  27. package/front_end/panels/console/console.ts +3 -0
  28. package/front_end/panels/elements/AdoptedStyleSheetTreeElement.ts +0 -1
  29. package/front_end/panels/elements/ElementsTreeElement.ts +53 -61
  30. package/front_end/panels/elements/PropertiesWidget.ts +1 -1
  31. package/front_end/panels/emulation/DeviceModeToolbar.ts +152 -119
  32. package/front_end/panels/sources/WatchExpressionsSidebarPane.ts +12 -4
  33. package/front_end/panels/timeline/components/liveMetricsView.css +2 -2
  34. package/front_end/third_party/chromium/README.chromium +1 -1
  35. package/front_end/ui/components/text_editor/TextEditor.ts +1 -3
  36. package/front_end/{core → ui}/dom_extension/DOMExtension.ts +1 -1
  37. package/front_end/ui/legacy/Treeoutline.ts +5 -1
  38. package/front_end/ui/legacy/Widget.ts +1 -1
  39. package/front_end/ui/visual_logging/KnownContextValues.ts +4 -0
  40. package/package.json +1 -1
  41. /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 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';
@@ -349,7 +349,7 @@ inspectorBackend.registerCommand("CrashReportContext.getEntries", [], ["entries"
349
349
  inspectorBackend.registerType("CrashReportContext.CrashReportContextEntry", [{"name": "key", "type": "string", "optional": false, "description": "", "typeRef": null}, {"name": "value", "type": "string", "optional": false, "description": "", "typeRef": null}, {"name": "frameId", "type": "string", "optional": false, "description": "The ID of the frame where the key-value pair was set.", "typeRef": "Page.FrameId"}]);
350
350
 
351
351
  // DOM.
352
- inspectorBackend.registerEnum("DOM.PseudoType", {FirstLine: "first-line", FirstLetter: "first-letter", Checkmark: "checkmark", Before: "before", After: "after", ExpandIcon: "expand-icon", PickerIcon: "picker-icon", InterestHint: "interest-hint", Marker: "marker", Backdrop: "backdrop", Column: "column", Selection: "selection", SearchText: "search-text", TargetText: "target-text", SpellingError: "spelling-error", GrammarError: "grammar-error", Highlight: "highlight", FirstLineInherited: "first-line-inherited", ScrollMarker: "scroll-marker", ScrollMarkerGroup: "scroll-marker-group", ScrollButton: "scroll-button", Scrollbar: "scrollbar", ScrollbarThumb: "scrollbar-thumb", ScrollbarButton: "scrollbar-button", ScrollbarTrack: "scrollbar-track", ScrollbarTrackPiece: "scrollbar-track-piece", ScrollbarCorner: "scrollbar-corner", Resizer: "resizer", InputListButton: "input-list-button", ViewTransition: "view-transition", ViewTransitionGroup: "view-transition-group", ViewTransitionImagePair: "view-transition-image-pair", ViewTransitionGroupChildren: "view-transition-group-children", ViewTransitionOld: "view-transition-old", ViewTransitionNew: "view-transition-new", Placeholder: "placeholder", FileSelectorButton: "file-selector-button", DetailsContent: "details-content", Picker: "picker", PermissionIcon: "permission-icon", OverscrollAreaParent: "overscroll-area-parent"});
352
+ inspectorBackend.registerEnum("DOM.PseudoType", {FirstLine: "first-line", FirstLetter: "first-letter", Checkmark: "checkmark", Before: "before", After: "after", ExpandIcon: "expand-icon", PickerIcon: "picker-icon", InterestButton: "interest-button", Marker: "marker", Backdrop: "backdrop", Column: "column", Selection: "selection", SearchText: "search-text", TargetText: "target-text", SpellingError: "spelling-error", GrammarError: "grammar-error", Highlight: "highlight", FirstLineInherited: "first-line-inherited", ScrollMarker: "scroll-marker", ScrollMarkerGroup: "scroll-marker-group", ScrollButton: "scroll-button", Scrollbar: "scrollbar", ScrollbarThumb: "scrollbar-thumb", ScrollbarButton: "scrollbar-button", ScrollbarTrack: "scrollbar-track", ScrollbarTrackPiece: "scrollbar-track-piece", ScrollbarCorner: "scrollbar-corner", Resizer: "resizer", InputListButton: "input-list-button", ViewTransition: "view-transition", ViewTransitionGroup: "view-transition-group", ViewTransitionImagePair: "view-transition-image-pair", ViewTransitionGroupChildren: "view-transition-group-children", ViewTransitionOld: "view-transition-old", ViewTransitionNew: "view-transition-new", Placeholder: "placeholder", FileSelectorButton: "file-selector-button", DetailsContent: "details-content", Picker: "picker", PermissionIcon: "permission-icon", OverscrollAreaParent: "overscroll-area-parent"});
353
353
  inspectorBackend.registerEnum("DOM.ShadowRootType", {UserAgent: "user-agent", Open: "open", Closed: "closed"});
354
354
  inspectorBackend.registerEnum("DOM.CompatibilityMode", {QuirksMode: "QuirksMode", LimitedQuirksMode: "LimitedQuirksMode", NoQuirksMode: "NoQuirksMode"});
355
355
  inspectorBackend.registerEnum("DOM.PhysicalAxes", {Horizontal: "Horizontal", Vertical: "Vertical", Both: "Both"});
@@ -4604,7 +4604,7 @@ export namespace DOM {
4604
4604
  After = 'after',
4605
4605
  ExpandIcon = 'expand-icon',
4606
4606
  PickerIcon = 'picker-icon',
4607
- InterestHint = 'interest-hint',
4607
+ InterestButton = 'interest-button',
4608
4608
  Marker = 'marker',
4609
4609
  Backdrop = 'backdrop',
4610
4610
  Column = 'column',
@@ -251,7 +251,7 @@ export interface PerformanceTraceAiWidget {
251
251
  export interface PerfInsightAiWidget {
252
252
  name: 'PERF_INSIGHT';
253
253
  data: {
254
- insight: 'lcp',
254
+ insight: Trace.Insights.Types.InsightKeys,
255
255
  insightData: Trace.Insights.Types.InsightModel,
256
256
  };
257
257
  }
@@ -222,6 +222,13 @@ enum ScorePriority {
222
222
  DEFAULT = 1,
223
223
  }
224
224
 
225
+ const SUPPORTED_INSIGHT_WIDGETS = new Set<Trace.Insights.Types.InsightKeys>([
226
+ Trace.Insights.Types.InsightKeys.LCP_BREAKDOWN,
227
+ Trace.Insights.Types.InsightKeys.RENDER_BLOCKING,
228
+ Trace.Insights.Types.InsightKeys.LCP_DISCOVERY,
229
+ Trace.Insights.Types.InsightKeys.CLS_CULPRITS,
230
+ ]);
231
+
225
232
  export class PerformanceTraceContext extends ConversationContext<AgentFocus> {
226
233
  static fromParsedTrace(parsedTrace: Trace.TraceModel.ParsedTrace): PerformanceTraceContext {
227
234
  return new PerformanceTraceContext(AgentFocus.fromParsedTrace(parsedTrace));
@@ -334,8 +341,8 @@ export class PerformanceTraceContext extends ConversationContext<AgentFocus> {
334
341
  const failingInsightSuggestions =
335
342
  Object.values(insightSet.model)
336
343
  .filter(model => {
337
- return model.state !== 'pass' &&
338
- !poorMetrics.has(model.insightKey as Trace.Insights.Types.InsightKeys);
344
+ return model.state !== 'pass' && Trace.Insights.Common.isInsightKey(model.insightKey) &&
345
+ !poorMetrics.has(model.insightKey);
339
346
  })
340
347
  .map(model => new PerformanceInsightFormatter(focus, model).getSuggestions().at(-1))
341
348
  .filter((suggestion): suggestion is ConversationSuggestion => !!suggestion)
@@ -536,15 +543,18 @@ export class PerformanceAgent extends AiAgent<AgentFocus> {
536
543
  return widgets;
537
544
  }
538
545
 
539
- // Case 2: LCP Insight -> LCP breakdown & CWV widgets
540
- if (focus.insight && Trace.Insights.Models.LCPBreakdown.isLCPBreakdownInsight(focus.insight)) {
541
- widgets.push({
542
- name: 'PERF_INSIGHT',
543
- data: {
544
- insight: 'lcp',
545
- insightData: focus.insight,
546
- },
547
- });
546
+ // Case 2: Insight -> PERF_INSIGHT widget
547
+ if (focus.insight) {
548
+ const insightKey = focus.insight.insightKey;
549
+ if (Trace.Insights.Common.isInsightKey(insightKey) && SUPPORTED_INSIGHT_WIDGETS.has(insightKey)) {
550
+ widgets.push({
551
+ name: 'PERF_INSIGHT',
552
+ data: {
553
+ insight: insightKey,
554
+ insightData: focus.insight,
555
+ },
556
+ });
557
+ }
548
558
  }
549
559
 
550
560
  // Case 3: Whole Trace or insight other than LCP -> CWV widget
@@ -1006,15 +1016,17 @@ export class PerformanceAgent extends AiAgent<AgentFocus> {
1006
1016
  }
1007
1017
  }
1008
1018
  }
1009
- if (params.insightName === 'LCPBreakdown') {
1010
- widgets.push({
1011
- name: 'PERF_INSIGHT',
1012
- data: {
1013
- insight: 'lcp',
1014
- insightData: insight as Trace.Insights.Types.InsightModel,
1015
- },
1016
- });
1017
- }
1019
+ }
1020
+
1021
+ const insightKey = params.insightName;
1022
+ if (Trace.Insights.Common.isInsightKey(insightKey) && SUPPORTED_INSIGHT_WIDGETS.has(insightKey)) {
1023
+ widgets.push({
1024
+ name: 'PERF_INSIGHT',
1025
+ data: {
1026
+ insight: insightKey,
1027
+ insightData: insight as Trace.Insights.Types.InsightModel,
1028
+ },
1029
+ });
1018
1030
  }
1019
1031
 
1020
1032
  const key = `getInsightDetails('${params.insightSetId}', '${params.insightName}')`;
@@ -21,10 +21,11 @@ import {NetworkProject} from './NetworkProject.js';
21
21
  import type {ResourceMapping} from './ResourceMapping.js';
22
22
  import {type ResourceScriptFile, ResourceScriptMapping} from './ResourceScriptMapping.js';
23
23
  import {
24
+ isErrorLike,
24
25
  type SymbolizedError,
25
26
  SymbolizedErrorObject,
26
27
  SymbolizedSyntaxError,
27
- UnparsableError
28
+ UnparsableError,
28
29
  } from './SymbolizedError.js';
29
30
 
30
31
  export class DebuggerWorkspaceBinding implements SDK.TargetManager.SDKModelObserver<SDK.DebuggerModel.DebuggerModel> {
@@ -247,6 +248,9 @@ export class DebuggerWorkspaceBinding implements SDK.TargetManager.SDKModelObser
247
248
  }
248
249
  } else if (remoteObject.type === 'string') {
249
250
  errorStack = remoteObject.description || '';
251
+ if (!isErrorLike(errorStack)) {
252
+ return null;
253
+ }
250
254
  } else {
251
255
  return null;
252
256
  }
@@ -11,6 +11,10 @@ import type * as Workspace from '../workspace/workspace.js';
11
11
  import type {DebuggerWorkspaceBinding} from './DebuggerWorkspaceBinding.js';
12
12
  import {type LiveLocation, LiveLocationPool} from './LiveLocation.js';
13
13
 
14
+ export function isErrorLike(stack: string): boolean {
15
+ return /\n\s*at\s/.test(stack) || stack.startsWith('SyntaxError:');
16
+ }
17
+
14
18
  export type SymbolizedError = SymbolizedErrorObject|SymbolizedSyntaxError|UnparsableError;
15
19
 
16
20
  export class UnparsableError extends Common.ObjectWrapper.ObjectWrapper<EventTypes> {
@@ -21,6 +25,19 @@ export class UnparsableError extends Common.ObjectWrapper.ObjectWrapper<EventTyp
21
25
  super();
22
26
  this.errorStack = errorStack;
23
27
  this.cause = cause;
28
+
29
+ this.cause?.addEventListener(Events.UPDATED, this.#fireUpdated, this);
30
+ }
31
+
32
+ dispose(): void {
33
+ this.cause?.removeEventListener(Events.UPDATED, this.#fireUpdated, this);
34
+ if (this.cause instanceof SymbolizedErrorObject || this.cause instanceof UnparsableError) {
35
+ this.cause.dispose();
36
+ }
37
+ }
38
+
39
+ #fireUpdated(): void {
40
+ this.dispatchEventToListeners(Events.UPDATED);
24
41
  }
25
42
  }
26
43
 
@@ -42,7 +59,7 @@ export class SymbolizedErrorObject extends Common.ObjectWrapper.ObjectWrapper<Ev
42
59
  dispose(): void {
43
60
  this.stackTrace.removeEventListener(StackTrace.StackTrace.Events.UPDATED, this.#fireUpdated, this);
44
61
  this.cause?.removeEventListener(Events.UPDATED, this.#fireUpdated, this);
45
- if (this.cause instanceof SymbolizedErrorObject) {
62
+ if (this.cause instanceof SymbolizedErrorObject || this.cause instanceof UnparsableError) {
46
63
  this.cause.dispose();
47
64
  }
48
65
  }
@@ -193,6 +193,31 @@ export const genericNavigationEntryMarkedSkippable = {
193
193
  }],
194
194
  };
195
195
 
196
+ export const genericFormModelContextMissingToolName = {
197
+ file: 'genericFormModelContextMissingToolName.md',
198
+ links: [],
199
+ };
200
+
201
+ export const genericFormModelContextMissingToolDescription = {
202
+ file: 'genericFormModelContextMissingToolDescription.md',
203
+ links: [],
204
+ };
205
+
206
+ export const genericFormModelContextParameterMissingTitleAndDescription = {
207
+ file: 'genericFormModelContextParameterMissingTitleAndDescription.md',
208
+ links: [],
209
+ };
210
+
211
+ export const genericFormModelContextRequiredParameterMissingName = {
212
+ file: 'genericFormModelContextRequiredParameterMissingName.md',
213
+ links: [],
214
+ };
215
+
216
+ export const genericFormModelContextParameterMissingName = {
217
+ file: 'genericFormModelContextParameterMissingName.md',
218
+ links: [],
219
+ };
220
+
196
221
  const issueDescriptions = new Map<Protocol.Audits.GenericIssueErrorType, LazyMarkdownIssueDescription>([
197
222
  [Protocol.Audits.GenericIssueErrorType.FormLabelForNameError, genericFormLabelForNameError],
198
223
  [Protocol.Audits.GenericIssueErrorType.FormInputWithNoLabelError, genericFormInputWithNoLabelError],
@@ -233,6 +258,26 @@ const issueDescriptions = new Map<Protocol.Audits.GenericIssueErrorType, LazyMar
233
258
  Protocol.Audits.GenericIssueErrorType.NavigationEntryMarkedSkippable,
234
259
  genericNavigationEntryMarkedSkippable,
235
260
  ],
261
+ [
262
+ Protocol.Audits.GenericIssueErrorType.FormModelContextMissingToolName,
263
+ genericFormModelContextMissingToolName,
264
+ ],
265
+ [
266
+ Protocol.Audits.GenericIssueErrorType.FormModelContextMissingToolDescription,
267
+ genericFormModelContextMissingToolDescription,
268
+ ],
269
+ [
270
+ Protocol.Audits.GenericIssueErrorType.FormModelContextParameterMissingTitleAndDescription,
271
+ genericFormModelContextParameterMissingTitleAndDescription,
272
+ ],
273
+ [
274
+ Protocol.Audits.GenericIssueErrorType.FormModelContextRequiredParameterMissingName,
275
+ genericFormModelContextRequiredParameterMissingName,
276
+ ],
277
+ [
278
+ Protocol.Audits.GenericIssueErrorType.FormModelContextParameterMissingName,
279
+ genericFormModelContextParameterMissingName,
280
+ ],
236
281
  ]);
237
282
 
238
283
  const issueTypes = new Map<Protocol.Audits.GenericIssueErrorType, IssueKind>([
@@ -249,5 +294,9 @@ const issueTypes = new Map<Protocol.Audits.GenericIssueErrorType, IssueKind>([
249
294
  [Protocol.Audits.GenericIssueErrorType.FormLabelForMatchesNonExistingIdError, IssueKind.PAGE_ERROR],
250
295
  [Protocol.Audits.GenericIssueErrorType.FormLabelHasNeitherForNorNestedInputError, IssueKind.IMPROVEMENT],
251
296
  [Protocol.Audits.GenericIssueErrorType.FormInputHasWrongButWellIntendedAutocompleteValueError, IssueKind.IMPROVEMENT],
252
-
297
+ [Protocol.Audits.GenericIssueErrorType.FormModelContextMissingToolName, IssueKind.PAGE_ERROR],
298
+ [Protocol.Audits.GenericIssueErrorType.FormModelContextMissingToolDescription, IssueKind.PAGE_ERROR],
299
+ [Protocol.Audits.GenericIssueErrorType.FormModelContextParameterMissingTitleAndDescription, IssueKind.PAGE_ERROR],
300
+ [Protocol.Audits.GenericIssueErrorType.FormModelContextRequiredParameterMissingName, IssueKind.PAGE_ERROR],
301
+ [Protocol.Audits.GenericIssueErrorType.FormModelContextParameterMissingName, IssueKind.PAGE_ERROR],
253
302
  ]);
@@ -0,0 +1,5 @@
1
+ # Missing tool description for a WebMCP declarative tool definition
2
+
3
+ This form is intended to be used as a WebMCP declarative tool definition, but it lacks a description explaining what the tool does. Descriptions are critical for AI models to decide when to use a given tool.
4
+
5
+ To fix this issue, provide a tool description via the `tooldescription` attribute.
@@ -0,0 +1,5 @@
1
+ # Missing tool name for a WebMCP declarative tool definition
2
+
3
+ This form is intended to be used as a WebMCP declarative tool definition, but it does not specify a name. A tool name is required for an AI model to correctly identify and invoke the tool.
4
+
5
+ To fix this issue, provide a tool name via the `toolname` attribute.
@@ -0,0 +1,5 @@
1
+ # Parameter missing name for a WebMCP declarative tool definition
2
+
3
+ An input element within this form lacks a `name` attribute. Without a name, the AI model cannot format the parameter payload correctly when invoking the tool.
4
+
5
+ To fix this issue, ensure form parameters have a `name` attribute.
@@ -0,0 +1,5 @@
1
+ # Missing parameter title and description for a WebMCP declarative tool definition
2
+
3
+ A parameter within this form (such as an `<input>` field) is missing both a title and a description. An AI model needs this metadata to understand what information should be passed to this parameter when invoking the tool.
4
+
5
+ To fix this issue, add a title and description to the form parameter.
@@ -0,0 +1,5 @@
1
+ # Required parameter missing name for a WebMCP declarative tool definition
2
+
3
+ A required input element within this form lacks a `name` attribute. Without a name, the AI model cannot format the parameter payload correctly when invoking the tool.
4
+
5
+ To fix this issue, ensure all required form parameters have a `name` attribute.