| // 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. |
| |
| const {assert} = chai; |
| import * as ProtocolMonitor from '../../../../../../front_end/panels/protocol_monitor/protocol_monitor.js'; |
| import { |
| getEventPromise, |
| dispatchKeyDownEvent, |
| dispatchMouseMoveEvent, |
| dispatchClickEvent, |
| renderElementIntoDOM, |
| raf, |
| } from '../../../helpers/DOMHelpers.js'; |
| import * as ProtocolComponents from '../../../../../../front_end/panels/protocol_monitor/components/components.js'; |
| import type * as SuggestionInput from '../../../../../../front_end/ui/components/suggestion_input/suggestion_input.js'; |
| import type * as IconButton from '../../../../../../front_end/ui/components/icon_button/icon_button.js'; |
| |
| import {describeWithEnvironment} from '../../../helpers/EnvironmentHelpers.js'; |
| import * as Menus from '../../../../../../front_end/ui/components/menus/menus.js'; |
| import * as Host from '../../../../../../front_end/core/host/host.js'; |
| import * as UI from '../../../../../../front_end/ui/legacy/legacy.js'; |
| |
| describeWithEnvironment('JSONEditor', () => { |
| const renderJSONEditor = () => { |
| const jsonEditor = new ProtocolComponents.JSONEditor.JSONEditor(); |
| jsonEditor.metadataByCommand = new Map(); |
| jsonEditor.typesByName = new Map(); |
| jsonEditor.enumsByName = new Map(); |
| jsonEditor.connectedCallback(); |
| renderElementIntoDOM(jsonEditor); |
| return jsonEditor; |
| }; |
| |
| const populateMetadata = async(jsonEditor: ProtocolComponents.JSONEditor.JSONEditor): Promise<void> => { |
| 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': true, |
| '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, |
| }], |
| 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': true, |
| 'description': '', |
| 'typeRef': 'Tracing.TraceConfig', |
| }, |
| ], |
| description: 'Description9.', |
| replyArgs: ['Test9'], |
| }, |
| 'Test.test10': { |
| parameters: [ |
| { |
| 'name': 'NoTypeRef', |
| 'type': 'object', |
| 'optional': true, |
| 'description': '', |
| 'typeRef': 'NoTypeRef', |
| }, |
| ], |
| description: 'Description9.', |
| replyArgs: ['Test9'], |
| }, |
| 'Test.test11': { |
| parameters: [{ |
| 'optional': false, |
| 'type': 'array', |
| 'name': 'test11', |
| 'typeRef': 'Test.arrayTypeRef', |
| }], |
| }, |
| }, |
| }, |
| ] 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, enumsByName?: Map<string, Record<string, string>>): Promise<string[]> => { |
| const jsonEditor = renderJSONEditor(); |
| |
| await populateMetadata(jsonEditor); |
| jsonEditor.command = command; |
| if (enumsByName) { |
| jsonEditor.enumsByName = enumsByName; |
| } |
| jsonEditor.populateParametersForCommandWithDefaultValues(); |
| await jsonEditor.updateComplete; |
| |
| const inputs = jsonEditor.renderRoot.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 = (): string|null|undefined => { |
| const container = document.body.querySelector<HTMLDivElement>('[data-devtools-glass-pane]'); |
| const hintDetailView = container?.shadowRoot?.querySelector('devtools-css-hint-details-view'); |
| return hintDetailView?.shadowRoot?.textContent?.replaceAll(/\s/g, ''); |
| }; |
| |
| const renderEditorForCommand = async(command: string, parameters: {[paramName: string]: unknown}): Promise<{ |
| inputs: NodeListOf<SuggestionInput.SuggestionInput.SuggestionInput>, |
| displayedCommand: string, |
| jsonEditor: ProtocolComponents.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.renderRoot; |
| const inputs = shadowRoot.querySelectorAll('devtools-suggestion-input'); |
| const displayedCommand = jsonEditor.command; |
| return {inputs, displayedCommand, jsonEditor}; |
| }; |
| |
| const renderParamsWithDefaultValues = |
| async(command: string): Promise<SuggestionInput.SuggestionInput.SuggestionInput> => { |
| const jsonEditor = renderJSONEditor(); |
| |
| await populateMetadata(jsonEditor); |
| jsonEditor.command = command; |
| jsonEditor.populateParametersForCommandWithDefaultValues(); |
| await jsonEditor.updateComplete; |
| |
| const param = jsonEditor.renderRoot.querySelector('[data-paramId]'); |
| await renderHoveredElement(param); |
| |
| const setDefaultValueButton = jsonEditor.renderRoot.querySelector('devtools-button'); |
| if (!setDefaultValueButton) { |
| throw new Error('No button'); |
| } |
| dispatchClickEvent(setDefaultValueButton, { |
| bubbles: true, |
| composed: true, |
| }); |
| |
| await jsonEditor.updateComplete; |
| |
| const input = jsonEditor.renderRoot.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>>): Promise<IconButton.Icon.Icon> => { |
| 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.renderRoot.querySelectorAll('devtools-suggestion-input')[1]; |
| if (!input) { |
| throw Error('No editable content displayed'); |
| } |
| input.value = 'Not an accepted value'; |
| await jsonEditor.updateComplete; |
| input.focus(); |
| input.blur(); |
| await jsonEditor.updateComplete; |
| const warningIcon = jsonEditor.renderRoot.querySelector('devtools-icon'); |
| if (!warningIcon) { |
| throw 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.renderRoot.textContent?.replaceAll(/\s/g, ''); |
| const expectedValue = 'test'; |
| assert.deepStrictEqual(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.renderRoot.textContent?.replaceAll(/\s/g, ''); |
| const expectedValue = 'test'; |
| assert.deepStrictEqual(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.renderRoot.textContent?.replaceAll(/\s/g, ''); |
| const expectedValue = 'test1'; |
| assert.deepStrictEqual(value, expectedValue); |
| }); |
| 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.deepStrictEqual(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.deepStrictEqual(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.deepStrictEqual(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.deepStrictEqual(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.deepStrictEqual(displayedCommand, ''); |
| }); |
| }); |
| |
| describe('Display command written in editor inside input bar', () => { |
| it('should display the command edited inside the CDP editor into the input bar', async () => { |
| const split = new UI.SplitWidget.SplitWidget(true, false, 'protocol-monitor-split-container', 400); |
| const editorWidget = new ProtocolMonitor.ProtocolMonitor.EditorWidget(); |
| const jsonEditor = editorWidget.jsonEditor; |
| jsonEditor.command = 'Test.test'; |
| jsonEditor.parameters = [ |
| { |
| name: 'test', |
| type: ProtocolComponents.JSONEditor.ParameterType.String, |
| description: 'test', |
| optional: false, |
| value: 'test', |
| }, |
| ]; |
| const dataGrid = new ProtocolMonitor.ProtocolMonitor.ProtocolMonitorDataGrid(split); |
| split.setMainWidget(dataGrid); |
| split.setSidebarWidget(editorWidget); |
| // Needed to resolve pending promises in the after each hook |
| await new Promise(resolve => { |
| split.toggleSidebar(); |
| setTimeout(resolve, 500); |
| }); |
| |
| // The first input bar corresponds to the filter bar, so we query the second one which corresponds to the CDP one. |
| const toolbarInput = dataGrid.element.shadowRoot?.querySelectorAll('.toolbar')[1].shadowRoot?.querySelector( |
| '.toolbar-input-prompt'); |
| assert.deepStrictEqual(toolbarInput?.innerHTML, '{"command":"Test.test","parameters":{"test":"test"}}'); |
| }); |
| |
| it('should not display the command into the input bar if the command is empty string', async () => { |
| const split = new UI.SplitWidget.SplitWidget(true, false, 'protocol-monitor-split-container', 400); |
| const editorWidget = new ProtocolMonitor.ProtocolMonitor.EditorWidget(); |
| const jsonEditor = editorWidget.jsonEditor; |
| jsonEditor.command = ''; |
| const dataGrid = new ProtocolMonitor.ProtocolMonitor.ProtocolMonitorDataGrid(split); |
| split.setMainWidget(dataGrid); |
| split.setSidebarWidget(editorWidget); |
| // Needed to resolve pending promises in the after each hook |
| await new Promise(resolve => { |
| split.toggleSidebar(); |
| setTimeout(resolve, 500); |
| }); |
| |
| // The first input bar corresponds to the filter bar, so we query the second one which corresponds to the CDP one. |
| const toolbarInput = dataGrid.element.shadowRoot?.querySelectorAll('.toolbar')[1].shadowRoot?.querySelector( |
| '.toolbar-input-prompt'); |
| assert.deepStrictEqual(toolbarInput?.innerHTML, ''); |
| }); |
| }); |
| 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 ProtocolComponents.JSONEditor.Parameter[]; |
| const jsonEditor = renderJSONEditor(); |
| |
| jsonEditor.parameters = inputParameters; |
| await jsonEditor.updateComplete; |
| const param = jsonEditor.renderRoot.querySelector('[data-paramId]'); |
| |
| await renderHoveredElement(param); |
| const popupContent = serializePopupContent(); |
| const expectedPopupContent = 'test.Type:arrayLearnMore'; |
| assert.deepStrictEqual(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.renderRoot.querySelector('.command'); |
| await renderHoveredElement(command); |
| |
| const popupContent = serializePopupContent(); |
| |
| const expectedPopupContent = 'Description1.Returns:Test1LearnMore'; |
| assert.deepStrictEqual(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 suggestions = await renderSuggestionBox(command, enumsByName); |
| assert.deepStrictEqual(suggestions, ['test', 'test1', 'test2']); |
| }); |
| |
| it('should display suggestion box with correct suggestions when the parameter is a boolean', async () => { |
| const command = 'Test.test4'; |
| |
| const suggestions = await renderSuggestionBox(command); |
| |
| assert.deepStrictEqual(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.renderRoot.querySelector('[data-paramId]'); |
| await renderHoveredElement(param); |
| |
| const addParamButton = jsonEditor.renderRoot.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.renderRoot.querySelectorAll('devtools-suggestion-input'); |
| // inputs[0] corresponds to the devtools-recorder-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.deepStrictEqual(suggestions, ['test', 'test1', 'test2']); |
| }); |
| |
| it('should not display suggestion box when the parameter is neither a string or a boolean', async () => { |
| const command = 'Test.test8'; |
| |
| const suggestions = await renderSuggestionBox(command); |
| |
| assert.deepStrictEqual(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.deepStrictEqual(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.deepStrictEqual(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.deepStrictEqual(value, expectedValue); |
| }); |
| }); |
| |
| 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.renderRoot.querySelector('[data-paramId=\'test3\']'); |
| |
| await renderHoveredElement(param); |
| |
| const setDefaultValueButton = jsonEditor.renderRoot.querySelector('devtools-button'); |
| |
| if (!setDefaultValueButton) { |
| throw new Error('No button'); |
| } |
| dispatchClickEvent(setDefaultValueButton, { |
| bubbles: true, |
| composed: true, |
| }); |
| |
| await jsonEditor.updateComplete; |
| |
| const input = jsonEditor.renderRoot.querySelectorAll('devtools-suggestion-input'); |
| const values = [input[1].value, input[2].value]; |
| |
| const expectedValues = ['', '']; |
| |
| assert.deepStrictEqual(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 ProtocolComponents.JSONEditor.Parameter[]; |
| await jsonEditor.updateComplete; |
| |
| const param = jsonEditor.renderRoot.querySelector('[data-paramId=\'arrayParam\']'); |
| |
| await renderHoveredElement(param); |
| |
| const setDefaultValueButton = |
| jsonEditor.renderRoot.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.deepStrictEqual(value, []); |
| }); |
| }); |
| |
| 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 ProtocolComponents.JSONEditor.Parameter[]; |
| await jsonEditor.updateComplete; |
| |
| const shadowRoot = jsonEditor.renderRoot; |
| |
| 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.deepStrictEqual(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.renderRoot.querySelector('[data-paramId]'); |
| await renderHoveredElement(param); |
| |
| const addParamButton = jsonEditor.renderRoot.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.renderRoot.querySelectorAll('devtools-suggestion-input').length - 1; |
| |
| assert.deepStrictEqual(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 ProtocolComponents.JSONEditor.Parameter[]; |
| const responsePromise = getEventPromise(jsonEditor, ProtocolComponents.JSONEditor.SubmitEditorEvent.eventName); |
| |
| dispatchKeyDownEvent(jsonEditor, {key: 'Enter', ctrlKey: true, metaKey: true}); |
| |
| const response = await responsePromise as ProtocolComponents.JSONEditor.SubmitEditorEvent; |
| |
| assert.deepStrictEqual(response.data.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: ProtocolComponents.JSONEditor.ParameterType.String, |
| description: 'test', |
| optional: false, |
| value: 'testValue', |
| }, |
| ]; |
| await jsonEditor.updateComplete; |
| |
| const toolbar = jsonEditor.renderRoot.querySelector('devtools-pm-toolbar'); |
| if (!toolbar) { |
| throw Error('No toolbar found !'); |
| } |
| const event = new ProtocolComponents.Toolbar.SendCommandEvent(); |
| const responsePromise = getEventPromise(jsonEditor, ProtocolComponents.JSONEditor.SubmitEditorEvent.eventName); |
| |
| toolbar.dispatchEvent(event); |
| const response = await responsePromise as ProtocolComponents.JSONEditor.SubmitEditorEvent; |
| |
| const expectedParameters = { |
| 'testName': 'testValue', |
| }; |
| |
| assert.deepStrictEqual(response.data.parameters, expectedParameters); |
| }); |
| }); |
| |
| describe('Verify the type of the entered value', async () => { |
| 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.renderRoot.querySelectorAll('devtools-suggestion-input'); |
| const addButtons = jsonEditor.renderRoot.querySelectorAll('devtools-button[title="Add a parameter"]'); |
| |
| assert.deepStrictEqual(inputs.length, 1); |
| assert.deepStrictEqual(addButtons.length, 0); |
| }); |
| |
| it('checks that the selection of a target works', async () => { |
| const jsonEditor = renderJSONEditor(); |
| await jsonEditor.updateComplete; |
| const targetId = 'target1'; |
| const event = new Menus.SelectMenu.SelectMenuItemSelectedEvent('target1'); |
| |
| const shadowRoot = jsonEditor.renderRoot; |
| const selectMenu = shadowRoot.querySelector('devtools-select-menu'); |
| selectMenu?.dispatchEvent(event); |
| const expectedId = jsonEditor.targetId; |
| |
| assert.deepStrictEqual(targetId, expectedId); |
| }); |
| |
| it('should copy the CDP command to clipboard via copy event', async () => { |
| const jsonEditor = renderJSONEditor(); |
| jsonEditor.command = 'Test.test'; |
| jsonEditor.parameters = [ |
| { |
| name: 'test', |
| type: ProtocolComponents.JSONEditor.ParameterType.String, |
| description: 'test', |
| optional: false, |
| value: 'test', |
| }, |
| ]; |
| await jsonEditor.updateComplete; |
| const isCalled = sinon.promise(); |
| const copyText = sinon |
| .stub( |
| Host.InspectorFrontendHost.InspectorFrontendHostInstance, |
| 'copyText', |
| ) |
| .callsFake(() => { |
| void isCalled.resolve(true); |
| }); |
| const toolbar = jsonEditor.renderRoot.querySelector('devtools-pm-toolbar'); |
| if (!toolbar) { |
| throw Error('No toolbar found !'); |
| } |
| const event = new ProtocolComponents.Toolbar.CopyCommandEvent(); |
| toolbar.dispatchEvent(event); |
| await isCalled; |
| |
| assert.isTrue(copyText.calledWith(JSON.stringify({command: 'Test.test', parameters: {'test': 'test'}}))); |
| }); |
| |
| 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.renderRoot; |
| 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.deepStrictEqual(parameters.length, 6); |
| }); |
| |
| it('should return the parameters in a format understandable by the ProtocolMonitor when sending a command with object parameter that has no typeRef', |
| 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.renderRoot; |
| const parameters = shadowRoot.querySelector('.parameter'); |
| |
| await renderHoveredElement(parameters); |
| |
| const addParamButton = jsonEditor.renderRoot.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 responsePromise = getEventPromise(jsonEditor, ProtocolComponents.JSONEditor.SubmitEditorEvent.eventName); |
| |
| // We send the command |
| dispatchKeyDownEvent(jsonEditor, {key: 'Enter', ctrlKey: true, metaKey: true}); |
| |
| const response = await responsePromise as ProtocolComponents.JSONEditor.SubmitEditorEvent; |
| |
| const expectedParameters = { |
| 'NoTypeRef': { |
| 'testName1': 'testValue1', |
| 'testName2': 'testValue2', |
| }, |
| }; |
| |
| assert.deepStrictEqual(response.data.parameters, expectedParameters); |
| }); |
| |
| }); |