chrome-devtools-frontend
Version:
Chrome DevTools UI
1,259 lines (1,110 loc) • 52.1 kB
text/typescript
// 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