chrome-devtools-frontend
Version:
Chrome DevTools UI
1,322 lines (1,111 loc) • 45.2 kB
text/typescript
// 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 * as Host from '../../core/host/host.js';
import {
dispatchClickEvent,
dispatchKeyDownEvent,
dispatchMouseMoveEvent,
raf,
renderElementIntoDOM
} from '../../testing/DOMHelpers.js';
import {describeWithEnvironment} from '../../testing/EnvironmentHelpers.js';
import {expectCall} from '../../testing/ExpectStubCall.js';
import type * as SuggestionInput from '../../ui/components/suggestion_input/suggestion_input.js';
import * as ProtocolMonitor from './protocol_monitor.js';
describeWithEnvironment('JSONEditor', () => {
const renderJSONEditor = () => {
const jsonEditor = new ProtocolMonitor.JSONEditor.JSONEditor(document.createElement('div'));
jsonEditor.markAsRoot();
jsonEditor.show(renderElementIntoDOM(document.createElement('main')));
return jsonEditor;
};
const populateMetadata = async (jsonEditor: ProtocolMonitor.JSONEditor.JSONEditor) => {
const mockDomain = [
{
domain: 'Test',
metadata: {
'Test.test': {
parameters: [{
name: 'test',
type: 'string',
optional: false,
typeRef: 'Test.testRef',
}],
description: 'Description1.',
replyArgs: ['Test1'],
},
'Test.test2': {
parameters: [{
optional: true,
type: 'array',
name: 'test2',
typeRef: 'string',
}],
},
'Test.test3': {
parameters: [{
optional: false,
type: 'object',
value: [
{
optional: true,
type: 'string',
name: 'param1',
},
{
optional: true,
type: 'string',
name: 'param2',
},
],
name: 'test3',
typeRef: 'string',
}],
},
'Test.test4': {
parameters: [{
name: 'test',
type: 'boolean',
optional: false,
}],
description: 'Description4.',
replyArgs: ['Test4'],
},
'Test.test5': {
parameters: [
{
name: 'test',
type: 'string',
optional: true,
},
{
name: 'test2',
type: 'string',
optional: true,
},
],
description: 'Description5.',
replyArgs: ['Test5'],
},
'Test.test6': {
parameters: [{
name: 'test',
type: 'number',
optional: true,
}],
description: 'Description6.',
replyArgs: ['Test6'],
},
'Test.test7': {
parameters: [{
name: 'test',
type: 'boolean',
optional: true,
}],
description: 'Description7.',
replyArgs: ['Test7'],
},
'Test.test8': {
parameters: [{
name: 'test',
type: 'number',
optional: false,
}],
description: 'Description8.',
replyArgs: ['Test8'],
},
'Test.test9': {
parameters: [
{
name: 'traceConfig',
type: 'object',
optional: false,
description: '',
typeRef: 'Tracing.TraceConfig',
},
],
description: 'Description9.',
replyArgs: ['Test9'],
},
'Test.test10': {
parameters: [
{
name: 'NoTypeRef',
type: 'object',
optional: true,
description: '',
typeRef: 'NoTypeRef',
},
],
description: 'Description10.',
replyArgs: ['Test10'],
},
'Test.test11': {
parameters: [{
optional: false,
type: 'array',
name: 'test11',
typeRef: 'Test.arrayTypeRef',
}],
},
'Test.test12': {
parameters: [{
optional: true,
type: 'object',
value: [
{
optional: false,
type: 'string',
name: 'param1',
},
{
optional: false,
type: 'number',
name: 'param2',
},
],
name: 'test12',
typeRef: 'Optional.Object',
}],
},
'Test.test13': {
parameters: [{
name: 'newTest',
type: 'string',
optional: false,
typeRef: 'Test.newTestRef',
}],
description: 'Description13.',
replyArgs: ['Test13'],
},
'Test.test14': {
parameters: [
{
name: 'NoTypeRef',
type: 'object',
optional: true,
description: '',
},
],
description: 'Description14.',
replyArgs: ['Test14'],
},
},
},
] as Iterable<ProtocolMonitor.ProtocolMonitor.ProtocolDomain>;
const metadataByCommand = ProtocolMonitor.ProtocolMonitor.buildProtocolMetadata(mockDomain);
jsonEditor.metadataByCommand = metadataByCommand;
await jsonEditor.updateComplete;
};
const renderHoveredElement = async (element: Element|null) => {
if (element) {
const clock = sinon.useFakeTimers();
try {
dispatchMouseMoveEvent(element, {
bubbles: true,
composed: true,
});
clock.tick(300);
clock.restore();
} finally {
clock.restore();
}
await raf();
} else {
throw new Error('No parameter has been found');
}
};
const renderSuggestionBox = async (
command: string, jsonEditor: ProtocolMonitor.JSONEditor.JSONEditor,
enumsByName?: Map<string, Record<string, string>>) => {
jsonEditor.command = command;
if (enumsByName) {
jsonEditor.enumsByName = enumsByName;
}
jsonEditor.populateParametersForCommandWithDefaultValues();
await jsonEditor.updateComplete;
const inputs = jsonEditor.contentElement.querySelectorAll('devtools-suggestion-input');
// inputs[0] corresponds to the devtools-suggestion-input of the command
const suggestionInput = inputs[1];
// Reset the value to empty string because for boolean it will be set to false by default and the correct suggestions will not show
suggestionInput.value = '';
suggestionInput.focus();
await suggestionInput.updateComplete;
const suggestionBox = suggestionInput.renderRoot.querySelector('devtools-suggestion-box');
if (!suggestionBox) {
throw new Error('No suggestion box shown');
}
const suggestions = Array.from(suggestionBox.renderRoot.querySelectorAll('li')).map(item => {
if (!item.textContent) {
throw new Error('No text inside suggestion');
}
return (item.textContent.replaceAll(/\s/g, ''));
});
return suggestions;
};
const serializePopupContent = () => {
const container = document.body.querySelector<HTMLDivElement>('[data-devtools-glass-pane]');
const hintDetailView = container?.shadowRoot?.querySelector('devtools-css-hint-details-view');
return hintDetailView?.shadowRoot?.querySelector('.hint-popup-wrapper')?.textContent?.replaceAll(/\s/g, '');
};
const renderEditorForCommand = async(command: string, parameters: Record<string, unknown>): Promise<{
inputs: NodeListOf<SuggestionInput.SuggestionInput.SuggestionInput>,
displayedCommand: string,
jsonEditor: ProtocolMonitor.JSONEditor.JSONEditor,
}> => {
const typesByName = new Map();
typesByName.set('string', [
{name: 'param1', type: 'string', optional: false, description: 'display a string', typeRef: null},
{name: 'param2', type: 'string', optional: false, description: 'displays another string', typeRef: null},
]);
const jsonEditor = renderJSONEditor();
await populateMetadata(jsonEditor);
jsonEditor.typesByName = typesByName;
jsonEditor.displayCommand(command, parameters);
await jsonEditor.updateComplete;
const shadowRoot = jsonEditor.contentElement;
const inputs = shadowRoot.querySelectorAll('devtools-suggestion-input');
const displayedCommand = jsonEditor.command;
return {inputs, displayedCommand, jsonEditor};
};
const renderParamsWithDefaultValues = async (command: string) => {
const jsonEditor = renderJSONEditor();
await populateMetadata(jsonEditor);
jsonEditor.command = command;
jsonEditor.populateParametersForCommandWithDefaultValues();
await jsonEditor.updateComplete;
const param = jsonEditor.contentElement.querySelector('[data-paramId]');
await renderHoveredElement(param);
const setDefaultValueButton = jsonEditor.contentElement.querySelector('devtools-button');
if (!setDefaultValueButton) {
throw new Error('No button');
}
dispatchClickEvent(setDefaultValueButton, {
bubbles: true,
composed: true,
});
await jsonEditor.updateComplete;
const input = jsonEditor.contentElement.querySelectorAll('devtools-suggestion-input');
const paramInput = input[1];
if (!paramInput) {
throw new Error('No input shown');
}
return paramInput;
};
const renderWarningIcon = async (command: string, enumsByName?: Map<string, Record<string, string>>) => {
const jsonEditor = renderJSONEditor();
await populateMetadata(jsonEditor);
jsonEditor.command = command;
if (enumsByName) {
jsonEditor.enumsByName = enumsByName;
}
jsonEditor.populateParametersForCommandWithDefaultValues();
await jsonEditor.updateComplete;
// inputs[0] corresponds to the devtools-suggestion-input of the command
const input = jsonEditor.contentElement.querySelectorAll('devtools-suggestion-input')[1];
if (!input) {
throw new Error('No editable content displayed');
}
input.value = 'Not an accepted value';
await jsonEditor.updateComplete;
input.focus();
input.blur();
await jsonEditor.updateComplete;
const warningIcon = jsonEditor.contentElement.querySelector('devtools-icon');
if (!warningIcon) {
throw new Error('No icon displayed');
}
return warningIcon;
};
describe('Binding input bar', () => {
it('should show the command written in the input bar inside the editor when parameters are strings with the correct value',
async () => {
const cdpCommand = {
command: 'Test.test',
parameters: {
test: 'test',
},
};
const {command, parameters} = ProtocolMonitor.ProtocolMonitor.parseCommandInput(JSON.stringify(cdpCommand));
const {inputs} = await renderEditorForCommand(command, parameters);
const parameterRecorderInput = inputs[1];
const value = parameterRecorderInput.value;
const expectedValue = 'test';
assert.deepEqual(value, expectedValue);
});
it('should show the command written in the input bar inside the editor when parameters are arrays with the correct value',
async () => {
const cdpCommand = {
command: 'Test.test2',
parameters: {
test2: ['test'],
},
};
const {command, parameters} = ProtocolMonitor.ProtocolMonitor.parseCommandInput(JSON.stringify(cdpCommand));
const {inputs} = await renderEditorForCommand(command, parameters);
const parameterRecorderInput = inputs[1];
const value = parameterRecorderInput.value;
const expectedValue = 'test';
assert.deepEqual(value, expectedValue);
});
it('should show the command written in the input bar inside the editor when parameters are object with the correct value',
async () => {
const cdpCommand = {
command: 'Test.test3',
parameters: {
test3: {
param1: 'test1',
param2: 'test2',
},
},
};
const {command, parameters} = ProtocolMonitor.ProtocolMonitor.parseCommandInput(JSON.stringify(cdpCommand));
const {inputs} = await renderEditorForCommand(command, parameters);
const parameterRecorderInput = inputs[1];
const value = parameterRecorderInput.value;
const expectedValue = 'test1';
assert.deepEqual(value, expectedValue);
});
it('should should every parameter of a command as undefined even if some parameters have not been entered inside the input bar',
async () => {
const cdpCommand = {
command: 'Test.test5',
parameters: {
test: 'test',
},
};
const {command, parameters} = ProtocolMonitor.ProtocolMonitor.parseCommandInput(JSON.stringify(cdpCommand));
const jsonEditor = renderJSONEditor();
await populateMetadata(jsonEditor);
jsonEditor.displayCommand(command, parameters);
await jsonEditor.updateComplete;
const shadowRoot = jsonEditor.contentElement;
const displayedParameters = shadowRoot.querySelectorAll('.parameter');
// Two parameters (test and test2) should be displayed because in the metadata, Test.test5 accepts two parameters
assert.lengthOf(displayedParameters, 2);
});
it('does not output parameters if the input is invalid json', async () => {
const cdpCommand = '"command": "Test.test", "parameters":';
const {command, parameters} = ProtocolMonitor.ProtocolMonitor.parseCommandInput(cdpCommand);
const {inputs} = await renderEditorForCommand(command, parameters);
assert.deepEqual(inputs.length, Object.keys(parameters).length + 1);
});
it('does not output parameters if the parameters field is not an object', async () => {
const cdpCommand = '"command": "test", "parameters": 1234';
const {command, parameters} = ProtocolMonitor.ProtocolMonitor.parseCommandInput(cdpCommand);
const {inputs} = await renderEditorForCommand(command, parameters);
assert.deepEqual(inputs.length, Object.keys(parameters).length + 1);
});
it('does not output parameters if there is no parameter inserted in the input bar', async () => {
const cdpCommand = '"command": "test"';
const {command, parameters} = ProtocolMonitor.ProtocolMonitor.parseCommandInput(cdpCommand);
const {inputs} = await renderEditorForCommand(command, parameters);
assert.deepEqual(inputs.length, Object.keys(parameters).length + 1);
});
it('checks that the command input field remains empty when there is no command parameter entered', async () => {
const cdpCommand = {
parameters: {
test: 'test',
},
};
const {command, parameters} = ProtocolMonitor.ProtocolMonitor.parseCommandInput(JSON.stringify(cdpCommand));
const {displayedCommand} = await renderEditorForCommand(command, parameters);
assert.deepEqual(displayedCommand, '');
});
it('checks that the command input field remains if the command is not supported', async () => {
const cdpCommand = 'dummyCommand';
const {command, parameters} = ProtocolMonitor.ProtocolMonitor.parseCommandInput(JSON.stringify(cdpCommand));
const {displayedCommand} = await renderEditorForCommand(command, parameters);
assert.deepEqual(displayedCommand, '');
});
});
describe('Descriptions', () => {
it('should show the popup with the correct description for the description of parameters', async () => {
const inputParameters = [
{
type: 'array',
optional: false,
value: [
{name: '0', value: 'value0', optional: true, type: 'string'},
{name: '1', value: 'value1', optional: true, type: 'string'},
{name: '2', value: 'value2', optional: true, type: 'string'},
],
name: 'arrayParam',
typeRef: 'string',
description: 'test.',
},
] as ProtocolMonitor.JSONEditor.Parameter[];
const jsonEditor = renderJSONEditor();
jsonEditor.parameters = inputParameters;
await jsonEditor.updateComplete;
const param = jsonEditor.contentElement.querySelector('[data-paramId]');
await renderHoveredElement(param);
const popupContent = serializePopupContent();
const expectedPopupContent = 'test.Type:arrayLearnMore';
assert.deepEqual(popupContent, expectedPopupContent);
});
it('should show the popup with the correct description for the description of command', async () => {
const cdpCommand = 'Test.test';
const jsonEditor = renderJSONEditor();
await populateMetadata(jsonEditor);
jsonEditor.command = cdpCommand;
await jsonEditor.updateComplete;
const command = jsonEditor.contentElement.querySelector('.command');
await renderHoveredElement(command);
const popupContent = serializePopupContent();
const expectedPopupContent = 'Description1.Returns:Test1LearnMore';
assert.deepEqual(popupContent, expectedPopupContent);
});
});
describe('Suggestion box', () => {
it('should display suggestion box with correct suggestions when the parameter is an enum', async () => {
const enumsByName = new Map([
['Test.testRef', {Test: 'test', Test1: 'test1', Test2: 'test2'}],
]);
const command = 'Test.test';
const jsonEditor = renderJSONEditor();
await populateMetadata(jsonEditor);
const suggestions = await renderSuggestionBox(command, jsonEditor, enumsByName);
assert.deepEqual(suggestions, ['test', 'test1', 'test2']);
});
it('should display suggestion box with correct suggestions when the parameter is a boolean', async () => {
const command = 'Test.test4';
const jsonEditor = renderJSONEditor();
await populateMetadata(jsonEditor);
const suggestions = await renderSuggestionBox(command, jsonEditor);
assert.deepEqual(suggestions, ['false', 'true']);
});
it('should show the suggestion box for enum parameters nested inside arrays', async () => {
const enumsByName = new Map([
['Test.arrayTypeRef', {Test: 'test', Test1: 'test1', Test2: 'test2'}],
]);
const command = 'Test.test11';
const jsonEditor = renderJSONEditor();
await populateMetadata(jsonEditor);
jsonEditor.enumsByName = enumsByName;
jsonEditor.command = command;
jsonEditor.populateParametersForCommandWithDefaultValues();
await jsonEditor.updateComplete;
const param = jsonEditor.contentElement.querySelector('[data-paramId]');
await renderHoveredElement(param);
const addParamButton = jsonEditor.contentElement.querySelector('devtools-button[title="Add a parameter"]');
if (!addParamButton) {
throw new Error('No button');
}
dispatchClickEvent(addParamButton, {
bubbles: true,
composed: true,
});
await jsonEditor.updateComplete;
const inputs = jsonEditor.contentElement.querySelectorAll('devtools-suggestion-input');
// inputs[0] corresponds to the devtools-suggestion-input of the command
const suggestionInput = inputs[1];
// Reset the value to empty string because for boolean it will be set to false by default and the correct suggestions will not show
suggestionInput.value = '';
suggestionInput.focus();
await suggestionInput.updateComplete;
const suggestionBox = suggestionInput.renderRoot.querySelector('devtools-suggestion-box');
if (!suggestionBox) {
throw new Error('No suggestion box shown');
}
const suggestions = Array.from(suggestionBox.renderRoot.querySelectorAll('li')).map(item => {
if (!item.textContent) {
throw new Error('No text inside suggestion');
}
return (item.textContent.replaceAll(/\s/g, ''));
});
assert.deepEqual(suggestions, ['test', 'test1', 'test2']);
});
it('should update the values inside the suggestion box when the command changes', async () => {
const enumsByName = new Map();
enumsByName.set('Test.testRef', {Test: 'test', Test1: 'test1', Test2: 'test2'});
enumsByName.set('Test.newTestRef', {NewTest: 'newtest', NewTest1: 'newtest1', NewTest2: 'newtest2'});
const command = 'Test.test';
const jsonEditor = renderJSONEditor();
await populateMetadata(jsonEditor);
await renderSuggestionBox(command, jsonEditor, enumsByName);
const newCommand = 'Test.test13';
const newSuggestions = await renderSuggestionBox(newCommand, jsonEditor, enumsByName);
assert.deepEqual(newSuggestions, ['newtest', 'newtest1', 'newtest2']);
});
it('should not display suggestion box when the parameter is neither a string or a boolean', async () => {
const command = 'Test.test8';
const jsonEditor = renderJSONEditor();
await populateMetadata(jsonEditor);
const suggestions = await renderSuggestionBox(command, jsonEditor);
assert.deepEqual(suggestions, []);
});
});
describe('Display with default values', () => {
it('should show <empty_string> inside the placeholder when clicking on plus button for optional string parameter',
async () => {
const command = 'Test.test5';
const placeholder = (await renderParamsWithDefaultValues(command)).placeholder;
const expectedPlaceholder = '<empty_string>';
assert.deepEqual(placeholder, expectedPlaceholder);
});
it('should show 0 as a value inside input when clicking on plus button for optional number parameter', async () => {
const command = 'Test.test6';
const value = Number((await renderParamsWithDefaultValues(command)).value);
const expectedValue = 0;
assert.deepEqual(value, expectedValue);
});
it('should show false as a value inside input when clicking on plus button for optional boolean parameter',
async () => {
const command = 'Test.test7';
const value = Boolean((await renderParamsWithDefaultValues(command)).value);
const expectedValue = false;
assert.deepEqual(value, expectedValue);
});
it('should show the keys with default values when clicking of plus button for optional object parameters',
async () => {
const command = 'Test.test12';
const typesByName = new Map();
typesByName.set('Optional.Object', [
{
optional: false,
type: 'string',
name: 'param1',
},
{
optional: false,
type: 'number',
name: 'param2',
},
]);
const jsonEditor = renderJSONEditor();
jsonEditor.typesByName = typesByName;
await populateMetadata(jsonEditor);
jsonEditor.command = command;
jsonEditor.populateParametersForCommandWithDefaultValues();
await jsonEditor.updateComplete;
const param = jsonEditor.contentElement.querySelector('[data-paramId]');
await renderHoveredElement(param);
const showDefaultValuesButton =
jsonEditor.contentElement.querySelector('devtools-button[title="Add a parameter"]');
if (!showDefaultValuesButton) {
throw new Error('No button');
}
dispatchClickEvent(showDefaultValuesButton, {
bubbles: true,
composed: true,
});
await jsonEditor.updateComplete;
// The -1 is need to not take into account the input for the command
const numberOfInputs = jsonEditor.contentElement.querySelectorAll('devtools-suggestion-input').length - 1;
assert.deepEqual(numberOfInputs, 2);
});
});
describe('Reset to default values', () => {
it('should reset the value of keys of object parameter to default value when clicking on clear button',
async () => {
const cdpCommand = {
command: 'Test.test3',
parameters: {
test3: {
param1: 'test1',
param2: 'test2',
},
},
};
const {command, parameters} = ProtocolMonitor.ProtocolMonitor.parseCommandInput(JSON.stringify(cdpCommand));
const {jsonEditor} = await renderEditorForCommand(command, parameters);
const param = jsonEditor.contentElement.querySelector('[data-paramId=\'test3\']');
await renderHoveredElement(param);
const setDefaultValueButton = jsonEditor.contentElement.querySelector('devtools-button');
if (!setDefaultValueButton) {
throw new Error('No button');
}
dispatchClickEvent(setDefaultValueButton, {
bubbles: true,
composed: true,
});
await jsonEditor.updateComplete;
const input = jsonEditor.contentElement.querySelectorAll('devtools-suggestion-input');
const values = [input[1].value, input[2].value];
const expectedValues = ['', ''];
assert.deepEqual(values, expectedValues);
});
it('should reset the value of array parameter to empty array when clicking on clear button', async () => {
const inputParameters = [
{
type: 'array',
optional: false,
value: [
{name: '0', value: 'value0', optional: true, type: 'string'},
{name: '1', value: 'value1', optional: true, type: 'string'},
{name: '2', value: 'value2', optional: true, type: 'string'},
],
name: 'arrayParam',
typeRef: 'string',
},
];
const jsonEditor = renderJSONEditor();
jsonEditor.parameters = inputParameters as ProtocolMonitor.JSONEditor.Parameter[];
await jsonEditor.updateComplete;
const param = jsonEditor.contentElement.querySelector('[data-paramId=\'arrayParam\']');
await renderHoveredElement(param);
const setDefaultValueButton =
jsonEditor.contentElement.querySelector('devtools-button[title="Reset to default value"]');
if (!setDefaultValueButton) {
throw new Error('No button');
}
dispatchClickEvent(setDefaultValueButton, {
bubbles: true,
composed: true,
});
await jsonEditor.updateComplete;
const value = jsonEditor.parameters[0].value;
assert.deepEqual(value, []);
});
it('should reset the value of optional object parameter to undefined after clicking on clear button', async () => {
const command = 'Test.test12';
const typesByName = new Map();
typesByName.set('Optional.Object', [
{
optional: false,
type: 'string',
name: 'param1',
},
{
optional: false,
type: 'number',
name: 'param2',
},
]);
const jsonEditor = renderJSONEditor();
jsonEditor.typesByName = typesByName;
await populateMetadata(jsonEditor);
jsonEditor.command = command;
jsonEditor.populateParametersForCommandWithDefaultValues();
await jsonEditor.updateComplete;
const param = jsonEditor.contentElement.querySelector('[data-paramId]');
await renderHoveredElement(param);
const showDefaultValuesButton =
jsonEditor.contentElement.querySelector('devtools-button[title="Add a parameter"]');
if (!showDefaultValuesButton) {
throw new Error('No button');
}
dispatchClickEvent(showDefaultValuesButton, {
bubbles: true,
composed: true,
});
await jsonEditor.updateComplete;
await renderHoveredElement(param);
const clearButton = jsonEditor.contentElement.querySelector('devtools-button[title="Reset to default value"]');
if (!clearButton) {
throw new Error('No clear button');
}
dispatchClickEvent(clearButton, {
bubbles: true,
composed: true,
});
await jsonEditor.updateComplete;
// The -1 is need to not take into account the input for the command
const numberOfInputs = jsonEditor.contentElement.querySelectorAll('devtools-suggestion-input').length - 1;
assert.deepEqual(numberOfInputs, 0);
});
});
describe('Delete and add for array parameters', () => {
it('should delete the specified array parameter by clicking the "Delete" button', async () => {
const inputParameters = [
{
type: 'array',
optional: false,
value: [
{name: '0', value: 'value0', optional: true, type: 'string'},
{name: '1', value: 'value1', optional: true, type: 'string'},
{name: '2', value: 'value2', optional: true, type: 'string'},
],
name: 'arrayParam',
typeRef: 'string',
},
];
const expectedParams = {
arrayParam: ['value1', 'value2'],
};
const jsonEditor = renderJSONEditor();
jsonEditor.parameters = inputParameters as ProtocolMonitor.JSONEditor.Parameter[];
await jsonEditor.updateComplete;
const shadowRoot = jsonEditor.contentElement;
const parameterIndex = 0;
const deleteButtons = shadowRoot.querySelectorAll('devtools-button[title="Delete parameter"]');
if (deleteButtons.length > parameterIndex) {
deleteButtons[parameterIndex].dispatchEvent(new Event('click'));
}
const resultedParams = jsonEditor.getParameters();
assert.deepEqual(expectedParams, resultedParams);
});
it('should add parameters when clicking on "Plus" button for array parameters', async () => {
const command = 'Test.test2';
const jsonEditor = renderJSONEditor();
await populateMetadata(jsonEditor);
jsonEditor.command = command;
jsonEditor.populateParametersForCommandWithDefaultValues();
await jsonEditor.updateComplete;
const param = jsonEditor.contentElement.querySelector('[data-paramId]');
await renderHoveredElement(param);
const addParamButton = jsonEditor.contentElement.querySelector('devtools-button[title="Add a parameter"]');
if (!addParamButton) {
throw new Error('No button');
}
dispatchClickEvent(addParamButton, {
bubbles: true,
composed: true,
});
dispatchClickEvent(addParamButton, {
bubbles: true,
composed: true,
});
await jsonEditor.updateComplete;
// The -1 is need to not take into account the input for the command
const numberOfInputs = jsonEditor.contentElement.querySelectorAll('devtools-suggestion-input').length - 1;
assert.deepEqual(numberOfInputs, 2);
});
});
describe('Send parameters in a correct format', () => {
it('should return the parameters in a format understandable by the ProtocolMonitor when sending a command via CTRL + Enter',
async () => {
const jsonEditor = renderJSONEditor();
const inputParameters = [
{
optional: true,
type: 'string',
value: 'test0',
name: 'test0',
},
{
optional: true,
type: 'string',
value: 'test1',
name: 'test1',
},
{
optional: false,
type: 'string',
value: 'test2',
name: 'test2',
},
{
optional: true,
type: 'array',
value: [
{
optional: true,
type: 'string',
value: 'param1Value',
name: 'param1',
},
{
optional: true,
type: 'string',
value: 'param2Value',
name: 'param2',
},
],
name: 'test3',
},
{
optional: true,
type: 'object',
value: [
{
optional: true,
type: 'string',
value: 'param1Value',
name: 'param1',
},
{
optional: true,
type: 'string',
value: 'param2Value',
name: 'param2',
},
],
name: 'test4',
},
];
const expectedParameters = {
test0: 'test0',
test1: 'test1',
test2: 'test2',
test3: ['param1Value', 'param2Value'],
test4: {
param1: 'param1Value',
param2: 'param2Value',
},
};
jsonEditor.parameters = inputParameters as ProtocolMonitor.JSONEditor.Parameter[];
await jsonEditor.updateComplete;
const promise = jsonEditor.once(ProtocolMonitor.JSONEditor.Events.SUBMIT_EDITOR);
dispatchKeyDownEvent(
jsonEditor.contentElement.querySelector('.editor-wrapper')!, {key: 'Enter', ctrlKey: true, metaKey: true});
const response = await promise;
assert.deepEqual(response.parameters, expectedParameters);
});
it('should return the parameters in a format understandable by the ProtocolMonitor when sending a command via the send button',
async () => {
const jsonEditor = renderJSONEditor();
jsonEditor.command = 'Test.test';
jsonEditor.parameters = [
{
name: 'testName',
type: ProtocolMonitor.JSONEditor.ParameterType.STRING,
description: 'test',
optional: false,
value: 'testValue',
},
];
await jsonEditor.updateComplete;
const toolbar = jsonEditor.contentElement.querySelector('devtools-toolbar');
if (!toolbar) {
throw new Error('No toolbar found !');
}
const promise = jsonEditor.once(ProtocolMonitor.JSONEditor.Events.SUBMIT_EDITOR);
dispatchClickEvent(toolbar.querySelector('devtools-button[title^="Send command"]')!);
const response = await promise;
const expectedParameters = {
testName: 'testValue',
};
assert.deepEqual(response.parameters, expectedParameters);
});
});
describe('Verify the type of the entered value', () => {
it('should show a warning icon if the type of the parameter is number but the entered value is not', async () => {
const command = 'Test.test8';
const warningIcon = await renderWarningIcon(command);
assert.isNotNull(warningIcon);
});
it('should show a warning icon if the type of the parameter is boolean but the entered value is not true or false',
async () => {
const command = 'Test.test4';
const warningIcon = await renderWarningIcon(command);
assert.isNotNull(warningIcon);
});
it('should show a warning icon if the type of the parameter is enum but the entered value is not among the accepted values',
async () => {
const enumsByName = new Map([
['Test.testRef', {Test: 'test', Test1: 'test1', Test2: 'test2'}],
]);
const command = 'Test.test';
const warningIcon = await renderWarningIcon(command, enumsByName);
assert.isNotNull(warningIcon);
});
});
it('should not display parameters if a command is unknown', async () => {
const cdpCommand = 'Unknown';
const jsonEditor = renderJSONEditor();
await populateMetadata(jsonEditor);
jsonEditor.command = cdpCommand;
await jsonEditor.updateComplete;
const inputs = jsonEditor.contentElement.querySelectorAll('devtools-suggestion-input');
const addButtons = jsonEditor.contentElement.querySelectorAll('devtools-button[title="Add a parameter"]');
assert.lengthOf(inputs, 1);
assert.lengthOf(addButtons, 0);
});
it('checks that the selection of a target works', async () => {
const jsonEditor = renderJSONEditor();
await jsonEditor.updateComplete;
const targetId = 'target1';
const shadowRoot = jsonEditor.contentElement;
const selectElement = shadowRoot.querySelector<HTMLSelectElement>('select');
const option = document.createElement('option');
option.value = targetId;
selectElement?.appendChild(option);
selectElement!.selectedIndex = 0;
selectElement!.dispatchEvent(new Event('change'));
const actualId = jsonEditor.targetId;
assert.deepEqual(actualId, targetId);
});
it('should copy the CDP command to clipboard via copy event', async () => {
const jsonEditor = renderJSONEditor();
jsonEditor.command = 'Test.test';
jsonEditor.parameters = [
{
name: 'test',
type: ProtocolMonitor.JSONEditor.ParameterType.STRING,
description: 'test',
optional: false,
value: 'test',
},
];
await jsonEditor.updateComplete;
const copyText = expectCall(sinon.stub(
Host.InspectorFrontendHost.InspectorFrontendHostInstance,
'copyText',
));
const toolbar = jsonEditor.contentElement.querySelector('devtools-toolbar');
if (!toolbar) {
throw new Error('No toolbar found !');
}
dispatchClickEvent(toolbar.querySelector('devtools-button[title="Copy command"]')!);
const [text] = await copyText;
assert.strictEqual(JSON.stringify({command: 'Test.test', parameters: {test: 'test'}}), text);
});
it('should display the correct parameters with a command with array nested inside object', async () => {
const command = 'Test.test9';
const typesByName = new Map();
// This nested object contains every subtype including array
typesByName.set('Tracing.TraceConfig', [
{
name: 'recordMode',
type: 'string',
optional: true,
description: 'Controls how the trace buffer stores data.',
typeRef: null,
},
{
name: 'traceBufferSizeInKb',
type: 'number',
optional: true,
description:
'Size of the trace buffer in kilobytes. If not specified or zero is passed, a default value of 200 MB would be used.',
typeRef: null,
},
{
name: 'enableSystrace',
type: 'boolean',
optional: true,
description: 'Turns on system tracing.',
typeRef: null,
},
{
name: 'includedCategories',
type: 'array',
optional: true,
description: 'Included category filters.',
typeRef: 'string',
},
{
name: 'memoryDumpConfig',
type: 'object',
optional: true,
description: 'Configuration for memory dump triggers. Used only when \\"memory-infra\\" category is enabled.',
typeRef:
'Tracing.MemoryDumpConfig', // This typeref is on purpose not added to show that this param will be treated as a string parameter
},
]);
const jsonEditor = renderJSONEditor();
await populateMetadata(jsonEditor);
jsonEditor.typesByName = typesByName;
jsonEditor.command = command;
jsonEditor.populateParametersForCommandWithDefaultValues();
await jsonEditor.updateComplete;
const shadowRoot = jsonEditor.contentElement;
const parameters = shadowRoot.querySelectorAll('.parameter');
// This expected value is equal to 6 because there are 5 different parameters inside typesByName + 1
// for the name of the parameter (traceConfig)
assert.lengthOf(parameters, 6);
});
it('should return the parameters in a format understandable by the ProtocolMonitor when sending a command with object parameter that has no typeRef found in map',
async () => {
const command = 'Test.test10';
const typesByName = new Map();
// We set the map typesBynames without the key NoTypeRef
typesByName.set('Tracing.TraceConfig', [
{
name: 'memoryDumpConfig',
type: 'object',
optional: true,
description:
'Configuration for memory dump triggers. Used only when \\"memory-infra\\" category is enabled.',
typeRef:
'Tracing.MemoryDumpConfig', // This typeref is on purpose not added to show that this param will be treated as a string parameter
},
]);
const jsonEditor = renderJSONEditor();
await populateMetadata(jsonEditor);
jsonEditor.typesByName = typesByName;
jsonEditor.command = command;
jsonEditor.populateParametersForCommandWithDefaultValues();
await jsonEditor.updateComplete;
const shadowRoot = jsonEditor.contentElement;
const parameters = shadowRoot.querySelector('.parameter');
await renderHoveredElement(parameters);
const addParamButton = jsonEditor.contentElement.querySelector('devtools-button[title="Add custom property"]');
if (!addParamButton) {
throw new Error('No button');
}
// We click two times to display two parameters with key/value pairs
dispatchClickEvent(addParamButton, {
bubbles: true,
composed: true,
});
dispatchClickEvent(addParamButton, {
bubbles: true,
composed: true,
});
await jsonEditor.updateComplete;
const editors = shadowRoot.querySelectorAll('devtools-suggestion-input');
// Editors[0] refers to the command editor, so we start at index 1
// We populate the key/value pairs
editors[1].value = 'testName1';
await jsonEditor.updateComplete;
editors[1].focus();
editors[1].blur();
await jsonEditor.updateComplete;
editors[2].value = 'testValue1';
await jsonEditor.updateComplete;
editors[2].focus();
editors[2].blur();
await jsonEditor.updateComplete;
editors[3].value = 'testName2';
await jsonEditor.updateComplete;
editors[3].focus();
editors[3].blur();
await jsonEditor.updateComplete;
editors[4].value = 'testValue2';
await jsonEditor.updateComplete;
editors[4].focus();
editors[4].blur();
await jsonEditor.updateComplete;
const promise = jsonEditor.once(ProtocolMonitor.JSONEditor.Events.SUBMIT_EDITOR);
// We send the command
dispatchKeyDownEvent(
jsonEditor.contentElement.querySelector('.editor-wrapper')!, {key: 'Enter', ctrlKey: true, metaKey: true});
const response = await promise;
const expectedParameters = {
NoTypeRef: {
testName1: 'testValue1',
testName2: 'testValue2',
},
};
assert.deepEqual(response.parameters, expectedParameters);
});
it('should show the custom editor for an object param that has no type ref', async () => {
const command = 'Test.test14';
const jsonEditor = renderJSONEditor();
await populateMetadata(jsonEditor);
jsonEditor.command = command;
jsonEditor.populateParametersForCommandWithDefaultValues();
await jsonEditor.updateComplete;
const shadowRoot = jsonEditor.contentElement;
const parameters = shadowRoot.querySelector('.parameter');
await renderHoveredElement(parameters);
const addParamButton = jsonEditor.contentElement.querySelector('devtools-button[title="Add custom property"]');
if (!addParamButton) {
throw new Error('No button');
}
// We click two times to display two parameters with key/value pairs
dispatchClickEvent(addParamButton, {
bubbles: true,
composed: true,
});
dispatchClickEvent(addParamButton, {
bubbles: true,
composed: true,
});
await jsonEditor.updateComplete;
// The -1 is need to not take into account the input for the command
const numberOfInputs = jsonEditor.contentElement.querySelectorAll('devtools-suggestion-input').length - 1;
assert.deepEqual(numberOfInputs, 4);
});
describe('Command suggestion filter', () => {
it('filters the commands by substring match', async () => {
assert(ProtocolMonitor.JSONEditor.suggestionFilter('Test', 'Tes'));
assert(ProtocolMonitor.JSONEditor.suggestionFilter('Test', 'est'));
assert.isNotOk(ProtocolMonitor.JSONEditor.suggestionFilter('Test', 'dest'));
});
});
});