blob: f2134a961f64d5f9c277453f1d913e7157d60297 [file] [log] [blame]
// Copyright 2025 The Chromium Authors
// 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 Workspace from '../../models/workspace/workspace.js';
import {describeWithEnvironment, updateHostConfig} from '../../testing/EnvironmentHelpers.js';
import * as TextEditor from '../../ui/components/text_editor/text_editor.js';
import * as SourceFrame from '../../ui/legacy/components/source_frame/source_frame.js';
import * as PanelCommon from '../common/common.js';
import {AiCodeCompletionPlugin} from './sources.js';
const {urlString} = Platform.DevToolsPath;
function createUiSourceCodeStub({
url = urlString`file://`,
contentType = Common.ResourceType.resourceTypes.Script,
}: {
url?: Platform.DevToolsPath.UrlString,
contentType?: Common.ResourceType.ResourceType,
} = {}): sinon.SinonStubbedInstance<Workspace.UISourceCode.UISourceCode> {
return sinon.createStubInstance(Workspace.UISourceCode.UISourceCode, {
url,
contentType,
});
}
describeWithEnvironment('AiCodeCompletionPlugin', () => {
describe('accepts', () => {
it('holds true for scripts', () => {
assert.isTrue(AiCodeCompletionPlugin.AiCodeCompletionPlugin.accepts(createUiSourceCodeStub({
contentType: Common.ResourceType.resourceTypes.Script,
})));
});
it('holds true for stylesheets', () => {
assert.isTrue(AiCodeCompletionPlugin.AiCodeCompletionPlugin.accepts(
createUiSourceCodeStub({contentType: Common.ResourceType.resourceTypes.Stylesheet})));
});
it('holds true for documents', () => {
assert.isTrue(AiCodeCompletionPlugin.AiCodeCompletionPlugin.accepts(
createUiSourceCodeStub({contentType: Common.ResourceType.resourceTypes.Document})));
});
});
it('does not create a plugin when the feature is disabled', () => {
updateHostConfig({
devToolsAiCodeCompletion: {
enabled: false,
},
});
const uiSourceCode = createUiSourceCodeStub();
assert.throws(
() => new AiCodeCompletionPlugin.AiCodeCompletionPlugin(uiSourceCode),
'AI code completion feature is not enabled.');
});
describe('provider callbacks', () => {
let clock: sinon.SinonFakeTimers;
beforeEach(() => {
clock = sinon.useFakeTimers();
updateHostConfig({
devToolsAiCodeCompletion: {
enabled: true,
},
aidaAvailability: {
enabled: true,
blockedByAge: false,
blockedByGeo: false,
}
});
sinon.stub(TextEditor.AiCodeCompletionProvider.AiCodeCompletionProvider, 'createInstance')
.returns(sinon.createStubInstance(TextEditor.AiCodeCompletionProvider.AiCodeCompletionProvider));
sinon.stub(Host.AidaClient.HostConfigTracker, 'instance').returns({
addEventListener: () => {},
removeEventListener: () => {},
dispose: () => {},
} as unknown as Host.AidaClient.HostConfigTracker);
});
afterEach(() => {
clock.restore();
});
function setupPlugin() {
const uiSourceCode = createUiSourceCodeStub();
const plugin = new AiCodeCompletionPlugin.AiCodeCompletionPlugin(uiSourceCode);
return plugin;
}
it('initializes toolbar when the feature is enabled', async () => {
const plugin = setupPlugin();
await clock.tickAsync(0);
const providerConfig = plugin.aiCodeCompletionConfig;
providerConfig.onFeatureEnabled();
const toolbarItems = plugin.rightToolbarItems();
assert.lengthOf(toolbarItems, 1);
assert.isTrue(toolbarItems[0].element.classList.contains('ai-code-completion-disclaimer-container'));
assert.deepEqual(toolbarItems[0].element.childElementCount, 1);
});
it('cleans up toolbar when the feature is disabled', async () => {
const plugin = setupPlugin();
await clock.tickAsync(0);
const providerConfig = plugin.aiCodeCompletionConfig;
providerConfig.onFeatureEnabled();
let toolbarItems = plugin.rightToolbarItems();
assert.deepEqual(toolbarItems[0].element.childElementCount, 1);
providerConfig.onFeatureDisabled();
toolbarItems = plugin.rightToolbarItems();
assert.deepEqual(toolbarItems[0].element.childElementCount, 0);
});
it('shows a loading state when a request is triggered', async () => {
const fakeLoadingSetter = sinon.fake();
sinon.stub(PanelCommon.AiCodeCompletionDisclaimer.prototype, 'loading').set(fakeLoadingSetter);
const plugin = setupPlugin();
await clock.tickAsync(0);
const providerConfig = plugin.aiCodeCompletionConfig;
providerConfig.onFeatureEnabled();
providerConfig.onRequestTriggered();
sinon.assert.calledOnce(fakeLoadingSetter);
assert.isTrue(fakeLoadingSetter.firstCall.args[0]);
});
it('hides the loading indicator when a response is received', async () => {
const fakeLoadingSetter = sinon.fake();
sinon.stub(PanelCommon.AiCodeCompletionDisclaimer.prototype, 'loading').set(fakeLoadingSetter);
const plugin = setupPlugin();
await clock.tickAsync(0);
const providerConfig = plugin.aiCodeCompletionConfig;
providerConfig.onFeatureEnabled();
providerConfig.onRequestTriggered();
sinon.assert.calledOnce(fakeLoadingSetter);
assert.isTrue(fakeLoadingSetter.firstCall.args[0]);
providerConfig.onResponseReceived();
sinon.assert.calledTwice(fakeLoadingSetter);
assert.isFalse(fakeLoadingSetter.secondCall.args[0]);
});
it('attaches the citations toolbar when a suggestion with citations is accepted', async () => {
const updateCitationsSpy = sinon.spy(PanelCommon.AiCodeCompletionSummaryToolbar.prototype, 'updateCitations');
const plugin = setupPlugin();
await clock.tickAsync(0);
const providerConfig = plugin.aiCodeCompletionConfig;
const editor = new TextEditor.TextEditor.TextEditor();
const editorDispatchSpy = sinon.spy(editor, 'dispatch');
plugin.editorInitialized(editor);
providerConfig.onFeatureEnabled();
providerConfig.onResponseReceived();
providerConfig.onSuggestionAccepted([{uri: 'https://example.com/source'}]);
sinon.assert.calledOnce(updateCitationsSpy);
assert.deepEqual(updateCitationsSpy.firstCall.args, [['https://example.com/source']]);
sinon.assert.calledWith(editorDispatchSpy, {
effects: sinon.match(effect => effect.is(SourceFrame.SourceFrame.addSourceFrameInfobar)),
});
});
it('does not attach the citations toolbar if there are no citations', async () => {
const updateCitationsSpy = sinon.spy(PanelCommon.AiCodeCompletionSummaryToolbar.prototype, 'updateCitations');
const plugin = setupPlugin();
await clock.tickAsync(0);
const providerConfig = plugin.aiCodeCompletionConfig;
const editor = new TextEditor.TextEditor.TextEditor();
const editorDispatchSpy = sinon.spy(editor, 'dispatch');
plugin.editorInitialized(editor);
providerConfig.onFeatureEnabled();
providerConfig.onResponseReceived();
providerConfig.onSuggestionAccepted([]);
sinon.assert.notCalled(updateCitationsSpy);
sinon.assert.notCalled(editorDispatchSpy);
});
});
});