chrome-devtools-frontend
Version:
Chrome DevTools UI
287 lines (235 loc) • 13.1 kB
text/typescript
// 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 SDK from '../../core/sdk/sdk.js';
import * as Bindings from '../../models/bindings/bindings.js';
import * as Trace from '../../models/trace/trace.js';
import * as Workspace from '../../models/workspace/workspace.js';
import {dispatchClickEvent, renderElementIntoDOM} from '../../testing/DOMHelpers.js';
import {
describeWithEnvironment,
registerNoopActions,
} from '../../testing/EnvironmentHelpers.js';
import {TraceLoader} from '../../testing/TraceLoader.js';
import * as UI from '../../ui/legacy/legacy.js';
import * as Timeline from './timeline.js';
describeWithEnvironment('TimelinePanel', function() {
let timeline: Timeline.TimelinePanel.TimelinePanel;
let traceModel: Trace.TraceModel.Model;
beforeEach(() => {
registerNoopActions(
['timeline.toggle-recording', 'timeline.record-reload', 'timeline.show-history', 'components.collect-garbage']);
const resourceMapping = new Bindings.ResourceMapping.ResourceMapping(
SDK.TargetManager.TargetManager.instance(),
Workspace.Workspace.WorkspaceImpl.instance(),
);
Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance({
forceNew: true,
resourceMapping,
targetManager: SDK.TargetManager.TargetManager.instance(),
});
Bindings.IgnoreListManager.IgnoreListManager.instance({
forceNew: true,
debuggerWorkspaceBinding: Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance(),
});
traceModel = Trace.TraceModel.Model.createWithAllHandlers();
timeline = Timeline.TimelinePanel.TimelinePanel.instance({forceNew: true, isNode: false, traceModel});
renderElementIntoDOM(timeline);
});
afterEach(() => {
timeline.detach();
Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.removeInstance();
Bindings.IgnoreListManager.IgnoreListManager.removeInstance();
UI.ActionRegistry.ActionRegistry.reset();
Timeline.TimelinePanel.TimelinePanel.removeInstance();
});
it('should keep other tracks when the custom tracks setting is toggled', async function() {
const events =
await TraceLoader.rawEvents(this, 'extension-tracks-and-marks.json.gz') as Trace.Types.Events.Event[];
await timeline.loadingComplete(events, null, null);
const tracksBeforeDisablingSetting = timeline.getFlameChart().getMainDataProvider().timelineData().groups;
const parsedTrace = traceModel.parsedTrace();
const extensionTracksInTrace = parsedTrace?.ExtensionTraceData.extensionTrackData;
const extensionTrackInTraceNames = extensionTracksInTrace?.flatMap(
track => track.isTrackGroup ? [...Object.keys(track.entriesByTrack), track.name] : track.name);
assert.exists(extensionTrackInTraceNames);
// Test that extension tracks from the trace model are rendered in
// the flamechart data.
const extensionTracksInFlamechartBeforeDisabling =
tracksBeforeDisablingSetting
.filter(
track => track.jslogContext === Timeline.CompatibilityTracksAppender.VisualLoggingTrackName.EXTENSION)
.map(track => track.name.split(' — Custom track')[0]);
const nonExtensionTrackNames =
tracksBeforeDisablingSetting
.filter(
track => track.jslogContext !== Timeline.CompatibilityTracksAppender.VisualLoggingTrackName.EXTENSION)
.map(track => track.name);
assert.includeMembers(extensionTracksInFlamechartBeforeDisabling, extensionTrackInTraceNames.map(track => track));
assert.lengthOf(extensionTracksInFlamechartBeforeDisabling, extensionTrackInTraceNames.length);
// Disable setting
const customTracksSetting = Timeline.TimelinePanel.TimelinePanel.extensionDataVisibilitySetting();
customTracksSetting.set(false);
// Test that extension tracks are not rendered in the flamechart,
// but other tracks are.
const tracksAfterDisablingSetting = timeline.getFlameChart().getMainDataProvider().timelineData().groups;
const extensionTracksAfterDisabling = tracksAfterDisablingSetting.filter(
track => track.jslogContext === Timeline.CompatibilityTracksAppender.VisualLoggingTrackName.EXTENSION);
const trackNamesAfterDisablingSetting =
tracksAfterDisablingSetting.map(track => track.name.split(' — Custom track')[0]);
assert.lengthOf(extensionTracksAfterDisabling, 0);
assert.deepEqual(trackNamesAfterDisablingSetting, nonExtensionTrackNames);
// Enable setting again
customTracksSetting.set(true);
const tracksAfterEnablingSetting = timeline.getFlameChart().getMainDataProvider().timelineData().groups;
assert.deepEqual(tracksBeforeDisablingSetting, tracksAfterEnablingSetting);
});
it('should keep marker overlays when the custom tracks setting is toggled', async function() {
const events = await TraceLoader.rawEvents(this, 'web-dev.json.gz') as Trace.Types.Events.Event[];
await timeline.loadingComplete(events, null, null);
const overlaysBeforeDisablingSetting = timeline.getFlameChart().overlays().allOverlays();
// Test that overlays are rendered in the timeline
assert.isAbove(overlaysBeforeDisablingSetting.length, 0);
// Disable setting
const customTracksSetting = Timeline.TimelinePanel.TimelinePanel.extensionDataVisibilitySetting();
customTracksSetting.set(false);
// Test that overlays remain untouched
const overlaysAfterDisablingSetting = timeline.getFlameChart().overlays().allOverlays();
assert.deepEqual(overlaysBeforeDisablingSetting, overlaysAfterDisablingSetting);
// Enable setting again
customTracksSetting.set(true);
const overlaysAfterEnablingSetting = timeline.getFlameChart().overlays().allOverlays();
assert.deepEqual(overlaysBeforeDisablingSetting, overlaysAfterEnablingSetting);
});
it('keeps entries set to be dimmed as so after toggling the custom tracks setting', async function() {
const events = await TraceLoader.rawEvents(this, 'web-dev.json.gz') as Trace.Types.Events.Event[];
await timeline.loadingComplete(events, null, null);
const thirdPartyDimSetting = Common.Settings.Settings.instance().createSetting('timeline-dim-third-parties', false);
// Dim 3P entries.
thirdPartyDimSetting.set(true);
const dimIndicesBeforeToggle = timeline.getFlameChart().getMainFlameChart().getDimIndices();
assert.exists(dimIndicesBeforeToggle);
assert.isAbove(dimIndicesBeforeToggle.length, 0);
// Toggle the custom track setting and verify 3P entries remain dimmed.
Timeline.TimelinePanel.TimelinePanel.extensionDataVisibilitySetting().set(true);
const dimIndicesAfterToggle = timeline.getFlameChart().getMainFlameChart().getDimIndices();
assert.exists(dimIndicesAfterToggle);
assert.isAbove(dimIndicesAfterToggle.length, 0);
});
it('keeps annotations after toggling the custom tracks setting', async function() {
const events = await TraceLoader.rawEvents(this, 'web-dev.json.gz') as Trace.Types.Events.Event[];
await timeline.loadingComplete(events, null, null);
const parsedTrace = traceModel.parsedTrace();
assert.isOk(parsedTrace?.Meta.traceBounds.min);
const modificationsManager = Timeline.ModificationsManager.ModificationsManager.activeManager();
assert.isOk(modificationsManager);
// Add an annotation
modificationsManager.createAnnotation({
bounds: Trace.Helpers.Timing.traceWindowFromMicroSeconds(
parsedTrace.Meta.traceBounds.min, parsedTrace.Meta.traceBounds.max),
type: 'TIME_RANGE',
label: '',
});
const annotationsBeforeToggle =
timeline.getFlameChart().overlays().allOverlays().filter(e => e.type === 'TIME_RANGE');
assert.exists(annotationsBeforeToggle);
assert.isAbove(annotationsBeforeToggle.length, 0);
// Toggle the custom track setting and verify annotations remain.
Timeline.TimelinePanel.TimelinePanel.extensionDataVisibilitySetting().set(true);
const annotationsAfterToggle =
timeline.getFlameChart().overlays().allOverlays().filter(e => e.type === 'TIME_RANGE');
assert.exists(annotationsAfterToggle);
assert.isAbove(annotationsAfterToggle.length, 0);
});
it('clears out AI related contexts when the user presses "Clear"', async () => {
const context = UI.Context.Context.instance();
const {AICallTree, InsightAIContext} = Timeline.Utils;
const callTree = sinon.createStubInstance(AICallTree.AICallTree);
const insight = sinon.createStubInstance(InsightAIContext.ActiveInsight);
context.setFlavor(AICallTree.AICallTree, callTree);
context.setFlavor(InsightAIContext.ActiveInsight, insight);
const clearButton = timeline.element.querySelector('[aria-label="Clear"]');
assert.isOk(clearButton);
dispatchClickEvent(clearButton);
assert.isNull(context.flavor(AICallTree.AICallTree));
assert.isNull(context.flavor(InsightAIContext.ActiveInsight));
});
it('saves visual track config metadata to disk if the user has modified it', async function() {
const events = await TraceLoader.rawEvents(this, 'web-dev.json.gz') as Trace.Types.Events.Event[];
await timeline.loadingComplete(events, null, null);
const flameChartView = timeline.getFlameChart();
const FAKE_METADATA: Trace.Types.File.PersistedTraceVisualConfig = {
main: [{hidden: true, expanded: false, originalIndex: 0, visualIndex: 0}],
network: [{hidden: false, expanded: false, originalIndex: 0, visualIndex: 0}],
};
sinon.stub(flameChartView, 'getPersistedConfigMetadata').callsFake(() => {
return FAKE_METADATA;
});
const fileManager = Workspace.FileManager.FileManager.instance();
const saveSpy = sinon.stub(fileManager, 'save').callsFake((): Promise<Workspace.FileManager.SaveCallbackParam> => {
return Promise.resolve({});
});
const closeSpy = sinon.stub(fileManager, 'close');
await timeline.saveToFile({
savingEnhancedTrace: false,
addModifications: true,
});
sinon.assert.calledOnce(saveSpy);
sinon.assert.calledOnce(closeSpy);
const [fileName, traceAsString] = saveSpy.getCall(0).args;
// Matches Trace-20250613T132120.json
assert.match(fileName, /Trace-[\d|T]+\.json$/);
// easier to assert on the data if we parse it back
const parsedData = JSON.parse(traceAsString) as Trace.Types.File.TraceFile;
assert.deepEqual(parsedData.metadata.visualTrackConfig, FAKE_METADATA);
});
it('does not save visual track config if the user does not save with modifications', async function() {
const events = await TraceLoader.rawEvents(this, 'web-dev.json.gz') as Trace.Types.Events.Event[];
await timeline.loadingComplete(events, null, null);
const flameChartView = timeline.getFlameChart();
const FAKE_METADATA: Trace.Types.File.PersistedTraceVisualConfig = {
main: [{hidden: true, expanded: false, originalIndex: 0, visualIndex: 0}],
network: [{hidden: false, expanded: false, originalIndex: 0, visualIndex: 0}],
};
sinon.stub(flameChartView, 'getPersistedConfigMetadata').callsFake(() => {
return FAKE_METADATA;
});
const fileManager = Workspace.FileManager.FileManager.instance();
const saveSpy = sinon.stub(fileManager, 'save').callsFake((): Promise<Workspace.FileManager.SaveCallbackParam> => {
return Promise.resolve({});
});
sinon.stub(fileManager, 'close');
await timeline.saveToFile({
savingEnhancedTrace: false,
addModifications: false,
});
sinon.assert.calledOnce(saveSpy);
const [, traceAsString] = saveSpy.getCall(0).args;
// easier to assert on the data if we parse it back
const parsedData = JSON.parse(traceAsString) as Trace.Types.File.TraceFile;
assert.isUndefined(parsedData.metadata.visualTrackConfig);
});
it('does not save visual track config if the user has not made any', async function() {
const events = await TraceLoader.rawEvents(this, 'web-dev.json.gz') as Trace.Types.Events.Event[];
await timeline.loadingComplete(events, null, null);
const flameChartView = timeline.getFlameChart();
sinon.stub(flameChartView, 'getPersistedConfigMetadata').callsFake(() => {
return {main: null, network: null};
});
const fileManager = Workspace.FileManager.FileManager.instance();
const saveSpy = sinon.stub(fileManager, 'save').callsFake((): Promise<Workspace.FileManager.SaveCallbackParam> => {
return Promise.resolve({});
});
sinon.stub(fileManager, 'close');
await timeline.saveToFile({
savingEnhancedTrace: false,
addModifications: true,
});
sinon.assert.calledOnce(saveSpy);
const [, traceAsString] = saveSpy.getCall(0).args;
// easier to assert on the data if we parse it back
const parsedData = JSON.parse(traceAsString) as Trace.Types.File.TraceFile;
assert.isUndefined(parsedData.metadata.visualTrackConfig);
});
});