2025-04-01 10:38:02 +09:00

285 lines
11 KiB
TypeScript

// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
jest.mock('./scroll-page');
import ScrollManager, { Accessors } from './ScrollManager';
import traceGenerator from './demo/trace-generators';
import { scrollBy, scrollTo } from './scroll-page';
import { Trace, TraceSpanData, TraceSpanReference } from './types/trace';
const SPAN_HEIGHT = 2;
function getTrace(): Trace {
const generatedTrace = traceGenerator.trace({ numberOfSpans: 10 });
generatedTrace.duration = 2000;
generatedTrace.startTime = 1000;
generatedTrace.spans.forEach((span: TraceSpanData, index: number) => {
span.duration = 1;
span.startTime = 1000;
span.spanID = (index + 1).toString();
});
return generatedTrace;
}
function getAccessors() {
return {
getViewRange: jest.fn(() => [0, 1] as [number, number]),
getSearchedSpanIDs: jest.fn(),
getCollapsedChildren: jest.fn(),
getViewHeight: jest.fn(() => SPAN_HEIGHT * 2),
getBottomRowIndexVisible: jest.fn(),
getTopRowIndexVisible: jest.fn(),
getRowPosition: jest.fn(),
mapRowIndexToSpanIndex: jest.fn((n) => n),
mapSpanIndexToRowIndex: jest.fn((n) => n),
};
}
describe('ScrollManager', () => {
let trace: Trace;
let accessors: Accessors;
let manager: ScrollManager;
beforeEach(() => {
jest.mocked(scrollBy).mockReset();
jest.mocked(scrollTo).mockReset();
trace = getTrace();
accessors = getAccessors();
manager = new ScrollManager(trace, { scrollBy, scrollTo });
manager.setAccessors(accessors);
});
it('saves the accessors', () => {
accessors = getAccessors();
manager.setAccessors(accessors);
expect(manager._accessors).toBe(accessors);
});
describe('_scrollPast()', () => {
it('throws if accessors is not set', () => {
expect(manager._scrollPast).toThrow();
});
it('is a noop if an invalid rowPosition is returned by the accessors', () => {
// eslint-disable-next-line no-console
const oldWarn = console.warn;
// eslint-disable-next-line no-console
console.warn = () => {};
manager._scrollPast(-2, 1);
expect(jest.mocked(accessors.getRowPosition).mock.calls.length).toBe(1);
expect(jest.mocked(accessors.getViewHeight).mock.calls.length).toBe(0);
expect(jest.mocked(scrollTo).mock.calls.length).toBe(0);
// eslint-disable-next-line no-console
console.warn = oldWarn;
});
it('scrolls up with direction is `-1`', () => {
const y = 10;
const expectTo = y - 0.5 * accessors.getViewHeight();
jest.mocked(accessors.getRowPosition).mockReturnValue({ y, height: SPAN_HEIGHT });
manager._scrollPast(NaN, -1);
expect(jest.mocked(scrollTo).mock.calls).toEqual([[expectTo]]);
});
it('scrolls down with direction `1`', () => {
const y = 10;
const vh = accessors.getViewHeight();
const expectTo = y + SPAN_HEIGHT - 0.5 * vh;
jest.mocked(accessors.getRowPosition).mockReturnValue({ y, height: SPAN_HEIGHT });
manager._scrollPast(NaN, 1);
expect(jest.mocked(scrollTo).mock.calls).toEqual([[expectTo]]);
});
});
describe('_scrollToVisibleSpan()', () => {
function getRefs(spanID: string | undefined) {
return [{ refType: 'CHILD_OF', spanID }] as TraceSpanReference[];
}
let scrollPastMock: jest.Mock;
beforeEach(() => {
scrollPastMock = jest.fn();
manager._scrollPast = scrollPastMock;
});
it('throws if accessors is not set', () => {
expect(manager._scrollToVisibleSpan).toThrow();
});
it('exits if the trace is not set', () => {
manager.setTrace(null);
manager._scrollToVisibleSpan(1);
expect(scrollPastMock.mock.calls.length).toBe(0);
});
it('does nothing if already at the boundary', () => {
jest.mocked(accessors.getTopRowIndexVisible).mockReturnValue(0);
jest.mocked(accessors.getBottomRowIndexVisible).mockReturnValue(trace.spans.length - 1);
manager._scrollToVisibleSpan(-1);
expect(scrollPastMock.mock.calls.length).toBe(0);
manager._scrollToVisibleSpan(1);
expect(scrollPastMock.mock.calls.length).toBe(0);
});
it('centers the current top or bottom span', () => {
jest.mocked(accessors.getTopRowIndexVisible).mockReturnValue(5);
jest.mocked(accessors.getBottomRowIndexVisible).mockReturnValue(5);
manager._scrollToVisibleSpan(-1);
expect(scrollPastMock).lastCalledWith(5, -1);
manager._scrollToVisibleSpan(1);
expect(scrollPastMock).lastCalledWith(5, 1);
});
it('skips spans that are out of view', () => {
trace.spans[4].startTime = trace.startTime + trace.duration * 0.5;
accessors.getViewRange = () => [0.4, 0.6];
jest.mocked(accessors.getTopRowIndexVisible).mockReturnValue(trace.spans.length - 1);
jest.mocked(accessors.getBottomRowIndexVisible).mockReturnValue(0);
manager._scrollToVisibleSpan(1);
expect(scrollPastMock).lastCalledWith(4, 1);
manager._scrollToVisibleSpan(-1);
expect(scrollPastMock).lastCalledWith(4, -1);
});
it('skips spans that do not match the text search', () => {
jest.mocked(accessors.getTopRowIndexVisible).mockReturnValue(trace.spans.length - 1);
jest.mocked(accessors.getBottomRowIndexVisible).mockReturnValue(0);
accessors.getSearchedSpanIDs = () => new Set([trace.spans[4].spanID]);
manager._scrollToVisibleSpan(1);
expect(scrollPastMock).lastCalledWith(4, 1);
manager._scrollToVisibleSpan(-1);
expect(scrollPastMock).lastCalledWith(4, -1);
});
it('scrolls to boundary when scrolling away from closest spanID in findMatches', () => {
const closetFindMatchesSpanID = 4;
jest.mocked(accessors.getTopRowIndexVisible).mockReturnValue(closetFindMatchesSpanID - 1);
jest.mocked(accessors.getBottomRowIndexVisible).mockReturnValue(closetFindMatchesSpanID + 1);
accessors.getSearchedSpanIDs = () => new Set([trace.spans[closetFindMatchesSpanID].spanID]);
manager._scrollToVisibleSpan(1);
expect(scrollPastMock).lastCalledWith(trace.spans.length - 1, 1);
manager._scrollToVisibleSpan(-1);
expect(scrollPastMock).lastCalledWith(0, -1);
});
it('scrolls to last visible row when boundary is hidden', () => {
const parentOfLastRowWithHiddenChildrenIndex = trace.spans.length - 2;
jest.mocked(accessors.getBottomRowIndexVisible).mockReturnValue(0);
accessors.getCollapsedChildren = () => new Set([trace.spans[parentOfLastRowWithHiddenChildrenIndex].spanID]);
accessors.getSearchedSpanIDs = () => new Set([trace.spans[0].spanID]);
trace.spans[trace.spans.length - 1].references = getRefs(
trace.spans[parentOfLastRowWithHiddenChildrenIndex].spanID
);
manager._scrollToVisibleSpan(1);
expect(scrollPastMock).lastCalledWith(parentOfLastRowWithHiddenChildrenIndex, 1);
});
describe('scrollToNextVisibleSpan() and scrollToPrevVisibleSpan()', () => {
beforeEach(() => {
// change spans so 0 and 4 are top-level and their children are collapsed
const spans = trace.spans;
let parentID;
for (let i = 0; i < spans.length; i++) {
switch (i) {
case 0:
case 4:
parentID = spans[i].spanID;
break;
default:
spans[i].references = getRefs(parentID);
}
}
// set which spans are "in-view" and which have collapsed children
jest.mocked(accessors.getTopRowIndexVisible).mockReturnValue(trace.spans.length - 1);
jest.mocked(accessors.getBottomRowIndexVisible).mockReturnValue(0);
jest.mocked(accessors.getCollapsedChildren).mockReturnValue(new Set([spans[0].spanID, spans[4].spanID]));
});
it('skips spans that are hidden because their parent is collapsed', () => {
manager.scrollToNextVisibleSpan();
expect(scrollPastMock).lastCalledWith(4, 1);
manager.scrollToPrevVisibleSpan();
expect(scrollPastMock).lastCalledWith(4, -1);
});
it('ignores references with unknown types', () => {
// modify spans[2] so that it has an unknown refType
const spans = trace.spans;
spans[2].references = [{ refType: 'OTHER' }] as unknown as TraceSpanReference[];
manager.scrollToNextVisibleSpan();
expect(scrollPastMock).lastCalledWith(2, 1);
manager.scrollToPrevVisibleSpan();
expect(scrollPastMock).lastCalledWith(4, -1);
});
it('handles more than one level of ancestry', () => {
// modify spans[2] so that it has an unknown refType
const spans = trace.spans;
spans[2].references = getRefs(spans[1].spanID);
manager.scrollToNextVisibleSpan();
expect(scrollPastMock).lastCalledWith(4, 1);
manager.scrollToPrevVisibleSpan();
expect(scrollPastMock).lastCalledWith(4, -1);
});
});
describe('scrollToFirstVisibleSpan', () => {
beforeEach(() => {
jest.spyOn(manager, '_scrollToVisibleSpan');
});
it('calls _scrollToVisibleSpan searching downwards from first span', () => {
manager.scrollToFirstVisibleSpan();
expect(manager._scrollToVisibleSpan).toHaveBeenCalledWith(1, 0);
});
});
});
describe('scrollPageDown() and scrollPageUp()', () => {
it('scrolls by +/~ viewHeight when invoked', () => {
manager.scrollPageDown();
expect(scrollBy).lastCalledWith(0.95 * accessors.getViewHeight(), true);
manager.scrollPageUp();
expect(scrollBy).lastCalledWith(-0.95 * accessors.getViewHeight(), true);
});
it('is a no-op if _accessors or _scroller is not defined', () => {
manager._accessors = null;
manager.scrollPageDown();
manager.scrollPageUp();
expect(jest.mocked(scrollBy).mock.calls.length).toBe(0);
manager._accessors = accessors;
manager._scroller = null;
manager.scrollPageDown();
manager.scrollPageUp();
expect(jest.mocked(scrollBy).mock.calls.length).toBe(0);
});
});
describe('destroy()', () => {
it('disposes', () => {
expect(manager._trace).toBeDefined();
expect(manager._accessors).toBeDefined();
expect(manager._scroller).toBeDefined();
manager.destroy();
expect(manager._trace).not.toBeDefined();
expect(manager._accessors).not.toBeDefined();
expect(manager._scroller).not.toBeDefined();
});
});
});