chrome-devtools-frontend 1.0.1666631 → 1.0.1668390

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 (103) hide show
  1. package/.agents/skills/devtools-setting-migration/SKILL.md +290 -0
  2. package/.agents/skills/ui-eng-vision-widget-promoter/SKILL.md +11 -4
  3. package/.agents/skills/ui-widgets/SKILL.md +25 -1
  4. package/docs/ui_engineering.md +44 -1
  5. package/front_end/core/common/Settings.ts +11 -4
  6. package/front_end/core/host/AidaClient.ts +6 -4
  7. package/front_end/core/host/InspectorFrontendHostAPI.ts +1 -0
  8. package/front_end/core/host/InspectorFrontendHostStub.ts +3 -3
  9. package/front_end/core/host/UserMetrics.ts +49 -1
  10. package/front_end/core/root/Runtime.ts +5 -0
  11. package/front_end/core/sdk/CSSMatchedStyles.ts +2 -5
  12. package/front_end/core/sdk/CSSModel.test.api.ts +102 -0
  13. package/front_end/core/sdk/CSSModel.ts +3 -2
  14. package/front_end/core/sdk/CSSPropertyParserMatchers.ts +12 -0
  15. package/front_end/core/sdk/Connections.ts +1 -1
  16. package/front_end/core/sdk/DOMModel.test.api.ts +104 -0
  17. package/front_end/core/sdk/DOMModel.ts +23 -11
  18. package/front_end/core/sdk/DebuggerModel.ts +12 -11
  19. package/front_end/core/sdk/IsolateManager.ts +1 -1
  20. package/front_end/core/sdk/NetworkManager.ts +105 -4
  21. package/front_end/core/sdk/OverlayModel.ts +15 -14
  22. package/front_end/core/sdk/RemoteObject.ts +1 -1
  23. package/front_end/core/sdk/SDKSettings.ts +19 -0
  24. package/front_end/core/sdk/sdk-meta.ts +0 -62
  25. package/front_end/core/sdk/sdk.ts +2 -0
  26. package/front_end/devtools_compatibility.js +1 -0
  27. package/front_end/entrypoints/main/MainImpl.ts +15 -5
  28. package/front_end/entrypoints/main/SimpleApp.ts +9 -3
  29. package/front_end/foundation/Universe.ts +13 -0
  30. package/front_end/models/ai_assistance/AiAgent2.ts +1 -1
  31. package/front_end/models/ai_assistance/BuiltInAi.ts +11 -6
  32. package/front_end/models/ai_assistance/ChangeManager.ts +17 -1
  33. package/front_end/models/ai_assistance/contexts/PerformanceTraceContext.ts +23 -10
  34. package/front_end/models/computed_style/ComputedStyleModel.ts +4 -7
  35. package/front_end/models/crux-manager/CrUXManager.ts +4 -0
  36. package/front_end/models/extensions/ExtensionAPI.ts +3 -3
  37. package/front_end/models/heap_snapshot/HeapSnapshotProxy.ts +2 -2
  38. package/front_end/models/javascript_metadata/NativeFunctions.js +22 -9
  39. package/front_end/models/live-metrics/LiveMetrics.ts +59 -19
  40. package/front_end/models/live-metrics/web-vitals-injected/README.md +1 -1
  41. package/front_end/models/live-metrics/web-vitals-injected/spec/spec.ts +6 -1
  42. package/front_end/models/live-metrics/web-vitals-injected/web-vitals-injected.ts +11 -3
  43. package/front_end/models/logs/LogManager.ts +1 -1
  44. package/front_end/models/persistence/FileSystemWorkspaceBinding.ts +1 -1
  45. package/front_end/models/source_map_scopes/FunctionCodeResolver.ts +6 -7
  46. package/front_end/models/source_map_scopes/NamesResolver.ts +23 -17
  47. package/front_end/models/workspace_diff/WorkspaceDiff.ts +13 -14
  48. package/front_end/panels/ai_assistance/AiAssistancePanel.ts +15 -7
  49. package/front_end/panels/application/BackgroundServiceView.ts +18 -20
  50. package/front_end/panels/application/ServiceWorkerUpdateCycleView.ts +78 -118
  51. package/front_end/panels/application/preloading/components/preloadingDetailsReportView.css +13 -13
  52. package/front_end/panels/autofill/AutofillView.ts +17 -17
  53. package/front_end/panels/autofill/autofillView.css +7 -8
  54. package/front_end/panels/browser_debugger/CategorizedBreakpointsSidebarPane.ts +31 -31
  55. package/front_end/panels/browser_debugger/DOMBreakpointsSidebarPane.ts +15 -15
  56. package/front_end/panels/browser_debugger/XHRBreakpointsSidebarPane.ts +12 -12
  57. package/front_end/panels/browser_debugger/browser_debugger-meta.ts +27 -27
  58. package/front_end/panels/common/AiCodeCompletionDisclaimer.ts +14 -6
  59. package/front_end/panels/common/AiCodeCompletionSummaryToolbar.ts +14 -6
  60. package/front_end/panels/common/AiCodeCompletionTeaser.ts +13 -6
  61. package/front_end/panels/common/GeminiRebrandPromoDialog.ts +1 -1
  62. package/front_end/panels/console/ConsoleInsightTeaser.ts +16 -8
  63. package/front_end/panels/console/ConsoleView.ts +6 -6
  64. package/front_end/panels/console_counters/WarningErrorCounter.ts +4 -4
  65. package/front_end/panels/coverage/CoverageModel.ts +11 -1
  66. package/front_end/panels/elements/StylePropertiesSection.ts +50 -17
  67. package/front_end/panels/elements/StylesAiCodeCompletionProvider.ts +18 -7
  68. package/front_end/panels/emulation/AdvancedApp.ts +18 -8
  69. package/front_end/panels/emulation/DeviceModeToolbar.ts +2 -2
  70. package/front_end/panels/emulation/DeviceModeView.ts +63 -104
  71. package/front_end/panels/event_listeners/EventListenersView.ts +8 -8
  72. package/front_end/panels/explain/components/ConsoleInsight.ts +15 -7
  73. package/front_end/panels/network/NetworkLogView.ts +13 -14
  74. package/front_end/panels/screencast/ScreencastApp.ts +12 -6
  75. package/front_end/panels/screencast/ScreencastView.ts +37 -11
  76. package/front_end/panels/sensors/LocationsSettingsTab.ts +244 -34
  77. package/front_end/panels/sensors/locationsSettingsTab.css +78 -0
  78. package/front_end/panels/settings/AISettingsTab.ts +13 -6
  79. package/front_end/panels/sources/DebuggerPlugin.ts +15 -4
  80. package/front_end/panels/sources/sources-meta.ts +55 -0
  81. package/front_end/panels/timeline/components/LiveMetricsView.ts +14 -1
  82. package/front_end/panels/timeline/components/cpuThrottlingSelector.css +4 -0
  83. package/front_end/panels/timeline/components/liveMetricsView.css +28 -0
  84. package/front_end/panels/timeline/timeline-meta.ts +13 -0
  85. package/front_end/third_party/chromium/README.chromium +1 -1
  86. package/front_end/ui/components/menus/menu.css +4 -0
  87. package/front_end/ui/components/menus/menuItem.css +4 -0
  88. package/front_end/ui/components/menus/selectMenu.css +4 -0
  89. package/front_end/ui/components/menus/selectMenuButton.css +4 -0
  90. package/front_end/ui/components/text_editor/AiCodeCompletionProvider.ts +21 -10
  91. package/front_end/ui/components/text_editor/AiCodeGenerationProvider.ts +22 -10
  92. package/front_end/ui/components/text_editor/config.ts +7 -0
  93. package/front_end/ui/legacy/AppProvider.ts +2 -1
  94. package/front_end/ui/legacy/Dialog.ts +93 -4
  95. package/front_end/ui/legacy/InspectorView.ts +1 -5
  96. package/front_end/ui/legacy/PopoverHelper.ts +5 -5
  97. package/front_end/ui/legacy/RootView.ts +8 -1
  98. package/front_end/ui/legacy/UniverseRequestEvent.ts +19 -0
  99. package/front_end/ui/legacy/Widget.ts +49 -6
  100. package/front_end/ui/legacy/inspectorCommon.css +1 -1
  101. package/front_end/ui/legacy/legacy.ts +2 -0
  102. package/front_end/ui/visual_logging/KnownContextValues.ts +3 -0
  103. package/package.json +1 -1
