UNPKG

chrome-devtools-frontend

Version:
1,322 lines (1,111 loc) • 45.2 kB
// 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')); }); }); });