UNPKG

chrome-devtools-frontend

Version:
287 lines (235 loc) 13.1 kB
// 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); }); });