@@ -0,0 +1,102 @@
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 {assert} from 'chai';
6
+ import sinon from 'sinon';
7
+
8
+ // eslint-disable-next-line @devtools/es-modules-import
9
+ import * as SDK from './sdk.js';
10
+
11
+ interface GlobalWithCSS {
12
+ // eslint-disable-next-line @typescript-eslint/naming-convention
13
+ CSS?: unknown;
14
+ }
15
+
16
+ describe('CSSModel API Test', () => {
17
+ it('adds a semicolon when enabling a property that lacked one, inserts properties, and enables them correctly',
18
+ async ({inspectedPage, universe}) => {
19
+ // Mock CSS.supports in Node environment for API tests.
20
+ let cssMocked = false;
21
+ if (typeof globalThis.CSS === 'undefined') {
22
+ (globalThis as unknown as GlobalWithCSS).CSS = undefined;
23
+ cssMocked = true;
24
+ }
25
+ const stub = sinon.stub(globalThis, 'CSS').value({
26
+ supports: () => true,
27
+ });
28
+
29
+ try {
30
+ const primaryTarget = universe.targetManager.primaryPageTarget();
31
+ assert.isNotNull(primaryTarget);
32
+
33
+ const domModel = primaryTarget.model(SDK.DOMModel.DOMModel);
34
+ assert.isNotNull(domModel);
35
+
36
+ const cssModel = primaryTarget.model(SDK.CSSModel.CSSModel);
37
+ assert.isNotNull(cssModel);
38
+
39
+ await inspectedPage.goToHtml(`
40
+ <style>
41
+ #formatted {
42
+ color: red;
43
+ margin: 0
44
+ }
45
+ </style>
46
+ <div id="formatted">Formatted</div>
47
+ `);
48
+
49
+ const documentNode = await domModel.requestDocument();
50
+ assert.isNotNull(documentNode);
51
+
52
+ // Retrieve the subtree to populate the DOMModel cache.
53
+ await documentNode.getSubtree(5, true);
54
+
55
+ const nodeId = await domModel.querySelector(documentNode.id, '#formatted');
56
+ assert.isNotNull(nodeId);
57
+
58
+ const matchedResult = await cssModel.getMatchedStyles(nodeId);
59
+ assert.isNotNull(matchedResult);
60
+
61
+ const style = matchedResult.nodeStyles()[1];
62
+ assert.isNotNull(style);
63
+
64
+ cssModel.addEventListener(SDK.CSSModel.Events.StyleSheetChanged, event => {
65
+ const {edit} = event.data;
66
+ if (edit) {
67
+ style.rebase(edit);
68
+ }
69
+ });
70
+
71
+ const marginProperty = style.allProperties()[1];
72
+ assert.strictEqual(marginProperty.name, 'margin');
73
+ assert.strictEqual(marginProperty.value, '0');
74
+
75
+ let success = await marginProperty.setDisabled(true);
76
+ assert.isTrue(success);
77
+ assert.include(style.cssText, '/* margin: 0; */');
78
+
79
+ const insertPromise = new Promise<void>(resolve => {
80
+ style.insertPropertyAt(2, 'endProperty', 'endValue', success => {
81
+ assert.isTrue(success);
82
+ resolve();
83
+ });
84
+ });
85
+ await insertPromise;
86
+ assert.include(style.cssText, '/* margin: 0; */');
87
+ assert.include(style.cssText, 'endProperty: endValue;');
88
+
89
+ const updatedMarginProperty = style.allProperties()[1];
90
+ success = await updatedMarginProperty.setDisabled(false);
91
+ assert.isTrue(success);
92
+ assert.include(style.cssText, 'margin: 0;');
93
+ assert.include(style.cssText, 'endProperty: endValue;');
94
+ } finally {
95
+ // Clean up the mock to avoid polluting the global scope.
96
+ stub.restore();
97
+ if (cssMocked) {
98
+ delete (globalThis as unknown as GlobalWithCSS).CSS;
99
+ }
100
+ }
101
+ });
102
+ });
@@ -25,6 +25,7 @@ import {
25
25
  ResourceTreeModel,
26
26
  } from './ResourceTreeModel.js';
