UNPKG

chrome-devtools-frontend

Version:
1,259 lines (1,110 loc) • 52.1 kB
// Copyright 2024 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 Common from '../../core/common/common.js'; import * as Host from '../../core/host/host.js'; import * as Platform from '../../core/platform/platform.js'; import * as SDK from '../../core/sdk/sdk.js'; import * as Workspace from '../../models/workspace/workspace.js'; import {mockAidaClient} from '../../testing/AiAssistanceHelpers.js'; import {findMenuItemWithLabel, getMenu} from '../../testing/ContextMenuHelpers.js'; import {dispatchClickEvent} from '../../testing/DOMHelpers.js'; import {describeWithEnvironment, getGetHostConfigStub, registerNoopActions} from '../../testing/EnvironmentHelpers.js'; import * as UI from '../../ui/legacy/legacy.js'; import * as ElementsPanel from '../elements/elements.js'; import * as NetworkPanel from '../network/network.js'; import * as SourcesPanel from '../sources/sources.js'; import * as TimelinePanel from '../timeline/timeline.js'; import * as TimelineUtils from '../timeline/utils/utils.js'; import * as AiAssistance from './ai_assistance.js'; const {urlString} = Platform.DevToolsPath; function getTestSyncInfo(): Host.InspectorFrontendHostAPI.SyncInformation { return {isSyncActive: true}; } async function drainMicroTasks() { await new Promise(resolve => setTimeout(resolve, 0)); } describeWithEnvironment('FreestylerPanel', () => { let mockView: sinon.SinonStub<[AiAssistance.Props, unknown, HTMLElement]>; let panel: AiAssistance.AiAssistancePanel; beforeEach(() => { mockView = sinon.stub(); registerNoopActions(['elements.toggle-element-search']); UI.Context.Context.instance().setFlavor(ElementsPanel.ElementsPanel.ElementsPanel, null); UI.Context.Context.instance().setFlavor(NetworkPanel.NetworkPanel.NetworkPanel, null); UI.Context.Context.instance().setFlavor(SourcesPanel.SourcesPanel.SourcesPanel, null); UI.Context.Context.instance().setFlavor(TimelinePanel.TimelinePanel.TimelinePanel, null); }); afterEach(() => { panel.detach(); }); describe('consent view', () => { it('should render consent view when the consent is not given before', async () => { panel = new AiAssistance.AiAssistancePanel(mockView, { aidaClient: mockAidaClient([[{explanation: 'test'}]]), aidaAvailability: Host.AidaClient.AidaAccessPreconditions.AVAILABLE, syncInfo: getTestSyncInfo(), }); panel.markAsRoot(); panel.show(document.body); sinon.assert.calledWith(mockView, sinon.match({state: AiAssistance.State.CONSENT_VIEW})); }); it('should switch from consent view to chat view when enabling setting', async () => { panel = new AiAssistance.AiAssistancePanel(mockView, { aidaClient: mockAidaClient([[{explanation: 'test'}]]), aidaAvailability: Host.AidaClient.AidaAccessPreconditions.AVAILABLE, syncInfo: getTestSyncInfo(), }); panel.markAsRoot(); panel.show(document.body); sinon.assert.calledWith(mockView, sinon.match({state: AiAssistance.State.CONSENT_VIEW})); Common.Settings.moduleSetting('ai-assistance-enabled').set(true); sinon.assert.calledWith(mockView, sinon.match({state: AiAssistance.State.CHAT_VIEW})); await drainMicroTasks(); }); it('should render chat view when the consent is given before', async () => { Common.Settings.moduleSetting('ai-assistance-enabled').set(true); panel = new AiAssistance.AiAssistancePanel(mockView, { aidaClient: mockAidaClient([[{explanation: 'test'}]]), aidaAvailability: Host.AidaClient.AidaAccessPreconditions.AVAILABLE, syncInfo: getTestSyncInfo(), }); panel.markAsRoot(); panel.show(document.body); sinon.assert.calledWith(mockView, sinon.match({state: AiAssistance.State.CHAT_VIEW})); }); it('should render the consent view when the setting is disabled', async () => { Common.Settings.moduleSetting('ai-assistance-enabled').set(true); Common.Settings.moduleSetting('ai-assistance-enabled').setDisabled(true); const chatUiStates: AiAssistance.State[] = []; const viewStub = sinon.stub().callsFake(props => { chatUiStates.push(props.state); }); panel = new AiAssistance.AiAssistancePanel(viewStub, { aidaClient: mockAidaClient([[{explanation: 'test'}]]), aidaAvailability: Host.AidaClient.AidaAccessPreconditions.AVAILABLE, syncInfo: getTestSyncInfo(), }); panel.markAsRoot(); panel.show(document.body); await drainMicroTasks(); sinon.assert.calledWith(viewStub, sinon.match({state: AiAssistance.State.CONSENT_VIEW})); assert.isFalse(chatUiStates.includes(AiAssistance.State.CHAT_VIEW)); Common.Settings.moduleSetting('ai-assistance-enabled').setDisabled(false); }); it('should render the consent view when blocked by age', async () => { Common.Settings.moduleSetting('ai-assistance-enabled').set(true); const stub = getGetHostConfigStub({ aidaAvailability: { blockedByAge: true, }, devToolsFreestyler: { enabled: true, }, }); panel = new AiAssistance.AiAssistancePanel(mockView, { aidaClient: mockAidaClient([[{explanation: 'test'}]]), aidaAvailability: Host.AidaClient.AidaAccessPreconditions.AVAILABLE, syncInfo: getTestSyncInfo(), }); panel.markAsRoot(); panel.show(document.body); sinon.assert.calledWith(mockView, sinon.match({state: AiAssistance.State.CONSENT_VIEW})); stub.restore(); }); it('updates when the user logs in', async () => { Common.Settings.moduleSetting('ai-assistance-enabled').set(true); panel = new AiAssistance.AiAssistancePanel(mockView, { aidaClient: mockAidaClient([[{explanation: 'test'}]]), aidaAvailability: Host.AidaClient.AidaAccessPreconditions.NO_ACCOUNT_EMAIL, syncInfo: getTestSyncInfo(), }); panel.markAsRoot(); panel.show(document.body); await drainMicroTasks(); sinon.assert.calledWith(mockView, sinon.match({ state: AiAssistance.State.CHAT_VIEW, aidaAvailability: Host.AidaClient.AidaAccessPreconditions.NO_ACCOUNT_EMAIL, })); mockView.reset(); const stub = sinon.stub(Host.AidaClient.AidaClient, 'checkAccessPreconditions') .returns(Promise.resolve(Host.AidaClient.AidaAccessPreconditions.AVAILABLE)); Host.AidaClient.HostConfigTracker.instance().dispatchEventToListeners( Host.AidaClient.Events.AIDA_AVAILABILITY_CHANGED); await drainMicroTasks(); sinon.assert.calledWith(mockView, sinon.match({ state: AiAssistance.State.CHAT_VIEW, aidaAvailability: Host.AidaClient.AidaAccessPreconditions.AVAILABLE, })); stub.restore(); }); }); describe('on rate click', () => { it('renders a button linking to settings', () => { const stub = sinon.stub(UI.ViewManager.ViewManager.instance(), 'showView'); panel = new AiAssistance.AiAssistancePanel(mockView, { aidaClient: mockAidaClient([[{explanation: 'test'}]]), aidaAvailability: Host.AidaClient.AidaAccessPreconditions.AVAILABLE, syncInfo: getTestSyncInfo(), }); const button = panel.contentElement.querySelector('devtools-button[aria-label=\'Settings\']'); assert.instanceOf(button, HTMLElement); button.click(); assert.isTrue(stub.calledWith('chrome-ai')); stub.restore(); }); it('should allow logging if configured', () => { const stub = getGetHostConfigStub({ aidaAvailability: { disallowLogging: false, }, }); const aidaClient = mockAidaClient([[{explanation: 'test'}]]); panel = new AiAssistance.AiAssistancePanel(mockView, { aidaClient, aidaAvailability: Host.AidaClient.AidaAccessPreconditions.AVAILABLE, syncInfo: getTestSyncInfo(), }); panel.markAsRoot(); panel.show(document.body); const callArgs = mockView.getCall(0)?.args[0]; mockView.reset(); callArgs.onFeedbackSubmit(0, Host.AidaClient.Rating.POSITIVE); sinon.assert.match((aidaClient.registerClientEvent as sinon.SinonStub).firstCall.firstArg, sinon.match({ disable_user_content_logging: false, })); stub.restore(); }); it('should send POSITIVE rating to aida client when the user clicks on positive rating', () => { const RPC_ID = 0; const aidaClient = mockAidaClient([[{explanation: 'test'}]]); panel = new AiAssistance.AiAssistancePanel(mockView, { aidaClient, aidaAvailability: Host.AidaClient.AidaAccessPreconditions.AVAILABLE, syncInfo: getTestSyncInfo(), }); panel.markAsRoot(); panel.show(document.body); const callArgs = mockView.getCall(0).args[0]; mockView.reset(); callArgs.onFeedbackSubmit(RPC_ID, Host.AidaClient.Rating.POSITIVE); sinon.assert.match((aidaClient.registerClientEvent as sinon.SinonStub).firstCall.firstArg, sinon.match({ corresponding_aida_rpc_global_id: RPC_ID, do_conversation_client_event: { user_feedback: { sentiment: 'POSITIVE', }, }, disable_user_content_logging: true, })); }); it('should send NEGATIVE rating to aida client when the user clicks on positive rating', () => { const RPC_ID = 0; const aidaClient = mockAidaClient([[{explanation: 'test'}]]); panel = new AiAssistance.AiAssistancePanel(mockView, { aidaClient, aidaAvailability: Host.AidaClient.AidaAccessPreconditions.AVAILABLE, syncInfo: getTestSyncInfo(), }); panel.markAsRoot(); panel.show(document.body); const callArgs = mockView.getCall(0).args[0]; mockView.reset(); callArgs.onFeedbackSubmit(RPC_ID, Host.AidaClient.Rating.NEGATIVE); sinon.assert.match((aidaClient.registerClientEvent as sinon.SinonStub).firstCall.firstArg, sinon.match({ corresponding_aida_rpc_global_id: RPC_ID, do_conversation_client_event: { user_feedback: { sentiment: 'NEGATIVE', }, }, disable_user_content_logging: true, })); }); it('should send feedback text with data', () => { const RPC_ID = 0; const feedback = 'This helped me a ton.'; const aidaClient = mockAidaClient([[{explanation: 'test'}]]); panel = new AiAssistance.AiAssistancePanel(mockView, { aidaClient, aidaAvailability: Host.AidaClient.AidaAccessPreconditions.AVAILABLE, syncInfo: getTestSyncInfo(), }); panel.markAsRoot(); panel.show(document.body); const callArgs = mockView.getCall(0).args[0]; mockView.reset(); callArgs.onFeedbackSubmit(RPC_ID, Host.AidaClient.Rating.POSITIVE, feedback); sinon.assert.match((aidaClient.registerClientEvent as sinon.SinonStub).firstCall.firstArg, sinon.match({ corresponding_aida_rpc_global_id: RPC_ID, do_conversation_client_event: { user_feedback: { sentiment: 'POSITIVE', user_input: { comment: feedback, }, }, }, disable_user_content_logging: true, })); }); }); describe('flavor change listeners', () => { describe('SDK.DOMModel.DOMNode flavor changes for selected element', () => { it('should set the selected element when the widget is shown', () => { UI.Context.Context.instance().setFlavor(SDK.DOMModel.DOMNode, null); panel = new AiAssistance.AiAssistancePanel(mockView, { aidaClient: mockAidaClient([[{explanation: 'test'}]]), aidaAvailability: Host.AidaClient.AidaAccessPreconditions.AVAILABLE, syncInfo: getTestSyncInfo(), }); const node = sinon.createStubInstance(SDK.DOMModel.DOMNode, { nodeType: Node.ELEMENT_NODE, }); UI.Context.Context.instance().setFlavor(SDK.DOMModel.DOMNode, node); panel.markAsRoot(); panel.show(document.body); panel.handleAction('freestyler.elements-floating-button'); sinon.assert.calledWith(mockView, sinon.match({ selectedContext: new AiAssistance.NodeContext(node), })); }); it('should update the selected element when the changed DOMNode flavor is an ELEMENT_NODE', () => { UI.Context.Context.instance().setFlavor(SDK.DOMModel.DOMNode, null); panel = new AiAssistance.AiAssistancePanel(mockView, { aidaClient: mockAidaClient([[{explanation: 'test'}]]), aidaAvailability: Host.AidaClient.AidaAccessPreconditions.AVAILABLE, syncInfo: getTestSyncInfo(), }); panel.markAsRoot(); panel.show(document.body); panel.handleAction('freestyler.elements-floating-button'); sinon.assert.calledWith(mockView, sinon.match({ selectedContext: null, })); const node = sinon.createStubInstance(SDK.DOMModel.DOMNode, { nodeType: Node.ELEMENT_NODE, }); UI.Context.Context.instance().setFlavor(SDK.DOMModel.DOMNode, node); sinon.assert.calledWith(mockView, sinon.match({ selectedContext: new AiAssistance.NodeContext(node), })); }); it('should set selected element to null when the change DOMNode flavor is not an ELEMENT_NODE', () => { UI.Context.Context.instance().setFlavor(SDK.DOMModel.DOMNode, null); panel = new AiAssistance.AiAssistancePanel(mockView, { aidaClient: mockAidaClient([[{explanation: 'test'}]]), aidaAvailability: Host.AidaClient.AidaAccessPreconditions.AVAILABLE, syncInfo: getTestSyncInfo(), }); panel.markAsRoot(); panel.show(document.body); panel.handleAction('freestyler.elements-floating-button'); sinon.assert.calledWith(mockView, sinon.match({ selectedContext: null, })); const node = sinon.createStubInstance(SDK.DOMModel.DOMNode, { nodeType: Node.COMMENT_NODE, }); UI.Context.Context.instance().setFlavor(SDK.DOMModel.DOMNode, node); sinon.assert.calledWith(mockView, sinon.match({ selectedContext: null, })); }); it('should not handle DOMNode flavor changes if the widget is not shown', () => { UI.Context.Context.instance().setFlavor(SDK.DOMModel.DOMNode, null); panel = new AiAssistance.AiAssistancePanel(mockView, { aidaClient: mockAidaClient([[{explanation: 'test'}]]), aidaAvailability: Host.AidaClient.AidaAccessPreconditions.AVAILABLE, syncInfo: getTestSyncInfo(), }); const node = sinon.createStubInstance(SDK.DOMModel.DOMNode, { nodeType: Node.ELEMENT_NODE, }); UI.Context.Context.instance().setFlavor(SDK.DOMModel.DOMNode, node); sinon.assert.notCalled(mockView); }); }); describe('SDK.NetworkRequest.NetworkRequest flavor changes for selected network request', () => { it('should set the selected network request when the widget is shown', () => { UI.Context.Context.instance().setFlavor(SDK.NetworkRequest.NetworkRequest, null); panel = new AiAssistance.AiAssistancePanel(mockView, { aidaClient: mockAidaClient([[{explanation: 'test'}]]), aidaAvailability: Host.AidaClient.AidaAccessPreconditions.AVAILABLE, syncInfo: getTestSyncInfo(), }); const networkRequest = sinon.createStubInstance(SDK.NetworkRequest.NetworkRequest); UI.Context.Context.instance().setFlavor(SDK.NetworkRequest.NetworkRequest, networkRequest); panel.markAsRoot(); panel.show(document.body); panel.handleAction('drjones.network-floating-button'); sinon.assert.calledWith(mockView, sinon.match({ selectedContext: new AiAssistance.RequestContext(networkRequest), })); }); it('should set selected network request when the NetworkRequest flavor changes', () => { UI.Context.Context.instance().setFlavor(SDK.NetworkRequest.NetworkRequest, null); panel = new AiAssistance.AiAssistancePanel(mockView, { aidaClient: mockAidaClient([[{explanation: 'test'}]]), aidaAvailability: Host.AidaClient.AidaAccessPreconditions.AVAILABLE, syncInfo: getTestSyncInfo(), }); panel.markAsRoot(); panel.show(document.body); panel.handleAction('drjones.network-floating-button'); sinon.assert.calledWith(mockView, sinon.match({ selectedContext: null, })); const networkRequest = sinon.createStubInstance(SDK.NetworkRequest.NetworkRequest); UI.Context.Context.instance().setFlavor(SDK.NetworkRequest.NetworkRequest, networkRequest); sinon.assert.calledWith(mockView, sinon.match({ selectedContext: new AiAssistance.RequestContext(networkRequest), })); }); it('should not handle NetworkRequest flavor changes if the widget is not shown', () => { UI.Context.Context.instance().setFlavor(SDK.NetworkRequest.NetworkRequest, null); panel = new AiAssistance.AiAssistancePanel(mockView, { aidaClient: mockAidaClient([[{explanation: 'test'}]]), aidaAvailability: Host.AidaClient.AidaAccessPreconditions.AVAILABLE, syncInfo: getTestSyncInfo(), }); const networkRequest = sinon.createStubInstance(SDK.NetworkRequest.NetworkRequest); UI.Context.Context.instance().setFlavor(SDK.NetworkRequest.NetworkRequest, networkRequest); sinon.assert.notCalled(mockView); }); }); describe('TimelineUtils.AICallTree.AICallTree flavor changes for selected call tree', () => { it('should set the selected call tree when the widget is shown', () => { UI.Context.Context.instance().setFlavor(TimelineUtils.AICallTree.AICallTree, null); panel = new AiAssistance.AiAssistancePanel(mockView, { aidaClient: mockAidaClient([[{explanation: 'test'}]]), aidaAvailability: Host.AidaClient.AidaAccessPreconditions.AVAILABLE, syncInfo: getTestSyncInfo(), }); const selectedAiCallTree = {}; UI.Context.Context.instance().setFlavor(TimelineUtils.AICallTree.AICallTree, selectedAiCallTree); panel.markAsRoot(); panel.show(document.body); panel.handleAction('drjones.performance-panel-context'); sinon.assert.calledWith(mockView, sinon.match({ selectedContext: new AiAssistance.CallTreeContext(selectedAiCallTree as TimelineUtils.AICallTree.AICallTree), })); }); it('should set selected call tree when the AICallTree flavor changes', () => { UI.Context.Context.instance().setFlavor(TimelineUtils.AICallTree.AICallTree, null); panel = new AiAssistance.AiAssistancePanel(mockView, { aidaClient: mockAidaClient([[{explanation: 'test'}]]), aidaAvailability: Host.AidaClient.AidaAccessPreconditions.AVAILABLE, syncInfo: getTestSyncInfo(), }); panel.markAsRoot(); panel.show(document.body); panel.handleAction('drjones.performance-panel-context'); sinon.assert.calledWith(mockView, sinon.match({ selectedContext: null, })); const selectedAiCallTree = {}; UI.Context.Context.instance().setFlavor(TimelineUtils.AICallTree.AICallTree, selectedAiCallTree); sinon.assert.calledWith(mockView, sinon.match({ selectedContext: new AiAssistance.CallTreeContext(selectedAiCallTree as TimelineUtils.AICallTree.AICallTree), })); }); it('should not handle AICallTree flavor changes if the widget is not shown', () => { UI.Context.Context.instance().setFlavor(TimelineUtils.AICallTree.AICallTree, null); panel = new AiAssistance.AiAssistancePanel(mockView, { aidaClient: mockAidaClient([[{explanation: 'test'}]]), aidaAvailability: Host.AidaClient.AidaAccessPreconditions.AVAILABLE, syncInfo: getTestSyncInfo(), }); const selectedAiCallTree = {}; UI.Context.Context.instance().setFlavor(TimelineUtils.AICallTree.AICallTree, selectedAiCallTree); sinon.assert.notCalled(mockView); }); }); describe('Workspace.UISourceCode.UISourceCode flavor changes for selected network request', () => { it('should set selected file when the widget is shown', () => { UI.Context.Context.instance().setFlavor(Workspace.UISourceCode.UISourceCode, null); panel = new AiAssistance.AiAssistancePanel(mockView, { aidaClient: mockAidaClient([[{explanation: 'test'}]]), aidaAvailability: Host.AidaClient.AidaAccessPreconditions.AVAILABLE, syncInfo: getTestSyncInfo(), }); const uiSourceCode = sinon.createStubInstance(Workspace.UISourceCode.UISourceCode); UI.Context.Context.instance().setFlavor(Workspace.UISourceCode.UISourceCode, uiSourceCode); panel.markAsRoot(); panel.show(document.body); panel.handleAction('drjones.sources-panel-context'); sinon.assert.calledWith(mockView, sinon.match({ selectedContext: new AiAssistance.FileContext(uiSourceCode), })); }); it('should set selected file when the UISourceCode flavor changes', () => { UI.Context.Context.instance().setFlavor(Workspace.UISourceCode.UISourceCode, null); panel = new AiAssistance.AiAssistancePanel(mockView, { aidaClient: mockAidaClient([[{explanation: 'test'}]]), aidaAvailability: Host.AidaClient.AidaAccessPreconditions.AVAILABLE, syncInfo: getTestSyncInfo(), }); panel.markAsRoot(); panel.show(document.body); panel.handleAction('drjones.sources-panel-context'); sinon.assert.calledWith(mockView, sinon.match({ selectedContext: null, })); const uiSourceCode = sinon.createStubInstance(Workspace.UISourceCode.UISourceCode); UI.Context.Context.instance().setFlavor(Workspace.UISourceCode.UISourceCode, uiSourceCode); sinon.assert.calledWith(mockView, sinon.match({ selectedContext: new AiAssistance.FileContext(uiSourceCode), })); }); it('should not handle NetworkRequest flavor changes if the widget is not shown', () => { UI.Context.Context.instance().setFlavor(Workspace.UISourceCode.UISourceCode, null); panel = new AiAssistance.AiAssistancePanel(mockView, { aidaClient: mockAidaClient([[{explanation: 'test'}]]), aidaAvailability: Host.AidaClient.AidaAccessPreconditions.AVAILABLE, syncInfo: getTestSyncInfo(), }); const uiSourceCode = sinon.createStubInstance(Workspace.UISourceCode.UISourceCode); UI.Context.Context.instance().setFlavor(Workspace.UISourceCode.UISourceCode, uiSourceCode); sinon.assert.notCalled(mockView); }); }); }); describe('toggle search element action', () => { let toggleSearchElementAction: UI.ActionRegistration.Action; beforeEach(() => { toggleSearchElementAction = UI.ActionRegistry.ActionRegistry.instance().getAction('elements.toggle-element-search'); toggleSearchElementAction.setToggled(false); }); it('should set inspectElementToggled when the widget is shown', () => { panel = new AiAssistance.AiAssistancePanel(mockView, { aidaClient: mockAidaClient([[{explanation: 'test'}]]), aidaAvailability: Host.AidaClient.AidaAccessPreconditions.AVAILABLE, syncInfo: getTestSyncInfo(), }); toggleSearchElementAction.setToggled(true); panel.markAsRoot(); panel.show(document.body); sinon.assert.calledWith(mockView, sinon.match({ inspectElementToggled: true, })); }); it('should update inspectElementToggled when the action is toggled', () => { toggleSearchElementAction.setToggled(false); panel = new AiAssistance.AiAssistancePanel(mockView, { aidaClient: mockAidaClient([[{explanation: 'test'}]]), aidaAvailability: Host.AidaClient.AidaAccessPreconditions.AVAILABLE, syncInfo: getTestSyncInfo(), }); panel.markAsRoot(); panel.show(document.body); sinon.assert.calledWith(mockView, sinon.match({ inspectElementToggled: false, })); toggleSearchElementAction.setToggled(true); sinon.assert.calledWith(mockView, sinon.match({ inspectElementToggled: true, })); }); it('should not update toggleSearchElementAction even after the action is toggled when the widget is not shown', () => { toggleSearchElementAction.setToggled(false); panel = new AiAssistance.AiAssistancePanel(mockView, { aidaClient: mockAidaClient([[{explanation: 'test'}]]), aidaAvailability: Host.AidaClient.AidaAccessPreconditions.AVAILABLE, syncInfo: getTestSyncInfo(), }); toggleSearchElementAction.setToggled(true); sinon.assert.notCalled(mockView); }); }); describe('history interactions', () => { it('should have empty messages after new chat', async () => { panel = new AiAssistance.AiAssistancePanel(mockView, { aidaClient: mockAidaClient([[{explanation: 'test'}]]), aidaAvailability: Host.AidaClient.AidaAccessPreconditions.AVAILABLE, syncInfo: getTestSyncInfo(), }); panel.handleAction('freestyler.elements-floating-button'); mockView.lastCall.args[0].onTextSubmit('test'); await drainMicroTasks(); assert.deepEqual(mockView.lastCall.args[0].messages, [ { entity: AiAssistance.ChatMessageEntity.USER, text: 'test', }, { answer: 'test', entity: AiAssistance.ChatMessageEntity.MODEL, rpcId: undefined, suggestions: undefined, steps: [], }, ]); const button = panel.contentElement.querySelector('devtools-button[aria-label=\'New chat\']'); assert.instanceOf(button, HTMLElement); dispatchClickEvent(button); assert.deepEqual(mockView.lastCall.args[0].messages, []); }); it('should select default agent after new chat', async () => { const stub = getGetHostConfigStub({ devToolsFreestyler: { enabled: true, }, }); panel = new AiAssistance.AiAssistancePanel(mockView, { aidaClient: mockAidaClient([[{explanation: 'test'}]]), aidaAvailability: Host.AidaClient.AidaAccessPreconditions.AVAILABLE, syncInfo: getTestSyncInfo(), }); panel.handleAction('freestyler.elements-floating-button'); mockView.lastCall.args[0].onTextSubmit('test'); await drainMicroTasks(); UI.Context.Context.instance().setFlavor( ElementsPanel.ElementsPanel.ElementsPanel, sinon.createStubInstance(ElementsPanel.ElementsPanel.ElementsPanel)); assert.deepEqual(mockView.lastCall.args[0].messages, [ { entity: AiAssistance.ChatMessageEntity.USER, text: 'test', }, { answer: 'test', entity: AiAssistance.ChatMessageEntity.MODEL, rpcId: undefined, suggestions: undefined, steps: [], }, ]); const button = panel.contentElement.querySelector('devtools-button[aria-label=\'New chat\']'); assert.instanceOf(button, HTMLElement); dispatchClickEvent(button); assert.deepEqual(mockView.lastCall.args[0].messages, []); assert.deepEqual(mockView.lastCall.args[0].agentType, AiAssistance.AgentType.STYLING); stub.restore(); }); it('should switch agents and restore history', async () => { panel = new AiAssistance.AiAssistancePanel(mockView, { aidaClient: mockAidaClient([[{explanation: 'test'}], [{explanation: 'test2'}]]), aidaAvailability: Host.AidaClient.AidaAccessPreconditions.AVAILABLE, syncInfo: getTestSyncInfo(), }); panel.handleAction('freestyler.elements-floating-button'); mockView.lastCall.args[0].onTextSubmit('User question to Freestyler?'); await drainMicroTasks(); assert.deepEqual(mockView.lastCall.args[0].messages, [ { entity: AiAssistance.ChatMessageEntity.USER, text: 'User question to Freestyler?', }, { answer: 'test', entity: AiAssistance.ChatMessageEntity.MODEL, rpcId: undefined, suggestions: undefined, steps: [], }, ]); panel.handleAction('drjones.network-floating-button'); mockView.lastCall.args[0].onTextSubmit('User question to DrJones?'); await drainMicroTasks(); assert.deepEqual(mockView.lastCall.args[0].messages, [ { entity: AiAssistance.ChatMessageEntity.USER, text: 'User question to DrJones?', }, { answer: 'test2', entity: AiAssistance.ChatMessageEntity.MODEL, rpcId: undefined, suggestions: undefined, steps: [], }, ]); const button = panel.contentElement.querySelector('devtools-button[aria-label=\'History\']'); assert.instanceOf(button, HTMLElement); const contextMenu = getMenu(() => { dispatchClickEvent(button); }); const freestylerEntry = findMenuItemWithLabel(contextMenu.defaultSection(), 'User question to Freestyler?')!; assert.isDefined(freestylerEntry); contextMenu.invokeHandler(freestylerEntry.id()); await drainMicroTasks(); assert.deepEqual(mockView.lastCall.args[0].messages, [ { entity: AiAssistance.ChatMessageEntity.USER, text: 'User question to Freestyler?', }, { answer: 'test', entity: AiAssistance.ChatMessageEntity.MODEL, rpcId: undefined, suggestions: undefined, steps: [], }, ]); }); }); it('should have empty state after clear chat', async () => { panel = new AiAssistance.AiAssistancePanel(mockView, { aidaClient: mockAidaClient([[{explanation: 'test'}]]), aidaAvailability: Host.AidaClient.AidaAccessPreconditions.AVAILABLE, syncInfo: getTestSyncInfo(), }); panel.handleAction('freestyler.elements-floating-button'); mockView.lastCall.args[0].onTextSubmit('test'); await drainMicroTasks(); assert.deepEqual(mockView.lastCall.args[0].messages, [ { entity: AiAssistance.ChatMessageEntity.USER, text: 'test', }, { answer: 'test', entity: AiAssistance.ChatMessageEntity.MODEL, rpcId: undefined, suggestions: undefined, steps: [], }, ]); const button = panel.contentElement.querySelector('devtools-button[aria-label=\'Delete local chat\']'); assert.instanceOf(button, HTMLElement); dispatchClickEvent(button); assert.deepEqual(mockView.lastCall.args[0].messages, []); assert.isUndefined(mockView.lastCall.args[0].agentType); }); it('should select default agent based on open panel after clearing the chat', async () => { const stub = getGetHostConfigStub({ devToolsFreestyler: { enabled: true, }, }); panel = new AiAssistance.AiAssistancePanel(mockView, { aidaClient: mockAidaClient([[{explanation: 'test'}]]), aidaAvailability: Host.AidaClient.AidaAccessPreconditions.AVAILABLE, syncInfo: getTestSyncInfo(), }); panel.handleAction('freestyler.elements-floating-button'); mockView.lastCall.args[0].onTextSubmit('test'); await drainMicroTasks(); UI.Context.Context.instance().setFlavor( ElementsPanel.ElementsPanel.ElementsPanel, sinon.createStubInstance(ElementsPanel.ElementsPanel.ElementsPanel)); assert.deepEqual(mockView.lastCall.args[0].messages, [ { entity: AiAssistance.ChatMessageEntity.USER, text: 'test', }, { answer: 'test', entity: AiAssistance.ChatMessageEntity.MODEL, rpcId: undefined, suggestions: undefined, steps: [], }, ]); const button = panel.contentElement.querySelector('devtools-button[aria-label=\'Delete local chat\']'); assert.instanceOf(button, HTMLElement); dispatchClickEvent(button); assert.deepEqual(mockView.lastCall.args[0].messages, []); assert.deepEqual(mockView.lastCall.args[0].agentType, AiAssistance.AgentType.STYLING); stub.restore(); }); it('should have empty state after clear chat history', async () => { panel = new AiAssistance.AiAssistancePanel(mockView, { aidaClient: mockAidaClient([[{explanation: 'test'}], [{explanation: 'test2'}]]), aidaAvailability: Host.AidaClient.AidaAccessPreconditions.AVAILABLE, syncInfo: getTestSyncInfo(), }); panel.handleAction('freestyler.elements-floating-button'); mockView.lastCall.args[0].onTextSubmit('User question to Freestyler?'); await drainMicroTasks(); assert.deepEqual(mockView.lastCall.args[0].messages, [ { entity: AiAssistance.ChatMessageEntity.USER, text: 'User question to Freestyler?', }, { answer: 'test', entity: AiAssistance.ChatMessageEntity.MODEL, rpcId: undefined, suggestions: undefined, steps: [], }, ]); panel.handleAction('drjones.network-floating-button'); mockView.lastCall.args[0].onTextSubmit('User question to DrJones?'); await drainMicroTasks(); assert.deepEqual(mockView.lastCall.args[0].messages, [ { entity: AiAssistance.ChatMessageEntity.USER, text: 'User question to DrJones?', }, { answer: 'test2', entity: AiAssistance.ChatMessageEntity.MODEL, rpcId: undefined, suggestions: undefined, steps: [], }, ]); let button = panel.contentElement.querySelector('devtools-button[aria-label=\'History\']'); assert.instanceOf(button, HTMLElement); let contextMenu = getMenu(() => { dispatchClickEvent(button!); }); const clearAll = findMenuItemWithLabel(contextMenu.footerSection(), 'Clear local chats')!; assert.isDefined(clearAll); contextMenu.invokeHandler(clearAll.id()); await drainMicroTasks(); assert.deepEqual(mockView.lastCall.args[0].messages, []); assert.isUndefined(mockView.lastCall.args[0].agentType); await drainMicroTasks(); contextMenu.discard(); await drainMicroTasks(); button = panel.contentElement.querySelector('devtools-button[aria-label=\'History\']'); assert.instanceOf(button, HTMLElement); contextMenu = getMenu(() => { dispatchClickEvent(button); }); const menuItem = findMenuItemWithLabel(contextMenu.defaultSection(), 'No past conversations'); assert(menuItem); }); describe('cross-origin', () => { it('blocks input on cross origin requests', async () => { const networkRequest = sinon.createStubInstance(SDK.NetworkRequest.NetworkRequest, { url: urlString`https://a.test`, }); UI.Context.Context.instance().setFlavor(SDK.NetworkRequest.NetworkRequest, networkRequest); panel = new AiAssistance.AiAssistancePanel(mockView, { aidaClient: mockAidaClient([[{explanation: 'test'}]]), aidaAvailability: Host.AidaClient.AidaAccessPreconditions.AVAILABLE, syncInfo: getTestSyncInfo(), }); panel.markAsRoot(); panel.show(document.body); panel.handleAction('drjones.network-floating-button'); sinon.assert.calledWith(mockView, sinon.match({ selectedContext: new AiAssistance.RequestContext(networkRequest), blockedByCrossOrigin: false, })); // Send a query for https://a.test. panel.handleAction('drjones.network-floating-button'); mockView.lastCall.args[0].onTextSubmit('test'); await drainMicroTasks(); // Change context to https://b.test. const networkRequest2 = sinon.createStubInstance(SDK.NetworkRequest.NetworkRequest, { url: urlString`https://b.test`, }); UI.Context.Context.instance().setFlavor(SDK.NetworkRequest.NetworkRequest, networkRequest2); panel.handleAction('drjones.network-floating-button'); await drainMicroTasks(); sinon.assert.calledWith(mockView, sinon.match({ selectedContext: new AiAssistance.RequestContext(networkRequest2), blockedByCrossOrigin: true, })); }); it('should be able to continue same-origin requests', async () => { const stub = getGetHostConfigStub({ devToolsFreestyler: { enabled: true, }, }); panel = new AiAssistance.AiAssistancePanel(mockView, { aidaClient: mockAidaClient([[{explanation: 'test'}], [{explanation: 'test2'}]]), aidaAvailability: Host.AidaClient.AidaAccessPreconditions.AVAILABLE, syncInfo: getTestSyncInfo(), }); UI.Context.Context.instance().setFlavor( ElementsPanel.ElementsPanel.ElementsPanel, sinon.createStubInstance(ElementsPanel.ElementsPanel.ElementsPanel)); panel.handleAction('freestyler.elements-floating-button'); mockView.lastCall.args[0].onTextSubmit('test'); await drainMicroTasks(); assert.deepEqual(mockView.lastCall.args[0].messages, [ { entity: AiAssistance.ChatMessageEntity.USER, text: 'test', }, { answer: 'test', entity: AiAssistance.ChatMessageEntity.MODEL, rpcId: undefined, suggestions: undefined, steps: [], }, ]); UI.Context.Context.instance().setFlavor( ElementsPanel.ElementsPanel.ElementsPanel, sinon.createStubInstance(ElementsPanel.ElementsPanel.ElementsPanel)); panel.handleAction('freestyler.elements-floating-button'); mockView.lastCall.args[0].onTextSubmit('test2'); await drainMicroTasks(); assert.isFalse(mockView.lastCall.args[0].isReadOnly); assert.deepEqual(mockView.lastCall.args[0].messages, [ { entity: AiAssistance.ChatMessageEntity.USER, text: 'test', }, { answer: 'test', entity: AiAssistance.ChatMessageEntity.MODEL, rpcId: undefined, suggestions: undefined, steps: [], }, { entity: AiAssistance.ChatMessageEntity.USER, text: 'test2', }, { answer: 'test2', entity: AiAssistance.ChatMessageEntity.MODEL, rpcId: undefined, suggestions: undefined, steps: [], }, ]); stub.restore(); }); }); describe('auto agent selection for panels', () => { describe('Elements panel', () => { it('should select FREESTYLER agent when the Elements panel is open in initial render', () => { const stub = getGetHostConfigStub({ devToolsFreestyler: { enabled: true, }, }); UI.Context.Context.instance().setFlavor( ElementsPanel.ElementsPanel.ElementsPanel, sinon.createStubInstance(ElementsPanel.ElementsPanel.ElementsPanel)); panel = new AiAssistance.AiAssistancePanel(mockView, { aidaClient: mockAidaClient([[{explanation: 'test'}]]), aidaAvailability: Host.AidaClient.AidaAccessPreconditions.AVAILABLE, syncInfo: getTestSyncInfo(), }); panel.markAsRoot(); panel.show(document.body); sinon.assert.calledWith(mockView, sinon.match({ agentType: AiAssistance.AgentType.STYLING, })); stub.restore(); }); it('should update to no agent state when the Elements panel is closed and no other panels are open', () => { const stub = getGetHostConfigStub({ devToolsFreestyler: { enabled: true, }, }); UI.Context.Context.instance().setFlavor( ElementsPanel.ElementsPanel.ElementsPanel, sinon.createStubInstance(ElementsPanel.ElementsPanel.ElementsPanel)); panel = new AiAssistance.AiAssistancePanel(mockView, { aidaClient: mockAidaClient([[{explanation: 'test'}]]), aidaAvailability: Host.AidaClient.AidaAccessPreconditions.AVAILABLE, syncInfo: getTestSyncInfo(), }); panel.markAsRoot(); panel.show(document.body); sinon.assert.calledWith(mockView, sinon.match({ agentType: AiAssistance.AgentType.STYLING, })); UI.Context.Context.instance().setFlavor(ElementsPanel.ElementsPanel.ElementsPanel, null); assert.isUndefined(mockView.lastCall.args[0].agentType); stub.restore(); }); it('should render no agent state when Elements panel is open but Freestyler is not enabled', () => { const stub = getGetHostConfigStub({ devToolsFreestyler: { enabled: false, }, }); UI.Context.Context.instance().setFlavor( ElementsPanel.ElementsPanel.ElementsPanel, sinon.createStubInstance(ElementsPanel.ElementsPanel.ElementsPanel)); panel = new AiAssistance.AiAssistancePanel(mockView, { aidaClient: mockAidaClient([[{explanation: 'test'}]]), aidaAvailability: Host.AidaClient.AidaAccessPreconditions.AVAILABLE, syncInfo: getTestSyncInfo(), }); panel.markAsRoot(); panel.show(document.body); assert.isUndefined(mockView.lastCall.args[0].agentType); stub.restore(); }); }); describe('Network panel', () => { it('should select DRJONES_NETWORK agent when the Network panel is open in initial render', () => { const stub = getGetHostConfigStub({ devToolsAiAssistanceNetworkAgent: { enabled: true, }, }); UI.Context.Context.instance().setFlavor( NetworkPanel.NetworkPanel.NetworkPanel, sinon.createStubInstance(NetworkPanel.NetworkPanel.NetworkPanel)); panel = new AiAssistance.AiAssistancePanel(mockView, { aidaClient: mockAidaClient([[{explanation: 'test'}]]), aidaAvailability: Host.AidaClient.AidaAccessPreconditions.AVAILABLE, syncInfo: getTestSyncInfo(), }); panel.markAsRoot(); panel.show(document.body); sinon.assert.calledWith(mockView, sinon.match({ agentType: AiAssistance.AgentType.NETWORK, })); stub.restore(); }); it('should update to no agent state when the Network panel is closed and no other panels are open', () => { const stub = getGetHostConfigStub({ devToolsAiAssistanceNetworkAgent: { enabled: true, }, }); UI.Context.Context.instance().setFlavor( NetworkPanel.NetworkPanel.NetworkPanel, sinon.createStubInstance(NetworkPanel.NetworkPanel.NetworkPanel)); panel = new AiAssistance.AiAssistancePanel(mockView, { aidaClient: mockAidaClient([[{explanation: 'test'}]]), aidaAvailability: Host.AidaClient.AidaAccessPreconditions.AVAILABLE, syncInfo: getTestSyncInfo(), }); panel.markAsRoot(); panel.show(document.body); sinon.assert.calledWith(mockView, sinon.match({ agentType: AiAssistance.AgentType.NETWORK, })); UI.Context.Context.instance().setFlavor(NetworkPanel.NetworkPanel.NetworkPanel, null); assert.isUndefined(mockView.lastCall.args[0].agentType); stub.restore(); }); it('should render no agent state when Network panel is open but devToolsAiAssistanceNetworkAgent is not enabled', () => { const stub = getGetHostConfigStub({ devToolsAiAssistanceNetworkAgent: { enabled: false, }, }); UI.Context.Context.instance().setFlavor( NetworkPanel.NetworkPanel.NetworkPanel, sinon.createStubInstance(NetworkPanel.NetworkPanel.NetworkPanel)); panel = new AiAssistance.AiAssistancePanel(mockView, { aidaClient: mockAidaClient([[{explanation: 'test'}]]), aidaAvailability: Host.AidaClient.AidaAccessPreconditions.AVAILABLE, syncInfo: getTestSyncInfo(), }); panel.markAsRoot(); panel.show(document.body); assert.isUndefined(mockView.lastCall.args[0].agentType); stub.restore(); }); }); describe('Sources panel', () => { it('should select DRJONES_FILE agent when the Sources panel is open in initial render', () => { const stub = getGetHostConfigStub({ devToolsAiAssistanceFileAgent: { enabled: true, }, }); UI.Context.Context.instance().setFlavor( SourcesPanel.SourcesPanel.SourcesPanel, sinon.createStubInstance(SourcesPanel.SourcesPanel.SourcesPanel)); panel = new AiAssistance.AiAssistancePanel(mockView, { aidaClient: mockAidaClient([[{explanation: 'test'}]]), aidaAvailability: Host.AidaClient.AidaAccessPreconditions.AVAILABLE, syncInfo: getTestSyncInfo(), }); panel.markAsRoot(); panel.show(document.body); sinon.assert.calledWith(mockView, sinon.match({ agentType: AiAssistance.AgentType.FILE, })); stub.restore(); }); it('should update to no agent state when the Sources panel is closed and no other panels are open', () => { const stub = getGetHostConfigStub({ devToolsAiAssistanceFileAgent: { enabled: true, }, }); UI.Context.Context.instance().setFlavor( SourcesPanel.SourcesPanel.SourcesPanel, sinon.createStubInstance(SourcesPanel.SourcesPanel.SourcesPanel)); panel = new AiAssistance.AiAssistancePanel(mockView, { aidaClient: mockAidaClient([[{explanation: 'test'}]]), aidaAvailability: Host.AidaClient.AidaAccessPreconditions.AVAILABLE, syncInfo: getTestSyncInfo(), }); panel.markAsRoot(); panel.show(document.body); sinon.assert.calledWith(mockView, sinon.match({ agentType: AiAssistance.AgentType.FILE, })); UI.Context.Context.instance().setFlavor(SourcesPanel.SourcesPanel.SourcesPanel, null); assert.isUndefined(mockView.lastCall.args[0].agentType); stub.restore(); }); it('should render no agent state when Sources panel is open but devToolsAiAssistanceFileAgent is not enabled', () => { const stub = getGetHostConfigStub({ devToolsAiAssistanceFileAgent: { enabled: false, }, }); UI.Context.Context.instance().setFlavor( SourcesPanel.SourcesPanel.SourcesPanel, sinon.createStubInstance(SourcesPanel.SourcesPanel.SourcesPanel)); panel = new AiAssistance.AiAssistancePanel(mockView, { aidaClient: mockAidaClient([[{explanation: 'test'}]]), aidaAvailability: Host.AidaClient.AidaAccessPreconditions.AVAILABLE, syncInfo: getTestSyncInfo(), }); panel.markAsRoot(); panel.show(document.body); assert.isUndefined(mockView.lastCall.args[0].agentType); stub.restore(); }); }); describe('Performance panel', () => { it('should select DRJONES_PERFORMANCE agent when the Performance panel is open in initial render', () => { const stub = getGetHostConfigStub({ devToolsAiAssistancePerformanceAgent: { enabled: true, }, }); UI.Context.Context.instance().setFlavor( TimelinePanel.TimelinePanel.TimelinePanel, sinon.createStubInstance(TimelinePanel.TimelinePanel.TimelinePanel)); panel = new AiAssistance.AiAssistancePanel(mockView, { aidaClient: mockAidaClient([[{explanation: 'test'}]]), aidaAvailability: Host.AidaClient.AidaAccessPreconditions.AVAILABLE, syncInfo: getTestSyncInfo(), }); panel.markAsRoot(); panel.show(document.body); sinon.assert.calledWith(mockView, sinon.match({ agentType: AiAssistance.AgentType.PERFORMANCE, })); stub.restore(); }); it('should update to no agent state when the Performance panel is closed and no other panels are open', () => { const stub = getGetHostConfigStub({ devToolsAiAssistancePerformanceAgent: { enabled: true, }, }); UI.Context.Context.instance().setFlavor( TimelinePanel.TimelinePanel.TimelinePanel, sinon.createStubInstance(TimelinePanel.TimelinePanel.TimelinePanel)); panel = new AiAssistance.AiAssistancePanel(mockView, { aidaClient: mockAidaClient([[{explanation: 'test'}]]), aidaAvailability: Host.AidaClient.AidaAccessPreconditions.AVAILABLE, syncInfo: getTestSyncInfo(), }); panel.markAsRoot(); panel.show(document.body); sinon.assert.calledWith(mockView, sinon.match({ agentType: AiAssistance.AgentType.PERFORMANCE, })); UI.Context.Context.instance().setFlavor(TimelinePanel.TimelinePanel.TimelinePanel, null); assert.isUndefined(mockView.lastCall.args[0].agentType); stub.restore(); }); it('should render no agent state when Performance panel is open but de