chrome-devtools-frontend 1.0.1585664 → 1.0.1587572

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 (48) hide show
  1. package/front_end/core/common/Srcset.ts +61 -0
  2. package/front_end/core/common/common.ts +2 -0
  3. package/front_end/entrypoint_template.html +5 -1
  4. package/front_end/generated/InspectorBackendCommands.ts +1 -1
  5. package/front_end/generated/protocol.ts +2 -0
  6. package/front_end/models/ai_assistance/AiConversation.ts +6 -2
  7. package/front_end/models/javascript_metadata/NativeFunctions.js +2 -2
  8. package/front_end/panels/ai_assistance/AiAssistancePanel.ts +1 -0
  9. package/front_end/panels/application/ServiceWorkerCacheViews.ts +1 -2
  10. package/front_end/panels/console/PromptBuilder.ts +15 -3
  11. package/front_end/panels/elements/ElementsPanel.ts +114 -15
  12. package/front_end/panels/elements/ElementsTreeElement.ts +4 -56
  13. package/front_end/panels/network/NetworkItemView.ts +4 -3
  14. package/front_end/panels/network/{components/RequestHeadersView.ts → RequestHeadersView.ts} +112 -184
  15. package/front_end/panels/network/components/RequestHeaderSection.ts +3 -0
  16. package/front_end/panels/network/components/components.ts +0 -2
  17. package/front_end/panels/network/network.ts +3 -0
  18. package/front_end/panels/sources/CallStackSidebarPane.ts +3 -7
  19. package/front_end/panels/utils/utils.ts +13 -5
  20. package/front_end/third_party/chromium/README.chromium +1 -1
  21. package/front_end/third_party/puppeteer/README.chromium +2 -2
  22. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/cdp/FrameManager.js +1 -1
  23. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/cdp/FrameManager.js.map +1 -1
  24. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/common/util.d.ts.map +1 -1
  25. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/common/util.js +7 -7
  26. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/common/util.js.map +1 -1
  27. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/revisions.d.ts +3 -3
  28. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/revisions.js +3 -3
  29. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/util/version.d.ts +1 -1
  30. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/util/version.js +1 -1
  31. package/front_end/third_party/puppeteer/package/lib/es5-iife/puppeteer-core-browser.js +7 -7
  32. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/cdp/FrameManager.js +1 -1
  33. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/cdp/FrameManager.js.map +1 -1
  34. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/common/util.d.ts.map +1 -1
  35. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/common/util.js +7 -7
  36. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/common/util.js.map +1 -1
  37. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/revisions.d.ts +3 -3
  38. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/revisions.js +3 -3
  39. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/util/version.d.ts +1 -1
  40. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/util/version.js +1 -1
  41. package/front_end/third_party/puppeteer/package/package.json +2 -2
  42. package/front_end/third_party/puppeteer/package/src/cdp/FrameManager.ts +1 -1
  43. package/front_end/third_party/puppeteer/package/src/common/util.ts +9 -8
  44. package/front_end/third_party/puppeteer/package/src/revisions.ts +3 -3
  45. package/front_end/third_party/puppeteer/package/src/util/version.ts +1 -1
  46. package/front_end/ui/legacy/UIUtils.ts +31 -14
  47. package/front_end/ui/legacy/components/utils/JSPresentationUtils.ts +135 -142
  48. package/package.json +1 -1