27
27
  import {SDKModel} from './SDKModel.js';
28
+ import {cssSourceMapsEnabledSettingDescriptor} from './SDKSettings.js';
28
29
  import {SourceMapManager} from './SourceMapManager.js';
29
30
  import {Capability, type Target} from './Target.js';
30
31
 
@@ -81,8 +82,8 @@ export class CSSModel extends SDKModel<EventTypes> {
81
82
  }
82
83
 
83
84
  const settings = this.target().targetManager().settings;
84
- this.#sourceMapManager.setEnabled(settings.moduleSetting<boolean>('css-source-maps-enabled').get());
85
- settings.moduleSetting<boolean>('css-source-maps-enabled')
85
+ this.#sourceMapManager.setEnabled(settings.resolve(cssSourceMapsEnabledSettingDescriptor).get());
86
+ settings.resolve(cssSourceMapsEnabledSettingDescriptor)
86
87
  .addChangeListener(event => this.#sourceMapManager.setEnabled(event.data));
87
88
  }
88
89
 
@@ -243,6 +243,18 @@ function getCssEvaluationElement(): HTMLElement {
243
243
  return cssEvaluationElement;
244
244
  }
245
245
 
246
+ /**
247
+ * If a test calls localEvalCSS, an element is created on demand for this
248
+ * purpose. This element is not removed from the DOM and will leak between tests
249
+ * if not removed.
250
+ */
251
+ export function removeCSSEvaluationElement(): void {
252
+ if (cssEvaluationElement) {
253
+ document.body.removeChild(cssEvaluationElement);
254
+ cssEvaluationElement = null;
255
+ }
256
+ }
257
+
246
258
  /**
247
259
  * These functions use an element in the frontend to evaluate CSS. The advantage
248
260
  * of this is that it is synchronous and doesn't require a CDP method. The
@@ -189,7 +189,7 @@ export class StubTransport implements ProtocolClient.ConnectionTransport.Connect
189
189
  }
190
190
 
191
191
  sendRawMessage(message: string): void {
192
- window.setTimeout(this.respondWithError.bind(this, message), 0);
192
+ globalThis.setTimeout(this.respondWithError.bind(this, message), 0);
193
193
  }
194
194
 
195
195
  private respondWithError(message: string): void {
@@ -0,0 +1,104 @@
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 {assert} from 'chai';
6
+
7
+ import type * as Common from '../common/common.js';
8
+
9
+ // eslint-disable-next-line @devtools/es-modules-import
10
+ import * as SDK from './sdk.js';
11
+
12
+ function findNode(node: SDK.DOMModel.DOMNode, predicate: (node: SDK.DOMModel.DOMNode) => boolean): SDK.DOMModel.DOMNode|
13
+ null {
14
+ if (predicate(node)) {
15
+ return node;
16
+ }
17
+ const children = node.children();
18
+ if (children) {
19
+ for (const child of children) {
20
+ const result = findNode(child, predicate);
21
+ if (result) {
22
+ return result;
23
+ }
24
+ }
25
+ }
26
+ return null;
27
+ }
28
+
29
+ describe('DOMModel API Test', () => {
30
+ it('generates attribute updated event only when attribute is actually changed', async ({inspectedPage, universe}) => {
31
+ const primaryTarget = universe.targetManager.primaryPageTarget();
32
+ assert.isNotNull(primaryTarget);
33
+
34
+ const domModel = primaryTarget.model(SDK.DOMModel.DOMModel);
35
+ assert.isNotNull(domModel);
36
+
37
+ await inspectedPage.goToHtml(`
38
+ <div id="container">
39
+ <div id="node-set-new-value" style="color:red"></div>
40
+ <div id="node-set-same-value" style="color:red"></div>
41
+ </div>
42
+ `);
43
+
44
+ const documentNode = await domModel.requestDocument();
45
+ assert.isNotNull(documentNode);
46
+
47
+ // Retrieve the subtree to populate the DOMModel cache.
48
+ await documentNode.getSubtree(5, true);
49
+
50
+ const container = findNode(documentNode, n => n.getAttribute('id') === 'container');
51
+ assert.isNotNull(container);
52
+
53
+ const nodeSetNewValue = findNode(documentNode, n => n.getAttribute('id') === 'node-set-new-value');
54
+ assert.isNotNull(nodeSetNewValue);
55
+
56
+ const nodeSetSameValue = findNode(documentNode, n => n.getAttribute('id') === 'node-set-same-value');
57
+ assert.isNotNull(nodeSetSameValue);
58
+
59
+ if (!nodeSetNewValue || !nodeSetSameValue) {
60
+ assert.fail('Could not find test nodes');
61
+ }
62
+
63
+ // Verifies that setting a new style attribute value triggers the AttrModified event.
64
+ let attrModifiedPromise = domModel.once(SDK.DOMModel.Events.AttrModified);
65
+
66
+ await inspectedPage.evaluate(() => {
67
+ (document.getElementById('node-set-new-value') as HTMLElement).style.setProperty('color', 'blue');
68
+ });
69
+
70
+ let eventData = await attrModifiedPromise;
71
+ assert.strictEqual(eventData.node, nodeSetNewValue);
72
+ assert.strictEqual(eventData.name, 'style');
73
+ assert.strictEqual(nodeSetNewValue.getAttribute('style'), 'color: blue;');
74
+
75
+ // Verifies that setting the style attribute to the same value does not trigger the AttrModified event.
76
+ let attrModifiedFired = false;
77
+ const listener =
78
+ (event: Common.EventTarget
79
+ .EventTargetEvent<SDK.DOMModel.EventTypes[SDK.DOMModel.Events.AttrModified], SDK.DOMModel.EventTypes>):
80
+ void => {
81
+ if (event.data.node === nodeSetSameValue) {
82
+ attrModifiedFired = true;
83
+ }
84
+ };
85
+ domModel.addEventListener(SDK.DOMModel.Events.AttrModified, listener);
86
+
87
+ await inspectedPage.evaluate(() => {
88
+ (document.getElementById('node-set-same-value') as HTMLElement).style.setProperty('color', 'red');
89
+ });
90
+
91
+ // Flushes pending events by triggering a style change on the new value node.
92
+ attrModifiedPromise = domModel.once(SDK.DOMModel.Events.AttrModified);
93
+
94
+ await inspectedPage.evaluate(() => {
95
+ (document.getElementById('node-set-new-value') as HTMLElement).style.setProperty('color', 'green');
96
+ });
97
+
98
+ eventData = await attrModifiedPromise;
99
+ assert.strictEqual(eventData.node, nodeSetNewValue);
100
+ assert.isFalse(attrModifiedFired, 'AttrModified should not have fired for same value');
101
+
102
+ domModel.removeEventListener(SDK.DOMModel.Events.AttrModified, listener);
103
+ });
104
+ });
@@ -50,6 +50,18 @@ import {SDKModel} from './SDKModel.js';
50
50
  import {Capability, type Target} from './Target.js';
51
51
  import type {TargetManager} from './TargetManager.js';
52
52
 
53
+ export const enum NodeType {
54
+ ELEMENT_NODE = 1,
55
+ ATTRIBUTE_NODE = 2,
56
+ TEXT_NODE = 3,
57
+ CDATA_SECTION_NODE = 4,
58
+ PROCESSING_INSTRUCTION_NODE = 7,
59
+ COMMENT_NODE = 8,
60
+ DOCUMENT_NODE = 9,
61
+ DOCUMENT_TYPE_NODE = 10,
62
+ DOCUMENT_FRAGMENT_NODE = 11,
63
+ }
64
+
53
65
  /** Keep this list in sync with https://w3c.github.io/aria/#state_prop_def **/
