UNPKG

chrome-devtools-frontend

Version:
547 lines (487 loc) • 19.6 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 * as Platform from '../../core/platform/platform.js'; import * as ProtocolClient from '../../core/protocol_client/protocol_client.js'; import * as SDK from '../../core/sdk/sdk.js'; import {findMenuItemWithLabel} from '../../testing/ContextMenuHelpers.js'; import {assertScreenshot, renderElementIntoDOM} from '../../testing/DOMHelpers.js'; import {describeWithEnvironment} from '../../testing/EnvironmentHelpers.js'; import {expectCall} from '../../testing/ExpectStubCall.js'; import {stubFileManager} from '../../testing/FileManagerHelpers.js'; import {createViewFunctionStub, type ViewFunctionStub} from '../../testing/ViewFunctionHelpers.js'; import * as UI from '../../ui/legacy/legacy.js'; import * as ProtocolMonitor from './protocol_monitor.js'; const {InspectorBackend} = ProtocolClient; const {ProtocolMonitorImpl} = ProtocolMonitor.ProtocolMonitor; type ProtocolMonitorImpl = ProtocolMonitor.ProtocolMonitor.ProtocolMonitorImpl; const {JSONEditor} = ProtocolMonitor.JSONEditor; type JSONEditor = ProtocolMonitor.JSONEditor.JSONEditor; let view!: ViewFunctionStub<typeof ProtocolMonitorImpl>; let protocolMonitor!: ProtocolMonitorImpl; let jsonEditor!: JSONEditor; let sendRawMessageStub!: sinon.SinonStub; describeWithEnvironment('ProtocolMonitor', () => { let originalSendRawMessage: typeof InspectorBackend.test.sendRawMessage; beforeEach(() => { sendRawMessageStub = sinon.stub(); originalSendRawMessage = InspectorBackend.test.sendRawMessage; InspectorBackend.test.sendRawMessage = sendRawMessageStub; jsonEditor = new JSONEditor(document.createElement('div')); view = createViewFunctionStub(ProtocolMonitorImpl, {editorWidget: jsonEditor}); protocolMonitor = new ProtocolMonitorImpl(view); }); afterEach(() => { InspectorBackend.test.sendRawMessage = originalSendRawMessage; }); it('sends commands', async () => { view.input.onCommandSubmitted( new CustomEvent('submit', {detail: '{"command":"Test.test","parameters":{"test":"test"}}'})); sinon.assert.calledOnce(sendRawMessageStub); sinon.assert.calledOnce(sendRawMessageStub); assert.strictEqual(sendRawMessageStub.getCall(0).args[0], 'Test.test'); assert.deepEqual(sendRawMessageStub.getCall(0).args[1], {test: 'test'}); assert.deepEqual(sendRawMessageStub.getCall(0).args[3], ''); }); it('includes previous commands into autocomplete', async () => { view.input.onCommandSubmitted(new CustomEvent('submit', {detail: 'Test.test1'})); view.input.onCommandSubmitted(new CustomEvent('submit', {detail: 'Test.test2'})); protocolMonitor.requestUpdate(); assert.includeOrderedMembers( (await view.nextInput).commandSuggestions, ['Test.test2', 'Test.test1', 'Accessibility.disable']); }); it('records commands', async () => { protocolMonitor.wasShown(); InspectorBackend.test.onMessageSent?.({domain: 'Test', method: 'Test.test', params: {test: 'test'}, id: 1}, null); assert.deepEqual((await view.nextInput).messages.map(m => ({method: m.method, params: m.params, id: m.id})), [ { method: 'Test.test', params: {test: 'test'}, id: 1, }, ]); InspectorBackend.test.onMessageReceived?.( { id: 1, method: 'Test.test', params: {test: 'test'}, requestTime: 0, result: {test: 'test'}, }, null); assert.deepEqual( (await view.nextInput).messages.map(m => ({method: m.method, params: m.params, id: m.id, result: m.result})), [ { method: 'Test.test', params: {test: 'test'}, id: 1, result: {test: 'test'}, }, ]); }); it('only records commands if recording is enabled', async () => { InspectorBackend.test.onMessageSent?.({domain: 'Test', method: 'Test.test', params: {test: 'test'}, id: 1}, null); protocolMonitor.wasShown(); InspectorBackend.test.onMessageSent?.({domain: 'Test', method: 'Test.test', params: {test: 'test'}, id: 2}, null); assert.deepEqual((await view.nextInput).messages.map(m => ({method: m.method, params: m.params, id: m.id})), [ { method: 'Test.test', params: {test: 'test'}, id: 2, }, ]); view.input.onRecord({target: {toggled: false}} as unknown as Event); InspectorBackend.test.onMessageSent?.({domain: 'Test', method: 'Test.test', params: {test: 'test'}, id: 3}, null); view.input.onRecord({target: {toggled: true}} as unknown as Event); InspectorBackend.test.onMessageSent?.({domain: 'Test', method: 'Test.test', params: {test: 'test'}, id: 4}, null); assert.deepEqual((await view.nextInput).messages.map(m => ({method: m.method, params: m.params, id: m.id})), [ { method: 'Test.test', params: {test: 'test'}, id: 2, }, { method: 'Test.test', params: {test: 'test'}, id: 4, }, ]); }); it('clears messages', async () => { protocolMonitor.wasShown(); InspectorBackend.test.onMessageSent?.({domain: 'Test', method: 'Test.test', params: {test: 'test'}, id: 2}, null); assert.lengthOf((await view.nextInput).messages, 1); view.input.onClear(); assert.lengthOf((await view.nextInput).messages, 0); }); it('saves to file', async () => { const fileManager = stubFileManager(); const fileManagerCloseCall = expectCall(fileManager.close); protocolMonitor.wasShown(); InspectorBackend.test.onMessageSent?.({domain: 'Test', method: 'Test.test', params: {test: 'test'}, id: 2}, null); const TIMESTAMP = 42; const clock = sinon.useFakeTimers(); clock.tick(TIMESTAMP); const FILENAME = 'ProtocolMonitor-' + Platform.DateUtilities.toISO8601Compact(new Date(TIMESTAMP)) + '.json' as Platform.DevToolsPath.RawPathString; (await view.nextInput).onSave(); sinon.assert.calledOnce(fileManager.save); assert.isTrue(fileManager.save.calledOnceWith(FILENAME, '', true, false)); await fileManagerCloseCall; assert.isTrue(fileManager.append.calledOnceWith(FILENAME, sinon.match('"method": "Test.test"'))); clock.restore(); }); describe('context menu', () => { let menu!: UI.ContextMenu.ContextMenu; let element!: HTMLElement; function triggerContextMenu(index: number) { menu = new UI.ContextMenu.ContextMenu(new Event('contextmenu')); element = {dataset: {index: `${index}`}} as unknown as HTMLElement; view.input.onSelect(new CustomEvent('select', {detail: element})); view.input.onContextMenu(new CustomEvent('contextmenu', {detail: {menu, element}})); } beforeEach(() => { menu = new UI.ContextMenu.ContextMenu(new Event('contextmenu')); protocolMonitor.wasShown(); InspectorBackend.test.onMessageSent?.( {domain: 'Test', method: 'Test.test1', params: {test: 'test'}, id: 2}, null); InspectorBackend.test.onMessageSent?.( {domain: 'Test', method: 'Test.test2', params: {test: 'test'}, id: 2}, null); triggerContextMenu(1); }); it('priovides edit and resend context menu item', async () => { assert.isFalse(view.input.sidebarVisible); let editAndResend = findMenuItemWithLabel(menu.editSection(), 'Edit and resend'); assert.exists(editAndResend); menu.invokeHandler(editAndResend.id()); assert.strictEqual((await view.nextInput).command, '{"command":"Test.test2","parameters":{"test":"test"}}'); assert.isTrue(view.input.sidebarVisible); const displayCommandStub = sinon.stub(jsonEditor, 'displayCommand'); triggerContextMenu(0); editAndResend = findMenuItemWithLabel(menu.editSection(), 'Edit and resend'); assert.exists(editAndResend); menu.invokeHandler(editAndResend.id()); assert.isTrue(displayCommandStub.calledOnceWith('Test.test1', {test: 'test'}, '')); }); it('priovides filter context menu item', async () => { const filter = findMenuItemWithLabel(menu.editSection(), 'Filter'); assert.exists(filter); menu.invokeHandler(filter.id()); assert.strictEqual((await view.nextInput).filter, 'method:Test.test2'); }); it('priovides documentation context menu item', async () => { const documentation = findMenuItemWithLabel(menu.footerSection(), 'Documentation'); assert.exists(documentation); const openInNewTabStub = sinon.stub(Host.InspectorFrontendHost.InspectorFrontendHostInstance, 'openInNewTab'); menu.invokeHandler(documentation.id()); assert.isTrue( openInNewTabStub.calledOnceWith('https://chromedevtools.github.io/devtools-protocol/tot/Test#method-test2')); }); }); describe('Display command written in editor inside input bar', () => { it('should display the command edited inside the CDP editor into the input bar', async () => { jsonEditor.command = 'Test.test'; jsonEditor.parameters = [ { name: 'test', type: ProtocolMonitor.JSONEditor.ParameterType.STRING, description: 'test', optional: false, value: 'test', }, ]; view.input.onSplitChange(new CustomEvent('change', {detail: 'OnlyMain'})); assert.deepEqual((await view.nextInput).command, '{"command":"Test.test","parameters":{"test":"test"}}'); }); it('should update the selected target inside the input bar', async () => { jsonEditor.targetId = 'value2'; sinon.stub(SDK.TargetManager.TargetManager.instance(), 'targets').returns([ {id: () => 'value1'} as SDK.Target.Target, {id: () => 'value2'} as SDK.Target.Target, ]); view.input.onSplitChange(new CustomEvent('change', {detail: 'OnlyMain'})); assert.deepEqual((await view.nextInput).selectedTargetId, 'value2'); }); it('should not display the command into the input bar if the command is empty string', async () => { jsonEditor.command = ''; view.input.onSplitChange(new CustomEvent('change', {detail: 'OnlyMain'})); assert.deepEqual((await view.nextInput).command, ''); }); }); describe('parseCommandInput', () => { it('parses various JSON formats', async () => { const input = { command: 'Input.dispatchMouseEvent', parameters: {parameter1: 'value1'}, }; // "command" variations. assert.deepEqual( ProtocolMonitor.ProtocolMonitor.parseCommandInput(JSON.stringify({ command: input.command, parameters: input.parameters, })), input); assert.deepEqual( ProtocolMonitor.ProtocolMonitor.parseCommandInput(JSON.stringify({ cmd: input.command, parameters: input.parameters, })), input); assert.deepEqual( ProtocolMonitor.ProtocolMonitor.parseCommandInput(JSON.stringify({ method: input.command, parameters: input.parameters, })), input); // "parameters" variations. assert.deepEqual( ProtocolMonitor.ProtocolMonitor.parseCommandInput(JSON.stringify({ command: input.command, params: input.parameters, })), input); assert.deepEqual( ProtocolMonitor.ProtocolMonitor.parseCommandInput(JSON.stringify({ cmd: input.command, args: input.parameters, })), input); assert.deepEqual( ProtocolMonitor.ProtocolMonitor.parseCommandInput(JSON.stringify({ method: input.command, arguments: input.parameters, })), input); }); it('parses non-JSON data as a command name', async () => { assert.deepEqual(ProtocolMonitor.ProtocolMonitor.parseCommandInput('Input.dispatchMouseEvent'), { command: 'Input.dispatchMouseEvent', parameters: {}, }); }); it('should correctly creates a map of CDP commands with their corresponding metadata', async () => { const domains = [ { domain: 'Test', metadata: { 'Test.test': { parameters: [{ name: 'test', type: 'test', optional: true, }], description: 'Description1', replyArgs: ['Test1'], }, }, }, { domain: 'Test2', metadata: { 'Test2.test2': { parameters: [{ name: 'test2', type: 'test2', optional: true, }], description: 'Description2', replyArgs: ['Test2'], }, 'Test2.test3': { parameters: [{ name: 'test3', type: 'test3', optional: true, }], description: 'Description3', replyArgs: ['Test3'], }, }, }, ] as Iterable<ProtocolMonitor.ProtocolMonitor.ProtocolDomain>; const expectedCommands = new Map(); expectedCommands.set('Test.test', { parameters: [{ name: 'test', type: 'test', optional: true, }], description: 'Description1', replyArgs: ['Test1'], }); expectedCommands.set('Test2.test2', { parameters: [{ name: 'test2', type: 'test2', optional: true, }], description: 'Description2', replyArgs: ['Test2'], }); expectedCommands.set('Test2.test3', { parameters: [{ name: 'test3', type: 'test3', optional: true, }], description: 'Description3', replyArgs: ['Test3'], }); const metadataByCommand = ProtocolMonitor.ProtocolMonitor.buildProtocolMetadata(domains); assert.deepEqual(metadataByCommand, expectedCommands); }); }); describe('HistoryAutocompleteDataProvider', () => { it('should create completions with no history', async () => { const provider = new ProtocolMonitor.ProtocolMonitor.CommandAutocompleteSuggestionProvider(); assert.deepEqual(await provider.buildTextPromptCompletions('test', 'test'), []); }); it('should build completions in the reverse insertion order', async () => { const provider = new ProtocolMonitor.ProtocolMonitor.CommandAutocompleteSuggestionProvider(); provider.addEntry('test1'); provider.addEntry('test2'); provider.addEntry('test3'); assert.deepEqual(await provider.buildTextPromptCompletions('test', 'test'), [ {text: 'test3'}, {text: 'test2'}, {text: 'test1'}, ]); provider.addEntry('test1'); assert.deepEqual(await provider.buildTextPromptCompletions('test', 'test'), [ {text: 'test1'}, {text: 'test3'}, {text: 'test2'}, ]); }); it('should limit the number of completions', async () => { const provider = new ProtocolMonitor.ProtocolMonitor.CommandAutocompleteSuggestionProvider(2); provider.addEntry('test1'); provider.addEntry('test2'); provider.addEntry('test3'); assert.deepEqual(await provider.buildTextPromptCompletions('test', 'test'), [ {text: 'test3'}, {text: 'test2'}, ]); }); }); }); describeWithEnvironment('view', () => { let target!: HTMLElement; const view = ProtocolMonitor.ProtocolMonitor.DEFAULT_VIEW; beforeEach(async () => { const container = document.createElement('div'); renderElementIntoDOM(container); const widget = new UI.Widget.Widget(); widget.markAsRoot(); widget.show(container); target = widget.element; target.style.display = 'flex'; target.style.width = '780px'; target.style.height = '400px'; }); it('basic', async () => { const viewInput = { messages: [ { id: 1, method: 'Test.test1', result: {result: 'Test1'}, params: {test: 'Test'}, requestTime: 1, elapsedTime: 2, }, { id: 2, method: 'Test.test2', params: {test: 'Test'}, requestTime: 1, elapsedTime: 2, }, { method: 'Test.test2', result: {test: 'Test'}, requestTime: 1, elapsedTime: 2, } ], selectedMessage: undefined, sidebarVisible: false, command: 'Test.test3', commandSuggestions: [], filterKeys: ['method', 'request', 'response', 'target', 'session'], filter: '', parseFilter: (_: string) => [], onSplitChange: (_: CustomEvent<string>) => {}, onRecord: (_: Event) => {}, onClear: () => {}, onSave: () => {}, onSelect: (_: CustomEvent<HTMLElement|null>) => {}, onContextMenu: (_: CustomEvent<{menu: UI.ContextMenu.ContextMenu, element: HTMLElement}>) => {}, onCommandChange: (_: CustomEvent<string>) => {}, onCommandSubmitted: (_: CustomEvent<string>) => {}, onFilterChanged: (_: CustomEvent<string>) => {}, onTargetChange: (_: Event) => {}, onToggleSidebar: (_: Event) => {}, targets: [], selectedTargetId: 'main', }; const viewOutput = {set editorWidget(_value: ProtocolMonitor.JSONEditor.JSONEditor) {}}; view(viewInput, viewOutput, target); await assertScreenshot('protocol_monitor/basic.png'); }); it('advanced', async () => { const messages = [ { id: 1, method: 'Test.test1', result: {result: 'Test1'}, params: {test: 'Test'}, requestTime: 1, elapsedTime: 2, }, { id: 2, method: 'Test.test2', params: {test: 'Test'}, requestTime: 1, elapsedTime: 2, }, { method: 'Test.test3', result: {test: 'Test'}, requestTime: 1, elapsedTime: 2, } ]; const viewInput = { messages, selectedMessage: messages[2], sidebarVisible: false, command: '{"command": "Test.test3"}', commandSuggestions: [], filterKeys: ['method', 'request', 'response', 'target', 'session'], filter: 'method:Test.test3', parseFilter: (_: string) => [{key: 'method', text: 'test3', negative: false}], onSplitChange: (_: CustomEvent<string>) => {}, onRecord: (_: Event) => {}, onClear: () => {}, onSave: () => {}, onSelect: (_: CustomEvent<HTMLElement|null>) => {}, onContextMenu: (_: CustomEvent<{menu: UI.ContextMenu.ContextMenu, element: HTMLElement}>) => {}, onCommandChange: (_: CustomEvent<string>) => {}, onCommandSubmitted: (_: CustomEvent<string>) => {}, onFilterChanged: (_: CustomEvent<string>) => {}, onTargetChange: (_: Event) => {}, onToggleSidebar: (_: Event) => {}, targets: [ {id: () => 'main', name: () => 'Main', inspectedURL: () => 'www.example.com'}, {id: () => 'prerender', name: () => 'Prerender', inspectedURL: () => 'www.example.com/prerender'} ] as SDK.Target.Target[], selectedTargetId: 'prerender', }; const viewOutput = {set editorWidget(_value: ProtocolMonitor.JSONEditor.JSONEditor) {}}; view(viewInput, viewOutput, target); await assertScreenshot('protocol_monitor/advanced.png'); }); });