@@ -0,0 +1,61 @@
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
+ export const enum TokenType {
6
+ LITERAL = 0,
7
+ URL = 1
8
+ }
9
+
10
+ export interface Token {
11
+ type: TokenType;
12
+ value: string;
13
+ }
14
+
15
+ /**
16
+ * Parsing of
17
+ * https://html.spec.whatwg.org/multipage/images.html#srcset-attribute and href
18
+ * attributes to identify URLs vs other text in the srcset.
19
+ *
20
+ * Note: this is probably not spec compliant.
21
+ */
22
+ export function parseSrcset(value: string): Token[] {
23
+ const result: Token[] = [];
24
+ let i = 0;
25
+ while (value.length) {
26
+ if (i++ > 0) {
27
+ result.push({value: ' ', type: TokenType.LITERAL});
28
+ }
29
+ value = value.trim();
30
+ let url = '';
31
+ let descriptor = '';
32
+ const indexOfSpace = value.search(/\s/);
33
+ if (indexOfSpace === -1) {
34
+ url = value;
35
+ } else if (indexOfSpace > 0 && value[indexOfSpace - 1] === ',') {
36
+ url = value.substring(0, indexOfSpace);
37
+ } else {
38
+ url = value.substring(0, indexOfSpace);
39
+ const indexOfComma = value.indexOf(',', indexOfSpace);
40
+ if (indexOfComma !== -1) {
41
+ descriptor = value.substring(indexOfSpace, indexOfComma + 1);
42
+ } else {
43
+ descriptor = value.substring(indexOfSpace);
44
+ }
45
+ }
46
+
47
+ if (url) {
48
+ if (url.endsWith(',')) {
49
+ result.push({value: url.substring(0, url.length - 1), type: TokenType.URL});
50
+ result.push({type: TokenType.LITERAL, value: ','});
51
+ } else {
52
+ result.push({value: url, type: TokenType.URL});
53
+ }
54
+ }
55
+ if (descriptor) {
56
+ result.push({type: TokenType.LITERAL, value: descriptor});
57
+ }
58
+ value = value.substring(url.length + descriptor.length);
59
+ }
60
+ return result;
61
+ }
@@ -29,6 +29,7 @@ import * as SegmentedRange from './SegmentedRange.js';
29
29
  import * as SettingRegistration from './SettingRegistration.js';
30
30
  import * as Settings from './Settings.js';
31
31
  import * as SimpleHistoryManager from './SimpleHistoryManager.js';
32
+ import * as Srcset from './Srcset.js';
32
33
  import * as StringOutputStream from './StringOutputStream.js';
33
34
  import * as TextDictionary from './TextDictionary.js';
34
35
  import * as Throttler from './Throttler.js';
@@ -68,6 +69,7 @@ export {
68
69
  SettingRegistration,
69
70
  Settings,
70
71
  SimpleHistoryManager,
72
+ Srcset,
71
73
  StringOutputStream,
72
74
  TextDictionary,
73
75
  Throttler,
@@ -14,7 +14,11 @@
14
14
  }
15
15
  }
16
16
  </style>
17
- <meta http-equiv="Content-Security-Policy" content="object-src 'none'; script-src 'self' https://chrome-devtools-frontend.appspot.com">
17
+ <meta
18
+ http-equiv="Content-Security-Policy"
19
+ content="default-src 'self' devtools: data:; style-src 'self' 'unsafe-inline' devtools:; object-src 'none'; script-src
20
+ 'self' https://chrome-devtools-frontend.appspot.com; img-src 'self' data:; frame-src * data:; connect-src data:
21
+ https://chromeuxreport.googleapis.com 'self' devtools:;">
18
22
  <meta name="referrer" content="no-referrer">
19
23
  <script type="module" src="./entrypoints/%ENTRYPOINT_NAME%/%ENTRYPOINT_NAME%.js"></script>
20
24
  <link href="./application_tokens.css" rel="stylesheet">
