chrome-devtools-frontend
Version:
Chrome DevTools UI
359 lines (319 loc) • 14.8 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 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);
});
});
});