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.
- 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/generated/InspectorBackendCommands.ts +1 -1
- package/front_end/generated/protocol.ts +1 -1
- package/front_end/models/ai_assistance/agents/AiAgent.ts +1 -1
- package/front_end/models/ai_assistance/agents/PerformanceAgent.ts +32 -20
- package/front_end/models/bindings/DebuggerWorkspaceBinding.ts +5 -1
- package/front_end/models/bindings/SymbolizedError.ts +18 -1
- package/front_end/models/issues_manager/GenericIssue.ts +50 -1
- package/front_end/models/issues_manager/descriptions/genericFormModelContextMissingToolDescription.md +5 -0
- package/front_end/models/issues_manager/descriptions/genericFormModelContextMissingToolName.md +5 -0
- package/front_end/models/issues_manager/descriptions/genericFormModelContextParameterMissingName.md +5 -0
- package/front_end/models/issues_manager/descriptions/genericFormModelContextParameterMissingTitleAndDescription.md +5 -0
- package/front_end/models/issues_manager/descriptions/genericFormModelContextRequiredParameterMissingName.md +5 -0
- package/front_end/models/javascript_metadata/NativeFunctions.js +8 -0
- package/front_end/models/trace/insights/Common.ts +4 -0
- package/front_end/models/trace/insights/types.ts +1 -1
- package/front_end/models/web_mcp/WebMCPModel.ts +11 -1
- package/front_end/panels/ai_assistance/components/ChatMessage.ts +94 -23
- package/front_end/panels/application/WebMCPView.ts +16 -7
- package/front_end/panels/console/SymbolizedErrorWidget.ts +69 -0
- package/front_end/panels/console/console.ts +3 -0
- package/front_end/panels/elements/AdoptedStyleSheetTreeElement.ts +0 -1
- package/front_end/panels/elements/ElementsTreeElement.ts +53 -61
- package/front_end/panels/elements/PropertiesWidget.ts +1 -1
- 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/ui/components/text_editor/TextEditor.ts +1 -3
- package/front_end/{core → ui}/dom_extension/DOMExtension.ts +1 -1
- package/front_end/ui/legacy/Treeoutline.ts +5 -1
- package/front_end/ui/legacy/Widget.ts +1 -1
- package/front_end/ui/visual_logging/KnownContextValues.ts +4 -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';
|
|
@@ -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",
|
|
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
|
-
|
|
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:
|
|
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
|
|
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:
|
|
540
|
-
if (focus.insight
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
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
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
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.
|
package/front_end/models/issues_manager/descriptions/genericFormModelContextMissingToolName.md
ADDED
|
@@ -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.
|
package/front_end/models/issues_manager/descriptions/genericFormModelContextParameterMissingName.md
ADDED
|
@@ -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.
|