chrome-devtools-frontend 1.0.1624583 → 1.0.1625079

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.
@@ -111,9 +111,9 @@ Protocol requests/responses dynamically generated by Chrome (like Network condit
111
111
 
112
112
  ```ts
113
113
  const emulateSpy = sinon.spy();
114
- connection.setHandler('Network.emulateNetworkConditionsByRule', request => {
114
+ connection.setSuccessHandler('Network.emulateNetworkConditionsByRule', request => {
115
115
  emulateSpy(request);
116
- return {result: {ruleIds: []}};
116
+ return {ruleIds: []};
117
117
  });
118
118
 
119
119
  // Matches only the fields you care about, ignoring extra protocol fields
@@ -3,7 +3,7 @@
3
3
  // found in the LICENSE file.
4
4
 
5
5
  import type * as SDK from '../../../core/sdk/sdk.js';
6
- import type * as Protocol from '../../../generated/protocol.js';
6
+ import * as Protocol from '../../../generated/protocol.js';
7
7
  import * as Annotations from '../../annotations/annotations.js';
8
8
  import * as Logs from '../../logs/logs.js';
9
9
  import * as NetworkTimeCalculator from '../../network_time_calculator/network_time_calculator.js';
@@ -115,7 +115,11 @@ export class NetworkRequestFormatter {
115
115
  }): string {
116
116
  const lines = [];
117
117
  if (reasons.blockedReason) {
118
- lines.push(`Blocked reason: ${reasons.blockedReason}`);
118
+ if (reasons.blockedReason === Protocol.Network.BlockedReason.Inspector) {
119
+ lines.push('Blocked reason: a custom network condition in DevTools is blocking this request');
120
+ } else {
121
+ lines.push(`Blocked reason: ${reasons.blockedReason}`);
122
+ }
119
123
  }
120
124
  if (reasons.corsErrorStatus) {
121
125
  lines.push(`CORS error: ${reasons.corsErrorStatus.corsError} ${reasons.corsErrorStatus.failedParameter}`);
@@ -7024,6 +7024,10 @@ export const NativeFunctions = [
7024
7024
  name: "prependHTMLUnsafe",
7025
7025
  signatures: [["html","?options"]]
7026
7026
  },
7027
+ {
7028
+ name: "matchContainer",
7029
+ signatures: [["query"]]
7030
+ },
7027
7031
  {
7028
7032
  name: "scrollIntoViewIfNeeded",
7029
7033
  signatures: [["?centerIfNeeded"]]
@@ -31,6 +31,10 @@ export const UIStrings = {
31
31
  columnSource: 'Source',
32
32
  /** Label for a column in a data table; entries will be the number of wasted bytes due to duplication of a web resource. */
33
33
  columnDuplicatedBytes: 'Duplicated bytes',
34
+ /**
35
+ * @description Message shown when no duplicated JavaScript is found.
36
+ */
37
+ noDuplicatedJavaScript: 'No duplicated JavaScript found',
34
38
  } as const;
35
39
 
36
40
  const str_ = i18n.i18n.registerUIStrings('models/trace/insights/DuplicatedJavaScript.ts', UIStrings);
@@ -28,6 +28,10 @@ export const UIStrings = {
28
28
  fontColumn: 'Font',
29
29
  /** Column for the amount of time wasted. */
30
30
  wastedTimeColumn: 'Wasted time',
31
+ /**
32
+ * @description Message shown when no fonts with suboptimal font-display are found.
33
+ */
34
+ noFonts: 'No fonts with suboptimal font-display found',
31
35
  } as const;
32
36
 
33
37
  const str_ = i18n.i18n.registerUIStrings('models/trace/insights/FontDisplay.ts', UIStrings);
@@ -33,6 +33,10 @@ export const UIStrings = {
33
33
  columnScript: 'Script',
34
34
  /** Label for a column in a data table; entries will be the number of wasted bytes (aka the estimated savings in terms of bytes). */
35
35
  columnWastedBytes: 'Wasted bytes',
36
+ /**
37
+ * @description Message shown when no legacy JavaScript is found.
38
+ */
39
+ noLegacyJavaScript: 'No legacy JavaScript found',
36
40
  } as const;
37
41
 
38
42
  const str_ = i18n.i18n.registerUIStrings('models/trace/insights/LegacyJavaScript.ts', UIStrings);
@@ -2,6 +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
  /* eslint-disable @devtools/no-imperative-dom-api */
5
+ /* eslint-disable @devtools/no-lit-render-outside-of-view */
5
6
 
6
7
  import '../../ui/legacy/legacy.js';
7
8
 
@@ -11,8 +12,8 @@ import * as i18n from '../../core/i18n/i18n.js';
11
12
  import * as Platform from '../../core/platform/platform.js';
12
13
  import * as EmulationModel from '../../models/emulation/emulation.js';
13
14
  import * as Buttons from '../../ui/components/buttons/buttons.js';
14
- import * as uiI18n from '../../ui/i18n/i18n.js';
15
15
  import * as UI from '../../ui/legacy/legacy.js';
16
+ import {Directives, html, i18nTemplate, type LitTemplate, render} from '../../ui/lit/lit.js';
16
17
  import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';
17
18
  import * as MobileThrottling from '../mobile_throttling/mobile_throttling.js';
18
19
 
@@ -187,6 +188,9 @@ const UIStrings = {
187
188
  } as const;
188
189
  const str_ = i18n.i18n.registerUIStrings('panels/emulation/DeviceModeToolbar.ts', UIStrings);
189
190
  const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
191
+ const {ref, classMap} = Directives;
192
+ const {widget} = UI.Widget;
193
+ const {bindToSetting} = UI.UIUtils;
190
194
 
191
195
  /**
192
196
  * Even though the emulation panel uses all UI elements, the tooltips are not supported.
@@ -218,8 +222,8 @@ export class DeviceModeToolbar {
218
222
  private spanButton!: Buttons.Button.Button;
219
223
  private postureItem!: HTMLSelectElement;
220
224
  private modeButton!: Buttons.Button.Button;
221
- private widthInput: HTMLInputElement;
222
- private heightInput: HTMLInputElement;
225
+ private widthInput!: HTMLInputElement;
226
+ private heightInput!: HTMLInputElement;
223
227
  private deviceScaleItem!: HTMLSelectElement;
224
228
  private deviceScaleItems: HTMLElement[] = [];
225
229
  private deviceSelectItem!: HTMLSelectElement;
@@ -255,9 +259,6 @@ export class DeviceModeToolbar {
255
259
 
256
260
  this.lastMode = new Map();
257
261
 
258
- this.widthInput = this.createSizeInput(i18nString(UIStrings.width), 'width');
259
- this.heightInput = this.createSizeInput(i18nString(UIStrings.heightLeaveEmptyForFull), 'height');
260
-
261
262
  this.#element = document.createElement('div');
262
263
  this.#element.classList.add('device-mode-toolbar');
263
264
  this.#element.setAttribute('jslog', `${VisualLogging.toolbar('device-mode').track({resize: true})}`);
@@ -304,16 +305,22 @@ export class DeviceModeToolbar {
304
305
  return element;
305
306
  }
306
307
 
307
- private createSizeInput(title: string, jslogContext: string): HTMLInputElement {
308
- const input = document.createElement('input');
309
- input.type = 'number';
310
- input.max = String(EmulationModel.DeviceModeModel.MaxDeviceSize);
311
- input.min = String(EmulationModel.DeviceModeModel.MinDeviceSize);
312
- input.title = title;
313
- input.classList.add('device-mode-size-input');
314
- input.setAttribute('jslog', `${VisualLogging.textField().track({change: true}).context(jslogContext)}`);
315
-
316
- input.addEventListener('keydown', (event: Event) => {
308
+ private createSizeInput(
309
+ title: string, jslogContext: string, value: string|undefined, disabled: boolean|undefined,
310
+ refCallback: (el: Element|undefined) => void, onChange: () => void): LitTemplate {
311
+ return html`
312
+ <input type="number"
313
+ max=${EmulationModel.DeviceModeModel.MaxDeviceSize}
314
+ min=${EmulationModel.DeviceModeModel.MinDeviceSize}
315
+ title=${title}
316
+ class="device-mode-size-input"
317
+ .disabled=${disabled ?? false}
318
+ jslog=${VisualLogging.textField().track({change: true}).context(jslogContext)}
319
+ .value=${value ?? ''}
320
+ @change=${onChange}
321
+ ${ref(refCallback)}
322
+ @keydown=${(event: Event): void => {
323
+ const input = event.target as HTMLInputElement;
317
324
  let modifiedValue = UI.UIUtils.modifiedFloatNumber(Number(input.value), event);
318
325
  if (modifiedValue === null) {
319
326
  return;
@@ -324,125 +331,134 @@ export class DeviceModeToolbar {
324
331
  event.preventDefault();
325
332
  input.value = String(modifiedValue);
326
333
  input.dispatchEvent(new Event('change'));
327
- });
328
- return input;
334
+ }}>`;
329
335
  }
330
336
 
331
337
  private createMainToolbar(): UI.Toolbar.Toolbar {
332
338
  const mainToolbar = this.#element.createChild('devtools-toolbar', 'main-toolbar');
333
-
334
- mainToolbar.append(this.createEmptyToolbarElement());
335
- this.deviceSelectItem = document.createElement('select');
336
- this.deviceSelectItem.classList.add('dark-text', 'toolbar-has-dropdown-shrinkable');
337
- this.deviceSelectItem.title = i18nString(UIStrings.deviceType);
338
- UI.ARIAUtils.setLabel(this.deviceSelectItem, i18nString(UIStrings.deviceType));
339
- this.deviceSelectItem.addEventListener('change', this.onDeviceChange.bind(this));
340
- this.deviceSelectItem.setAttribute('jslog', `${VisualLogging.dropDown().track({change: true}).context('device')}`);
341
- const dimensionsSpan = uiI18n.getFormatLocalizedString(str_, UIStrings.dimensions, {PH1: this.deviceSelectItem});
342
- mainToolbar.append(...dimensionsSpan.childNodes);
343
- mainToolbar.append(this.deviceSelectItem);
344
-
345
- this.widthInput.addEventListener('change', () => {
346
- const width = Number(this.widthInput.value);
347
- if (this.autoAdjustScaleSetting.get()) {
348
- this.model.setWidthAndScaleToFit(width);
349
- } else {
350
- this.model.setWidth(width);
351
- }
352
- });
353
- this.heightInput.addEventListener('change', () => {
354
- const height = Number(this.heightInput.value);
355
- if (this.autoAdjustScaleSetting.get()) {
356
- this.model.setHeightAndScaleToFit(height);
357
- } else {
358
- this.model.setHeight(height);
359
- }
360
- });
361
- mainToolbar.append(this.widthInput);
362
- this.xItem = document.createElement('div');
363
- this.xItem.classList.add('device-mode-x');
364
- this.xItem.textContent = '×';
365
- mainToolbar.append(this.xItem);
366
- mainToolbar.append(this.heightInput);
367
-
368
- mainToolbar.append(this.createEmptyToolbarElement());
369
- this.scaleItem = document.createElement('select');
370
- this.scaleItem.classList.add('dark-text', 'toolbar-has-dropdown-shrinkable');
371
- this.scaleItem.title = i18nString(UIStrings.zoom);
372
- UI.ARIAUtils.setLabel(this.scaleItem, i18nString(UIStrings.zoom));
373
- this.scaleItem.addEventListener('change', this.onScaleChange.bind(this));
374
- this.scaleItem.setAttribute('jslog', `${VisualLogging.dropDown().track({change: true}).context('scale')}`);
375
- mainToolbar.append(this.scaleItem);
376
-
377
- const autoAdjustScaleButton = new UI.Toolbar.ToolbarSettingToggle(
378
- this.autoAdjustScaleSetting, 'center-focus-weak', i18nString(UIStrings.autoadjustZoom));
379
- mainToolbar.appendToolbarItem(autoAdjustScaleButton);
380
-
381
- mainToolbar.append(this.createEmptyToolbarElement());
382
-
383
- this.deviceScaleItem = document.createElement('select');
384
- this.deviceScaleItem.classList.add('dark-text', 'toolbar-has-dropdown-shrinkable');
385
- this.deviceScaleItem.title = i18nString(UIStrings.devicePixelRatio);
386
- UI.ARIAUtils.setLabel(this.deviceScaleItem, i18nString(UIStrings.devicePixelRatio));
387
- this.deviceScaleItem.addEventListener('change', this.onDeviceScaleChange.bind(this));
388
- this.deviceScaleItem.setAttribute(
389
- 'jslog', `${VisualLogging.dropDown().track({change: true}).context('device-pixel-ratio')}`);
390
- const deviceScaleSpan = uiI18n.getFormatLocalizedString(str_, UIStrings.dpr, {PH1: this.deviceScaleItem});
339
+ // clang-format off
340
+ render(html`
341
+ <div class="device-mode-empty-toolbar-element"></div>
342
+ ${i18nTemplate(str_, UIStrings.dimensions, {PH1: html`
343
+ <select class="dark-text toolbar-has-dropdown-shrinkable"
344
+ title=${i18nString(UIStrings.deviceType)}
345
+ aria-label=${i18nString(UIStrings.deviceType)}
346
+ @change=${this.onDeviceChange.bind(this)}
347
+ jslog=${VisualLogging.dropDown().track({change: true}).context('device')}
348
+ ${ref(el => { this.deviceSelectItem = el as HTMLSelectElement; })}>
349
+ </select>`})}
350
+
351
+ ${this.createSizeInput(
352
+ i18nString(UIStrings.width), 'width', this.widthInput?.value, this.widthInput?.disabled,
353
+ el => { this.widthInput = el as HTMLInputElement; },
354
+ () => {
355
+ const width = Number(this.widthInput.value);
356
+ if (this.autoAdjustScaleSetting.get()) {
357
+ this.model.setWidthAndScaleToFit(width);
358
+ } else {
359
+ this.model.setWidth(width);
360
+ }
361
+ })}
362
+
363
+ <div class="device-mode-x" ${ref(el => { this.xItem = el as HTMLElement; })}>×</div>
364
+ ${this.createSizeInput(
365
+ i18nString(UIStrings.heightLeaveEmptyForFull), 'height', this.heightInput?.value, this.heightInput?.disabled,
366
+ el => { this.heightInput = el as HTMLInputElement; },
367
+ () => {
368
+ const height = Number(this.heightInput.value);
369
+ if (this.autoAdjustScaleSetting.get()) {
370
+ this.model.setHeightAndScaleToFit(height);
371
+ } else {
372
+ this.model.setHeight(height);
373
+ }
374
+ })}
375
+
376
+ <div class="device-mode-empty-toolbar-element"></div>
377
+ <select class="dark-text toolbar-has-dropdown-shrinkable"
378
+ title=${i18nString(UIStrings.zoom)}
379
+ aria-label=${i18nString(UIStrings.zoom)}
380
+ @change=${this.onScaleChange.bind(this)}
381
+ jslog=${VisualLogging.dropDown().track({change: true}).context('scale')}
382
+ ${ref((el: Element|undefined) => { this.scaleItem = el as HTMLSelectElement; })}>
383
+ </select>
384
+
385
+ <devtools-button .data=${{variant: Buttons.Button.Variant.TOOLBAR, iconName: 'center-focus-weak',
386
+ toggledIconName: 'center-focus-weak', toggleType: Buttons.Button.ToggleType.PRIMARY} as Buttons.Button.ButtonData}
387
+ class="toolbar-button" title=${i18nString(UIStrings.autoadjustZoom)}
388
+ ${bindToSetting(this.autoAdjustScaleSetting)}>
389
+ </devtools-button>
390
+
391
+ <div class="device-mode-empty-toolbar-element"></div>
392
+
393
+ <span>
394
+ ${i18nTemplate(str_, UIStrings.dpr, {
395
+ PH1: html`
396
+ <select class="dark-text toolbar-has-dropdown-shrinkable"
397
+ title=${i18nString(UIStrings.devicePixelRatio)}
398
+ aria-label=${i18nString(UIStrings.devicePixelRatio)}
399
+ @change=${this.onDeviceScaleChange.bind(this)}
400
+ jslog=${VisualLogging.dropDown().track({change: true}).context('device-pixel-ratio')}
401
+ ${ref(el => { this.deviceScaleItem = el as HTMLSelectElement; })}>
402
+ </select>`
403
+ })}
404
+ </span>
405
+
406
+ <div class="device-mode-empty-toolbar-element"></div>
407
+ <select class="dark-text toolbar-has-dropdown-shrinkable
408
+ ${classMap({hidden: !this.showUserAgentTypeSetting.get()})}"
409
+ title=${i18nString(UIStrings.deviceType)}
410
+ aria-label=${i18nString(UIStrings.deviceType)}
411
+ @change=${this.onUAChange.bind(this)}
412
+ jslog=${VisualLogging.dropDown().track({change: true}).context('device-type')}
413
+ ${ref(el => { this.uaItem = el as HTMLSelectElement; })}>
414
+ </select>
415
+ <select class="dark-text" ${widget(MobileThrottling.NetworkThrottlingSelector.NetworkThrottlingSelect, {
416
+ title: i18nString(UIStrings.throttling),
417
+ bindToGlobalConditions: true,
418
+ })}></select>
419
+ <select class="dark-text toolbar-has-dropdown-shrinkable" ${widget(
420
+ MobileThrottling.ThrottlingManager.SaveDataOverrideSelect)}></select>
421
+
422
+ <div class="device-mode-empty-toolbar-element"></div>
423
+ <devtools-button class="toolbar-button"
424
+ .data=${{variant: Buttons.Button.Variant.ICON, iconName: 'screen-rotation'} as Buttons.Button.ButtonData}
425
+ jslog=${VisualLogging.action('screen-rotation').track({click: true})}
426
+ @click=${this.modeMenuClicked.bind(this)}
427
+ ${ref(el => { this.modeButton = el as Buttons.Button.Button; })}>
428
+ </devtools-button>
429
+
430
+ <!-- Show dual screen toolbar -->
431
+ <devtools-button class="toolbar-button"
432
+ .data=${{variant: Buttons.Button.Variant.ICON, iconName: 'device-fold'} as Buttons.Button.ButtonData}
433
+ jslog=${VisualLogging.action('device-fold').track({click: true})}
434
+ @click=${this.spanClicked.bind(this)}
435
+ ${ref(el => { this.spanButton = el as Buttons.Button.Button; })}>
436
+ </devtools-button>
437
+
438
+ <!-- Show posture toolbar menu for foldable devices. -->
439
+ <div class="device-mode-empty-toolbar-element"></div>
440
+ <select class="dark-text toolbar-has-dropdown-shrinkable"
441
+ title=${i18nString(UIStrings.devicePosture)}
442
+ aria-label=${i18nString(UIStrings.devicePosture)}
443
+ @change=${this.onPostureChange.bind(this)}
444
+ jslog=${VisualLogging.dropDown().track({change: true}).context('device-posture')}
445
+ ${ref(el => { this.postureItem = el as HTMLSelectElement; })}>
446
+ </select>`,
447
+ mainToolbar);
448
+ // clang-format on
449
+ const deviceScaleSpan = this.deviceScaleItem.parentElement as HTMLElement;
391
450
  for (const node of Array.from(deviceScaleSpan.childNodes)) {
392
- if (node === this.deviceScaleItem) {
393
- this.deviceScaleItem.classList.toggle('hidden', !this.showDeviceScaleFactorSetting.get());
394
- this.deviceScaleItems.push(this.deviceScaleItem);
395
- mainToolbar.append(this.deviceScaleItem);
396
- } else {
397
- const item = new UI.Toolbar.ToolbarText(node.textContent || '');
398
- item.setVisible(this.showDeviceScaleFactorSetting.get());
399
- this.deviceScaleItems.push(item.element);
400
- mainToolbar.appendToolbarItem(item);
451
+ if (node instanceof HTMLElement) {
452
+ this.deviceScaleItems.push(node);
453
+ } else if (node instanceof Text) {
454
+ const span = document.createElement('span');
455
+ span.classList.add('toolbar-text');
456
+ span.appendChild(node);
457
+ this.deviceScaleItems.push(span);
401
458
  }
402
459
  }
403
- mainToolbar.append(this.createEmptyToolbarElement());
404
- this.uaItem = document.createElement('select');
405
- this.uaItem.classList.add('dark-text', 'toolbar-has-dropdown-shrinkable');
406
- this.uaItem.title = i18nString(UIStrings.deviceType);
407
- UI.ARIAUtils.setLabel(this.uaItem, i18nString(UIStrings.deviceType));
408
- this.uaItem.addEventListener('change', this.onUAChange.bind(this));
409
- this.uaItem.setAttribute('jslog', `${VisualLogging.dropDown().track({change: true}).context('device-type')}`);
410
- this.uaItem.classList.toggle('hidden', !this.showUserAgentTypeSetting.get());
411
- mainToolbar.append(this.uaItem);
412
-
413
- MobileThrottling.NetworkThrottlingSelector.NetworkThrottlingSelect.createForGlobalConditions(
414
- mainToolbar, i18nString(UIStrings.throttling));
415
- const saveDataItem = MobileThrottling.ThrottlingManager.throttlingManager().createSaveDataOverrideSelector();
416
- saveDataItem.classList.add('dark-text', 'toolbar-has-dropdown-shrinkable');
417
- mainToolbar.append(saveDataItem);
418
-
419
- mainToolbar.append(this.createEmptyToolbarElement());
420
- this.modeButton = new Buttons.Button.Button();
421
- this.modeButton.classList.add('toolbar-button');
422
- this.modeButton.data = {variant: Buttons.Button.Variant.ICON, iconName: 'screen-rotation'};
423
- this.modeButton.setAttribute('jslog', `${VisualLogging.action('screen-rotation').track({click: true})}`);
424
- this.modeButton.addEventListener('click', this.modeMenuClicked.bind(this));
425
- mainToolbar.append(this.modeButton);
426
-
427
- // Show dual screen toolbar.
428
- this.spanButton = new Buttons.Button.Button();
429
- this.spanButton.classList.add('toolbar-button');
430
- this.spanButton.data = {variant: Buttons.Button.Variant.ICON, iconName: 'device-fold'};
431
- this.spanButton.setAttribute('jslog', `${VisualLogging.action('device-fold').track({click: true})}`);
432
- this.spanButton.addEventListener('click', this.spanClicked.bind(this));
433
- mainToolbar.append(this.spanButton);
434
-
435
- // Show posture toolbar menu for foldable devices.
436
- mainToolbar.append(this.createEmptyToolbarElement());
437
- this.postureItem = document.createElement('select');
438
- this.postureItem.classList.add('dark-text', 'toolbar-has-dropdown-shrinkable');
439
- this.postureItem.title = i18nString(UIStrings.devicePosture);
440
- UI.ARIAUtils.setLabel(this.postureItem, i18nString(UIStrings.devicePosture));
441
- this.postureItem.addEventListener('change', this.onPostureChange.bind(this));
442
- this.postureItem.setAttribute(
443
- 'jslog', `${VisualLogging.dropDown().track({change: true}).context('device-posture')}`);
444
- mainToolbar.append(this.postureItem);
445
-
460
+ deviceScaleSpan.replaceWith(...this.deviceScaleItems);
461
+ this.updateDeviceScaleFactorVisibility();
446
462
  return mainToolbar;
447
463
  }
448
464
 
@@ -206,7 +206,6 @@ export class SidebarSingleInsightSet extends UI.Widget.Widget {
206
206
  const activeInsight = this.#data.activeInsight;
207
207
  const agentFocus = AIAssistance.AIContext.AgentFocus.fromInsight(this.#data.parsedTrace, model);
208
208
  const isActiveInsight = activeInsight?.model === model;
209
-
210
209
  const componentClass = INSIGHT_NAME_TO_COMPONENT[insightName as keyof typeof INSIGHT_NAME_TO_COMPONENT];
211
210
  const widgetConfig = {
212
211
  selected: isActiveInsight,
@@ -222,10 +221,19 @@ export class SidebarSingleInsightSet extends UI.Widget.Widget {
222
221
  fieldMetrics,
223
222
  };
224
223
 
224
+ const items = [{componentClass, widgetConfig}];
225
+
225
226
  // clang-format off
226
- return html`<devtools-widget class="insight-component-widget" ?highlight-insight=${isActiveInsight && this.#isActiveInsightHighlighted}
227
- ${widget(componentClass, widgetConfig)}
228
- ></devtools-widget>`;
227
+ // We use `repeat` to force the widget to be recreated if the model
228
+ // changes (e.g. on new trace import). If Lit tries to reuse DOM
229
+ // across different traces, where the ordering of the sidebar
230
+ // insights changes, this causes errors.
231
+ const output = Lit.Directives.repeat(items, data => data.widgetConfig.model, data => {
232
+ return html`<devtools-widget class="insight-component-widget" ?highlight-insight=${isActiveInsight && this.#isActiveInsightHighlighted}
233
+ ${widget(data.componentClass, data.widgetConfig)}
234
+ ></devtools-widget>`;
235
+ });
236
+ return html`${output}`;
229
237
  // clang-format on
230
238
  }
231
239
 
@@ -58,6 +58,10 @@ export class DuplicatedJavaScript extends BaseInsightComponent<DuplicatedJavaScr
58
58
  return Lit.nothing;
59
59
  }
60
60
 
61
+ if (this.model.duplicationGroupedByNodeModules.size === 0) {
62
+ return html`<div class="insight-section">${i18nString(UIStrings.noDuplicatedJavaScript)}</div>`;
63
+ }
64
+
61
65
  const rows: TableDataRow[] =
62
66
  [...this.model.duplicationGroupedByNodeModules.entries()].slice(0, 10).map(([source, data]) => {
63
67
  const scriptToOverlay = new Map();
@@ -73,6 +73,10 @@ export class FontDisplay extends BaseInsightComponent<FontDisplayInsightModel> {
73
73
  return Lit.nothing;
74
74
  }
75
75
 
76
+ if (this.model.fonts.length === 0) {
77
+ return html`<div class="insight-section">${i18nString(UIStrings.noFonts)}</div>`;
78
+ }
79
+
76
80
  const rows = createLimitedRows(this.model.fonts, this);
77
81
 
78
82
  // clang-format off
@@ -58,6 +58,10 @@ export class LegacyJavaScript extends BaseInsightComponent<LegacyJavaScriptInsig
58
58
  return Lit.nothing;
59
59
  }
60
60
 
61
+ if (this.model.legacyJavaScriptResults.size === 0) {
62
+ return html`<div class="insight-section">${i18nString(UIStrings.noLegacyJavaScript)}</div>`;
63
+ }
64
+
61
65
  const rows: TableDataRow[] =
62
66
  [...this.model.legacyJavaScriptResults.entries()].slice(0, 10).map(([script, result]) => {
63
67
  const overlays: Trace.Types.Overlays.Overlay[] = [];
@@ -1,7 +1,7 @@
1
1
  Name: Dependencies sourced from the upstream `chromium` repository
2
2
  URL: Internal
3
3
  Version: N/A
4
- Revision: e388465394450fcfccc5529cf634c8561999f3b8
4
+ Revision: 9f3e9aaccba63bd2ec30334e45e0bfd07ebcc8f1
5
5
  Update Mechanism: Manual (https://crbug.com/428069060)
6
6
  License: BSD-3-Clause
7
7
  License File: LICENSE
@@ -152,18 +152,236 @@ interface NodeChildren {
152
152
  export interface ObjectTreeOptions {
153
153
  readonly propertiesMode: ObjectPropertiesMode;
154
154
  readonly readOnly: boolean;
155
+ readonly expansionTracker?: ObjectTreeExpansionTracker;
156
+ }
157
+
158
+ type KeyTypes = {
159
+ [K in keyof Required<NodeChildren>]: Key<Required<NodeChildren>[K][0]>;
160
+ };
161
+
162
+ type Key<T> = T extends ObjectTreeNode ? string : T extends ArrayGroupTreeNode ? ArrayGroupRange : never;
163
+ interface TrackingKey {
164
+ type: keyof KeyTypes;
165
+ key: KeyTypes[keyof KeyTypes];
166
+ }
167
+
168
+ class NodeExpansionLog {
169
+ properties = new Map<string, NodeExpansionLog>();
170
+ internalProperties = new Map<string, NodeExpansionLog>();
171
+ arrayRanges = new Map<string, NodeExpansionLog>();
172
+ accessors = new Map<string, NodeExpansionLog>();
173
+
174
+ remove(key: TrackingKey): boolean {
175
+ return this[key.type].delete(NodeExpansionLog.#serializeKey(key));
176
+ }
177
+
178
+ get(key: TrackingKey): NodeExpansionLog|undefined {
179
+ return this[key.type].get(NodeExpansionLog.#serializeKey(key));
180
+ }
181
+
182
+ getOrInsert(key: TrackingKey): NodeExpansionLog {
183
+ const map = this[key.type];
184
+ const serializedKey = NodeExpansionLog.#serializeKey(key);
185
+ const log = map.get(serializedKey) ?? new NodeExpansionLog();
186
+ if (!map.has(serializedKey)) {
187
+ map.set(serializedKey, log);
188
+ }
189
+ return log;
190
+ }
191
+
192
+ clear(type: keyof KeyTypes): void {
193
+ this[type].clear();
194
+ }
195
+
196
+ clearMissing(type: keyof KeyTypes, seen: TrackingKey[]): void {
197
+ const seenSet = new Set(seen.map(NodeExpansionLog.#serializeKey));
198
+ const map = this[type];
199
+ for (const key of map.keys()) {
200
+ if (!seenSet.has(key)) {
201
+ map.delete(key);
202
+ }
203
+ }
204
+ }
205
+
206
+ get empty(): boolean {
207
+ return this.properties.size === 0 && this.internalProperties.size === 0 && this.arrayRanges.size === 0 &&
208
+ this.accessors.size === 0;
209
+ }
210
+
211
+ static #serializeKey(key: TrackingKey): string {
212
+ if (typeof key.key === 'string') {
213
+ return `${key.type}:${key.key}`;
214
+ }
215
+ return `${key.type}:${key.key.fromIndex}-${key.key.toIndex}`;
216
+ }
217
+ }
218
+
219
+ export class ObjectTreeExpansionTracker {
220
+ #log: NodeExpansionLog|null = null;
221
+
222
+ /**
223
+ * For nodes physically nested within a parent's [[Prototype]] internal property, the node's
224
+ * parent property points to the logical parent (skipping the [[Prototype]] node). This helper
225
+ * finds that skipped [[Prototype]] node if it contains the given node.
226
+ */
227
+ static #protoParent(node: ObjectTreeNodeBase): ObjectTreeNode|undefined {
228
+ if (!(node instanceof ObjectTreeNode)) {
229
+ return undefined;
230
+ }
231
+ return node.parent?.children?.internalProperties?.find(
232
+ p => p.name === '[[Prototype]]' && p.children?.properties?.includes(node));
233
+ }
234
+
235
+ static #keyType(node: ObjectTreeNodeBase): keyof NodeChildren|null {
236
+ if (node instanceof ObjectTreeNode) {
237
+ if (node.parent?.children?.properties?.includes(node)) {
238
+ return 'properties';
239
+ }
240
+ if (node.parent?.children?.internalProperties?.includes(node)) {
241
+ return 'internalProperties';
242
+ }
243
+ if (node.parent?.children?.accessors?.includes(node)) {
244
+ return 'accessors';
245
+ }
246
+ const proto = ObjectTreeExpansionTracker.#protoParent(node);
247
+ if (proto) {
248
+ return 'properties';
249
+ }
250
+ }
251
+ if (node instanceof ArrayGroupTreeNode && node.parent?.children?.arrayRanges?.includes(node)) {
252
+ return 'arrayRanges';
253
+ }
254
+ return null;
255
+ }
256
+
257
+ static #key(type: keyof KeyTypes, node: ObjectTreeNodeBase): TrackingKey|null {
258
+ switch (type) {
259
+ case 'arrayRanges':
260
+ return node instanceof ArrayGroupTreeNode ? {type, key: node.range} : null;
261
+ default:
262
+ return node instanceof ObjectTreeNode ? {type, key: node.name} : null;
263
+ }
264
+ }
265
+
266
+ clear(): void {
267
+ this.#log = null;
268
+ }
269
+
270
+ async #apply(log: NodeExpansionLog, node: ObjectTreeNodeBase): Promise<void> {
271
+ const apply = async<KeyType extends keyof KeyTypes>(type: KeyType): Promise<void> => {
272
+ const nodes = node.children?.[type];
273
+ if (!nodes) {
274
+ log.clear(type);
275
+ return;
276
+ }
277
+ const seen: TrackingKey[] = [];
278
+ for (const childNode of nodes) {
279
+ const key = ObjectTreeExpansionTracker.#key(type, childNode);
280
+ if (!key) {
281
+ continue;
282
+ }
283
+ const childLog = log.get(key);
284
+ if (childLog) {
285
+ await this.#apply(childLog, childNode);
286
+ seen.push(key);
287
+ }
288
+ }
289
+ log.clearMissing(type, seen);
290
+ };
291
+
292
+ node.expanded = true;
293
+ await node.populateChildrenIfNeeded();
294
+ await apply('properties');
295
+ await apply('internalProperties');
296
+ await apply('arrayRanges');
297
+ await apply('accessors');
298
+ }
299
+
300
+ async apply(node: ObjectTree): Promise<void> {
301
+ if (this.#log) {
302
+ return await this.#apply(this.#log, node);
303
+ }
304
+ }
305
+
306
+ static * #path(node: ObjectTreeNodeBase|undefined): Generator<TrackingKey> {
307
+ if (!node) {
308
+ return;
309
+ }
310
+
311
+ const proto = ObjectTreeExpansionTracker.#protoParent(node);
312
+ if (proto) {
313
+ yield* this.#path(proto);
314
+ } else {
315
+ yield* this.#path(node.parent);
316
+ }
317
+
318
+ const keyType = this.#keyType(node);
319
+ const key = keyType && ObjectTreeExpansionTracker.#key(keyType, node);
320
+ if (key) {
321
+ yield key;
322
+ }
323
+ }
324
+
325
+ collapse(node: ObjectTreeNodeBase): void {
326
+ if (!this.#log) {
327
+ return;
328
+ }
329
+ if (node instanceof ObjectTree) {
330
+ this.#log = null;
331
+ return;
332
+ }
333
+
334
+ let lastKey;
335
+ let parent: NodeExpansionLog|null = null;
336
+ let log: NodeExpansionLog = this.#log;
337
+ for (const key of ObjectTreeExpansionTracker.#path(node)) {
338
+ lastKey = key;
339
+ parent = log;
340
+ const nextLog = log.get(key);
341
+ if (!nextLog) {
342
+ return;
343
+ }
344
+ log = nextLog;
345
+ }
346
+
347
+ if (lastKey && parent) {
348
+ parent.remove(lastKey);
349
+ }
350
+ }
351
+
352
+ expand(node: ObjectTreeNodeBase): void {
353
+ if (!this.#log) {
354
+ this.#log = new NodeExpansionLog();
355
+ }
356
+ let log: NodeExpansionLog = this.#log;
357
+ for (const key of ObjectTreeExpansionTracker.#path(node)) {
358
+ log = log.getOrInsert(key);
359
+ }
360
+ }
155
361
  }
156
362
 
157
363
  export abstract class ObjectTreeNodeBase extends Common.ObjectWrapper.ObjectWrapper<ObjectTreeNodeBase.EventTypes> {
158
364
  #children?: NodeChildren;
159
365
  protected filter: {includeNullOrUndefinedValues: boolean, regex: RegExp|null}|null = null;
160
366
  protected extraProperties: ObjectTreeNode[] = [];
161
- expanded = false;
367
+ #expanded = false;
162
368
 
163
369
  constructor(readonly parent: ObjectTreeNodeBase|undefined, protected readonly options: ObjectTreeOptions) {
164
370
  super();
165
371
  this.filter = parent?.filter ?? null;
166
372
  }
373
+ get expanded(): boolean {
374
+ return this.#expanded;
375
+ }
376
+
377
+ set expanded(val: boolean) {
378
+ if (val) {
379
+ this.options.expansionTracker?.expand(this);
380
+ } else {
381
+ this.options.expansionTracker?.collapse(this);
382
+ }
383
+ this.#expanded = val;
384
+ }
167
385
 
168
386
  get readOnly(): boolean {
169
387
  return this.options.readOnly;
@@ -253,11 +471,22 @@ export abstract class ObjectTreeNodeBase extends Common.ObjectWrapper.ObjectWrap
253
471
  return this.#children;
254
472
  }
255
473
 
474
+ #populatePromise?: Promise<NodeChildren>;
256
475
  async populateChildrenIfNeeded(): Promise<NodeChildren> {
257
- if (!this.#children) {
258
- this.#children = await this.populateChildrenIfNeededImpl();
476
+ if (this.#children) {
477
+ return this.#children;
259
478
  }
260
- return this.#children;
479
+ if (!this.#populatePromise) {
480
+ this.#populatePromise = this.populateChildrenIfNeededImpl()
481
+ .then(children => {
482
+ this.#children = children;
483
+ return children;
484
+ })
485
+ .finally(() => {
486
+ this.#populatePromise = undefined;
487
+ });
488
+ }
489
+ return await this.#populatePromise;
261
490
  }
262
491
 
263
492
  protected async populateChildrenIfNeededImpl(): Promise<NodeChildren> {
@@ -271,9 +500,12 @@ export abstract class ObjectTreeNodeBase extends Common.ObjectWrapper.ObjectWrap
271
500
  if (this.arrayLength > ARRAY_LOAD_THRESHOLD) {
272
501
  const ranges = await arrayRangeGroups(object, 0, this.arrayLength - 1);
273
502
  const arrayRanges = ranges?.ranges.map(
274
- ([fromIndex, toIndex, count]) => new ArrayGroupTreeNode(
275
- object, {fromIndex, toIndex, count}, effectiveParent,
276
- {readOnly: this.readOnly, propertiesMode: this.propertiesMode}));
503
+ ([fromIndex, toIndex, count]) =>
504
+ new ArrayGroupTreeNode(object, {fromIndex, toIndex, count}, effectiveParent, {
505
+ readOnly: this.readOnly,
506
+ propertiesMode: this.propertiesMode,
507
+ expansionTracker: this.options.expansionTracker
508
+ }));
277
509
  if (!arrayRanges) {
278
510
  return {};
279
511
  }
@@ -282,15 +514,18 @@ export abstract class ObjectTreeNodeBase extends Common.ObjectWrapper.ObjectWrap
282
514
  await SDK.RemoteObject.RemoteObject.loadFromObjectPerProto(
283
515
  this.object, true /* generatePreview */, true /* nonIndexedPropertiesOnly */);
284
516
 
285
- const properties = objectProperties?.map(
286
- p => new ObjectTreeNode(
287
- p, effectiveParent,
288
- {readOnly: this.readOnly, propertiesMode: ObjectPropertiesMode.OWN_AND_INTERNAL_AND_INHERITED}));
289
-
290
- const internalProperties = objectInternalProperties?.map(
291
- p => new ObjectTreeNode(
292
- p, effectiveParent,
293
- {readOnly: this.readOnly, propertiesMode: ObjectPropertiesMode.OWN_AND_INTERNAL_AND_INHERITED}));
517
+ const properties = objectProperties?.map(p => new ObjectTreeNode(p, effectiveParent, {
518
+ readOnly: this.readOnly,
519
+ propertiesMode: ObjectPropertiesMode.OWN_AND_INTERNAL_AND_INHERITED,
520
+ expansionTracker: this.options.expansionTracker
521
+ }));
522
+
523
+ const internalProperties =
524
+ objectInternalProperties?.map(p => new ObjectTreeNode(p, effectiveParent, {
525
+ readOnly: this.readOnly,
526
+ propertiesMode: ObjectPropertiesMode.OWN_AND_INTERNAL_AND_INHERITED,
527
+ expansionTracker: this.options.expansionTracker
528
+ }));
294
529
  return {arrayRanges, properties, internalProperties};
295
530
  }
296
531
 
@@ -307,18 +542,21 @@ export abstract class ObjectTreeNodeBase extends Common.ObjectWrapper.ObjectWrap
307
542
  break;
308
543
  }
309
544
 
310
- const properties = objectProperties?.map(
311
- p => new ObjectTreeNode(
312
- p, effectiveParent,
313
- {readOnly: this.readOnly, propertiesMode: ObjectPropertiesMode.OWN_AND_INTERNAL_AND_INHERITED}));
545
+ const properties = objectProperties?.map(p => new ObjectTreeNode(p, effectiveParent, {
546
+ readOnly: this.readOnly,
547
+ propertiesMode: ObjectPropertiesMode.OWN_AND_INTERNAL_AND_INHERITED,
548
+ expansionTracker: this.options.expansionTracker
549
+ }));
314
550
  properties?.push(...this.extraProperties);
315
551
  properties?.sort(ObjectPropertiesSection.compareProperties);
316
- const accessors = properties && ObjectTreeNodeBase.getGettersAndSetters(properties);
317
-
318
- const internalProperties = objectInternalProperties?.map(
319
- p => new ObjectTreeNode(
320
- p, effectiveParent,
321
- {readOnly: this.readOnly, propertiesMode: ObjectPropertiesMode.OWN_AND_INTERNAL_AND_INHERITED}));
552
+ const accessors = properties && ObjectTreeNodeBase.getGettersAndSetters(properties, this.options);
553
+
554
+ const internalProperties =
555
+ objectInternalProperties?.map(p => new ObjectTreeNode(p, effectiveParent, {
556
+ readOnly: this.readOnly,
557
+ propertiesMode: ObjectPropertiesMode.OWN_AND_INTERNAL_AND_INHERITED,
558
+ expansionTracker: this.options.expansionTracker
559
+ }));
322
560
  return {properties, internalProperties, accessors};
323
561
  }
324
562
 
@@ -336,26 +574,34 @@ export abstract class ObjectTreeNodeBase extends Common.ObjectWrapper.ObjectWrap
336
574
  }
337
575
 
338
576
  addExtraProperties(...properties: SDK.RemoteObject.RemoteObjectProperty[]): void {
339
- this.extraProperties.push(...properties.map(
340
- p => new ObjectTreeNode(
341
- p, this, {readOnly: this.readOnly, propertiesMode: ObjectPropertiesMode.OWN_AND_INTERNAL_AND_INHERITED})));
577
+ this.extraProperties.push(...properties.map(p => new ObjectTreeNode(p, this, {
578
+ readOnly: this.readOnly,
579
+ propertiesMode: ObjectPropertiesMode.OWN_AND_INTERNAL_AND_INHERITED,
580
+ expansionTracker: this.options.expansionTracker
581
+ })));
342
582
  }
343
583
 
344
- static getGettersAndSetters(properties: ObjectTreeNode[]): ObjectTreeNode[] {
584
+ static getGettersAndSetters(properties: ObjectTreeNode[], options: ObjectTreeOptions): ObjectTreeNode[] {
345
585
  const gettersAndSetters = [];
346
586
  for (const property of properties) {
347
587
  if (property.property.isOwn) {
348
588
  if (property.property.getter) {
349
589
  const getterProperty = new SDK.RemoteObject.RemoteObjectProperty(
350
590
  'get ' + property.property.name, property.property.getter, false);
351
- gettersAndSetters.push(new ObjectTreeNode(
352
- getterProperty, property.parent, {propertiesMode: property.propertiesMode, readOnly: property.readOnly}));
591
+ gettersAndSetters.push(new ObjectTreeNode(getterProperty, property.parent, {
592
+ propertiesMode: property.propertiesMode,
593
+ readOnly: property.readOnly,
594
+ expansionTracker: options.expansionTracker
595
+ }));
353
596
  }
354
597
  if (property.property.setter) {
355
598
  const setterProperty = new SDK.RemoteObject.RemoteObjectProperty(
356
599
  'set ' + property.property.name, property.property.setter, false);
357
- gettersAndSetters.push(new ObjectTreeNode(
358
- setterProperty, property.parent, {propertiesMode: property.propertiesMode, readOnly: property.readOnly}));
600
+ gettersAndSetters.push(new ObjectTreeNode(setterProperty, property.parent, {
601
+ propertiesMode: property.propertiesMode,
602
+ readOnly: property.readOnly,
603
+ expansionTracker: options.expansionTracker
604
+ }));
359
605
  }
360
606
  }
361
607
  }
@@ -388,12 +634,18 @@ export class ObjectTree extends ObjectTreeNodeBase {
388
634
  }
389
635
  }
390
636
 
391
- class ArrayGroupTreeNode extends ObjectTreeNodeBase {
637
+ interface ArrayGroupRange {
638
+ fromIndex: number;
639
+ toIndex: number;
640
+ count: number;
641
+ }
642
+
643
+ export class ArrayGroupTreeNode extends ObjectTreeNodeBase {
392
644
  readonly #object: SDK.RemoteObject.RemoteObject;
393
- readonly #range: {fromIndex: number, toIndex: number, count: number};
645
+ readonly #range: ArrayGroupRange;
394
646
  constructor(
395
- object: SDK.RemoteObject.RemoteObject, range: {fromIndex: number, toIndex: number, count: number},
396
- parent: ObjectTreeNodeBase, options: ObjectTreeOptions) {
647
+ object: SDK.RemoteObject.RemoteObject, range: ArrayGroupRange, parent: ObjectTreeNodeBase,
648
+ options: ObjectTreeOptions) {
397
649
  super(parent, options);
398
650
  this.#object = object;
399
651
  this.#range = range;
@@ -403,9 +655,11 @@ class ArrayGroupTreeNode extends ObjectTreeNodeBase {
403
655
  if (this.#range.count > ArrayGroupingTreeElement.bucketThreshold) {
404
656
  const ranges = await arrayRangeGroups(this.object, this.#range.fromIndex, this.#range.toIndex);
405
657
  const arrayRanges = ranges?.ranges.map(
406
- ([fromIndex, toIndex, count]) => new ArrayGroupTreeNode(
407
- this.object, {fromIndex, toIndex, count}, this,
408
- {readOnly: this.readOnly, propertiesMode: this.propertiesMode}));
658
+ ([fromIndex, toIndex, count]) => new ArrayGroupTreeNode(this.object, {fromIndex, toIndex, count}, this, {
659
+ readOnly: this.readOnly,
660
+ propertiesMode: this.propertiesMode,
661
+ expansionTracker: this.options.expansionTracker
662
+ }));
409
663
  return {arrayRanges};
410
664
  }
411
665
 
@@ -421,11 +675,14 @@ class ArrayGroupTreeNode extends ObjectTreeNodeBase {
421
675
  const allProperties =
422
676
  await arrayFragment.getAllProperties(false /* accessorPropertiesOnly */, true /* generatePreview */);
423
677
  arrayFragment.release();
424
- const properties = allProperties.properties?.map(
425
- p => new ObjectTreeNode(p, this, {propertiesMode: this.propertiesMode, readOnly: this.readOnly}));
678
+ const properties = allProperties.properties?.map(p => new ObjectTreeNode(p, this, {
679
+ propertiesMode: this.propertiesMode,
680
+ readOnly: this.readOnly,
681
+ expansionTracker: this.options.expansionTracker
682
+ }));
426
683
  properties?.push(...this.extraProperties);
427
684
  properties?.sort(ObjectPropertiesSection.compareProperties);
428
- const accessors = properties && ObjectTreeNodeBase.getGettersAndSetters(properties);
685
+ const accessors = properties && ObjectTreeNodeBase.getGettersAndSetters(properties, this.options);
429
686
  return {properties, accessors};
430
687
  }
431
688
 
@@ -433,7 +690,7 @@ class ArrayGroupTreeNode extends ObjectTreeNodeBase {
433
690
  return this.#range.fromIndex === this.#range.toIndex;
434
691
  }
435
692
 
436
- get range(): {fromIndex: number, toIndex: number, count: number} {
693
+ get range(): ArrayGroupRange {
437
694
  return this.#range;
438
695
  }
439
696
 
@@ -559,8 +816,10 @@ export class ObjectPropertiesSection extends UI.TreeOutline.TreeOutlineInShadow
559
816
  object: SDK.RemoteObject.RemoteObject, title?: string|Element|null, linkifier?: Components.Linkifier.Linkifier,
560
817
  showOverflow?: boolean, editable = true) {
561
818
  super();
562
- this.root = new ObjectTree(
563
- object, {readOnly: !editable, propertiesMode: ObjectPropertiesMode.OWN_AND_INTERNAL_AND_INHERITED});
819
+ this.root = new ObjectTree(object, {
820
+ readOnly: !editable,
821
+ propertiesMode: ObjectPropertiesMode.OWN_AND_INTERNAL_AND_INHERITED,
822
+ });
564
823
  if (!showOverflow) {
565
824
  this.setHideOverflow(true);
566
825
  }
@@ -995,15 +1254,20 @@ export class RootElement extends UI.TreeOutline.TreeElement {
995
1254
  this.toggleOnClick = true;
996
1255
  this.listItemElement.classList.add('object-properties-section-root-element');
997
1256
  this.listItemElement.addEventListener('contextmenu', this.onContextMenu.bind(this), false);
1257
+ if (object.expanded) {
1258
+ this.expand();
1259
+ }
998
1260
  }
999
1261
 
1000
1262
  override onexpand(): void {
1263
+ this.object.expanded = true;
1001
1264
  if (this.treeOutline) {
1002
1265
  this.treeOutline.element.classList.add('expanded');
1003
1266
  }
1004
1267
  }
1005
1268
 
1006
1269
  override oncollapse(): void {
1270
+ this.object.expanded = false;
1007
1271
  if (this.treeOutline) {
1008
1272
  this.treeOutline.element.classList.remove('expanded');
1009
1273
  }
@@ -1350,7 +1614,11 @@ export class ObjectPropertyTreeElement extends UI.TreeOutline.TreeElement {
1350
1614
  this.maxNumPropertiesToShow = InitialVisibleChildrenLimit;
1351
1615
  this.listItemElement.addEventListener('contextmenu', this.contextMenuFired.bind(this), false);
1352
1616
  this.listItemElement.dataset.objectPropertyNameForTest = property.name;
1617
+ this.updateExpandable();
1353
1618
  this.setExpandRecursively(property.name !== '[[Prototype]]');
1619
+ if (property.expanded) {
1620
+ this.expand();
1621
+ }
1354
1622
  }
1355
1623
 
1356
1624
  static async populate(
@@ -1523,10 +1791,12 @@ export class ObjectPropertyTreeElement extends UI.TreeOutline.TreeElement {
1523
1791
  }
1524
1792
 
1525
1793
  override onexpand(): void {
1794
+ this.property.expanded = true;
1526
1795
  this.#widget.expanded = true;
1527
1796
  }
1528
1797
 
1529
1798
  override oncollapse(): void {
1799
+ this.property.expanded = false;
1530
1800
  this.#widget.expanded = false;
1531
1801
  }
1532
1802
 
@@ -1750,6 +2020,9 @@ export class ArrayGroupingTreeElement extends UI.TreeOutline.TreeElement {
1750
2020
  this.#child.addEventListener(ObjectTreeNodeBase.Events.CHILDREN_CHANGED, this.onpopulate, this);
1751
2021
  this.toggleOnClick = true;
1752
2022
  this.linkifier = linkifier;
2023
+ if (child.expanded) {
2024
+ this.expand();
2025
+ }
1753
2026
  }
1754
2027
 
1755
2028
  static async populate(
@@ -1773,6 +2046,14 @@ export class ArrayGroupingTreeElement extends UI.TreeOutline.TreeElement {
1773
2046
  ObjectPropertyTreeElement.populateWithProperties(treeNode, children, false, false, linkifier);
1774
2047
  }
1775
2048
 
2049
+ override onexpand(): void {
2050
+ this.#child.expanded = true;
2051
+ }
2052
+
2053
+ override oncollapse(): void {
2054
+ this.#child.expanded = false;
2055
+ }
2056
+
1776
2057
  override async onpopulate(): Promise<void> {
1777
2058
  this.removeChildren();
1778
2059
  await ObjectPropertyTreeElement.populate(this, this.#child, false, false, this.linkifier);
package/package.json CHANGED
@@ -105,5 +105,5 @@
105
105
  "ip-address": "10.1.0",
106
106
  "basic-ftp": "5.2.2"
107
107
  },
108
- "version": "1.0.1624583"
108
+ "version": "1.0.1625079"
109
109
  }