UNPKG

chrome-devtools-frontend

Version:
359 lines (319 loc) 14.8 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 type * as Common from '../../../../core/common/common.js'; import * as Root from '../../../../core/root/root.js'; import * as Trace from '../../../../models/trace/trace.js'; import {dispatchClickEvent, renderElementIntoDOM} from '../../../../testing/DOMHelpers.js'; import {describeWithEnvironment, updateHostConfig} from '../../../../testing/EnvironmentHelpers.js'; import * as RenderCoordinator from '../../../../ui/components/render_coordinator/render_coordinator.js'; import * as UI from '../../../../ui/legacy/legacy.js'; import * as Lit from '../../../../ui/lit/lit.js'; import type {TimelineOverlay} from '../../overlays/OverlaysImpl.js'; import * as Utils from '../../utils/utils.js'; import * as Insights from './insights.js'; const {html} = Lit; describeWithEnvironment('BaseInsightComponent', () => { const {BaseInsightComponent} = Insights.BaseInsightComponent; class TestInsightComponentNoAISupport extends BaseInsightComponent<Trace.Insights.Types.InsightModel> { override internalName = 'test-insight'; override hasAskAiSupport() { return false; } override createOverlays(): TimelineOverlay[] { return []; } override renderContent(): Lit.LitTemplate { return html`<div>test content</div>`; } } class TestInsightComponentWithAISupport extends BaseInsightComponent<Trace.Insights.Types.InsightModel> { override internalName = 'test-insight'; override hasAskAiSupport() { return true; } override createOverlays(): TimelineOverlay[] { return []; } override renderContent(): Lit.LitTemplate { return html`<div>test content</div>`; } } customElements.define('test-insight-component-no-ai-support', TestInsightComponentNoAISupport); customElements.define('test-insight-component-ai-support', TestInsightComponentWithAISupport); describe('sidebar insight component rendering', () => { it('renders insight title even when not active', async () => { const component = new TestInsightComponentNoAISupport(); component.selected = false; component.model = { insightKey: 'LCPPhases', strings: {}, title: 'LCP by Phase' as Common.UIString.LocalizedString, description: 'some description' as Common.UIString.LocalizedString, category: Trace.Insights.Types.InsightCategory.ALL, state: 'fail', frameId: '123', }; renderElementIntoDOM(component); await RenderCoordinator.done(); assert.isNotNull(component.shadowRoot); const titleElement = component.shadowRoot.querySelector<HTMLElement>('.insight-title'); assert.isNotNull(titleElement); const descElement = component.shadowRoot.querySelector<HTMLElement>('.insight-description'); assert.isNull(descElement); const contentElement = component.shadowRoot.querySelector<HTMLElement>('.insight-content'); assert.isNull(contentElement); assert.deepEqual(titleElement.textContent, 'LCP by Phase'); }); it('renders title, description and content when toggled', async () => { const component = new TestInsightComponentNoAISupport(); component.selected = true; component.model = { insightKey: 'LCPPhases', strings: {}, title: 'LCP by Phase' as Common.UIString.LocalizedString, description: 'some description' as Common.UIString.LocalizedString, category: Trace.Insights.Types.InsightCategory.ALL, state: 'fail', frameId: '123', }; renderElementIntoDOM(component); await RenderCoordinator.done(); assert.isNotNull(component.shadowRoot); const titleElement = component.shadowRoot.querySelector<HTMLElement>('.insight-title'); assert.isNotNull(titleElement); assert.deepEqual(titleElement.textContent, 'LCP by Phase'); const descElement = component.shadowRoot.querySelector<HTMLElement>('.insight-description'); assert.isNotNull(descElement); // It's in the markdown component. assert.include(descElement.children[0].shadowRoot?.textContent?.trim(), 'some description'); const contentElement = component.shadowRoot.querySelector<HTMLElement>('.insight-content'); assert.isNotNull(contentElement); assert.strictEqual(contentElement.textContent, 'test content'); }); }); describe('estimated savings output', () => { let testComponentIndex = 0; // used for defining the custom element and making it unique function makeTestComponent(opts: {wastedBytes?: number, timeSavings?: number}) { class TestInsight extends BaseInsightComponent<Trace.Insights.Types.InsightModel> { override internalName = 'test-insight'; override createOverlays(): TimelineOverlay[] { return []; } override getEstimatedSavingsTime(): Trace.Types.Timing.Milli|null { return opts.timeSavings ? Trace.Types.Timing.Milli(opts.timeSavings) : null; } override getEstimatedSavingsBytes(): number|null { return opts.wastedBytes ?? null; } override renderContent(): Lit.LitTemplate { return html`<div>test content</div>`; } } customElements.define(`test-insight-est-savings-${testComponentIndex++}`, TestInsight); return new TestInsight(); } it('outputs the correct estimated savings for both bytes and time', async () => { const component = makeTestComponent({wastedBytes: 5_000, timeSavings: 50}); component.model = { insightKey: 'LCPPhases', strings: {}, title: 'LCP by Phase' as Common.UIString.LocalizedString, description: 'some description' as Common.UIString.LocalizedString, category: Trace.Insights.Types.InsightCategory.ALL, state: 'fail', frameId: '123', }; renderElementIntoDOM(component); await RenderCoordinator.done(); const estSavings = component.shadowRoot?.querySelector<HTMLElement>('slot[name=insight-savings]'); assert.isOk(estSavings); assert.strictEqual(estSavings.innerText, 'Est savings: 50 ms & 5.0 kB'); }); it('outputs the correct estimated savings for bytes only', async () => { const component = makeTestComponent({wastedBytes: 5_000}); component.model = { insightKey: 'LCPPhases', strings: {}, title: 'LCP by Phase' as Common.UIString.LocalizedString, description: 'some description' as Common.UIString.LocalizedString, category: Trace.Insights.Types.InsightCategory.ALL, state: 'fail', frameId: '123', }; renderElementIntoDOM(component); await RenderCoordinator.done(); const estSavings = component.shadowRoot?.querySelector<HTMLElement>('slot[name=insight-savings]'); assert.isOk(estSavings); assert.strictEqual(estSavings.innerText, 'Est savings: 5.0 kB'); }); it('outputs the correct estimated savings for time only', async () => { const component = makeTestComponent({timeSavings: 50}); component.model = { insightKey: 'LCPPhases', strings: {}, title: 'LCP by Phase' as Common.UIString.LocalizedString, description: 'some description' as Common.UIString.LocalizedString, category: Trace.Insights.Types.InsightCategory.ALL, state: 'fail', frameId: '123', }; renderElementIntoDOM(component); await RenderCoordinator.done(); const estSavings = component.shadowRoot?.querySelector<HTMLElement>('slot[name=insight-savings]'); assert.isOk(estSavings); assert.strictEqual(estSavings.innerText, 'Est savings: 50 ms'); }); it('includes the output in the insight aria label', async () => { const component = makeTestComponent({wastedBytes: 5_000, timeSavings: 50}); component.model = { insightKey: 'LCPPhases', strings: {}, title: 'LCP by Phase' as Common.UIString.LocalizedString, description: 'some description' as Common.UIString.LocalizedString, category: Trace.Insights.Types.InsightCategory.ALL, state: 'fail', frameId: '123', }; renderElementIntoDOM(component); await RenderCoordinator.done(); const label = component.shadowRoot?.querySelector('header')?.getAttribute('aria-label'); assert.isOk(label); assert.strictEqual( label, 'View details for LCP by Phase insight. Estimated savings for this insight: 50 ms and 5.0 kB transfer size'); }); }); describe('Ask AI Insights', () => { const FAKE_PARSED_TRACE = {} as unknown as Trace.Handlers.Types.ParsedTrace; const FAKE_LCP_MODEL = { insightKey: 'LCPPhases', strings: {}, title: 'LCP by Phase' as Common.UIString.LocalizedString, description: 'some description' as Common.UIString.LocalizedString, category: Trace.Insights.Types.InsightCategory.ALL, state: 'fail', frameId: '123', } as const; async function renderComponent({insightHasAISupport}: {insightHasAISupport: boolean}): Promise<TestInsightComponentNoAISupport|TestInsightComponentWithAISupport> { const component = insightHasAISupport ? new TestInsightComponentWithAISupport() : new TestInsightComponentNoAISupport(); component.selected = true; component.model = FAKE_LCP_MODEL; // We don't need a real trace for these tests. component.parsedTrace = FAKE_PARSED_TRACE; renderElementIntoDOM(component); await RenderCoordinator.done(); return component; } it('renders the "Ask AI" button when perf insights AI is enabled and the Insight supports it', async () => { updateHostConfig({ devToolsAiAssistancePerformanceAgent: { enabled: true, insightsEnabled: true, } }); const component = await renderComponent({insightHasAISupport: true}); assert.isOk(component.shadowRoot); const button = component.shadowRoot.querySelector('devtools-button[data-insights-ask-ai]'); assert.isOk(button); }); it('adds a descriptive aria label to the button', async () => { updateHostConfig({ devToolsAiAssistancePerformanceAgent: { enabled: true, insightsEnabled: true, } }); const component = await renderComponent({insightHasAISupport: true}); assert.isOk(component.shadowRoot); const button = component.shadowRoot.querySelector('devtools-button[data-insights-ask-ai]'); assert.isOk(button); assert.strictEqual(button.getAttribute('aria-label'), 'Ask AI about LCP by Phase insight'); }); it('does not render the "Ask AI" button if disabled by enterprise policy', async () => { updateHostConfig({ devToolsAiAssistancePerformanceAgent: { enabled: true, insightsEnabled: true, }, aidaAvailability: { enterprisePolicyValue: Root.Runtime.GenAiEnterprisePolicyValue.DISABLE, } }); const component = await renderComponent({insightHasAISupport: true}); assert.isOk(component.shadowRoot); const button = component.shadowRoot.querySelector('devtools-button[data-insights-ask-ai]'); assert.isNull(button); }); it('does not show the button if the feature is enabled but the Insight does not support it', async () => { updateHostConfig({ devToolsAiAssistancePerformanceAgent: { enabled: true, insightsEnabled: true, } }); const component = await renderComponent({insightHasAISupport: false}); assert.isOk(component.shadowRoot); const button = component.shadowRoot.querySelector('devtools-button[data-insights-ask-ai]'); assert.isNull(button); }); it('sets the context when the user clicks the button', async () => { updateHostConfig({ devToolsAiAssistancePerformanceAgent: { enabled: true, insightsEnabled: true, } }); const component = await renderComponent({insightHasAISupport: true}); assert.isOk(component.shadowRoot); const button = component.shadowRoot.querySelector('devtools-button[data-insights-ask-ai]'); assert.isOk(button); sinon.stub(UI.ActionRegistry.ActionRegistry.instance(), 'hasAction') .withArgs(sinon.match(/drjones\.performance-insight-context/)) .returns(true); const FAKE_ACTION = sinon.createStubInstance(UI.ActionRegistration.Action); sinon.stub(UI.ActionRegistry.ActionRegistry.instance(), 'getAction') .withArgs(sinon.match(/drjones\.performance-insight-context/)) .returns(FAKE_ACTION); dispatchClickEvent(button); const context = UI.Context.Context.instance().flavor(Utils.InsightAIContext.ActiveInsight); assert.instanceOf(context, Utils.InsightAIContext.ActiveInsight); }); it('clears the active context when it gets toggled shut', async () => { const FAKE_ACTIVE_INSIGHT = {} as unknown as Utils.InsightAIContext.ActiveInsight; UI.Context.Context.instance().setFlavor(Utils.InsightAIContext.ActiveInsight, FAKE_ACTIVE_INSIGHT); const component = await renderComponent({insightHasAISupport: true}); const header = component.shadowRoot?.querySelector('header'); assert.isOk(header); dispatchClickEvent(header); const context = UI.Context.Context.instance().flavor(Utils.InsightAIContext.ActiveInsight); assert.isNull(context); }); it('does not render the "Ask AI" button when the perf agent is not enabled', async () => { updateHostConfig({ devToolsAiAssistancePerformanceAgent: { enabled: false, } }); const component = await renderComponent( {insightHasAISupport: true}); // The Insight supports it, but the feature is not enabled assert.isOk(component.shadowRoot); const button = component.shadowRoot.querySelector('devtools-button[data-insights-ask-ai]'); assert.isNull(button); }); it('does not render the "Ask AI" button when the perf agent is enabled but the insights ai is not', async () => { updateHostConfig({ devToolsAiAssistancePerformanceAgent: { enabled: true, insightsEnabled: false, } }); const component = await renderComponent({insightHasAISupport: true}); assert.isOk(component.shadowRoot); const button = component.shadowRoot.querySelector('devtools-button[data-insights-ask-ai]'); assert.isNull(button); }); }); });