blob: 2ae12dcd71d71cb2981c05c8f9dfb8a5d9d24f54 [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as Common from '../../../../core/common/common.js';
import * as Host from '../../../../core/host/host.js';
import * as SDK from '../../../../core/sdk/sdk.js';
import {assertScreenshot, dispatchClickEvent, renderElementIntoDOM} from '../../../../testing/DOMHelpers.js';
import {describeWithEnvironment} from '../../../../testing/EnvironmentHelpers.js';
import {expectCall} from '../../../../testing/ExpectStubCall.js';
import * as UI from '../../legacy.js';
import * as ObjectUI from './object_ui.js';
describe('ObjectPropertiesSection', () => {
describeWithEnvironment('ObjectPropertiesSection', () => {
it('properties with null and undefined values are shown by default', async () => {
const object = SDK.RemoteObject.RemoteObject.fromLocalObject({
s: 'string',
n: null,
u: undefined,
});
const section = new ObjectUI.ObjectPropertiesSection.ObjectPropertiesSection(object, 'title');
const rootElement = section.objectTreeElement();
await rootElement.onpopulate();
assert.strictEqual(rootElement.childCount(), 3);
const properties = [rootElement.childAt(0)!, rootElement.childAt(1)!, rootElement.childAt(2)!] as
ObjectUI.ObjectPropertiesSection.ObjectPropertyTreeElement[];
const n = properties.find(p => p.property.name === 'n')!;
const s = properties.find(p => p.property.name === 's')!;
const u = properties.find(p => p.property.name === 'u')!;
assert.isFalse(n.hidden);
assert.isFalse(s.hidden);
assert.isFalse(u.hidden);
});
it('properties with null and undefined values are hidden when the setting is disabled', async () => {
const object = SDK.RemoteObject.RemoteObject.fromLocalObject({
s: 'string',
n: null,
u: undefined,
});
const section = new ObjectUI.ObjectPropertiesSection.ObjectPropertiesSection(object, 'title');
section.root.includeNullOrUndefinedValues = false;
const rootElement = section.objectTreeElement();
await rootElement.onpopulate();
const properties = [rootElement.childAt(0)!, rootElement.childAt(1)!, rootElement.childAt(2)!].map(
x => x as ObjectUI.ObjectPropertiesSection.ObjectPropertyTreeElement);
const n = properties.find(p => p.property.name === 'n')!;
const s = properties.find(p => p.property.name === 's')!;
const u = properties.find(p => p.property.name === 'u')!;
assert.isTrue(n.hidden);
assert.isFalse(s.hidden);
assert.isTrue(u.hidden);
});
it('shows "Show all" in context menu', () => {
const object = SDK.RemoteObject.RemoteObject.fromLocalObject({});
const section = new ObjectUI.ObjectPropertiesSection.ObjectPropertiesSection(object, 'title');
const rootElement = section.objectTreeElement();
const event = new MouseEvent('contextmenu');
const showSpy = sinon.stub(UI.ContextMenu.ContextMenu.prototype, 'show').resolves();
const appendCheckboxItemSpy = sinon.spy(UI.ContextMenu.Section.prototype, 'appendCheckboxItem');
(rootElement as unknown as {onContextMenu: (e: Event) => void}).onContextMenu(event);
sinon.assert.called(appendCheckboxItemSpy);
const showAllItem = appendCheckboxItemSpy.args.find(args => args[0] === 'Show all');
assert.exists(showAllItem);
assert.isTrue(showAllItem[2]?.checked);
showSpy.restore();
appendCheckboxItemSpy.restore();
});
describe('appendMemoryIcon', () => {
it('appends a memory icon for inspectable object types', () => {
const object = sinon.createStubInstance(SDK.RemoteObject.RemoteObject);
object.isLinearMemoryInspectable.returns(true);
const div = document.createElement('div');
assert.isFalse(div.hasChildNodes());
ObjectUI.ObjectPropertiesSection.ObjectPropertiesSection.appendMemoryIcon(div, object);
assert.isTrue(div.hasChildNodes());
const icon = div.querySelector('devtools-icon');
assert.isNotNull(icon);
});
it('doesn\'t append a memory icon for non-inspectable object types', () => {
const object = sinon.createStubInstance(SDK.RemoteObject.RemoteObject);
object.isLinearMemoryInspectable.returns(false);
const div = document.createElement('div');
assert.isFalse(div.hasChildNodes());
ObjectUI.ObjectPropertiesSection.ObjectPropertiesSection.appendMemoryIcon(div, object);
assert.strictEqual(div.childElementCount, 0);
});
it('triggers the correct revealer upon \'click\'', () => {
const object = sinon.createStubInstance(SDK.RemoteObject.RemoteObject);
object.isLinearMemoryInspectable.returns(true);
const expression = 'foo';
const div = document.createElement('div');
ObjectUI.ObjectPropertiesSection.ObjectPropertiesSection.appendMemoryIcon(div, object, expression);
const icon = div.querySelector('devtools-icon');
assert.exists(icon);
const reveal = sinon.stub(Common.Revealer.RevealerRegistry.prototype, 'reveal');
dispatchClickEvent(icon);
sinon.assert.calledOnceWithMatch(reveal, sinon.match({object, expression}), false);
});
});
});
});
describeWithEnvironment('ObjectPropertyTreeElement', () => {
it('populates the context menu with a copy option for LocalJSONObjects', () => {
const parentObject = SDK.RemoteObject.RemoteObject.fromLocalObject({foo: 'bar'});
const parentProperty = new SDK.RemoteObject.RemoteObjectProperty('parentNode', parentObject);
const parentNode = new ObjectUI.ObjectPropertiesSection.ObjectTreeNode(parentProperty);
const childObject = SDK.RemoteObject.RemoteObject.fromLocalObject('bar');
const childProperty = new SDK.RemoteObject.RemoteObjectProperty('foo', childObject);
const childNode = new ObjectUI.ObjectPropertiesSection.ObjectTreeNode(childProperty, undefined, parentNode);
const treeElement = new ObjectUI.ObjectPropertiesSection.ObjectPropertyTreeElement(childNode);
const event = new MouseEvent('contextmenu');
const contextMenu = treeElement.getContextMenu(event);
const copyValueItem = contextMenu.clipboardSection().items.find(
(item: UI.ContextMenu.Item) => item.buildDescriptor().label === 'Copy value');
assert.exists(copyValueItem);
const copyText = sinon.stub(Host.InspectorFrontendHost.InspectorFrontendHostInstance, 'copyText');
contextMenu.invokeHandler(copyValueItem!.id());
sinon.assert.calledWith(copyText, 'bar');
});
it('does not edit readonly values', async () => {
const property = new SDK.RemoteObject.RemoteObjectProperty(
'name', SDK.RemoteObject.RemoteObject.fromLocalObject(42), true, true);
const container = document.createElement('div');
const input = {
editable: false,
startEditing: sinon.stub(),
invokeGetter: sinon.stub(),
onAutoComplete: sinon.stub(),
linkifier: undefined,
completions: [],
expanded: false,
editing: false,
editingEnded: sinon.stub(),
editingCommitted: sinon.stub(),
node: new ObjectUI.ObjectPropertiesSection.ObjectTreeNode(property),
};
const output = {valueElement: undefined, nameElement: undefined};
ObjectUI.ObjectPropertiesSection.OBJECT_PROPERTY_DEFAULT_VIEW(input, output, container);
sinon.assert.notCalled(input.startEditing);
const event = new MouseEvent('dblclick', {bubbles: true, cancelable: true});
const valueElement = container.querySelector('.value');
assert.exists(valueElement);
assert.strictEqual(valueElement, output.valueElement);
valueElement.dispatchEvent(event);
sinon.assert.notCalled(input.startEditing);
});
it('can edit values', async () => {
const property = new SDK.RemoteObject.RemoteObjectProperty(
'name', SDK.RemoteObject.RemoteObject.fromLocalObject(42), true, true);
const container = document.createElement('div');
const input = {
editable: true,
startEditing: sinon.stub(),
invokeGetter: sinon.stub(),
onAutoComplete: sinon.stub(),
linkifier: undefined,
completions: [],
expanded: false,
editing: false,
editingEnded: sinon.stub(),
editingCommitted: sinon.stub(),
node: new ObjectUI.ObjectPropertiesSection.ObjectTreeNode(property),
};
const output = {valueElement: undefined, nameElement: undefined};
ObjectUI.ObjectPropertiesSection.OBJECT_PROPERTY_DEFAULT_VIEW(input, output, container);
sinon.assert.notCalled(input.startEditing);
const event = new MouseEvent('dblclick', {bubbles: true, cancelable: true});
const valueElement = container.querySelector('.value');
assert.exists(valueElement);
assert.strictEqual(valueElement, output.valueElement);
valueElement.dispatchEvent(event);
sinon.assert.calledOnce(input.startEditing);
const viewFunction = sinon.stub<[ObjectUI.ObjectPropertiesSection.ObjectPropertyViewInput, object, HTMLElement]>();
const section = new ObjectUI.ObjectPropertiesSection.ObjectPropertyWidget(undefined, viewFunction);
section.property = new ObjectUI.ObjectPropertiesSection.ObjectTreeNode(property),
renderElementIntoDOM(section);
const firstExpectedCall = expectCall(viewFunction);
section.performUpdate();
const [firstInput] = await firstExpectedCall;
assert.isFalse(firstInput.editing);
const secondExpectedCall = expectCall(viewFunction);
firstInput.startEditing();
const [secondInput] = await secondExpectedCall;
assert.isTrue(secondInput.editing);
});
it('shows expandable text contents for lengthy strings', async () => {
const longString = `l${'o'.repeat(15000)}ng`;
const value = ObjectUI.ObjectPropertiesSection.ObjectPropertiesSection.createPropertyValue(
SDK.RemoteObject.RemoteObject.fromLocalObject(longString), false, true);
renderElementIntoDOM(value, {includeCommonStyles: true});
await assertScreenshot('object_ui/expandable_strings.png');
const copyStub = sinon.stub(Host.InspectorFrontendHost.InspectorFrontendHostInstance, 'copyText');
const copyButton = value.querySelector<HTMLButtonElement>('[data-text="Copy"]');
assert.exists(copyButton);
const expandButton = value.querySelector<HTMLButtonElement>('[data-text="Show more (15.0\xA0kB)"]');
assert.exists(expandButton);
sinon.assert.notCalled(copyStub);
copyButton.click();
sinon.assert.calledOnceWithExactly(copyStub, `"${longString}"`);
assert.notStrictEqual(value.textContent, `"${longString}"`);
expandButton.click();
await assertScreenshot('object_ui/expanded_strings.png');
assert.strictEqual(value.textContent, `"${longString}"`);
});
});