| // Copyright 2025 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 type * as SDK from '../../core/sdk/sdk.js'; |
| import * as Protocol from '../../generated/protocol.js'; |
| import * as AiAssistanceModel from '../../models/ai_assistance/ai_assistance.js'; |
| import {renderElementIntoDOM} from '../../testing/DOMHelpers.js'; |
| import {describeWithEnvironment, updateHostConfig} from '../../testing/EnvironmentHelpers.js'; |
| import {createViewFunctionStub} from '../../testing/ViewFunctionHelpers.js'; |
| import * as Tooltips from '../../ui/components/tooltips/tooltips.js'; |
| import * as UI from '../../ui/legacy/legacy.js'; |
| import * as PanelCommon from '../common/common.js'; |
| |
| import * as Console from './console.js'; |
| |
| const consoleViewMessage = { |
| consoleMessage: () => { |
| return { |
| level: Protocol.Log.LogEntryLevel.Error, |
| } as SDK.ConsoleModel.ConsoleMessage; |
| }, |
| } as Console.ConsoleViewMessage.ConsoleViewMessage; |
| |
| describeWithEnvironment('ConsoleInsightTeaser', () => { |
| let originalLanguageModel: AiAssistanceModel.BuiltInAi.LanguageModel|undefined; |
| |
| beforeEach(() => { |
| // @ts-expect-error |
| originalLanguageModel = window.LanguageModel; |
| }); |
| |
| afterEach(() => { |
| AiAssistanceModel.BuiltInAi.BuiltInAi.removeInstance(); |
| // @ts-expect-error |
| window.LanguageModel = originalLanguageModel; |
| }); |
| |
| it('renders the loading state', async () => { |
| const view = createViewFunctionStub(Console.ConsoleInsightTeaser.ConsoleInsightTeaser); |
| new Console.ConsoleInsightTeaser.ConsoleInsightTeaser('test-uuid', consoleViewMessage, undefined, view); |
| const input = await view.nextInput; |
| assert.isFalse(input.isInactive); |
| assert.isEmpty(input.mainText); |
| assert.isEmpty(input.headerText); |
| }); |
| |
| it('shows the tooltip', async () => { |
| const tooltip = new Tooltips.Tooltip.Tooltip(); |
| tooltip.setAttribute('popover', 'manual'); |
| const showTooltipStub = sinon.stub(tooltip, 'showTooltip'); |
| |
| const view = createViewFunctionStub(Console.ConsoleInsightTeaser.ConsoleInsightTeaser, {tooltip}); |
| const teaser = |
| new Console.ConsoleInsightTeaser.ConsoleInsightTeaser('test-uuid', consoleViewMessage, undefined, view); |
| await view.nextInput; |
| sinon.assert.calledOnce(showTooltipStub); |
| |
| showTooltipStub.resetHistory(); |
| await teaser.maybeGenerateTeaser(); |
| await view.nextInput; |
| sinon.assert.notCalled(showTooltipStub); |
| }); |
| |
| const setupBuiltInAi = (generateResponse: () => AsyncGenerator): Console.ConsoleViewMessage.ConsoleViewMessage => { |
| updateHostConfig({ |
| devToolsAiPromptApi: { |
| enabled: true, |
| allowWithoutGpu: true, |
| }, |
| }); |
| |
| const mockLanguageModel = { |
| destroy: () => {}, |
| clone: () => mockLanguageModel, |
| promptStreaming: generateResponse, |
| }; |
| // @ts-expect-error |
| window.LanguageModel = { |
| availability: () => 'available', |
| create: () => mockLanguageModel, |
| }; |
| |
| return { |
| consoleMessage: () => { |
| return { |
| runtimeModel: () => null, |
| getAffectedResources: () => undefined, |
| } as SDK.ConsoleModel.ConsoleMessage; |
| }, |
| toMessageTextString: () => 'message text string', |
| contentElement: () => document.createElement('div') as HTMLElement, |
| } as Console.ConsoleViewMessage.ConsoleViewMessage; |
| }; |
| |
| it('renders the generated response', async () => { |
| const consoleViewMessage = setupBuiltInAi(async function*() { |
| yield 'This is the'; |
| yield ' explanation'; |
| }); |
| |
| const builtInAi = AiAssistanceModel.BuiltInAi.BuiltInAi.instance(); |
| assert.isDefined(builtInAi); |
| await builtInAi.initDoneForTesting; |
| |
| const view = createViewFunctionStub(Console.ConsoleInsightTeaser.ConsoleInsightTeaser); |
| const teaser = |
| new Console.ConsoleInsightTeaser.ConsoleInsightTeaser('test-uuid', consoleViewMessage, undefined, view); |
| teaser.maybeGenerateTeaser(); |
| const input = await view.nextInput; |
| assert.isFalse(input.isInactive); |
| assert.strictEqual(input.headerText, 'message text string'); |
| assert.strictEqual(input.mainText, 'This is the explanation'); |
| }); |
| |
| it('can download the AI model', async () => { |
| updateHostConfig({ |
| devToolsAiPromptApi: { |
| enabled: true, |
| allowWithoutGpu: true, |
| }, |
| }); |
| |
| let resolveCreate; |
| // @ts-expect-error |
| window.LanguageModel = { |
| availability: () => 'downloadable', |
| create: () => { |
| return new Promise(resolve => { |
| resolveCreate = resolve; |
| }); |
| }, |
| }; |
| const builtInAi = AiAssistanceModel.BuiltInAi.BuiltInAi.instance(); |
| const view = createViewFunctionStub(Console.ConsoleInsightTeaser.ConsoleInsightTeaser); |
| const teaser = |
| new Console.ConsoleInsightTeaser.ConsoleInsightTeaser('test-uuid', consoleViewMessage, undefined, view); |
| teaser.maybeGenerateTeaser(); |
| let input = await view.nextInput; |
| assert.strictEqual(input.state, 'no-model'); |
| |
| input.onDownloadModelClick(new Event('click')); |
| input = await view.nextInput; |
| assert.strictEqual(input.state, 'downloading'); |
| |
| builtInAi.dispatchEventToListeners(AiAssistanceModel.BuiltInAi.Events.DOWNLOAD_PROGRESS_CHANGED, 0.35); |
| input = await view.nextInput; |
| assert.strictEqual(input.state, 'downloading'); |
| assert.strictEqual(input.downloadProgress, 0.35); |
| |
| resolveCreate!({}); |
| input = await view.nextInput; |
| assert.strictEqual(input.state, 'ready'); |
| }); |
| |
| it('executes action on "Tell me more" click if onboarding is completed', async () => { |
| const action = sinon.spy(); |
| sinon.stub(UI.ActionRegistry.ActionRegistry.instance(), 'getAction').returns({ |
| execute: action, |
| } as unknown as UI.ActionRegistration.Action); |
| const view = createViewFunctionStub(Console.ConsoleInsightTeaser.ConsoleInsightTeaser); |
| new Console.ConsoleInsightTeaser.ConsoleInsightTeaser('test-uuid', consoleViewMessage, undefined, view); |
| const input = await view.nextInput; |
| input.onTellMeMoreClick(new Event('click')); |
| sinon.assert.calledOnce(action); |
| }); |
| |
| it('shows FRE dialog on "Tell me more" click', async () => { |
| Common.Settings.settingForTest('console-insights-enabled').set(false); |
| const show = sinon.stub(PanelCommon.FreDialog, 'show'); |
| const view = createViewFunctionStub(Console.ConsoleInsightTeaser.ConsoleInsightTeaser); |
| new Console.ConsoleInsightTeaser.ConsoleInsightTeaser('test-uuid', consoleViewMessage, undefined, view); |
| const input = await view.nextInput; |
| await input.onTellMeMoreClick(new Event('click')); |
| sinon.assert.calledOnce(show); |
| Common.Settings.settingForTest('console-insights-enabled').set(true); |
| show.restore(); |
| }); |
| |
| it('disables teasers on "Dont show" change', async () => { |
| const view = createViewFunctionStub(Console.ConsoleInsightTeaser.ConsoleInsightTeaser); |
| new Console.ConsoleInsightTeaser.ConsoleInsightTeaser('test-uuid', consoleViewMessage, undefined, view); |
| const input = await view.nextInput; |
| const event = { |
| target: { |
| checked: true, |
| } as unknown as EventTarget, |
| } as Event; |
| assert.isTrue(Common.Settings.moduleSetting('console-insight-teasers-enabled').get()); |
| input.dontShowChanged(event); |
| assert.isFalse(Common.Settings.moduleSetting('console-insight-teasers-enabled').get()); |
| Common.Settings.settingForTest('console-insight-teasers-enabled').set(true); |
| }); |
| |
| it('updates its view if teaser generation is slow', async () => { |
| const consoleViewMessage = setupBuiltInAi(async function*() { |
| await new Promise(() => {}); |
| yield 'unreached'; |
| }); |
| |
| const clock = sinon.useFakeTimers({toFake: ['setTimeout']}); |
| const view = createViewFunctionStub(Console.ConsoleInsightTeaser.ConsoleInsightTeaser); |
| const teaser = |
| new Console.ConsoleInsightTeaser.ConsoleInsightTeaser('test-uuid', consoleViewMessage, undefined, view); |
| let input = await view.nextInput; |
| assert.isFalse(input.isInactive); |
| assert.isEmpty(input.mainText); |
| assert.isEmpty(input.headerText); |
| assert.isFalse(input.isSlowGeneration); |
| await teaser.maybeGenerateTeaser(); |
| |
| clock.runAll(); |
| input = await view.nextInput; |
| assert.isFalse(input.isInactive); |
| assert.isEmpty(input.mainText); |
| assert.strictEqual(input.headerText, 'message text string'); |
| assert.isTrue(input.isSlowGeneration); |
| clock.restore(); |
| }); |
| |
| it('can show error state', async () => { |
| const consoleViewMessage = setupBuiltInAi(async function*() { |
| yield 'This is an incomplete'; |
| throw new Error('something went wrong'); |
| }); |
| |
| // The error is logged to the console. We don't need that noise in the test output. |
| sinon.stub(console, 'error'); |
| |
| const view = createViewFunctionStub(Console.ConsoleInsightTeaser.ConsoleInsightTeaser); |
| const teaser = |
| new Console.ConsoleInsightTeaser.ConsoleInsightTeaser('test-uuid', consoleViewMessage, undefined, view); |
| let input = await view.nextInput; |
| assert.isFalse(input.isInactive); |
| assert.isEmpty(input.mainText); |
| assert.isEmpty(input.headerText); |
| assert.strictEqual(input.state, 'no-model'); |
| await teaser.maybeGenerateTeaser(); |
| |
| input = await view.nextInput; |
| assert.isFalse(input.isInactive); |
| assert.strictEqual(input.mainText, 'This is an incomplete'); |
| assert.strictEqual(input.headerText, 'message text string'); |
| assert.strictEqual(input.state, 'error'); |
| }); |
| |
| it('shows the "Tell me more" button only when AIDA is available', async () => { |
| const checkAccessPreconditionsStub = sinon.stub(Host.AidaClient.AidaClient, 'checkAccessPreconditions'); |
| checkAccessPreconditionsStub.resolves(Host.AidaClient.AidaAccessPreconditions.AVAILABLE); |
| sinon.stub(UI.ActionRegistry.ActionRegistry.instance(), 'hasAction').returns(true); |
| const consoleViewMessage = setupBuiltInAi(async function*() { |
| yield JSON.stringify({ |
| header: 'test header', |
| explanation: 'test explanation', |
| }); |
| }); |
| |
| const view = createViewFunctionStub(Console.ConsoleInsightTeaser.ConsoleInsightTeaser); |
| const teaser = |
| new Console.ConsoleInsightTeaser.ConsoleInsightTeaser('test-uuid', consoleViewMessage, undefined, view); |
| teaser.markAsRoot(); |
| renderElementIntoDOM(teaser); |
| |
| await teaser.maybeGenerateTeaser(); |
| let input = await view.nextInput; |
| assert.isTrue(input.hasTellMeMoreButton); |
| |
| checkAccessPreconditionsStub.resolves(Host.AidaClient.AidaAccessPreconditions.NO_INTERNET); |
| Host.AidaClient.HostConfigTracker.instance().dispatchEventToListeners( |
| Host.AidaClient.Events.AIDA_AVAILABILITY_CHANGED); |
| input = await view.nextInput; |
| assert.isFalse(input.hasTellMeMoreButton); |
| teaser.detach(); |
| }); |
| }); |