chrome-devtools-frontend
Version:
Chrome DevTools UI
1,083 lines (923 loc) • 43.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 * as Trace from '../../../models/trace/trace.js';
import {dispatchClickEvent} from '../../../testing/DOMHelpers.js';
import {describeWithEnvironment} from '../../../testing/EnvironmentHelpers.js';
import {
makeInstantEvent,
microsecondsTraceWindow,
MockFlameChartDelegate,
setupIgnoreListManagerEnvironment,
} from '../../../testing/TraceHelpers.js';
import {TraceLoader} from '../../../testing/TraceLoader.js';
import * as RenderCoordinator from '../../../ui/components/render_coordinator/render_coordinator.js';
import * as PerfUI from '../../../ui/legacy/components/perf_ui/perf_ui.js';
import * as Timeline from '../timeline.js';
import * as Components from './components/components.js';
import * as Overlays from './overlays.js';
const FAKE_OVERLAY_ENTRY_QUERIES: Overlays.Overlays.OverlayEntryQueries = {
isEntryCollapsedByUser() {
return false;
},
firstVisibleParentForEntry() {
return null;
},
};
/**
* The Overlays expects to be provided with both the main and network charts
* and data providers. This function creates all of those and optionally sets
* the trace data for the providers if it is provided.
*/
function createCharts(parsedTrace?: Trace.Handlers.Types.ParsedTrace): Overlays.Overlays.TimelineCharts {
const mainProvider = new Timeline.TimelineFlameChartDataProvider.TimelineFlameChartDataProvider();
const networkProvider = new Timeline.TimelineFlameChartNetworkDataProvider.TimelineFlameChartNetworkDataProvider();
if (parsedTrace) {
mainProvider.setModel(parsedTrace);
networkProvider.setModel(parsedTrace);
}
const delegate = new MockFlameChartDelegate();
const mainChart = new PerfUI.FlameChart.FlameChart(mainProvider, delegate);
const networkChart = new PerfUI.FlameChart.FlameChart(networkProvider, delegate);
// Add to DOM for offsetWidth, etc working
document.body.append(mainChart.element, networkChart.element);
if (parsedTrace) {
// Force the charts to render. Normally the TimelineFlameChartView would do
// this, but we aren't creating one for these tests.
mainChart.update();
networkChart.update();
}
return {
mainProvider,
mainChart,
networkProvider,
networkChart,
};
}
describeWithEnvironment('Overlays', () => {
beforeEach(() => {
setupIgnoreListManagerEnvironment();
});
afterEach(() => {
// Remove any FlameChart elements from the DOM
document.body.querySelectorAll('widget').forEach(e => e.remove());
});
it('can calculate the x position of an event based on the dimensions and its timestamp', async () => {
const flameChartsContainer = document.createElement('div');
const mainFlameChartsContainer = flameChartsContainer.createChild('div');
const networkFlameChartsContainer = flameChartsContainer.createChild('div');
const container = flameChartsContainer.createChild('div');
const overlays = new Overlays.Overlays.Overlays({
container,
flameChartsContainers: {
main: mainFlameChartsContainer,
network: networkFlameChartsContainer,
},
charts: createCharts(),
entryQueries: FAKE_OVERLAY_ENTRY_QUERIES,
});
// Set up the dimensions so it is 100px wide
overlays.updateChartDimensions('main', {
widthPixels: 100,
heightPixels: 50,
scrollOffsetPixels: 0,
allGroupsCollapsed: false,
});
overlays.updateChartDimensions('network', {
widthPixels: 100,
heightPixels: 50,
scrollOffsetPixels: 0,
allGroupsCollapsed: false,
});
const windowMin = Trace.Types.Timing.Micro(0);
const windowMax = Trace.Types.Timing.Micro(100);
// Set the visible window to be 0-100 microseconds
overlays.updateVisibleWindow(Trace.Helpers.Timing.traceWindowFromMicroSeconds(windowMin, windowMax));
// Now set an event to be at 50 microseconds.
const event = makeInstantEvent('test-event', 50);
const xPosition = overlays.xPixelForEventStartOnChart(event);
assert.strictEqual(xPosition, 50);
});
it('can calculate the y position of a main chart event', async function() {
const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
const charts = createCharts(parsedTrace);
const flameChartsContainer = document.createElement('div');
const mainFlameChartsContainer = flameChartsContainer.createChild('div');
const networkFlameChartsContainer = flameChartsContainer.createChild('div');
const container = flameChartsContainer.createChild('div');
const overlays = new Overlays.Overlays.Overlays({
container,
flameChartsContainers: {
main: mainFlameChartsContainer,
network: networkFlameChartsContainer,
},
charts,
entryQueries: FAKE_OVERLAY_ENTRY_QUERIES,
});
overlays.updateChartDimensions('main', {
widthPixels: 1000,
heightPixels: 500,
scrollOffsetPixels: 0,
allGroupsCollapsed: false,
});
overlays.updateChartDimensions('network', {
widthPixels: 1000,
heightPixels: 200,
scrollOffsetPixels: 0,
allGroupsCollapsed: false,
});
// Set the visible window to be the entire trace.
overlays.updateVisibleWindow(parsedTrace.Meta.traceBounds);
const event = charts.mainProvider.eventByIndex?.(50);
assert.isOk(event);
const yPixel = overlays.yPixelForEventOnChart(event);
// The Y offset for the main chart is 280px, but we add 208px on (200px for the
// network chart, and 8px for the re-size handle) giving us the expected
// 441px.
assert.strictEqual(yPixel, 488);
});
it('can adjust the y position of a main chart event when the network track is collapsed', async function() {
const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
const charts = createCharts(parsedTrace);
const flameChartsContainer = document.createElement('div');
const mainFlameChartsContainer = flameChartsContainer.createChild('div');
const networkFlameChartsContainer = flameChartsContainer.createChild('div');
const container = flameChartsContainer.createChild('div');
const overlays = new Overlays.Overlays.Overlays({
container,
flameChartsContainers: {
main: mainFlameChartsContainer,
network: networkFlameChartsContainer,
},
charts,
entryQueries: FAKE_OVERLAY_ENTRY_QUERIES,
});
overlays.updateChartDimensions('main', {
widthPixels: 1000,
heightPixels: 500,
scrollOffsetPixels: 0,
allGroupsCollapsed: false,
});
overlays.updateChartDimensions('network', {
widthPixels: 1000,
heightPixels: 34,
scrollOffsetPixels: 0,
// Make the network track collapsed
allGroupsCollapsed: true,
});
// Set the visible window to be the entire trace.
overlays.updateVisibleWindow(parsedTrace.Meta.traceBounds);
const event = charts.mainProvider.eventByIndex?.(50);
assert.isOk(event);
const yPixel = overlays.yPixelForEventOnChart(event);
// The Y offset for the main chart is 280px, but we add 34px on (the height
// of the collapsed network chart, with no resizer bar as it is hidden when
// the network track is collapsed). This gives us 280+34 = 314.
assert.strictEqual(yPixel, 314);
});
it('can calculate the y position of a network chart event', async function() {
const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
const charts = createCharts(parsedTrace);
const flameChartsContainer = document.createElement('div');
const mainFlameChartsContainer = flameChartsContainer.createChild('div');
const networkFlameChartsContainer = flameChartsContainer.createChild('div');
const container = flameChartsContainer.createChild('div');
const overlays = new Overlays.Overlays.Overlays({
container,
flameChartsContainers: {
main: mainFlameChartsContainer,
network: networkFlameChartsContainer,
},
charts,
entryQueries: FAKE_OVERLAY_ENTRY_QUERIES,
});
overlays.updateChartDimensions('main', {
widthPixels: 1000,
heightPixels: 500,
scrollOffsetPixels: 0,
allGroupsCollapsed: false,
});
overlays.updateChartDimensions('network', {
widthPixels: 1000,
heightPixels: 200,
scrollOffsetPixels: 0,
allGroupsCollapsed: false,
});
// Set the visible window to be the entire trace.
overlays.updateVisibleWindow(parsedTrace.Meta.traceBounds);
// Fake the level being visible: because we don't fully render the chart we
// need to fake this for this test.
sinon.stub(charts.networkChart, 'levelIsVisible').callsFake(() => true);
// Find an event on the network chart
const event = charts.networkProvider.eventByIndex?.(0);
assert.isOk(event);
const yPixel = overlays.yPixelForEventOnChart(event);
// This event is in the first level, but the first level has some offset
// above it to allow for the header row and the row with the timestamps on
// it, hence why this value is not 0px.
assert.strictEqual(yPixel, 34);
});
describe('rendering overlays', () => {
function setupChartWithDimensionsAndAnnotationOverlayListeners(parsedTrace: Trace.Handlers.Types.ParsedTrace): {
container: HTMLElement,
overlays: Overlays.Overlays.Overlays,
charts: Overlays.Overlays.TimelineCharts,
} {
const charts = createCharts(parsedTrace);
const flameChartsContainer = document.createElement('div');
const mainFlameChartsContainer = flameChartsContainer.createChild('div');
const networkFlameChartsContainer = flameChartsContainer.createChild('div');
const container = flameChartsContainer.createChild('div');
const overlays = new Overlays.Overlays.Overlays({
container,
flameChartsContainers: {
main: mainFlameChartsContainer,
network: networkFlameChartsContainer,
},
charts,
entryQueries: FAKE_OVERLAY_ENTRY_QUERIES,
});
const currManager = Timeline.ModificationsManager.ModificationsManager.activeManager();
// The Annotations Overlays are added through the ModificationsManager listener
currManager?.addEventListener(Timeline.ModificationsManager.AnnotationModifiedEvent.eventName, async event => {
const {overlay, action} = (event as Timeline.ModificationsManager.AnnotationModifiedEvent);
if (action === 'Add') {
overlays.add(overlay);
}
await overlays.update();
});
// When an annotation overlay is remomved, this event is dispatched to the Modifications Manager.
overlays.addEventListener(Overlays.Overlays.AnnotationOverlayActionEvent.eventName, async event => {
const {overlay, action} = (event as Overlays.Overlays.AnnotationOverlayActionEvent);
if (action === 'Remove') {
overlays.remove(overlay);
}
await overlays.update();
});
overlays.updateChartDimensions('main', {
widthPixels: 1000,
heightPixels: 500,
scrollOffsetPixels: 0,
allGroupsCollapsed: false,
});
overlays.updateChartDimensions('network', {
widthPixels: 1000,
heightPixels: 200,
scrollOffsetPixels: 0,
allGroupsCollapsed: false,
});
// Set the visible window to be the entire trace.
overlays.updateVisibleWindow(parsedTrace.Meta.traceBounds);
return {overlays, container, charts};
}
it('can render an entry selected overlay', async function() {
const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
const {overlays, container, charts} = setupChartWithDimensionsAndAnnotationOverlayListeners(parsedTrace);
const event = charts.mainProvider.eventByIndex?.(50);
assert.isOk(event);
overlays.add({
type: 'ENTRY_SELECTED',
entry: event,
});
await overlays.update();
// Ensure that the overlay was created.
const overlayDOM = container.querySelector<HTMLElement>('.overlay-type-ENTRY_SELECTED');
assert.isOk(overlayDOM);
});
it('renders an ENTRY_OUTLINE even if the entry is also the ENTRY_SELECTED entry', async function() {
const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
const {overlays, container, charts} = setupChartWithDimensionsAndAnnotationOverlayListeners(parsedTrace);
const event = charts.mainProvider.eventByIndex?.(50);
assert.isOk(event);
overlays.add({
type: 'ENTRY_OUTLINE',
entry: event,
outlineReason: 'ERROR',
});
await overlays.update();
const outlineVisible =
container.querySelector<HTMLElement>('.overlay-type-ENTRY_OUTLINE')?.style.display === 'block';
assert.isTrue(outlineVisible, 'The ENTRY_OUTLINE should be visible');
// Now make a selected entry too
overlays.add({
type: 'ENTRY_SELECTED',
entry: event,
});
await overlays.update();
const outlineStillVisible =
container.querySelector<HTMLElement>('.overlay-type-ENTRY_OUTLINE')?.style.display === 'block';
assert.isTrue(outlineStillVisible, 'The ENTRY_OUTLINE should be visible');
});
it('only ever renders a single selected overlay', async function() {
const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
const {overlays, container, charts} = setupChartWithDimensionsAndAnnotationOverlayListeners(parsedTrace);
const event1 = charts.mainProvider.eventByIndex?.(50);
const event2 = charts.mainProvider.eventByIndex?.(51);
assert.isOk(event1);
assert.isOk(event2);
overlays.add({
type: 'ENTRY_SELECTED',
entry: event1,
});
await overlays.update();
overlays.add({
type: 'ENTRY_SELECTED',
entry: event2,
});
await overlays.update();
// There should only be one of these
const entrySelectedOverlays = container.querySelectorAll<HTMLElement>('.overlay-type-ENTRY_SELECTED');
assert.lengthOf(entrySelectedOverlays, 1);
});
it('can render entry label overlay', async function() {
const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
const {overlays, container, charts} = setupChartWithDimensionsAndAnnotationOverlayListeners(parsedTrace);
const event = charts.mainProvider.eventByIndex?.(50);
assert.isOk(event);
overlays.add({
type: 'ENTRY_LABEL',
entry: event,
label: 'entry label',
});
await overlays.update();
// Ensure that the overlay was created.
const overlayDOM = container.querySelector<HTMLElement>('.overlay-type-ENTRY_LABEL');
assert.isOk(overlayDOM);
});
it('dispatches an event when the entry label overlay is clicked', async function() {
const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
const {overlays, container, charts} = setupChartWithDimensionsAndAnnotationOverlayListeners(parsedTrace);
const event = charts.mainProvider.eventByIndex?.(50);
assert.isOk(event);
overlays.add({
type: 'ENTRY_LABEL',
entry: event,
label: 'entry label',
});
await overlays.update();
// Ensure that the overlay was created.
const overlayDOM = container.querySelector<HTMLElement>('.overlay-type-ENTRY_LABEL');
assert.isOk(overlayDOM);
const overlayClick = new Promise<Overlays.Overlays.EntryLabel>(resolve => {
overlays.addEventListener(Overlays.Overlays.EntryLabelMouseClick.eventName, e => {
const event = e as Overlays.Overlays.EntryLabelMouseClick;
resolve(event.overlay);
}, {once: true});
});
dispatchClickEvent(overlayDOM);
const overlayFromEvent = await overlayClick;
// Check that the event was dispatched on the right overlay.
assert.deepEqual(overlayFromEvent, {
type: 'ENTRY_LABEL',
entry: event,
label: 'entry label',
});
});
it('toggles overlays container display', async function() {
const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
const {overlays, container} = setupChartWithDimensionsAndAnnotationOverlayListeners(parsedTrace);
overlays.toggleAllOverlaysDisplayed(true);
await overlays.update();
assert.strictEqual(container.style.display, 'block');
overlays.toggleAllOverlaysDisplayed(false);
await overlays.update();
assert.strictEqual(container.style.display, 'none');
overlays.toggleAllOverlaysDisplayed(true);
await overlays.update();
assert.strictEqual(container.style.display, 'block');
});
it('only renders one TIMESTAMP_MARKER as it is a singleton', async function() {
const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
const {overlays, container} = setupChartWithDimensionsAndAnnotationOverlayListeners(parsedTrace);
overlays.add({
type: 'TIMESTAMP_MARKER',
timestamp: parsedTrace.Meta.traceBounds.min,
});
overlays.add({
type: 'TIMESTAMP_MARKER',
timestamp: parsedTrace.Meta.traceBounds.max,
});
await overlays.update();
assert.lengthOf(container.children, 1);
});
it('can render the label for entry label overlay', async function() {
const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
const {overlays, container, charts} = setupChartWithDimensionsAndAnnotationOverlayListeners(parsedTrace);
const event = charts.mainProvider.eventByIndex?.(50);
assert.isOk(event);
overlays.add({
type: 'ENTRY_LABEL',
entry: event,
label: 'entry label',
});
await overlays.update();
const overlayDOM = container.querySelector<HTMLElement>('.overlay-type-ENTRY_LABEL');
assert.isOk(overlayDOM);
const component = overlayDOM?.querySelector('devtools-entry-label-overlay');
assert.isOk(component?.shadowRoot);
const elementsWrapper = component.shadowRoot.querySelector<HTMLElement>('.label-parts-wrapper');
assert.isOk(elementsWrapper);
const inputField = elementsWrapper.querySelector<HTMLElement>('.input-field');
assert.isOk(inputField);
assert.strictEqual(inputField?.innerText, 'entry label');
});
it('Inputting `Enter`into label overlay makes it non-editable', async function() {
const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
const {overlays, container, charts} = setupChartWithDimensionsAndAnnotationOverlayListeners(parsedTrace);
const event = charts.mainProvider.eventByIndex?.(50);
assert.isOk(event);
// Create an entry label overlay
overlays.add({
type: 'ENTRY_LABEL',
entry: event,
label: 'label',
});
await overlays.update();
// Ensure that the overlay was created.
const overlayDOM = container.querySelector<HTMLElement>('.overlay-type-ENTRY_LABEL');
assert.isOk(overlayDOM);
const component = overlayDOM?.querySelector('devtools-entry-label-overlay');
assert.isOk(component?.shadowRoot);
component.connectedCallback();
const elementsWrapper = component.shadowRoot.querySelector<HTMLElement>('.label-parts-wrapper');
assert.isOk(elementsWrapper);
const inputField = elementsWrapper.querySelector<HTMLElement>('.input-field');
assert.isOk(inputField);
// Double click on the label box to make it editable and focus on it
inputField.dispatchEvent(new FocusEvent('dblclick', {bubbles: true}));
// Ensure the label content is editable
assert.isTrue(inputField.isContentEditable);
// Press `Enter` to make the label not editable
inputField.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter', cancelable: true, bubbles: true}));
// Ensure the label content is not editable
assert.isFalse(inputField.isContentEditable);
});
it('Inputting `Enter` into time range label field when the label is empty removes the overlay', async function() {
const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
const {overlays, container, charts} = setupChartWithDimensionsAndAnnotationOverlayListeners(parsedTrace);
const event = charts.mainProvider.eventByIndex?.(50);
assert.isOk(event);
// Create a time range overlay with an empty label
overlays.add({
type: 'TIME_RANGE',
label: '',
showDuration: true,
// Make this overlay the entire span of the trace
bounds: parsedTrace.Meta.traceBounds,
});
await overlays.update();
// Ensure that the overlay was created.
const overlayDOM = container.querySelector<HTMLElement>('.overlay-type-TIME_RANGE');
assert.isOk(overlayDOM);
const component = overlayDOM?.querySelector('devtools-time-range-overlay');
assert.isOk(component?.shadowRoot);
component.connectedCallback();
const rangeContainer = component.shadowRoot.querySelector<HTMLElement>('.range-container');
assert.isOk(rangeContainer);
const labelBox = rangeContainer.querySelector<HTMLElement>('.label-text');
assert.isOk(labelBox);
// Double click on the label box to make it editable and focus on it
labelBox.dispatchEvent(new FocusEvent('dblclick', {bubbles: true}));
// Press `Enter` on the label field
labelBox.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter', cancelable: true, bubbles: true}));
// Ensure that the entry overlay has been removed because it was saved empty
assert.lengthOf(overlays.overlaysOfType('TIME_RANGE'), 0);
});
it('Inputting `Enter` into time range label field when the label is not empty does not remove the overlay',
async function() {
const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
const {overlays, container, charts} = setupChartWithDimensionsAndAnnotationOverlayListeners(parsedTrace);
const event = charts.mainProvider.eventByIndex?.(50);
assert.isOk(event);
// Create a time range overlay with a label
overlays.add({
type: 'TIME_RANGE',
label: 'label',
showDuration: true,
// Make this overlay the entire span of the trace
bounds: parsedTrace.Meta.traceBounds,
});
await overlays.update();
// Ensure that the overlay was created.
const overlayDOM = container.querySelector<HTMLElement>('.overlay-type-TIME_RANGE');
assert.isOk(overlayDOM);
const component = overlayDOM?.querySelector('devtools-time-range-overlay');
assert.isOk(component?.shadowRoot);
component.connectedCallback();
const rangeContainer = component.shadowRoot.querySelector<HTMLElement>('.range-container');
assert.isOk(rangeContainer);
const labelBox = rangeContainer.querySelector<HTMLElement>('.label-text');
assert.isOk(labelBox);
// Double click on the label box to make it editable and focus on it
labelBox.dispatchEvent(new FocusEvent('dblclick', {bubbles: true}));
// Press `Enter` on the label field
labelBox.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter', cancelable: true, bubbles: true}));
// Ensure that the entry overlay has not been because it was has a non-empty label
assert.lengthOf(overlays.overlaysOfType('TIME_RANGE'), 1);
});
it('Can create multiple Time Range Overlays for Time Range annotations', async function() {
const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
const {overlays, charts} = setupChartWithDimensionsAndAnnotationOverlayListeners(parsedTrace);
const event = charts.mainProvider.eventByIndex?.(50);
assert.isOk(event);
Timeline.ModificationsManager.ModificationsManager.activeManager()?.createAnnotation({
type: 'TIME_RANGE',
label: 'label',
bounds: parsedTrace.Meta.traceBounds,
});
Timeline.ModificationsManager.ModificationsManager.activeManager()?.createAnnotation({
type: 'TIME_RANGE',
label: 'label2',
bounds: parsedTrace.Meta.traceBounds,
});
await overlays.update();
assert.lengthOf(overlays.overlaysOfType('TIME_RANGE'), 2);
});
it('Removes empty label if it is empty when navigated away from (removed focused from)', async function() {
const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
const {overlays, container, charts} = setupChartWithDimensionsAndAnnotationOverlayListeners(parsedTrace);
const event = charts.mainProvider.eventByIndex?.(50);
assert.isOk(event);
// Create an entry label overlay
Timeline.ModificationsManager.ModificationsManager.activeManager()?.createAnnotation({
type: 'ENTRY_LABEL',
entry: event as Trace.Types.Events.Event,
label: '',
});
await overlays.update();
// Ensure that the overlay was created.
const overlayDOM = container.querySelector<HTMLElement>('.overlay-type-ENTRY_LABEL');
assert.isOk(overlayDOM);
const component = overlayDOM?.querySelector('devtools-entry-label-overlay');
assert.isOk(component?.shadowRoot);
component.connectedCallback();
const elementsWrapper = component.shadowRoot.querySelector<HTMLElement>('.label-parts-wrapper');
assert.isOk(elementsWrapper);
const inputField = elementsWrapper.querySelector<HTMLElement>('.input-field');
assert.isOk(inputField);
// Double click on the label box to make it editable and focus on it
inputField.dispatchEvent(new FocusEvent('dblclick', {bubbles: true}));
// Ensure that the entry has 1 overlay
assert.lengthOf(overlays.overlaysForEntry(event), 1);
// Change the content to not editable by changing the element blur like when clicking outside of it.
// The label is empty since no initial value was passed into it and no characters were entered.
inputField.dispatchEvent(new FocusEvent('blur', {bubbles: true}));
// Ensure that the entry overlay has been removed because it was saved empty
assert.lengthOf(overlays.overlaysForEntry(event), 0);
});
it('Update label overlay when the label changes', async function() {
const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
const {overlays, container, charts} = setupChartWithDimensionsAndAnnotationOverlayListeners(parsedTrace);
const event = charts.mainProvider.eventByIndex?.(50);
assert.isOk(event);
// Create an entry label overlay
Timeline.ModificationsManager.ModificationsManager.activeManager()?.createAnnotation({
type: 'ENTRY_LABEL',
entry: event as Trace.Types.Events.Event,
label: '',
});
await overlays.update();
// Ensure that the overlay was created.
const overlayDOM = container.querySelector<HTMLElement>('.overlay-type-ENTRY_LABEL');
assert.isOk(overlayDOM);
const component = overlayDOM?.querySelector('devtools-entry-label-overlay');
assert.isOk(component?.shadowRoot);
component.connectedCallback();
component.dispatchEvent(new Components.EntryLabelOverlay.EntryLabelChangeEvent('new label'));
const updatedOverlay = overlays.overlaysForEntry(event)[0] as Overlays.Overlays.EntryLabel;
assert.isOk(updatedOverlay);
// Make sure the label was updated in the Overlay Object
assert.strictEqual(updatedOverlay.label, 'new label');
});
it('creates an overlay for a time range when an time range annotation is created', async function() {
const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
const {overlays, container} = setupChartWithDimensionsAndAnnotationOverlayListeners(parsedTrace);
// Since TIME_RANGE is AnnotationOverlay, create it through ModificationsManager
Timeline.ModificationsManager.ModificationsManager.activeManager()?.createAnnotation({
type: 'TIME_RANGE',
label: '',
// Make this overlay the entire span of the trace
bounds: parsedTrace.Meta.traceBounds,
});
await overlays.update();
const overlayDOM = container.querySelector<HTMLElement>('.overlay-type-TIME_RANGE');
assert.isOk(overlayDOM);
});
it('can render an overlay for a time range', async function() {
const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
const {overlays, container} = setupChartWithDimensionsAndAnnotationOverlayListeners(parsedTrace);
overlays.add({
type: 'TIME_RANGE',
label: '',
showDuration: true,
// Make this overlay the entire span of the trace
bounds: parsedTrace.Meta.traceBounds,
});
await overlays.update();
const overlayDOM = container.querySelector<HTMLElement>('.overlay-type-TIME_RANGE');
assert.isOk(overlayDOM);
});
it('can update a time range overlay with new bounds', async function() {
const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
const {overlays, container} = setupChartWithDimensionsAndAnnotationOverlayListeners(parsedTrace);
const rangeOverlay = overlays.add({
type: 'TIME_RANGE',
label: '',
showDuration: true,
// Make this overlay the entire span of the trace
bounds: parsedTrace.Meta.traceBounds,
});
await overlays.update();
const overlayDOM = container.querySelector<HTMLElement>('.overlay-type-TIME_RANGE');
assert.isOk(overlayDOM);
const firstWidth = window.parseInt(overlayDOM.style.width);
// change the bounds so the new min is +1second of time.
const newBounds = Trace.Helpers.Timing.traceWindowFromMicroSeconds(
Trace.Types.Timing.Micro(rangeOverlay.bounds.min + (1_000 * 1_000)),
rangeOverlay.bounds.max,
);
overlays.updateExisting(rangeOverlay, {bounds: newBounds});
await overlays.update();
const secondWidth = window.parseInt(overlayDOM.style.width);
// The new time range is smaller so the DOM element should have less width
assert.isTrue(secondWidth < firstWidth);
});
it('renders the overlay for a selected layout shift entry correctly', async function() {
const {parsedTrace} = await TraceLoader.traceEngine(this, 'cls-single-frame.json.gz');
const {overlays, container} = setupChartWithDimensionsAndAnnotationOverlayListeners(parsedTrace);
const layoutShiftEvent = parsedTrace.LayoutShifts.clusters.at(0)?.events.at(0);
if (!layoutShiftEvent) {
throw new Error('layoutShiftEvent was unexpectedly undefined');
}
overlays.add({
type: 'ENTRY_SELECTED',
entry: layoutShiftEvent,
});
const boundsRange = Trace.Types.Timing.Micro(20_000);
const boundsMax = Trace.Types.Timing.Micro(layoutShiftEvent.ts + boundsRange);
overlays.updateVisibleWindow({min: layoutShiftEvent.ts, max: boundsMax, range: boundsRange});
await overlays.update();
const overlayDOM = container.querySelector<HTMLElement>('.overlay-type-ENTRY_SELECTED');
assert.isOk(overlayDOM);
assert.strictEqual(window.parseInt(overlayDOM.style.width), 17);
});
it('renders the duration and label for a time range overlay', async function() {
const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
const {overlays, container} = setupChartWithDimensionsAndAnnotationOverlayListeners(parsedTrace);
overlays.add({
type: 'TIME_RANGE',
label: '',
showDuration: true,
// Make this overlay the entire span of the trace
bounds: parsedTrace.Meta.traceBounds,
});
await overlays.update();
await RenderCoordinator.done();
const overlayDOM = container.querySelector<HTMLElement>('.overlay-type-TIME_RANGE');
const component = overlayDOM?.querySelector('devtools-time-range-overlay');
assert.isOk(component?.shadowRoot);
const rangeContainer = component.shadowRoot.querySelector<HTMLElement>('.range-container');
assert.isOk(rangeContainer);
const duration = rangeContainer.querySelector<HTMLElement>('.duration');
assert.isOk(duration);
assert.strictEqual(duration?.innerText, '1.26\xA0s');
});
it('can remove an overlay', async function() {
const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
const {overlays, container, charts} = setupChartWithDimensionsAndAnnotationOverlayListeners(parsedTrace);
const event = charts.mainProvider.eventByIndex?.(50);
assert.isOk(event);
const selectedOverlay = overlays.add({
type: 'ENTRY_SELECTED',
entry: event,
});
await overlays.update();
assert.lengthOf(container.children, 1);
overlays.remove(selectedOverlay);
await overlays.update();
assert.lengthOf(container.children, 0);
});
it('can render an entry selected overlay for a frame', async function() {
const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev-with-commit.json.gz');
const {overlays, container, charts} = setupChartWithDimensionsAndAnnotationOverlayListeners(parsedTrace);
const timelineFrame = charts.mainProvider.eventByIndex?.(5);
assert.isOk(timelineFrame);
overlays.add({
type: 'ENTRY_SELECTED',
entry: timelineFrame,
});
await overlays.update();
// Ensure that the overlay was created.
const overlayDOM = container.querySelector<HTMLElement>('.overlay-type-ENTRY_SELECTED');
assert.isOk(overlayDOM);
});
it('can return a list of overlays for an entry', async function() {
const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
const {overlays, charts} = setupChartWithDimensionsAndAnnotationOverlayListeners(parsedTrace);
const event = charts.mainProvider.eventByIndex?.(50);
assert.isOk(event);
overlays.add({
type: 'ENTRY_SELECTED',
entry: event,
});
const existingOverlays = overlays.overlaysForEntry(event);
assert.deepEqual(existingOverlays, [{
type: 'ENTRY_SELECTED',
entry: event,
}]);
});
it('can delete overlays and remove them from the DOM', async function() {
const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
const {container, overlays, charts} = setupChartWithDimensionsAndAnnotationOverlayListeners(parsedTrace);
const event = charts.mainProvider.eventByIndex?.(50);
assert.isOk(event);
overlays.add({
type: 'ENTRY_SELECTED',
entry: event,
});
await overlays.update();
assert.lengthOf(container.children, 1);
const removedCount = overlays.removeOverlaysOfType('ENTRY_SELECTED');
assert.strictEqual(removedCount, 1);
assert.lengthOf(container.children, 0);
});
it('the label entry field is editable when created', async function() {
const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
const {overlays, container} = setupChartWithDimensionsAndAnnotationOverlayListeners(parsedTrace);
const charts = createCharts(parsedTrace);
const event = charts.mainProvider.eventByIndex?.(50);
assert.isOk(event);
// Since ENTRY_LABEL is AnnotationOverlay, create it through ModificationsManager
Timeline.ModificationsManager.ModificationsManager.activeManager()?.createAnnotation({
type: 'ENTRY_LABEL',
label: '',
entry: event as Trace.Types.Events.Event,
});
await overlays.update();
const overlayDOM = container.querySelector<HTMLElement>('.overlay-type-ENTRY_LABEL');
assert.isOk(overlayDOM);
const component = overlayDOM?.querySelector('devtools-entry-label-overlay');
assert.isOk(component?.shadowRoot);
const elementsWrapper = component.shadowRoot.querySelector<HTMLElement>('.label-parts-wrapper');
const inputField = elementsWrapper?.querySelector<HTMLElement>('.input-field') as HTMLSpanElement;
assert.isOk(inputField);
// The label input box should be editable after it is created and before anything else happened
assert.isTrue(inputField.isContentEditable);
});
it('the label entry field is in focus after being double clicked on', async function() {
const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
const {overlays, container} = setupChartWithDimensionsAndAnnotationOverlayListeners(parsedTrace);
const charts = createCharts(parsedTrace);
const event = charts.mainProvider.eventByIndex?.(50);
assert.isOk(event);
// Since ENTRY_LABEL is AnnotationOverlay, create it through ModificationsManager
Timeline.ModificationsManager.ModificationsManager.activeManager()?.createAnnotation({
type: 'ENTRY_LABEL',
label: '',
entry: event as Trace.Types.Events.Event,
});
await overlays.update();
const overlayDOM = container.querySelector<HTMLElement>('.overlay-type-ENTRY_LABEL');
assert.isOk(overlayDOM);
const component = overlayDOM?.querySelector('devtools-entry-label-overlay');
assert.isOk(component?.shadowRoot);
const elementsWrapper = component.shadowRoot.querySelector<HTMLElement>('.label-parts-wrapper');
assert.isOk(elementsWrapper);
const labelBox = elementsWrapper.querySelector<HTMLElement>('.input-field') as HTMLSpanElement;
assert.isOk(labelBox);
// The label input box should be editable after it is created and before anything else happened
assert.isTrue(labelBox.isContentEditable);
// Make the content to editable by changing the element blur like when clicking outside of it.
// When that happens, the content should be set to not editable.
labelBox.dispatchEvent(new FocusEvent('blur', {bubbles: true}));
assert.isFalse(labelBox.isContentEditable);
// Double click on the label to make it editable again
labelBox.dispatchEvent(new FocusEvent('dblclick', {bubbles: true}));
assert.isTrue(labelBox.isContentEditable);
});
});
describe('traceWindowContainingOverlays', () => {
it('calculates the smallest window that fits the overlay inside', () => {
const FAKE_EVENT_1 = {
ts: 0,
dur: 10,
} as Trace.Types.Events.Event;
const FAKE_EVENT_2 = {
ts: 5,
dur: 100,
} as Trace.Types.Events.Event;
const overlay1: Overlays.Overlays.EntryOutline = {
entry: FAKE_EVENT_1,
type: 'ENTRY_OUTLINE',
outlineReason: 'INFO',
};
const overlay2: Overlays.Overlays.EntryOutline = {
entry: FAKE_EVENT_2,
type: 'ENTRY_OUTLINE',
outlineReason: 'INFO',
};
const traceWindow = Overlays.Overlays.traceWindowContainingOverlays([overlay1, overlay2]);
if (!traceWindow) {
throw new Error('No trace window for overlays');
}
assert.strictEqual(traceWindow.min, 0);
assert.strictEqual(traceWindow.max, 105);
});
it('returns null for no overlays', () => {
const traceWindow = Overlays.Overlays.traceWindowContainingOverlays([]);
assert.isNull(traceWindow);
});
});
describe('jslogcontext for overlays', () => {
const FAKE_EVENT = {
ts: 0,
dur: 10,
} as Trace.Types.Events.Event;
it('does not define a log for an entry_selected overlay', () => {
const overlay: Overlays.Overlays.EntrySelected = {
type: 'ENTRY_SELECTED',
entry: FAKE_EVENT,
};
const context = Overlays.Overlays.jsLogContext(overlay);
assert.isNull(context);
});
it('defines a log for an entry outline based on its type', () => {
const overlayInfo: Overlays.Overlays.EntryOutline = {
type: 'ENTRY_OUTLINE',
outlineReason: 'INFO',
entry: FAKE_EVENT,
};
const overlayError: Overlays.Overlays.EntryOutline = {
type: 'ENTRY_OUTLINE',
outlineReason: 'ERROR',
entry: FAKE_EVENT,
};
const infoContext = Overlays.Overlays.jsLogContext(overlayInfo);
assert.strictEqual(infoContext, 'timeline.overlays.entry-outline-info');
const errorContext = Overlays.Overlays.jsLogContext(overlayError);
assert.strictEqual(errorContext, 'timeline.overlays.entry-outline-error');
});
it('defines a log for entry labels', () => {
const overlay: Overlays.Overlays.EntryLabel = {
type: 'ENTRY_LABEL',
entry: FAKE_EVENT,
label: 'hello world',
};
const context = Overlays.Overlays.jsLogContext(overlay);
assert.strictEqual(context, 'timeline.overlays.entry-label');
});
it('defines a log for time ranges', () => {
const overlay: Overlays.Overlays.TimeRangeLabel = {
showDuration: true,
type: 'TIME_RANGE',
bounds: microsecondsTraceWindow(1_000, 10_000),
label: 'hello world',
};
const context = Overlays.Overlays.jsLogContext(overlay);
assert.strictEqual(context, 'timeline.overlays.time-range');
});
it('defines a log for timespan breakdowns', () => {
const overlay: Overlays.Overlays.TimespanBreakdown = {
type: 'TIMESPAN_BREAKDOWN',
sections: [],
};
const context = Overlays.Overlays.jsLogContext(overlay);
assert.strictEqual(context, 'timeline.overlays.timespan-breakdown');
});
it('defines a log for cursor timestamp marker', () => {
const overlay: Overlays.Overlays.TimestampMarker = {
type: 'TIMESTAMP_MARKER',
timestamp: 1_000 as Trace.Types.Timing.Micro,
};
const context = Overlays.Overlays.jsLogContext(overlay);
assert.strictEqual(context, 'timeline.overlays.cursor-timestamp-marker');
});
it('defines a log for candy striped time ranges', () => {
const overlay: Overlays.Overlays.CandyStripedTimeRange = {
type: 'CANDY_STRIPED_TIME_RANGE',
bounds: microsecondsTraceWindow(1_000, 10_000),
entry: FAKE_EVENT,
};
const context = Overlays.Overlays.jsLogContext(overlay);
assert.strictEqual(context, 'timeline.overlays.candy-striped-time-range');
});
it('defines a log for entries links but only if they are connected', () => {
const overlayConnected: Overlays.Overlays.EntriesLink = {
type: 'ENTRIES_LINK',
entryFrom: FAKE_EVENT,
entryTo: FAKE_EVENT,
state: Trace.Types.File.EntriesLinkState.CONNECTED,
};
const overlayPending: Overlays.Overlays.EntriesLink = {
type: 'ENTRIES_LINK',
entryFrom: FAKE_EVENT,
entryTo: undefined,
state: Trace.Types.File.EntriesLinkState.PENDING_TO_EVENT,
};
const connectedContext = Overlays.Overlays.jsLogContext(overlayConnected);
assert.strictEqual(connectedContext, 'timeline.overlays.entries-link');
const pendingContext = Overlays.Overlays.jsLogContext(overlayPending);
assert.isNull(pendingContext);
});
});
});