Загрузка данных
import {
CHART_DRAWING_TOOLBAR_CONTAINER,
CHART_MODAL_CONTAINER_CLASSNAME,
CHART_PANE_OVERLAY_CONTAINER,
CHART_ROOT_CLASSNAME,
} from '@src/constants';
import { ContainerManager } from '../ContainerManager';
jest.mock('@src/constants', () => ({
CHART_DRAWING_TOOLBAR_CONTAINER: 'chart-drawing-toolbar-container',
CHART_MODAL_CONTAINER_CLASSNAME: 'chart-modal-container',
CHART_PANE_OVERLAY_CONTAINER: 'chart-pane-overlay-container',
CHART_ROOT_CLASSNAME: 'chart-root',
}));
describe('ContainerManager', () => {
let parentContainer: HTMLDivElement;
beforeEach(() => {
document.querySelectorAll('link[href*="fonts.googleapis.com"]').forEach((link) => link.remove());
parentContainer = document.createElement('div');
document.body.appendChild(parentContainer);
});
afterEach(() => {
parentContainer.remove();
});
describe('createContainers', () => {
it('должен создавать основные контейнеры графика с корректной структурой', () => {
const {
headerContainer,
footerContainer,
chartContainer,
chartAreaContainer,
toolBarContainer,
modalContainer,
controlBarContainer,
drawingToolbarContainer,
} = ContainerManager.createContainers({
parentContainer,
showBottomPanel: true,
showMenuButton: true,
});
expect(parentContainer.classList.contains(CHART_ROOT_CLASSNAME)).toBe(true);
expect(parentContainer.children[0]).toBe(headerContainer);
expect(parentContainer.children[1]).toBe(chartContainer);
expect(parentContainer.children[2]).toBe(footerContainer);
expect(chartContainer.contains(toolBarContainer)).toBe(true);
expect(chartContainer.contains(chartAreaContainer)).toBe(true);
expect(chartContainer.contains(modalContainer)).toBe(true);
expect(chartAreaContainer.contains(controlBarContainer)).toBe(true);
expect(chartAreaContainer.contains(drawingToolbarContainer)).toBe(true);
expect(drawingToolbarContainer.classList.contains(CHART_DRAWING_TOOLBAR_CONTAINER)).toBe(true);
expect(modalContainer.classList.contains(CHART_MODAL_CONTAINER_CLASSNAME)).toBe(true);
expect(modalContainer.hidden).toBe(true);
});
it('должен создавать сетку с нижней панелью', () => {
ContainerManager.createContainers({
parentContainer,
showBottomPanel: true,
showMenuButton: true,
});
expect(parentContainer.style.gridTemplateRows).toBe('36px minmax(0, 1fr) 32px');
});
it('должен создавать сетку без нижней панели', () => {
const { footerContainer } = ContainerManager.createContainers({
parentContainer,
showBottomPanel: false,
showMenuButton: true,
});
expect(parentContainer.style.gridTemplateRows).toBe('36px minmax(0, 1fr)');
expect(parentContainer.contains(footerContainer)).toBe(false);
});
it('должен отображать панель инструментов при включённой кнопке меню', () => {
const { chartContainer, toolBarContainer } = ContainerManager.createContainers({
parentContainer,
showMenuButton: true,
});
expect(chartContainer.contains(toolBarContainer)).toBe(true);
expect(chartContainer.style.gridTemplateColumns).toBe('42px minmax(0, 1fr)');
});
it('должен скрывать панель инструментов при выключенной кнопке меню', () => {
const { chartContainer, toolBarContainer } = ContainerManager.createContainers({
parentContainer,
showMenuButton: false,
});
expect(chartContainer.contains(toolBarContainer)).toBe(false);
expect(chartContainer.style.gridTemplateColumns).toBe('minmax(0, 1fr)');
});
it('должен переключать видимость панели инструментов', () => {
const { chartContainer, toolBarContainer, toggleToolbar } = ContainerManager.createContainers({
parentContainer,
showMenuButton: true,
});
expect(toggleToolbar()).toBe(false);
expect(chartContainer.contains(toolBarContainer)).toBe(false);
expect(chartContainer.style.gridTemplateColumns).toBe('minmax(0, 1fr)');
expect(toggleToolbar()).toBe(true);
expect(chartContainer.contains(toolBarContainer)).toBe(true);
expect(chartContainer.style.gridTemplateColumns).toBe('42px minmax(0, 1fr)');
});
it('должен удалять старое содержимое родительского контейнера перед созданием графика', () => {
const oldElement = document.createElement('div');
parentContainer.appendChild(oldElement);
ContainerManager.createContainers({
parentContainer,
showMenuButton: true,
});
expect(parentContainer.contains(oldElement)).toBe(false);
});
it('должен добавлять шрифт только один раз', () => {
ContainerManager.createContainers({
parentContainer,
showMenuButton: true,
});
ContainerManager.createContainers({
parentContainer,
showMenuButton: true,
});
const fontLinks = document.querySelectorAll('link[href*="fonts.googleapis.com"]');
expect(fontLinks).toHaveLength(1);
expect(fontLinks[0]?.getAttribute('href')).toBe(
'https://fonts.googleapis.com/css2?family=Inter:opsz,wght@14..32,100..900&display=swap',
);
});
});
describe('createPaneContainers', () => {
it('должен создавать контейнеры легенды и оверлея панели', () => {
const { legendContainer, paneOverlayContainer } = ContainerManager.createPaneContainers();
expect(legendContainer.style.position).toBe('absolute');
expect(legendContainer.style.pointerEvents).toBe('none');
expect(paneOverlayContainer.classList.contains(CHART_PANE_OVERLAY_CONTAINER)).toBe(true);
expect(paneOverlayContainer.style.position).toBe('absolute');
expect(paneOverlayContainer.style.pointerEvents).toBe('none');
});
it('должен размещать слои графика в корректном порядке', () => {
const { controlBarContainer, drawingToolbarContainer, modalContainer } = ContainerManager.createContainers({
parentContainer,
showMenuButton: true,
});
const { legendContainer, paneOverlayContainer } = ContainerManager.createPaneContainers();
const controlBarZIndex = Number(controlBarContainer.style.zIndex);
const paneOverlayZIndex = Number(paneOverlayContainer.style.zIndex);
const drawingZIndex = Number(drawingToolbarContainer.style.zIndex);
const legendZIndex = Number(legendContainer.style.zIndex);
const modalZIndex = Number(modalContainer.style.zIndex);
expect(controlBarZIndex).toBe(paneOverlayZIndex);
expect(legendZIndex).toBeGreaterThan(paneOverlayZIndex);
expect(drawingZIndex).toBeGreaterThan(legendZIndex);
expect(modalZIndex).toBeGreaterThan(drawingZIndex);
});
});
describe('clearContainers', () => {
it('должен очищать содержимое родительского контейнера', () => {
parentContainer.innerHTML = '<div>content</div>';
ContainerManager.clearContainers(parentContainer);
expect(parentContainer.innerHTML).toBe('');
});
});
});
import { MouseEventParams, Time } from 'lightweight-charts';
import { BehaviorSubject } from 'rxjs';
import { EventManager } from '@core/EventManager';
import { Indicator } from '@core/Indicator';
import { Legend, LegendModel, LegendParams } from '@core/Legend';
import { SeriesStrategies } from '@src/modules/series-strategies/SeriesFactory';
import type { OHLCConfig } from '@src/types';
describe('Legend', () => {
let crosshairMoveHandler: ((param: MouseEventParams) => void) | null;
let indicators: BehaviorSubject<Map<string, Indicator>>;
let mainSeriesSubject: BehaviorSubject<SeriesStrategies | null>;
let mainSeries: SeriesStrategies;
let eventManager: EventManager;
let getLegendData: jest.Mock;
let subscribeDataChanged: jest.Mock;
let unsubscribeDataChanged: jest.Mock;
let legend: Legend | null;
const config: OHLCConfig = {
show: true,
showAbsoluteChange: false,
showPercentageChange: true,
};
beforeEach(() => {
crosshairMoveHandler = null;
legend = null;
getLegendData = jest.fn().mockReturnValue({
open: {
value: 100,
color: 'white',
name: 'Open',
},
absoluteChange: {
value: 10,
color: 'green',
name: 'Absolute',
},
percentageChange: {
value: '10%',
color: 'green',
name: 'Percentage',
},
});
subscribeDataChanged = jest.fn();
unsubscribeDataChanged = jest.fn();
mainSeries = {
getLegendData,
subscribeDataChanged,
unsubscribeDataChanged,
} as unknown as SeriesStrategies;
eventManager = {
symbolName: jest.fn().mockReturnValue(new BehaviorSubject('AAPL')),
} as unknown as EventManager;
indicators = new BehaviorSubject<Map<string, Indicator>>(new Map());
mainSeriesSubject = new BehaviorSubject<SeriesStrategies | null>(mainSeries);
});
afterEach(() => {
legend?.destroy();
});
function createLegend(): Legend {
const subscribeChartEvent = jest.fn((event, handler) => {
if (event === 'crosshairMove') {
crosshairMoveHandler = handler as (param: MouseEventParams) => void;
}
return () => {};
}) as unknown as LegendParams['subscribeChartEvent'];
legend = new Legend({
config,
eventManager,
indicators,
subscribeChartEvent,
mainSeries: mainSeriesSubject,
paneId: 1,
paneIndex: () => 0,
openIndicatorSettings: jest.fn(),
});
return legend;
}
function createCrosshairParam(paneIndex = 0): MouseEventParams {
return {
point: {
x: 10,
y: 20,
},
time: 100 as Time,
paneIndex,
seriesData: new Map([['main-series', {}]]),
} as unknown as MouseEventParams;
}
function getMainSeriesValues(model: LegendModel) {
return model[0].values as unknown as Map<
string,
{
value: unknown;
color: string;
name?: string;
}
>;
}
it('должен добавлять абсолютное и относительное изменение в модель основной серии', () => {
const currentLegend = createLegend();
let model: LegendModel = [];
currentLegend.getLegendViewModel().subscribe((value) => {
model = value;
});
const values = getMainSeriesValues(model);
expect(values.get('absoluteChange')?.value).toBe(10);
expect(values.get('percentageChange')?.value).toBe('10%');
});
it('должен обновлять абсолютное и относительное изменение данными под курсором', () => {
getLegendData.mockImplementation((param?: MouseEventParams) => {
if (param) {
return {
absoluteChange: {
value: 20,
color: 'green',
name: 'Absolute',
},
percentageChange: {
value: '20%',
color: 'green',
name: 'Percentage',
},
};
}
return {
absoluteChange: {
value: 10,
color: 'green',
name: 'Absolute',
},
percentageChange: {
value: '10%',
color: 'green',
name: 'Percentage',
},
};
});
const currentLegend = createLegend();
crosshairMoveHandler?.(createCrosshairParam());
let model: LegendModel = [];
currentLegend.getLegendViewModel().subscribe((value) => {
model = value;
});
const values = getMainSeriesValues(model);
expect(values.get('absoluteChange')?.value).toBe(20);
expect(values.get('percentageChange')?.value).toBe('20%');
});
it('должен отображать тултип при наведении на текущий pane', () => {
const currentLegend = createLegend();
let visible = false;
let position = null;
currentLegend.subscribeCursorVisability((value) => {
visible = value;
});
currentLegend.subscribeCursorPosition((value) => {
position = value;
});
crosshairMoveHandler?.(createCrosshairParam());
expect(visible).toBe(true);
expect(position).toEqual({
x: 10,
y: 20,
});
});
it('должен скрывать тултип при наведении на другой pane', () => {
const currentLegend = createLegend();
let visible = true;
currentLegend.subscribeCursorVisability((value) => {
visible = value;
});
crosshairMoveHandler?.(createCrosshairParam(1));
expect(visible).toBe(false);
});
it('должен подписываться и отписываться от изменений основной серии', () => {
const currentLegend = createLegend();
expect(subscribeDataChanged).toHaveBeenCalledTimes(1);
currentLegend.destroy();
legend = null;
expect(unsubscribeDataChanged).toHaveBeenCalledTimes(1);
});
it('должен возвращать конфигурацию легенды', () => {
const currentLegend = createLegend();
expect(currentLegend.getConfig()).toBe(config);
});
});