[console] Fix 'Expand recursively' to not go on forever.
This limits the max depth for 'Expand recursively' in the objects UI
(primarily in the Console tab) to 100 and also adds some smarts to the
component to not expand [[Prototype]] this way, as that's very likely
not what the developer had in mind.
Fixed: chromium:1272450
Bug: chromium:1199286
Change-Id: Id96a57051c05c3cb8448c52c687d08ae72eddf0f
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/3553118
Auto-Submit: Benedikt Meurer <bmeurer@chromium.org>
Reviewed-by: Kim-Anh Tran <kimanh@chromium.org>
Commit-Queue: Kim-Anh Tran <kimanh@chromium.org>
diff --git a/front_end/ui/legacy/Treeoutline.ts b/front_end/ui/legacy/Treeoutline.ts
index 3bc7a6f..2f89c70 100644
--- a/front_end/ui/legacy/Treeoutline.ts
+++ b/front_end/ui/legacy/Treeoutline.ts
@@ -442,6 +442,7 @@
expanded: boolean;
selected: boolean;
private expandable!: boolean;
+ #expandRecursively: boolean = true;
private collapsible: boolean;
toggleOnClick: boolean;
button: HTMLButtonElement|null;
@@ -846,6 +847,14 @@
}
}
+ isExpandRecursively(): boolean {
+ return this.#expandRecursively;
+ }
+
+ setExpandRecursively(expandRecursively: boolean): void {
+ this.#expandRecursively = expandRecursively;
+ }
+
isCollapsible(): boolean {
return this.collapsible;
}
@@ -1044,16 +1053,17 @@
maxDepth = 3;
}
- while (item) {
- await item.populateIfNeeded();
+ do {
+ if (item.isExpandRecursively()) {
+ await item.populateIfNeeded();
- if (depth < maxDepth) {
- item.expand();
+ if (depth < maxDepth) {
+ item.expand();
+ }
}
-
- item = item.traverseNextTreeElement(false, this, (depth >= maxDepth), info);
+ item = item.traverseNextTreeElement(!item.isExpandRecursively(), this, true, info);
depth += info.depthChange;
- }
+ } while (item !== null);
}
collapseOrAscend(altKey: boolean): boolean {
diff --git a/front_end/ui/legacy/components/object_ui/ObjectPropertiesSection.ts b/front_end/ui/legacy/components/object_ui/ObjectPropertiesSection.ts
index 21e2cd6..ee36329 100644
--- a/front_end/ui/legacy/components/object_ui/ObjectPropertiesSection.ts
+++ b/front_end/ui/legacy/components/object_ui/ObjectPropertiesSection.ts
@@ -123,6 +123,7 @@
const str_ = i18n.i18n.registerUIStrings('ui/legacy/components/object_ui/ObjectPropertiesSection.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
const EXPANDABLE_MAX_LENGTH = 50;
+const EXPANDABLE_MAX_DEPTH = 100;
const parentMap = new WeakMap<SDK.RemoteObject.RemoteObjectProperty, SDK.RemoteObject.RemoteObject|null>();
@@ -548,7 +549,7 @@
if (this.object instanceof SDK.RemoteObject.LocalJSONObject) {
contextMenu.viewSection().appendItem(
i18nString(UIStrings.expandRecursively),
- this.objectTreeElementInternal.expandRecursively.bind(this.objectTreeElementInternal, Number.MAX_VALUE));
+ this.objectTreeElementInternal.expandRecursively.bind(this.objectTreeElementInternal, EXPANDABLE_MAX_DEPTH));
contextMenu.viewSection().appendItem(
i18nString(UIStrings.collapseChildren),
this.objectTreeElementInternal.collapseChildren.bind(this.objectTreeElementInternal));
@@ -653,7 +654,7 @@
}
contextMenu.viewSection().appendItem(
- i18nString(UIStrings.expandRecursively), this.expandRecursively.bind(this, Number.MAX_VALUE));
+ i18nString(UIStrings.expandRecursively), this.expandRecursively.bind(this, EXPANDABLE_MAX_DEPTH));
contextMenu.viewSection().appendItem(i18nString(UIStrings.collapseChildren), this.collapseChildren.bind(this));
void contextMenu.show();
}
@@ -696,6 +697,7 @@
this.maxNumPropertiesToShow = InitialVisibleChildrenLimit;
this.listItemElement.addEventListener('contextmenu', this.contextMenuFired.bind(this), false);
this.listItemElement.dataset.objectPropertyNameForTest = property.name;
+ this.setExpandRecursively(property.name !== '[[Prototype]]');
}
static async populate(
@@ -1114,7 +1116,7 @@
}
if (parentMap.get(this.property) instanceof SDK.RemoteObject.LocalJSONObject) {
contextMenu.viewSection().appendItem(
- i18nString(UIStrings.expandRecursively), this.expandRecursively.bind(this, Number.MAX_VALUE));
+ i18nString(UIStrings.expandRecursively), this.expandRecursively.bind(this, EXPANDABLE_MAX_DEPTH));
contextMenu.viewSection().appendItem(i18nString(UIStrings.collapseChildren), this.collapseChildren.bind(this));
}
if (this.propertyValue) {
diff --git a/test/e2e/console/BUILD.gn b/test/e2e/console/BUILD.gn
index 1064f26..d331d95 100644
--- a/test/e2e/console/BUILD.gn
+++ b/test/e2e/console/BUILD.gn
@@ -14,6 +14,7 @@
"console-eval-blocked-by-CSP_test.ts",
"console-eval-fake_test.ts",
"console-eval-global_test.ts",
+ "console-expand-recursively_test.ts",
"console-filter_test.ts",
"console-last-result_test.ts",
"console-log_test.ts",
diff --git a/test/e2e/console/console-expand-recursively_test.ts b/test/e2e/console/console-expand-recursively_test.ts
new file mode 100644
index 0000000..0592d8c
--- /dev/null
+++ b/test/e2e/console/console-expand-recursively_test.ts
@@ -0,0 +1,42 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {click, getBrowserAndPages, step, waitFor} from '../../shared/helper.js';
+import {describe, it} from '../../shared/mocha-extensions.js';
+import {clickOnContextMenu, CONSOLE_TAB_SELECTOR, focusConsolePrompt, typeIntoConsole} from '../helpers/console-helpers.js';
+
+describe('The Console Tab', async () => {
+ it('recursively expands objects', async () => {
+ const {frontend} = getBrowserAndPages();
+
+ await step('open the console tab and focus the prompt', async () => {
+ await click(CONSOLE_TAB_SELECTOR);
+ await focusConsolePrompt();
+ });
+
+ await typeIntoConsole(frontend, '({a: {x: 21}, b: {y: 42}})');
+
+ // Expand the object node recursively
+ await clickOnContextMenu('.console-view-object-properties-section', 'Expand recursively');
+ const root = await waitFor('.console-view-object-properties-section.expanded');
+
+ // Ensure that both a and b are expanded.
+ const [aChildren, bChildren] = await Promise.all([
+ waitFor('li[data-object-property-name-for-test="a"][aria-expanded=true] ~ ol.expanded', root),
+ waitFor('li[data-object-property-name-for-test="b"][aria-expanded=true] ~ ol.expanded', root),
+ ]);
+
+ // The x and y properties should be visible now.
+ await Promise.all([
+ waitFor('li[data-object-property-name-for-test="x"]', aChildren),
+ waitFor('li[data-object-property-name-for-test="y"]', bChildren),
+ ]);
+
+ // The [[Prototype]] internal properties should not be expanded now.
+ await Promise.all([
+ waitFor('li[data-object-property-name-for-test="[[Prototype]]"][aria-expanded=false]', aChildren),
+ waitFor('li[data-object-property-name-for-test="[[Prototype]]"][aria-expanded=false]', bChildren),
+ ]);
+ });
+});