Загрузка данных
/** @jest-environment jsdom */
import { BehaviorSubject, Subscription } from 'rxjs';
import { Chart } from '@core/Chart';
import { Intervals } from '@src/types/intervals';
interface ChartInternals {
eventManager: {
symbolId: () => BehaviorSubject<string>;
getInterval: () => BehaviorSubject<Intervals | null>;
getTimeframe: jest.Mock;
};
compareManager: {
itemsObs: () => BehaviorSubject<{ symbolId: string }[]>;
};
dataSource: {
setSymbols: jest.Mock;
loadTill: jest.Mock;
loadAllHistory: jest.Mock;
loadMoreHistory: jest.Mock;
getOldestTime: jest.Mock;
getLastCandle: jest.Mock;
};
lwcChart: {
timeScale: () => {
getVisibleRange: jest.Mock;
setVisibleRange: jest.Mock;
getVisibleLogicalRange: jest.Mock;
};
};
mouseEvents: {
subscribe: jest.Mock;
};
paneManager: {
setVisibleLogicalRange: jest.Mock;
};
subscriptions: Subscription;
activeSymbolIds: string[];
active: boolean;
historyBatchRunning: boolean;
setupDataSourceSubs: () => void;
setupHistoricalDataLoading: () => void;
scheduleHistoryBatch: () => void;
}
interface CreateChartOptions {
symbolId?: string;
interval?: Intervals | null;
compareSymbolIds?: string[];
}
interface ChartTestContext {
chart: ChartInternals;
symbolId$: BehaviorSubject<string>;
interval$: BehaviorSubject<Intervals | null>;
getTimeframe: jest.Mock;
compareItems$: BehaviorSubject<{ symbolId: string }[]>;
setSymbols: jest.Mock;
loadTill: jest.Mock;
loadAllHistory: jest.Mock;
loadMoreHistory: jest.Mock;
getOldestTime: jest.Mock;
getLastCandle: jest.Mock;
getVisibleRange: jest.Mock;
getVisibleLogicalRange: jest.Mock;
setVisibleRange: jest.Mock;
mouseEventsSubscribe: jest.Mock;
setVisibleLogicalRange: jest.Mock;
}
interface Deferred {
promise: Promise<void>;
resolve: () => void;
}
function createDeferred(): Deferred {
let resolve!: () => void;
const promise = new Promise<void>((resolver) => {
resolve = resolver;
});
return { promise, resolve };
}
async function flushPromises(): Promise<void> {
await Promise.resolve();
await Promise.resolve();
await Promise.resolve();
}
describe('Chart', () => {
let animationFrameCallbacks: FrameRequestCallback[];
let subscriptions: Subscription[];
beforeEach(() => {
animationFrameCallbacks = [];
subscriptions = [];
jest.spyOn(window, 'requestAnimationFrame').mockImplementation((callback) => {
animationFrameCallbacks.push(callback);
return animationFrameCallbacks.length;
});
});
afterEach(() => {
subscriptions.forEach((subscription) => subscription.unsubscribe());
jest.restoreAllMocks();
});
function runNextAnimationFrame(): void {
const callback = animationFrameCallbacks.shift();
callback?.(0);
}
function createChartInternals({
symbolId = '',
interval = null,
compareSymbolIds = ['SBER', 'SBER'],
}: CreateChartOptions = {}): ChartTestContext {
const symbolId$ = new BehaviorSubject(symbolId);
const interval$ = new BehaviorSubject<Intervals | null>(interval);
const compareItems$ = new BehaviorSubject(
compareSymbolIds.map((value) => ({
symbolId: value,
})),
);
const setSymbols = jest.fn();
const loadTill = jest.fn(() => Promise.resolve());
const loadAllHistory = jest.fn(() => Promise.resolve());
const loadMoreHistory = jest.fn(() => Promise.resolve());
const getOldestTime = jest.fn(() => 100);
const getLastCandle = jest.fn(() => ({ time: 200 }));
const getVisibleRange = jest.fn(() => null);
const getVisibleLogicalRange = jest.fn(() => ({ from: 0, to: 1000 }));
const setVisibleRange = jest.fn();
const getTimeframe = jest.fn(() => '1m');
const mouseEventsSubscribe = jest.fn();
const setVisibleLogicalRange = jest.fn();
const chart = Object.assign(Object.create(Chart.prototype), {
eventManager: {
symbolId: () => symbolId$,
getInterval: () => interval$,
getTimeframe,
},
compareManager: {
itemsObs: () => compareItems$,
},
dataSource: {
setSymbols,
loadTill,
loadAllHistory,
loadMoreHistory,
getOldestTime,
getLastCandle,
},
lwcChart: {
timeScale: () => ({
getVisibleRange,
getVisibleLogicalRange,
setVisibleRange,
}),
},
mouseEvents: {
subscribe: mouseEventsSubscribe,
},
paneManager: {
setVisibleLogicalRange,
},
subscriptions: new Subscription(),
activeSymbolIds: [],
active: true,
historyBatchRunning: false,
}) as ChartInternals;
subscriptions.push(chart.subscriptions);
return {
chart,
symbolId$,
interval$,
getTimeframe,
compareItems$,
setSymbols,
loadTill,
loadAllHistory,
loadMoreHistory,
getOldestTime,
getLastCandle,
getVisibleRange,
getVisibleLogicalRange,
setVisibleRange,
mouseEventsSubscribe,
setVisibleLogicalRange,
};
}
it('должен исключать пустой основной symbolId и удалять дубликаты', () => {
const { chart, setSymbols } = createChartInternals();
chart.setupDataSourceSubs();
expect(setSymbols).toHaveBeenCalledTimes(1);
expect(setSymbols).toHaveBeenCalledWith(['SBER']);
});
it('должен зарегистрировать символ до загрузки данных начального интервала', () => {
const callOrder: string[] = [];
const { chart, setSymbols, loadTill } = createChartInternals({
symbolId: 'SBER',
compareSymbolIds: [],
interval: Intervals['1Y'],
});
setSymbols.mockImplementation(() => {
callOrder.push('setSymbols');
});
loadTill.mockImplementation(() => {
callOrder.push('loadTill');
return Promise.resolve();
});
chart.setupDataSourceSubs();
expect(callOrder).toEqual(['setSymbols', 'loadTill']);
});
it('должен применить начальный интервал после загрузки данных', async () => {
const { chart, loadTill, setVisibleRange } = createChartInternals({
symbolId: 'SBER',
compareSymbolIds: [],
interval: Intervals['1Y'],
});
chart.setupDataSourceSubs();
expect(loadTill).toHaveBeenCalledTimes(1);
expect(loadTill).toHaveBeenCalledWith('SBER', expect.any(Number));
await flushPromises();
expect(setVisibleRange).toHaveBeenCalledTimes(1);
expect(setVisibleRange).toHaveBeenCalledWith({
from: expect.any(Number),
to: expect.any(Number),
});
});
it('не должен применять интервал при отсутствии данных', async () => {
const { chart, getOldestTime, setVisibleRange } = createChartInternals({
symbolId: 'SBER',
compareSymbolIds: [],
interval: Intervals['1Y'],
});
getOldestTime.mockReturnValue(null);
chart.setupDataSourceSubs();
await flushPromises();
expect(setVisibleRange).not.toHaveBeenCalled();
});
it('не должен применять результат устаревшего интервала', async () => {
const firstLoad = createDeferred();
const secondLoad = createDeferred();
const { chart, interval$, loadTill, loadAllHistory, setVisibleRange } =
createChartInternals({
symbolId: 'SBER',
compareSymbolIds: [],
interval: Intervals['1Y'],
});
loadTill.mockReturnValue(firstLoad.promise);
loadAllHistory.mockReturnValue(secondLoad.promise);
chart.setupDataSourceSubs();
expect(loadTill).toHaveBeenCalledTimes(1);
interval$.next(Intervals.All);
expect(loadAllHistory).toHaveBeenCalledTimes(1);
secondLoad.resolve();
await flushPromises();
expect(setVisibleRange).toHaveBeenCalledTimes(1);
expect(setVisibleRange).toHaveBeenCalledWith({
from: 100,
to: 200,
});
firstLoad.resolve();
await flushPromises();
expect(setVisibleRange).toHaveBeenCalledTimes(1);
});
it('не должен повторно регистрировать символы при смене только интервала', () => {
const { chart, interval$, setSymbols } = createChartInternals({
symbolId: 'SBER',
compareSymbolIds: [],
interval: Intervals['1Y'],
});
chart.setupDataSourceSubs();
expect(setSymbols).toHaveBeenCalledTimes(1);
interval$.next(Intervals.All);
expect(setSymbols).toHaveBeenCalledTimes(1);
expect(setSymbols).toHaveBeenCalledWith(['SBER']);
});
it('должен зарегистрировать новый основной символ и применить к нему активный интервал', async () => {
const { chart, symbolId$, setSymbols, loadTill } = createChartInternals({
symbolId: 'SBER',
compareSymbolIds: [],
interval: Intervals['1Y'],
});
chart.setupDataSourceSubs();
await flushPromises();
setSymbols.mockClear();
loadTill.mockClear();
symbolId$.next('GAZP');
expect(setSymbols).toHaveBeenCalledTimes(1);
expect(setSymbols).toHaveBeenCalledWith(['GAZP']);
expect(loadTill).toHaveBeenCalledTimes(1);
expect(loadTill).toHaveBeenCalledWith('GAZP', expect.any(Number));
});
it('должен загрузить всю историю для интервала All и применить общий диапазон', async () => {
const {
chart,
loadAllHistory,
getOldestTime,
getLastCandle,
setVisibleRange,
} = createChartInternals({
symbolId: 'SBER',
compareSymbolIds: ['GAZP'],
interval: Intervals.All,
});
getOldestTime.mockImplementation((symbolId: string) =>
symbolId === 'SBER' ? 100 : 50,
);
getLastCandle.mockImplementation((symbolId: string) => ({
time: symbolId === 'SBER' ? 200 : 300,
}));
chart.setupDataSourceSubs();
expect(loadAllHistory).toHaveBeenCalledTimes(2);
expect(loadAllHistory).toHaveBeenCalledWith('SBER');
expect(loadAllHistory).toHaveBeenCalledWith('GAZP');
await flushPromises();
expect(setVisibleRange).toHaveBeenCalledTimes(1);
expect(setVisibleRange).toHaveBeenCalledWith({
from: 50,
to: 300,
});
});
it('должен прогреть добавленный символ при отсутствии активного интервала', async () => {
const { chart, getVisibleRange, loadTill } = createChartInternals({
symbolId: 'SBER',
compareSymbolIds: [],
interval: null,
});
getVisibleRange.mockReturnValue({
from: 100,
to: 200,
});
chart.setupDataSourceSubs();
expect(loadTill).toHaveBeenCalledTimes(1);
expect(loadTill).toHaveBeenCalledWith('SBER', 100);
await flushPromises();
});
it('не должен применять интервал после отписки', async () => {
const loading = createDeferred();
const { chart, loadTill, setVisibleRange } = createChartInternals({
symbolId: 'SBER',
compareSymbolIds: [],
interval: Intervals['1Y'],
});
loadTill.mockReturnValue(loading.promise);
chart.setupDataSourceSubs();
expect(loadTill).toHaveBeenCalledTimes(1);
chart.subscriptions.unsubscribe();
loading.resolve();
await flushPromises();
expect(setVisibleRange).not.toHaveBeenCalled();
});
it('не должен повторно догружать историю, если старейшая свеча не изменилась', async () => {
const {
chart,
loadMoreHistory,
getOldestTime,
getVisibleLogicalRange,
} = createChartInternals({
symbolId: 'SBER',
compareSymbolIds: [],
});
chart.activeSymbolIds = ['SBER'];
getOldestTime.mockReturnValue(100);
chart.scheduleHistoryBatch();
runNextAnimationFrame();
await flushPromises();
expect(loadMoreHistory).toHaveBeenCalledTimes(1);
expect(getVisibleLogicalRange).not.toHaveBeenCalled();
expect(animationFrameCallbacks).toHaveLength(0);
});
it('должен продолжить догрузку истории, если старейшая свеча сдвинулась назад', async () => {
const {
chart,
loadMoreHistory,
getOldestTime,
getVisibleLogicalRange,
} = createChartInternals({
symbolId: 'SBER',
compareSymbolIds: [],
});
let oldestTime = 100;
chart.activeSymbolIds = ['SBER'];
getOldestTime.mockImplementation(() => oldestTime);
loadMoreHistory.mockImplementation(() => {
oldestTime = 90;
return Promise.resolve();
});
chart.scheduleHistoryBatch();
runNextAnimationFrame();
await flushPromises();
expect(loadMoreHistory).toHaveBeenCalledTimes(1);
expect(getVisibleLogicalRange).toHaveBeenCalledTimes(1);
expect(animationFrameCallbacks).toHaveLength(1);
});
it('должен считать появление первой старейшей свечи прогрессом', async () => {
const { chart, loadMoreHistory, getOldestTime } = createChartInternals({
symbolId: 'SBER',
compareSymbolIds: [],
});
let oldestTime: number | null = null;
chart.activeSymbolIds = ['SBER'];
getOldestTime.mockImplementation(() => oldestTime);
loadMoreHistory.mockImplementation(() => {
oldestTime = 100;
return Promise.resolve();
});
chart.scheduleHistoryBatch();
runNextAnimationFrame();
await flushPromises();
expect(animationFrameCallbacks).toHaveLength(1);
});
it('не должен останавливать новую цепочку загрузки при смене символа во время запроса', async () => {
const loading = createDeferred();
const { chart, loadMoreHistory, getOldestTime } = createChartInternals({
symbolId: 'SBER',
compareSymbolIds: [],
});
chart.activeSymbolIds = ['SBER'];
getOldestTime.mockImplementation((symbolId: string) =>
symbolId === 'SBER' ? 100 : 200,
);
loadMoreHistory.mockReturnValue(loading.promise);
chart.scheduleHistoryBatch();
runNextAnimationFrame();
chart.activeSymbolIds = ['GAZP'];
loading.resolve();
await flushPromises();
expect(animationFrameCallbacks).toHaveLength(1);
});
it('не должен останавливать новую цепочку загрузки при смене таймфрейма во время запроса', async () => {
const loading = createDeferred();
const { chart, getTimeframe, loadMoreHistory, getOldestTime } =
createChartInternals({
symbolId: 'SBER',
compareSymbolIds: [],
});
let timeframe = '1m';
chart.activeSymbolIds = ['SBER'];
getTimeframe.mockImplementation(() => timeframe);
getOldestTime.mockReturnValue(100);
loadMoreHistory.mockReturnValue(loading.promise);
chart.scheduleHistoryBatch();
runNextAnimationFrame();
timeframe = '5m';
loading.resolve();
await flushPromises();
expect(animationFrameCallbacks).toHaveLength(1);
});
it('должен запускать фоновую догрузку для интервала All', async () => {
const {
chart,
loadMoreHistory,
mouseEventsSubscribe,
setVisibleLogicalRange,
} = createChartInternals({
symbolId: 'SBER',
compareSymbolIds: [],
interval: Intervals.All,
});
chart.setupDataSourceSubs();
chart.setupHistoricalDataLoading();
const visibleLogicalRangeChangeHandler =
mouseEventsSubscribe.mock.calls[0][1] as (
logicalRange: { from: number; to: number } | null,
) => void;
visibleLogicalRangeChangeHandler({ from: 0, to: 1000 });
expect(setVisibleLogicalRange).toHaveBeenCalledWith({
from: 0,
to: 1000,
});
expect(animationFrameCallbacks).toHaveLength(1);
runNextAnimationFrame();
await flushPromises();
expect(loadMoreHistory).toHaveBeenCalledTimes(1);
expect(animationFrameCallbacks).toHaveLength(0);
});
it('не должен применять интервал к неактивному графику', async () => {
const { chart, loadTill, setVisibleRange } = createChartInternals({
symbolId: 'SBER',
compareSymbolIds: [],
interval: Intervals['1Y'],
});
chart.active = false;
chart.setupDataSourceSubs();
await flushPromises();
expect(loadTill).toHaveBeenCalledTimes(1);
expect(setVisibleRange).not.toHaveBeenCalled();
});
it('не должен планировать догрузку для неактивного графика', () => {
const { chart, loadMoreHistory } = createChartInternals();
chart.activeSymbolIds = ['SBER'];
chart.active = false;
chart.scheduleHistoryBatch();
expect(animationFrameCallbacks).toHaveLength(0);
expect(loadMoreHistory).not.toHaveBeenCalled();
});
it('не должен начинать запрос, если график выключен до выполнения кадра', () => {
const { chart, loadMoreHistory } = createChartInternals();
chart.activeSymbolIds = ['SBER'];
chart.scheduleHistoryBatch();
chart.active = false;
runNextAnimationFrame();
expect(loadMoreHistory).not.toHaveBeenCalled();
expect(chart.historyBatchRunning).toBe(false);
});
});
/** @jest-environment jsdom */
import {
CHART_DRAWING_TOOLBAR_CONTAINER,
CHART_MODAL_CONTAINER_CLASSNAME,
CHART_PANE_EMPTY_STATE_CONTAINER,
CHART_PANE_LEGEND_CONTAINER,
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_EMPTY_STATE_CONTAINER: 'chart-pane-empty-state-container',
CHART_PANE_LEGEND_CONTAINER: 'chart-pane-legend-container',
CHART_PANE_OVERLAY_CONTAINER: 'chart-pane-overlay-container',
CHART_ROOT_CLASSNAME: 'chart-root',
CHART_SCROLLABLE_CLASSNAME: 'chart-scrollable',
}));
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,
seasonalityContainer,
seasonalityControlsContainer,
seasonalityPlotContainer,
toolBarContainer,
modalContainer,
controlBarContainer,
drawingToolbarContainer,
} = ContainerManager.createContainers({
parentContainer,
showBottomPanel: true,
showMenuButton: true,
});
const chartViewContainer = chartContainer.parentElement;
expect(parentContainer.classList.contains(CHART_ROOT_CLASSNAME)).toBe(true);
expect(parentContainer.children[0]).toBe(headerContainer);
expect(chartViewContainer).not.toBeNull();
expect(parentContainer.children[1]).toBe(chartViewContainer);
expect(chartViewContainer?.children[0]).toBe(chartContainer);
expect(chartViewContainer?.children[1]).toBe(seasonalityContainer);
expect(parentContainer.children[2]).toBe(footerContainer);
expect(seasonalityContainer.children[0]).toBe(seasonalityControlsContainer);
expect(seasonalityContainer.children[1]).toBe(seasonalityPlotContainer);
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('должен создавать сезонность скрытой и неактивной', () => {
const { seasonalityContainer, chartAreaContainer } =
ContainerManager.createContainers({
parentContainer,
});
expect(seasonalityContainer.style.visibility).toBe('hidden');
expect(seasonalityContainer.style.pointerEvents).toBe('none');
expect(seasonalityContainer.inert).toBe(true);
expect(seasonalityContainer.tabIndex).toBe(-1);
expect(chartAreaContainer.tabIndex).toBe(-1);
expect(seasonalityContainer.style.outline).toBe('none');
expect(chartAreaContainer.style.outline).toBe('none');
});
it('должен размещать график и сезонность в одной ячейке сетки', () => {
const { chartContainer, seasonalityContainer } =
ContainerManager.createContainers({
parentContainer,
});
expect(chartContainer.parentElement).toBe(
seasonalityContainer.parentElement,
);
expect(chartContainer.parentElement?.style.display).toBe('grid');
expect(chartContainer.style.gridArea).toBe('1 / 1');
expect(seasonalityContainer.style.gridArea).toBe('1 / 1');
});
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,
});
expect(
document.querySelectorAll('link[href*="fonts.googleapis.com"]'),
).toHaveLength(1);
});
});
describe('createPaneContainers', () => {
it('должен создавать контейнеры pane с корректными настройками', () => {
const {
legendContainer,
paneOverlayContainer,
paneEmptyStateContainer,
} = ContainerManager.createPaneContainers();
expect(
legendContainer.classList.contains(CHART_PANE_LEGEND_CONTAINER),
).toBe(true);
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.inset).toBe('0');
expect(paneOverlayContainer.style.pointerEvents).toBe('none');
expect(
paneEmptyStateContainer.classList.contains(
CHART_PANE_EMPTY_STATE_CONTAINER,
),
).toBe(true);
expect(paneEmptyStateContainer.hidden).toBe(true);
});
it('должен создавать отдельные контейнеры для каждого pane', () => {
const first = ContainerManager.createPaneContainers();
const second = ContainerManager.createPaneContainers();
expect(first.legendContainer).not.toBe(second.legendContainer);
expect(first.paneOverlayContainer).not.toBe(second.paneOverlayContainer);
expect(first.paneEmptyStateContainer).not.toBe(
second.paneEmptyStateContainer,
);
});
});
describe('clearContainers', () => {
it('должен очищать содержимое контейнера', () => {
parentContainer.innerHTML = '<div>content</div>';
ContainerManager.clearContainers(parentContainer);
expect(parentContainer.innerHTML).toBe('');
});
});
});
/** @jest-environment jsdom */
import { EMPTY } from 'rxjs';
import { type IMoexChart, MoexChart } from '@core/MoexChart';
import type { ReactElement } from 'react';
interface HeaderTestProps {
copyImage?: () => Promise<void>;
setChartView?: (view: unknown) => void;
seasonalityStateObs: typeof EMPTY;
}
interface HeaderTestOptions {
showSnapshotButton?: boolean;
showSeasonalityButton?: boolean;
seasonalityActive?: boolean;
}
function createTestContext() {
const drawingsCollectionManager = {
selectedDrawing: jest.fn(),
updateSelectedDrawingSettings: jest.fn(),
toggleSelectedDrawingLock: jest.fn(),
openSelectedDrawingSettings: jest.fn(),
deleteSelectedDrawing: jest.fn(),
};
const mainChart = {
copyImage: jest.fn().mockResolvedValue(undefined),
getDrawingsCollectionManager: () => drawingsCollectionManager,
};
const seasonalityManager = {
back: jest.fn(),
getState: jest.fn(() => EMPTY),
isActive: jest.fn(() => false),
copyImage: jest.fn().mockResolvedValue(undefined),
};
const eventManager = {
clearSymbol: jest.fn(),
setSymbol: jest.fn(),
getTimeframeObs: jest.fn(),
setTimeframe: jest.fn(),
getSelectedSeries: jest.fn(),
setSeriesSelected: jest.fn(),
};
const headerRenderer = { renderComponent: jest.fn() };
const setChartView = jest.fn();
const chart = Object.assign(Object.create(MoexChart.prototype), {
chart: mainChart,
seasonalityManager,
eventManager,
headerRenderer,
drawingToolbarRenderer: { renderComponent: jest.fn() },
toolbarRenderer: undefined,
controlBarRenderer: undefined,
footerRenderer: undefined,
modalRenderer: {},
fullscreen: {},
setChartView,
}) as MoexChart;
return {
chart,
mainChart,
seasonalityManager,
eventManager,
headerRenderer,
setChartView,
};
}
function renderHeader({
showSnapshotButton,
showSeasonalityButton,
seasonalityActive = false,
}: HeaderTestOptions = {}) {
const context = createTestContext();
context.seasonalityManager.isActive.mockReturnValue(seasonalityActive);
const config = {
chartCollectionPreset: {
supportedTimeframes: [],
supportedChartSeriesTypes: [],
showSettingsButton: false,
showMenuButton: false,
showFullscreenButton: false,
undoRedoEnabled: false,
showCompareButton: false,
showSnapshotButton,
showSeasonalityButton,
theme: 'tr',
},
} as unknown as IMoexChart;
const internals = context.chart as unknown as {
renderAttachments: (
config: IMoexChart,
toggleToolbar: () => boolean,
) => void;
};
internals.renderAttachments(config, jest.fn(() => false));
const header = context.headerRenderer.renderComponent
.mock.calls[0][0] as ReactElement<HeaderTestProps>;
return { ...context, header };
}
describe('MoexChart', () => {
it('должен выйти из сезонности перед очисткой основного инструмента', () => {
const { chart, eventManager, seasonalityManager } = createTestContext();
chart.clearSymbol();
expect(seasonalityManager.back).toHaveBeenCalledTimes(1);
expect(eventManager.clearSymbol).toHaveBeenCalledTimes(1);
expect(
seasonalityManager.back.mock.invocationCallOrder[0],
).toBeLessThan(eventManager.clearSymbol.mock.invocationCallOrder[0]);
});
it('должен выйти из сезонности перед установкой основного инструмента', () => {
const { chart, eventManager, seasonalityManager } = createTestContext();
const symbol = {
symbolId: 'SBER',
symbol: 'SBER',
symbolName: 'Sberbank',
};
chart.setSymbol(symbol);
expect(seasonalityManager.back).toHaveBeenCalledTimes(1);
expect(eventManager.setSymbol).toHaveBeenCalledTimes(1);
expect(eventManager.setSymbol).toHaveBeenCalledWith(symbol);
expect(
seasonalityManager.back.mock.invocationCallOrder[0],
).toBeLessThan(eventManager.setSymbol.mock.invocationCallOrder[0]);
});
it('должен копировать основной график, если сезонность неактивна', async () => {
const { header, mainChart, seasonalityManager } = renderHeader({
showSnapshotButton: true,
});
expect(header.props.copyImage).toEqual(expect.any(Function));
await header.props.copyImage?.();
expect(mainChart.copyImage).toHaveBeenCalledTimes(1);
expect(seasonalityManager.copyImage).not.toHaveBeenCalled();
});
it('должен копировать сезонность, если она активна', async () => {
const { header, mainChart, seasonalityManager } = renderHeader({
showSnapshotButton: true,
seasonalityActive: true,
});
expect(header.props.copyImage).toEqual(expect.any(Function));
await header.props.copyImage?.();
expect(seasonalityManager.copyImage).toHaveBeenCalledTimes(1);
expect(mainChart.copyImage).not.toHaveBeenCalled();
});
it.each([undefined, false])(
'не должен передавать copyImage при showSnapshotButton=%s',
(showSnapshotButton) => {
const { header, mainChart, seasonalityManager } = renderHeader({
showSnapshotButton,
});
expect(header.props.copyImage).toBeUndefined();
expect(mainChart.copyImage).not.toHaveBeenCalled();
expect(seasonalityManager.copyImage).not.toHaveBeenCalled();
},
);
it('должен передавать переключение вида при включённой сезонности', () => {
const { header, setChartView } = renderHeader({
showSeasonalityButton: true,
});
expect(header.props.setChartView).toBe(setChartView);
});
it.each([undefined, false])(
'не должен передавать переключение вида при showSeasonalityButton=%s',
(showSeasonalityButton) => {
const { header } = renderHeader({ showSeasonalityButton });
expect(header.props.setChartView).toBeUndefined();
},
);
it('должен передавать состояние сезонности в Header', () => {
const { header, seasonalityManager } = renderHeader();
expect(seasonalityManager.getState).toHaveBeenCalledTimes(1);
expect(header.props.seasonalityStateObs).toBe(EMPTY);
});
});