@@ -857,7 +857,7 @@ inspectorBackend.registerCommand("Network.disable", [], [], "Disables network tr
857
857
  inspectorBackend.registerCommand("Network.emulateNetworkConditions", [{"name": "offline", "type": "boolean", "optional": false, "description": "True to emulate internet disconnection.", "typeRef": null}, {"name": "latency", "type": "number", "optional": false, "description": "Minimum latency from request sent to response headers received (ms).", "typeRef": null}, {"name": "downloadThroughput", "type": "number", "optional": false, "description": "Maximal aggregated download throughput (bytes/sec). -1 disables download throttling.", "typeRef": null}, {"name": "uploadThroughput", "type": "number", "optional": false, "description": "Maximal aggregated upload throughput (bytes/sec). -1 disables upload throttling.", "typeRef": null}, {"name": "connectionType", "type": "string", "optional": true, "description": "Connection type if known.", "typeRef": "Network.ConnectionType"}, {"name": "packetLoss", "type": "number", "optional": true, "description": "WebRTC packet loss (percent, 0-100). 0 disables packet loss emulation, 100 drops all the packets.", "typeRef": null}, {"name": "packetQueueLength", "type": "number", "optional": true, "description": "WebRTC packet queue length (packet). 0 removes any queue length limitations.", "typeRef": null}, {"name": "packetReordering", "type": "boolean", "optional": true, "description": "WebRTC packetReordering feature.", "typeRef": null}], [], "Activates emulation of network conditions. This command is deprecated in favor of the emulateNetworkConditionsByRule and overrideNetworkState commands, which can be used together to the same effect.");
858
858
  inspectorBackend.registerCommand("Network.emulateNetworkConditionsByRule", [{"name": "offline", "type": "boolean", "optional": false, "description": "True to emulate internet disconnection.", "typeRef": null}, {"name": "matchedNetworkConditions", "type": "array", "optional": false, "description": "Configure conditions for matching requests. If multiple entries match a request, the first entry wins. Global conditions can be configured by leaving the urlPattern for the conditions empty. These global conditions are also applied for throttling of p2p connections.", "typeRef": "Network.NetworkConditions"}], ["ruleIds"], "Activates emulation of network conditions for individual requests using URL match patterns. Unlike the deprecated Network.emulateNetworkConditions this method does not affect `navigator` state. Use Network.overrideNetworkState to explicitly modify `navigator` behavior.");
859
859
  inspectorBackend.registerCommand("Network.overrideNetworkState", [{"name": "offline", "type": "boolean", "optional": false, "description": "True to emulate internet disconnection.", "typeRef": null}, {"name": "latency", "type": "number", "optional": false, "description": "Minimum latency from request sent to response headers received (ms).", "typeRef": null}, {"name": "downloadThroughput", "type": "number", "optional": false, "description": "Maximal aggregated download throughput (bytes/sec). -1 disables download throttling.", "typeRef": null}, {"name": "uploadThroughput", "type": "number", "optional": false, "description": "Maximal aggregated upload throughput (bytes/sec). -1 disables upload throttling.", "typeRef": null}, {"name": "connectionType", "type": "string", "optional": true, "description": "Connection type if known.", "typeRef": "Network.ConnectionType"}], [], "Override the state of navigator.onLine and navigator.connection.");
860
- inspectorBackend.registerCommand("Network.enable", [{"name": "maxTotalBufferSize", "type": "number", "optional": true, "description": "Buffer size in bytes to use when preserving network payloads (XHRs, etc).", "typeRef": null}, {"name": "maxResourceBufferSize", "type": "number", "optional": true, "description": "Per-resource buffer size in bytes to use when preserving network payloads (XHRs, etc).", "typeRef": null}, {"name": "maxPostDataSize", "type": "number", "optional": true, "description": "Longest post body size (in bytes) that would be included in requestWillBeSent notification", "typeRef": null}, {"name": "reportDirectSocketTraffic", "type": "boolean", "optional": true, "description": "Whether DirectSocket chunk send/receive events should be reported.", "typeRef": null}, {"name": "enableDurableMessages", "type": "boolean", "optional": true, "description": "Enable storing response bodies outside of renderer, so that these survive a cross-process navigation. Requires maxTotalBufferSize to be set. Currently defaults to false. This field is being deprecated in favor of the dedicated configureDurableMessages command, due to the possibility of deadlocks when awaiting Network.enable before issuing Runtime.runIfWaitingForDebugger.", "typeRef": null}], [], "Enables network tracking, network events will now be delivered to the client.");
860
+ inspectorBackend.registerCommand("Network.enable", [{"name": "maxTotalBufferSize", "type": "number", "optional": true, "description": "Buffer size in bytes to use when preserving network payloads (XHRs, etc). This is the maximum number of bytes that will be collected by this DevTools session.", "typeRef": null}, {"name": "maxResourceBufferSize", "type": "number", "optional": true, "description": "Per-resource buffer size in bytes to use when preserving network payloads (XHRs, etc).", "typeRef": null}, {"name": "maxPostDataSize", "type": "number", "optional": true, "description": "Longest post body size (in bytes) that would be included in requestWillBeSent notification", "typeRef": null}, {"name": "reportDirectSocketTraffic", "type": "boolean", "optional": true, "description": "Whether DirectSocket chunk send/receive events should be reported.", "typeRef": null}, {"name": "enableDurableMessages", "type": "boolean", "optional": true, "description": "Enable storing response bodies outside of renderer, so that these survive a cross-process navigation. Requires maxTotalBufferSize to be set. Currently defaults to false. This field is being deprecated in favor of the dedicated configureDurableMessages command, due to the possibility of deadlocks when awaiting Network.enable before issuing Runtime.runIfWaitingForDebugger.", "typeRef": null}], [], "Enables network tracking, network events will now be delivered to the client.");
861
861
  inspectorBackend.registerCommand("Network.configureDurableMessages", [{"name": "maxTotalBufferSize", "type": "number", "optional": true, "description": "Buffer size in bytes to use when preserving network payloads (XHRs, etc).", "typeRef": null}, {"name": "maxResourceBufferSize", "type": "number", "optional": true, "description": "Per-resource buffer size in bytes to use when preserving network payloads (XHRs, etc).", "typeRef": null}], [], "Configures storing response bodies outside of renderer, so that these survive a cross-process navigation. If maxTotalBufferSize is not set, durable messages are disabled.");
862
862
  inspectorBackend.registerCommand("Network.getAllCookies", [], ["cookies"], "Returns all browser cookies. Depending on the backend support, will return detailed cookie information in the `cookies` field. Deprecated. Use Storage.getCookies instead.");
863
863
  inspectorBackend.registerCommand("Network.getCertificate", [{"name": "origin", "type": "string", "optional": false, "description": "Origin to get certificate for.", "typeRef": null}], ["tableNames"], "Returns the DER-encoded certificate.");
@@ -11896,6 +11896,8 @@ export namespace Network {
11896
11896
  export interface EnableRequest {
11897
11897
  /**
11898
11898
  * Buffer size in bytes to use when preserving network payloads (XHRs, etc).
11899
+ * This is the maximum number of bytes that will be collected by this
11900
+ * DevTools session.
11899
11901
  */
11900
11902
  maxTotalBufferSize?: integer;
11901
11903
  /**
@@ -456,16 +456,20 @@ Time: ${micros(time)}`;
456
456
  },
457
457
  options.multimodalInput,
458
458
  )) {
459
+ // Add to history if relevant
459
460
  if (shouldAddToHistory(data)) {
460
461
  void this.addHistoryItem(data);
461
462
  }
463
+ // Always yield the data
464
+ yield data;
465
+
466
+ // If we change the context
467
+ // requery with the specialized agent.
462
468
  if (data.type === ResponseType.CONTEXT_CHANGE) {
463
469
  this.setContext(data.context);
464
470
  yield* this.#runAgent(initialQuery, options);
465
471
  return;
466
472
  }
467
-
468
- yield data;
469
473
  }
470
474
  }
471
475
 
@@ -6624,8 +6624,8 @@ export const NativeFunctions = [
6624
6624
  signatures: [["type","?eventInitDict"]]
6625
6625
  },
6626
6626
  {
6627
- name: "setFormControlRange",
6628
- signatures: [["element","start","end"]]
6627
+ name: "getValueRange",
6628
+ signatures: [["start","end"]]
6629
6629
  },
6630
6630
  {
6631
6631
  name: "getBoxQuads",
@@ -1621,6 +1621,7 @@ export class AiAssistancePanel extends UI.Panel.Panel {
1621
1621
  this.#handleConversationContextChange(data.context);
1622
1622
  step.isLoading = false;
1623
1623
  commitStep();
1624
+ step = {isLoading: true};
1624
1625
 
1625
1626
  break;
1626
1627
  }
@@ -15,7 +15,6 @@ import * as LegacyWrapper from '../../ui/components/legacy_wrapper/legacy_wrappe
15
15
  import * as DataGrid from '../../ui/legacy/components/data_grid/data_grid.js';
16
16
  import * as UI from '../../ui/legacy/legacy.js';
17
17
  import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';
18
- import * as NetworkComponents from '../network/components/components.js';
19
18
  import * as Network from '../network/network.js';
20
19
 
21
20
  import * as ApplicationComponents from './components/components.js';
@@ -545,7 +544,7 @@ export class RequestView extends UI.Widget.VBox {
545
544
  this.tabbedPane.appendTab(
546
545
  'headers', i18nString(UIStrings.headers),
547
546
  LegacyWrapper.LegacyWrapper.legacyWrapper(
548
- UI.Widget.VBox, new NetworkComponents.RequestHeadersView.RequestHeadersView(request)));
547
+ UI.Widget.VBox, new Network.RequestHeadersView.RequestHeadersView(request)));
549
548
  this.tabbedPane.appendTab(
550
549
  'preview', i18nString(UIStrings.preview), new Network.RequestPreviewView.RequestPreviewView(request));
551
550
  this.tabbedPane.show(this.element);
@@ -9,6 +9,7 @@ import * as Formatter from '../../models/formatter/formatter.js';
9
9
  import * as Logs from '../../models/logs/logs.js';
10
10
  import * as TextUtils from '../../models/text_utils/text_utils.js';
11
11
  import * as Components from '../../ui/legacy/components/utils/utils.js';
12
+ import * as UI from '../../ui/legacy/legacy.js';
12
13
 
13
14
  import type {ConsoleViewMessage} from './ConsoleViewMessage.js';
14
15
 
@@ -85,7 +86,7 @@ export class PromptBuilder {
85
86
 
86
87
  const relatedCode = sourceCode?.text ? formatRelatedCode(sourceCode) : '';
87
88
  const relatedRequest = request ? formatNetworkRequest(request) : '';
88
- const stacktrace = sourcesTypes.includes(SourceType.STACKTRACE) ? formatStackTrace(this.#consoleMessage) : '';
89
+ const stacktrace = sourcesTypes.includes(SourceType.STACKTRACE) ? await formatStackTrace(this.#consoleMessage) : '';
89
90
 
90
91
  const message = formatConsoleMessage(this.#consoleMessage);
91
92
 
@@ -290,14 +291,25 @@ export function formatConsoleMessage(message: ConsoleViewMessage): string {
290
291
  * This formats the stacktrace from the console message which might or might not
291
292
  * match the content of stacktrace(s) in the console message arguments.
292
293
  */
293
- export function formatStackTrace(message: ConsoleViewMessage): string {
294
+ async function formatStackTrace(message: ConsoleViewMessage): Promise<string> {
294
295
  const previewContainer = message.contentElement().querySelector('.stack-preview-container');
295
296
 
296
297
  if (!previewContainer) {
297
298
  return '';
298
299
  }
299
300
 
300
- const preview = previewContainer.shadowRoot?.querySelector('.stack-preview-container') as HTMLElement;
301
+ const widget =
302
+ UI.Widget.Widget.get(previewContainer) as Components.JSPresentationUtils.StackTracePreviewContent | undefined;
303
+ if (!widget) {
304
+ return '';
305
+ }
306
+
307
+ await widget.updateComplete;
308
+
309
+ // TODO(crbug.com/433162438): Get the `StackTrace` from the widget and render that instead
310
+ // of crawling the DOM. `StackTrace` is source mapped and has ignore listing.
311
+
312
+ const preview = widget.contentElement.querySelector('.stack-preview-container') as HTMLElement;
301
313
 
302
314
  const nodes = preview.childTextNodes();
303
315
  // Gets application-level source mapped stack trace taking the ignore list
@@ -227,6 +227,7 @@ export class ElementsPanel extends UI.Panel.Panel implements UI.SearchableView.S
227
227
  domTreeButton?: HTMLElement;
228
228
  private selectedNodeOnReset?: SDK.DOMModel.DOMNode;
229
229
  private hasNonDefaultSelectedNode?: boolean;
230
+ #restorationGeneration = 0;
230
231
  private searchConfig?: UI.SearchableView.SearchConfig;
231
232
  private omitDefaultSelection?: boolean;
232
233
  private notFirstInspectElement?: boolean;
@@ -609,6 +610,7 @@ export class ElementsPanel extends UI.Panel.Panel implements UI.SearchableView.S
609
610
  if (focus) {
610
611
  this.selectedNodeOnReset = selectedNode;
611
612
  this.hasNonDefaultSelectedNode = true;
613
+ this.#restorationGeneration++;
612
614
  }
613
615
 
614
616
  const executionContexts = selectedNode.domModel().runtimeModel().executionContexts();
@@ -645,32 +647,128 @@ export class ElementsPanel extends UI.Panel.Panel implements UI.SearchableView.S
645
647
  }
646
648
 
647
649
  const savedSelectedNodeOnReset = this.selectedNodeOnReset;
648
- void restoreNode.call(this, domModel, this.selectedNodeOnReset || null);
650
+ void this.restoreSelectedNodeAfterUpdate(domModel, this.selectedNodeOnReset || null, savedSelectedNodeOnReset);
651
+ }
652
+
653
+ /**
654
+ * Best-effort restoration of the previously focused node after a reload.
655
+ *
656
+ * The CDP path-based mechanism works well for stable DOMs, but can be
657
+ * unreliable for pages that render asynchronously after the initial
658
+ * document update. To improve reliability we retry a few times, and also
659
+ * fall back to evaluating a JS path (document.querySelector(...)) when
660
+ * possible.
661
+ *
662
+ * Node resolution (computation) is separated from view state updates:
663
+ * resolveNode returns a DOMNode|null, and this method handles selection.
664
+ */
665
+ private async restoreSelectedNodeAfterUpdate(
666
+ domModel: SDK.DOMModel.DOMModel, staleNode: SDK.DOMModel.DOMNode|null,
667
+ savedSelectedNodeOnReset: SDK.DOMModel.DOMNode|undefined): Promise<void> {
668
+ // Fast path: no previous node to restore -- just select the fallback
669
+ // synchronously so callers that check selection immediately still work.
670
+ if (!staleNode) {
671
+ this.trySetFallbackSelection(domModel);
672
+ return;
673
+ }
674
+
675
+ const nodePath = staleNode.path();
676
+
677
+ // Keep the panel usable quickly by selecting a reasonable default node as
678
+ // soon as we can, but continue trying to restore the stale node.
679
+ let didSetFallbackSelection = false;
649
680
 
650
- async function restoreNode(
651
- this: ElementsPanel, domModel: SDK.DOMModel.DOMModel, staleNode: SDK.DOMModel.DOMNode|null): Promise<void> {
652
- const nodePath = staleNode ? staleNode.path() : null;
653
- const restoredNodeId = nodePath ? await domModel.pushNodeByPathToFrontend(nodePath) : null;
681
+ // Retry with exponential-ish backoff, capping total wait at ~3s.
682
+ // Most async-rendered pages settle well within this window.
683
+ const attemptDelaysMs = [0, 250, 500, 1000, 1500];
654
684
 
685
+ // Capture the restoration generation so any user interaction (node
686
+ // selection, style editing, node reveal, etc.) cancels pending retries.
687
+ const restorationGeneration = this.#restorationGeneration;
688
+
689
+ for (let attempt = 0; attempt < attemptDelaysMs.length; ++attempt) {
655
690
  if (savedSelectedNodeOnReset !== this.selectedNodeOnReset) {
656
691
  return;
657
692
  }
658
- let node = domModel.nodeForId(restoredNodeId);
659
- if (!node) {
660
- const inspectedDocument = domModel.existingDocument();
661
- node = inspectedDocument ? inspectedDocument.body || inspectedDocument.documentElement : null;
693
+ if (this.hasNonDefaultSelectedNode || this.pendingNodeReveal ||
694
+ restorationGeneration !== this.#restorationGeneration) {
695
+ return;
662
696
  }
663
- // If `node` is null here, the document hasn't been transmitted from the backend yet
664
- // and isn't in a valid state to have a default-selected node. Another document update
665
- // should be forthcoming. In the meantime, don't set the default-selected node or notify
666
- // the test that it's ready, because it isn't.
667
- if (node) {
668
- this.setDefaultSelectedNode(node);
697
+
698
+ if (attemptDelaysMs[attempt]) {
699
+ await new Promise<void>(resolve => window.setTimeout(resolve, attemptDelaysMs[attempt]));
700
+ }
701
+
702
+ if (savedSelectedNodeOnReset !== this.selectedNodeOnReset) {
703
+ return;
704
+ }
705
+ if (this.hasNonDefaultSelectedNode || this.pendingNodeReveal ||
706
+ restorationGeneration !== this.#restorationGeneration) {
707
+ return;
708
+ }
709
+
710
+ // Computation: resolve the node without touching view state.
711
+ const restoredNode = await this.resolveNodeForRestoration(domModel, nodePath);
712
+
713
+ if (restoredNode) {
714
+ this.setDefaultSelectedNode(restoredNode);
669
715
  this.lastSelectedNodeSelectedForTest();
716
+ return;
717
+ }
718
+
719
+ if (!didSetFallbackSelection) {
720
+ // If we cannot compute a fallback selection yet, the document likely
721
+ // has not been transmitted from the backend and isn't in a valid state
722
+ // to have a default-selected node. Another document update should be
723
+ // forthcoming. In the meantime, don't notify tests that selection is
724
+ // ready, because it isn't.
725
+ if (!this.trySetFallbackSelection(domModel)) {
726
+ return;
727
+ }
728
+ didSetFallbackSelection = true;
670
729
  }
671
730
  }
672
731
  }
673
732
 
733
+ /**
734
+ * Attempts to resolve a DOM node by its CDP path.
735
+ * Pure computation -- does not modify view state.
736
+ */
737
+ private async resolveNodeForRestoration(domModel: SDK.DOMModel.DOMModel, nodePath: string|null):
738
+ Promise<SDK.DOMModel.DOMNode|null> {
739
+ try {
740
+ if (nodePath) {
741
+ const restoredNodeId = await domModel.pushNodeByPathToFrontend(nodePath);
742
+ const restoredNode = domModel.nodeForId(restoredNodeId);
743
+ if (restoredNode) {
744
+ return restoredNode;
745
+ }
746
+ }
747
+ } catch {
748
+ // CDP calls (pushNodeByPathToFrontend) can reject when the target or
749
+ // session is closed, e.g. if the page navigates again while we are
750
+ // retrying. Safe to swallow: we either retry on the next iteration or
751
+ // fall through to the fallback node.
752
+ }
753
+ return null;
754
+ }
755
+
756
+ private trySetFallbackSelection(domModel: SDK.DOMModel.DOMModel): boolean {
757
+ const inspectedDocument = domModel.existingDocument();
758
+ const fallbackNode = inspectedDocument ? inspectedDocument.body || inspectedDocument.documentElement : null;
759
+ if (!fallbackNode) {
760
+ return false;
761
+ }
762
+
763
+ this.setDefaultSelectedNode(fallbackNode);
764
+ this.lastSelectedNodeSelectedForTest();
765
+ return true;
766
+ }
767
+
768
+ cancelPendingRestoration(): void {
769
+ this.#restorationGeneration++;
770
+ }
771
+
674
772
  private lastSelectedNodeSelectedForTest(): void {
675
773
  }
676
774
 
@@ -1373,6 +1471,7 @@ export class DOMNodeRevealer implements Common.Revealer.Revealer<
1373
1471
  omitFocus?: boolean): Promise<void> {
1374
1472
  const panel = ElementsPanel.instance();
1375
1473
  panel.pendingNodeReveal = true;
1474
+ panel.cancelPendingRestoration();
1376
1475
 
1377
1476
  return (new Promise<void>(revealPromise)).catch((reason: Error) => {
1378
1477
  let message: string;
@@ -634,64 +634,12 @@ function renderTitle(
634
634
  }
635
635
  }
636
636
 
637
- const enum SrcsetTokenType {
638
- LITERAL = 0,
639
- LINK = 1
640
- }
641
-
642
- interface SrcsetToken {
643
- type: SrcsetTokenType;
644
- value: string;
645
- }
646
-
647
- // FIXME: find a home for this in SDK.
648
- function parseSrcset(value: string): SrcsetToken[] {
649
- const result: SrcsetToken[] = [];
650
- let i = 0;
651
- while (value.length) {
652
- if (i++ > 0) {
653
- result.push({value: ' ', type: SrcsetTokenType.LITERAL});
654
- }
655
- value = value.trim();
656
- let url = '';
657
- let descriptor = '';
658
- const indexOfSpace = value.search(/\s/);
659
- if (indexOfSpace === -1) {
660
- url = value;
661
- } else if (indexOfSpace > 0 && value[indexOfSpace - 1] === ',') {
662
- url = value.substring(0, indexOfSpace);
663
- } else {
664
- url = value.substring(0, indexOfSpace);
665
- const indexOfComma = value.indexOf(',', indexOfSpace);
666
- if (indexOfComma !== -1) {
667
- descriptor = value.substring(indexOfSpace, indexOfComma + 1);
668
- } else {
669
- descriptor = value.substring(indexOfSpace);
670
- }
671
- }
672
-
673
- if (url) {
674
- if (url.endsWith(',')) {
675
- result.push({value: url.substring(0, url.length - 1), type: SrcsetTokenType.LINK});
676
- result.push({type: SrcsetTokenType.LITERAL, value: ','});
677
- } else {
678
- result.push({value: url, type: SrcsetTokenType.LINK});
679
- }
680
- }
681
- if (descriptor) {
682
- result.push({type: SrcsetTokenType.LITERAL, value: descriptor});
683
- }
684
- value = value.substring(url.length + descriptor.length);
685
- }
686
- return result;
687
- }
688
-
689
- function renderLinkifiedSrcset(tokens: SrcsetToken[], node: SDK.DOMModel.DOMNode): Lit.TemplateResult {
637
+ function renderLinkifiedSrcset(tokens: Common.Srcset.Token[], node: SDK.DOMModel.DOMNode): Lit.TemplateResult {
690
638
  return html`${repeat(tokens, token => {
691
639
  switch (token.type) {
692
- case SrcsetTokenType.LINK:
640
+ case Common.Srcset.TokenType.URL:
693
641
  return renderLinkifiedValue(token.value, node);
694
- case SrcsetTokenType.LITERAL:
642
+ case Common.Srcset.TokenType.LITERAL:
695
643
  return token.value;
696
644
  }
697
645
  })}`;
@@ -849,7 +797,7 @@ function renderAttribute(
849
797
  Boolean(updateRecord?.isAttributeModified(name) && hasText),
850
798
  DOM_UPDATE_ANIMATION_CLASS_NAME)} ${valueRelationRefDirective} ${withEntitiesRef}>
851
799
  ${valueType === ValueType.SRC ? renderLinkifiedValue(value, node) : nothing}
852
- ${valueType === ValueType.SRCSET ? renderLinkifiedSrcset(parseSrcset(value), node) : nothing}
800
+ ${valueType === ValueType.SRCSET ? renderLinkifiedSrcset(Common.Srcset.parseSrcset(value), node) : nothing}
853
801
  </span>"` :
854
802
  nothing}</span>`;
855
803
  // clang-format on
@@ -22,6 +22,7 @@ import * as NetworkComponents from './components/components.js';
22
22
  import {EventSourceMessagesView} from './EventSourceMessagesView.js';
23
23
  import {RequestCookiesView} from './RequestCookiesView.js';
24
24
  import {RequestDeviceBoundSessionsView} from './RequestDeviceBoundSessionsView.js';
25
+ import * as RequestHeadersView from './RequestHeadersView.js';
25
26
  import {RequestInitiatorView} from './RequestInitiatorView.js';
26
27
  import {RequestPayloadView} from './RequestPayloadView.js';
27
28
  import {RequestPreviewView} from './RequestPreviewView.js';
@@ -148,7 +149,7 @@ const requestToPreviewView = new WeakMap<SDK.NetworkRequest.NetworkRequest, Requ
148
149
  export class NetworkItemView extends UI.TabbedPane.TabbedPane {
149
150
  #request: SDK.NetworkRequest.NetworkRequest;
150
151
  readonly #resourceViewTabSetting: Common.Settings.Setting<NetworkForward.UIRequestLocation.UIRequestTabs>;
151
- readonly #headersViewComponent: NetworkComponents.RequestHeadersView.RequestHeadersView|undefined;
152
+ readonly #headersViewComponent: RequestHeadersView.RequestHeadersView|undefined;
152
153
  #payloadView: RequestPayloadView|null = null;
153
154
  readonly #responseView: RequestResponseView|undefined;
154
155
  #cookiesView: RequestCookiesView|null = null;
@@ -174,7 +175,7 @@ export class NetworkItemView extends UI.TabbedPane.TabbedPane {
174
175
  i18nString(UIStrings.headers));
175
176
  } else {
176
177
  this.#firstTab = NetworkForward.UIRequestLocation.UIRequestTabs.HEADERS_COMPONENT;
177
- this.#headersViewComponent = new NetworkComponents.RequestHeadersView.RequestHeadersView(request);
178
+ this.#headersViewComponent = new RequestHeadersView.RequestHeadersView(request);
178
179
  this.appendTab(
179
180
  NetworkForward.UIRequestLocation.UIRequestTabs.HEADERS_COMPONENT, i18nString(UIStrings.headers),
180
181
  LegacyWrapper.LegacyWrapper.legacyWrapper(UI.Widget.VBox, this.#headersViewComponent),
@@ -426,7 +427,7 @@ export class NetworkItemView extends UI.TabbedPane.TabbedPane {
426
427
  this.#headersViewComponent?.revealHeader(section, header);
427
428
  }
428
429
 
429
- getHeadersViewComponent(): NetworkComponents.RequestHeadersView.RequestHeadersView|undefined {
430
+ getHeadersViewComponent(): RequestHeadersView.RequestHeadersView|undefined {
430
431
  return this.#headersViewComponent;
431
432
  }
432
433
  }