54
66
  export const ARIA_ATTRIBUTES = new Set<string>([
55
67
  'role',
@@ -297,7 +309,7 @@ export class DOMNode extends Common.ObjectWrapper.ObjectWrapper<DOMNodeEventType
297
309
  this.#adProvenance = payload.adProvenance;
298
310
  }
299
311
 
300
- if (this.#nodeType === Node.ELEMENT_NODE) {
312
+ if (this.#nodeType === NodeType.ELEMENT_NODE) {
301
313
  // HTML and BODY from internal iframes should not overwrite top-level ones.
302
314
  if (this.ownerDocument && !this.ownerDocument.documentElement && this.#nodeName === 'HTML') {
303
315
  this.ownerDocument.documentElement = this;
@@ -305,11 +317,11 @@ export class DOMNode extends Common.ObjectWrapper.ObjectWrapper<DOMNodeEventType
305
317
  if (this.ownerDocument && !this.ownerDocument.body && this.#nodeName === 'BODY') {
306
318
  this.ownerDocument.body = this;
307
319
  }
308
- } else if (this.#nodeType === Node.DOCUMENT_TYPE_NODE) {
320
+ } else if (this.#nodeType === NodeType.DOCUMENT_TYPE_NODE) {
309
321
  this.publicId = payload.publicId;
310
322
  this.systemId = payload.systemId;
311
323
  this.internalSubset = payload.internalSubset;
312
- } else if (this.#nodeType === Node.ATTRIBUTE_NODE) {
324
+ } else if (this.#nodeType === NodeType.ATTRIBUTE_NODE) {
313
325
  this.name = payload.name;
314
326
  this.value = payload.value;
315
327
  }
@@ -354,7 +366,7 @@ export class DOMNode extends Common.ObjectWrapper.ObjectWrapper<DOMNodeEventType
354
366
  }
355
367
 
356
368
  isRootNode(): boolean {
357
- if (this.nodeType() === Node.ELEMENT_NODE && this.nodeName() === 'HTML') {
369
+ if (this.nodeType() === NodeType.ELEMENT_NODE && this.nodeName() === 'HTML') {
358
370
  return true;
359
371
  }
360
372
  return false;
@@ -762,7 +774,7 @@ export class DOMNode extends Common.ObjectWrapper.ObjectWrapper<DOMNodeEventType
762
774
  if (node.isShadowRoot()) {
763
775
  return node.shadowRootType() === DOMNode.ShadowRootTypes.UserAgent ? 'u' : 'a';
764
776
  }
765
- if (node.nodeType() === Node.DOCUMENT_NODE) {
777
+ if (node.nodeType() === NodeType.DOCUMENT_NODE) {
766
778
  return 'd';
767
779
  }
768
780
  return null;
@@ -1018,7 +1030,7 @@ export class DOMNode extends Common.ObjectWrapper.ObjectWrapper<DOMNodeEventType
1018
1030
  }
1019
1031
 
1020
1032
  isCustomElement(): boolean {
1021
- if (this.nodeType() !== Node.ELEMENT_NODE || this.isXMLNode()) {
1033
+ if (this.nodeType() !== NodeType.ELEMENT_NODE || this.isXMLNode()) {
1022
1034
  return false;
1023
1035
  }
1024
1036
  const localName = this.localName() || this.nodeName().toLowerCase();
@@ -1158,11 +1170,11 @@ export class DOMNode extends Common.ObjectWrapper.ObjectWrapper<DOMNodeEventType
1158
1170
 
1159
1171
  enclosingElementOrSelf(): DOMNode|null {
1160
1172
  let node: DOMNode|null = this;
1161
- if (node && node.nodeType() === Node.TEXT_NODE && node.parentNode) {
1173
+ if (node && node.nodeType() === NodeType.TEXT_NODE && node.parentNode) {
1162
1174
  node = node.parentNode;
1163
1175
  }
1164
1176
 
1165
- if (node && node.nodeType() !== Node.ELEMENT_NODE) {
1177
+ if (node && node.nodeType() !== NodeType.ELEMENT_NODE) {
1166
1178
  node = null;
1167
1179
  }
1168
1180
  return node;
@@ -1223,7 +1235,7 @@ export class DOMNode extends Common.ObjectWrapper.ObjectWrapper<DOMNodeEventType
1223
1235
 
1224
1236
  simpleSelector(): string {
1225
1237
  const lowerCaseName = this.localName() || this.nodeName().toLowerCase();
1226
- if (this.nodeType() !== Node.ELEMENT_NODE) {
1238
+ if (this.nodeType() !== NodeType.ELEMENT_NODE) {
1227
1239
  return lowerCaseName;
1228
1240
  }
1229
1241
  const type = this.getAttribute('type');
@@ -1474,7 +1486,7 @@ export class DOMModel extends SDKModel<EventTypes> {
1474
1486
  #lastMutationId!: number;
1475
1487
  #pendingDocumentRequestPromise: Promise<DOMDocument|null>|null = null;
1476
1488
  #frameOwnerNode?: DOMNode|null;
1477
- #loadNodeAttributesTimeout?: number;
1489
+ #loadNodeAttributesTimeout?: ReturnType<typeof setTimeout>;
1478
1490
  #searchId?: string;
1479
1491
  #topLayerThrottler = new Common.Throttler.Throttler(100);
1480
1492
  #topLayerNodes: DOMNode[] = [];
@@ -1663,7 +1675,7 @@ export class DOMModel extends SDKModel<EventTypes> {
1663
1675
  inlineStyleInvalidated(nodeIds: Protocol.DOM.NodeId[]): void {
1664
1676
  nodeIds.forEach(nodeId => this.#attributeLoadNodeIds.add(nodeId));
1665
1677
  if (!this.#loadNodeAttributesTimeout) {
1666
- this.#loadNodeAttributesTimeout = window.setTimeout(this.loadNodeAttributes.bind(this), 20);
1678
+ this.#loadNodeAttributesTimeout = globalThis.setTimeout(this.loadNodeAttributes.bind(this), 20);
1667
1679
  }
1668
1680
  }
1669
1681
 
@@ -15,6 +15,7 @@ import {Events as ResourceTreeModelEvents, ResourceTreeModel} from './ResourceTr
15
15
  import {type EvaluationOptions, type EvaluationResult, type ExecutionContext, RuntimeModel} from './RuntimeModel.js';
16
16
  import {Script} from './Script.js';
17
17
  import {SDKModel} from './SDKModel.js';
18
+ import {jsSourceMapsEnabledSettingDescriptor} from './SDKSettings.js';
18
19
  import {SourceMap} from './SourceMap.js';
19
20
  import {SourceMapManager} from './SourceMapManager.js';
20
21
  import {Capability, type Target} from './Target.js';
@@ -144,7 +145,7 @@ export class DebuggerModel extends SDKModel<EventTypes> {
144
145
  #selectedCallFrame: CallFrame|null = null;
145
146
  #debuggerEnabled = false;
146
147
  #debuggerId: string|null = null;
147
- #skipAllPausesTimeout = 0;
148
+ #skipAllPausesTimeout?: ReturnType<typeof setTimeout>;
148
149
  #beforePausedCallback: ((arg0: DebuggerPausedDetails, stepOver: Location|null) => Promise<boolean>)|null = null;
149
150
  #computeAutoStepRangesCallback: ((arg0: StepMode, arg1: CallFrame) => Promise<Array<{
150
151
  start: Location,
@@ -184,8 +185,8 @@ export class DebuggerModel extends SDKModel<EventTypes> {
184
185
  void this.enableDebugger();
185
186
  }
186
187
 
187
- this.#sourceMapManager.setEnabled(settings.moduleSetting('js-source-maps-enabled').get());
188
- settings.moduleSetting('js-source-maps-enabled')
188
+ this.#sourceMapManager.setEnabled(settings.resolve(jsSourceMapsEnabledSettingDescriptor).get());
189
+ settings.resolve(jsSourceMapsEnabledSettingDescriptor)
189
190
  .addChangeListener(event => this.#sourceMapManager.setEnabled((event.data as boolean)));
190
191
 
191
192
  const resourceTreeModel = (target.model(ResourceTreeModel) as ResourceTreeModel);
@@ -337,20 +338,20 @@ export class DebuggerModel extends SDKModel<EventTypes> {
337
338
  }
338
339
 
339
340
  private skipAllPauses(skip: boolean): void {
340
- if (this.#skipAllPausesTimeout) {
341
- clearTimeout(this.#skipAllPausesTimeout);
342
- this.#skipAllPausesTimeout = 0;
343
- }
341
+ clearTimeout(this.#skipAllPausesTimeout);
342
+
344
343
  void this.agent.invoke_setSkipAllPauses({skip});
345
344
  }
346
345
 
347
346
  skipAllPausesUntilReloadOrTimeout(timeout: number): void {
348
- if (this.#skipAllPausesTimeout) {
349
- clearTimeout(this.#skipAllPausesTimeout);
350
- }
347
+ clearTimeout(this.#skipAllPausesTimeout);
348
+
351
349
  void this.agent.invoke_setSkipAllPauses({skip: true});
352
350
  // If reload happens before the timeout, the flag will be already unset and the timeout callback won't change anything.
353
- this.#skipAllPausesTimeout = window.setTimeout(this.skipAllPauses.bind(this, false), timeout);
351
+ this.#skipAllPausesTimeout = globalThis.setTimeout(
352
+ this.skipAllPauses.bind(this, false),
353
+ timeout,
354
+ );
354
355
  }
355
356
 
356
357
  private pauseOnExceptionStateChanged(): void {
@@ -121,7 +121,7 @@ export class IsolateManager extends Common.ObjectWrapper.ObjectWrapper<EventType
121
121
  const pollId = this.#pollId;
122
122
  while (pollId === this.#pollId) {
123
123
  await Promise.all(Array.from(this.isolates(), isolate => isolate.update()));
124
- await new Promise(r => window.setTimeout(r, PollIntervalMs));
124
+ await new Promise(r => globalThis.setTimeout(r, PollIntervalMs));
125
125
  }
126
126
  }
127
127
  }
@@ -5,6 +5,7 @@
5
5
  import type * as ProtocolProxyApi from '../../generated/protocol-proxy-api.js';
6
6
  import * as Protocol from '../../generated/protocol.js';
7
7
  import * as Common from '../common/common.js';
8
+ import * as Host from '../host/host.js';
8
9
  import * as i18n from '../i18n/i18n.js';
9
10
  import * as Platform from '../platform/platform.js';
10
11
  import * as Root from '../root/root.js';
@@ -23,6 +24,7 @@ import {
23
24
  type NameValue,
24
25
  NetworkRequest,
25
26
  } from './NetworkRequest.js';
27
+ import {type ExecutionContext, RuntimeModel} from './RuntimeModel.js';
26
28
  import {SDKModel} from './SDKModel.js';
27
29
  import {Capability, type Target} from './Target.js';
28
30
  import {type SDKModelObserver, TargetManager} from './TargetManager.js';
@@ -116,6 +118,22 @@ const i18nLazyString = i18n.i18n.getLazilyComputedLocalizedString.bind(undefined
116
118
 
117
119
  const requestToManagerMap = new WeakMap<NetworkRequest, NetworkManager>();
118
120
 
121
+ /** Resource types eligible for resend with full fidelity. */
122
+ const FULL_FIDELITY_RESEND_TYPES = new Set([
123
+ Common.ResourceType.resourceTypes.XHR,
124
+ Common.ResourceType.resourceTypes.Fetch,
125
+ Common.ResourceType.resourceTypes.Script,
126
+ Common.ResourceType.resourceTypes.Stylesheet,
127
+ Common.ResourceType.resourceTypes.Image,
128
+ Common.ResourceType.resourceTypes.Media,
129
+ Common.ResourceType.resourceTypes.Font,
130
+ Common.ResourceType.resourceTypes.Wasm,
131
+ Common.ResourceType.resourceTypes.Manifest,
132
+ Common.ResourceType.resourceTypes.TextTrack,
133
+ Common.ResourceType.resourceTypes.SourceMapScript,
134
+ Common.ResourceType.resourceTypes.SourceMapStyleSheet,
135
+ ]);
136
+
119
137
  const CONNECTION_TYPES = new Map([
120
138
  ['2g', Protocol.Network.ConnectionType.Cellular2g],
121
139
  ['3g', Protocol.Network.ConnectionType.Cellular3g],
@@ -193,18 +211,101 @@ export class NetworkManager extends SDKModel<EventTypes> {
193
211
  return requestToManagerMap.get(request) || null;
194
212
  }
195
213
 
196
- static canReplayRequest(request: NetworkRequest): boolean {
197
- return Boolean(requestToManagerMap.get(request)) && Boolean(request.backendRequestId()) && !request.isRedirect() &&
198
- request.resourceType() === Common.ResourceType.resourceTypes.XHR;
214
+ static canResendRequest(request: NetworkRequest): boolean {
215
+ if (!requestToManagerMap.get(request) || !request.backendRequestId() || request.isRedirect()) {
216
+ return false;
217
+ }
218
+ return FULL_FIDELITY_RESEND_TYPES.has(request.resourceType());
199
219
  }
200
220
 
201
221
  static replayRequest(request: NetworkRequest): void {
222
+ void NetworkManager.resendRequest(request);
223
+ }
224
+
225
+ static async resendRequest(request: NetworkRequest): Promise<void> {
202
226
  const manager = requestToManagerMap.get(request);
203
227
  const requestId = request.backendRequestId();
204
228
  if (!manager || !requestId || request.isRedirect()) {
205
229
  return;
206
230
  }
207
- void manager.#networkAgent.invoke_replayXHR({requestId});
231
+
232
+ Host.userMetrics.resendRequest(Host.UserMetrics.resendRequestType(request.resourceType()));
233
+
234
+ // XHR requests use the existing CDP replay mechanism.
235
+ if (request.resourceType() === Common.ResourceType.resourceTypes.XHR) {
236
+ void manager.#networkAgent.invoke_replayXHR({requestId});
237
+ return;
238
+ }
239
+
240
+ // All other eligible types use fetch via Runtime.evaluate.
241
+ const target = manager.target();
242
+ const runtimeModel = target.model(RuntimeModel);
243
+ if (!runtimeModel) {
244
+ return;
245
+ }
246
+
247
+ // Resolve execution context: prefer the frame's default context.
248
+ let executionContext: ExecutionContext|null = null;
249
+ const frameId = request.frameId;
250
+ if (frameId) {
251
+ executionContext = runtimeModel.executionContexts().find(ctx => ctx.frameId === frameId && ctx.isDefault) ?? null;
252
+ }
253
+ const usesFallbackContext = !executionContext;
254
+ if (!executionContext) {
255
+ executionContext = runtimeModel.defaultExecutionContext();
256
+ }
257
+ if (!executionContext) {
258
+ return;
259
+ }
260
+
261
+ if (usesFallbackContext) {
262
+ runtimeModel.target().targetManager().getConsole().warn(
263
+ 'Resend: original execution context unavailable, using top-level context.');
264
+ }
265
+
266
+ // Build the fetch expression.
267
+ const method = request.requestMethod;
268
+ const url = request.url();
269
+ const headers: Array<[string, string]> = [];
270
+ for (const {name, value} of request.requestHeaders()) {
271
+ // Skip HTTP/2+ pseudo-headers (e.g. :authority, :method, :path, :scheme).
272
+ if (name.startsWith(':')) {
273
+ continue;
274
+ }
275
+ // Skip headers the browser sets automatically for fetch.
276
+ const lower = name.toLowerCase();
277
+ if (lower === 'host' || lower === 'connection' || lower === 'content-length' || lower === 'cookie' ||
278
+ lower === 'origin' || lower === 'referer') {
279
+ continue;
280
+ }
281
+ headers.push([name, value]);
282
+ }
283
+
284
+ const body = await request.requestFormData();
285
+ const fetchOptions: {method: string, headers: Array<[string, string]>, credentials: string, body?: string} = {
286
+ method,
287
+ headers,
288
+ credentials: 'include',
289
+ };
290
+ const isGetOrHead = method === 'GET' || method === 'HEAD';
291
+ if (body && !isGetOrHead) {
292
+ fetchOptions.body = body;
293
+ }
294
+
295
+ const expression = `fetch(${JSON.stringify(url)}, ${JSON.stringify(fetchOptions)})`;
296
+ const response = await target.runtimeAgent().invoke_evaluate({
297
+ expression,
298
+ // Use uniqueContextId if available, otherwise fall back to contextId.
299
+ ...(executionContext.uniqueId ? {uniqueContextId: executionContext.uniqueId} : {contextId: executionContext.id}),
300
+ silent: false,
301
+ awaitPromise: true,
302
+ });
303
+
304
+ if (response.getError() || response.exceptionDetails) {
305
+ const errorText = response.getError() || response.exceptionDetails?.exception?.description ||
306
+ response.exceptionDetails?.text || 'Unknown error';
307
+ runtimeModel.target().targetManager().getConsole().error(`Resend failed for ${url}: ${errorText}`);
308
+ }
208
309
  }
209
310
 
210
311
  static async searchInRequest(request: NetworkRequest, query: string, caseSensitive: boolean, isRegex: boolean):
@@ -86,7 +86,7 @@ export class OverlayModel extends SDKModel<EventTypes> implements ProtocolProxyA
86
86
  overlayAgent: ProtocolProxyApi.OverlayApi;
87
87
  readonly #debuggerModel: DebuggerModel|null;
88
88
  #inspectModeEnabled = false;
89
- #hideHighlightTimeout: number|null = null;
89
+ #hideHighlightTimeout?: ReturnType<typeof setTimeout>;
90
90
  #defaultHighlighter: Highlighter;
91
91
  #highlighter: Highlighter;
92
92
  #showPaintRectsSetting: Common.Settings.Setting<boolean>;
@@ -331,14 +331,13 @@ export class OverlayModel extends SDKModel<EventTypes> implements ProtocolProxyA
331
331
  // overlay, so that it is not cleared by the highlight
332
332
  return;
333
333
  }
334
- if (this.#hideHighlightTimeout) {
335
334
  clearTimeout(this.#hideHighlightTimeout);
336
- this.#hideHighlightTimeout = null;
337
- }
338
- const highlightConfig = this.buildHighlightConfig(mode);
339
- if (typeof showInfo !== 'undefined') {
340
- highlightConfig.showInfo = showInfo;
341
- }
335
+ this.#hideHighlightTimeout = undefined;
336
+
337
+ const highlightConfig = this.buildHighlightConfig(mode);
338
+ if (typeof showInfo !== 'undefined') {
339
+ highlightConfig.showInfo = showInfo;
340
+ }
342
341
  this.#highlighter.highlightInOverlay(data, highlightConfig);
343
342
  }
344
343
 
@@ -486,17 +485,19 @@ export class OverlayModel extends SDKModel<EventTypes> implements ProtocolProxyA
486
485
  }
487
486
 
488
487
  private delayedHideHighlight(delay: number): void {
489
- if (this.#hideHighlightTimeout === null) {
490
- this.#hideHighlightTimeout = window.setTimeout(() => this.highlightInOverlay({clear: true}), delay);
488
+ if (this.#hideHighlightTimeout === undefined) {
489
+ this.#hideHighlightTimeout = globalThis.setTimeout(
490
+ () => this.highlightInOverlay({clear: true}),
491
+ delay,
492
+ );
491
493
  }
492
494
  }
493
495
 
494
496
  highlightFrame(frameId: Protocol.Page.FrameId): void {
495
- if (this.#hideHighlightTimeout) {
496
497
  clearTimeout(this.#hideHighlightTimeout);
497
- this.#hideHighlightTimeout = null;
498
- }
499
- this.#highlighter.highlightFrame(frameId);
498
+ this.#hideHighlightTimeout = undefined;
499
+
500
+ this.#highlighter.highlightFrame(frameId);
500
501
  }
501
502
 
502
503
  showHingeForDualScreen(hinge: Hinge|null): void {
@@ -868,7 +868,7 @@ export class LocalJSONObject extends RemoteObject {
868
868
  Promise<GetPropertiesResult> {
869
869
  function isArrayIndex(name: string): boolean {
870
870
  const index = Number(name) >>> 0;
871
- return String(index) === name;
871
+ return String(index) === name && index < 4294967295;
872
872
  }
873
873
 
874
874
  let properties = this.children();
@@ -0,0 +1,19 @@
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 Common from '../common/common.js';
6
+
7
+ export const jsSourceMapsEnabledSettingDescriptor: Common.Settings.SettingDescriptor<boolean> = {
8
+ name: 'js-source-maps-enabled',
9
+ type: Common.Settings.SettingType.BOOLEAN,
10
+ defaultValue: true,
11
+ storageType: Common.Settings.SettingStorageType.SYNCED,
12
+ };
13
+
14
+ export const cssSourceMapsEnabledSettingDescriptor: Common.Settings.SettingDescriptor<boolean> = {
15
+ name: 'css-source-maps-enabled',
16
+ type: Common.Settings.SettingType.BOOLEAN,
17
+ defaultValue: true,
18
+ storageType: Common.Settings.SettingStorageType.SYNCED,
19
+ };