Загрузка данных
import { act, render } from '@testing-library/react';
import { MoexChart, Timeframes } from 'moex-chart';
import React from 'react';
import {
useChangeProperties,
useSelectProperties,
} from '@modules/widgetProperties';
import { useMoexChart } from '../components/MoexChart/hooks';
import type { ChartIndicativeData } from '@widgets/Chart/types';
jest.mock('@modules/widgetProperties');
jest.mock('../components/MoexChart/dataSourceProvide', () => ({
DataSourceProvider: jest.fn(),
}));
jest.mock('moex-chart', () => ({
MoexChart: jest.fn(),
Timeframes: {
'10s': '10s',
'1m': '1m',
'5m': '5m',
},
DateFormat: {
DD_MM_YYYY_HH_mm_ss: 'DD_MM_YYYY_HH_mm_ss',
},
IndicatorsIds: {
Volume: 'Volume',
},
Locale: {
rus: 'ru-RU',
eng: 'en-US',
},
}));
jest.mock('@api/index', () => ({
updateMenuLocked: jest.fn(),
widgetsController: {
delete: jest.fn(),
},
widgetPropertiesController: {
update: jest.fn(),
},
workspaceController: {
update: jest.fn(),
},
}));
jest.mock('@api/controllers/workspace', () => ({
workspaceController: {
update: jest.fn(),
},
}));
const mockedDataSourceProvide = jest.requireMock(
'../components/MoexChart/dataSourceProvide',
) as {
DataSourceProvider: jest.Mock;
};
type TimeframeValue =
(typeof Timeframes)[keyof typeof Timeframes];
interface MockCompareSeries {
name: string;
seriesOptions?: {
priceScaleId?: string;
color?: string;
};
}
interface MockIndicatorConfig {
symbol?: string;
label?: string;
newPane?: boolean;
series: MockCompareSeries[];
}
interface MockIndicatorSnapshot {
id?: string;
indicatorType?: string;
dataSource?: unknown;
config?: MockIndicatorConfig;
}
interface MockPaneSnapshot {
indicators: MockIndicatorSnapshot[];
}
interface MockChartSnapshot {
charts: {
symbol: string;
symbolName?: string;
timeframe: TimeframeValue;
chartSeriesType: string;
panes: MockPaneSnapshot[];
}[];
}
interface MockMoexChartConfig {
container: HTMLElement;
snapshot: MockChartSnapshot;
chartCollectionPreset: {
openCompareModal: () => void;
openSymbolSearchModal: () => void;
getDataSource: jest.Mock;
startRealtime: (
getSymbols: () => string[],
getTimeframe: () => TimeframeValue,
update: jest.Mock,
) => (() => void) | undefined;
};
}
interface MockMoexChartState {
timeframe: TimeframeValue;
initialInterval?: string;
savedData?: string;
}
interface MockPropertiesState {
moexChartState?: MockMoexChartState;
}
type UpdatePropertiesCallback = (
state: MockPropertiesState,
) => void;
type TimeframeChangeCallback = (
timeframe: TimeframeValue,
) => void;
interface TestComponentProps {
symbol?: string;
symbolName?: string;
indicativeData?: ChartIndicativeData;
}
describe('useMoexChart', () => {
const mockUseSelectProperties =
useSelectProperties as jest.Mock;
const mockUseChangeProperties =
useChangeProperties as jest.Mock;
const mockMoexChart = MoexChart as jest.Mock;
const mockDataSourceProvider =
mockedDataSourceProvide.DataSourceProvider;
const mockUpdateProperties = jest.fn();
const mockDestroy = jest.fn();
const mockGetSnapshot = jest.fn();
const mockSetSnapshot = jest.fn();
const mockSetSymbol = jest.fn();
const mockGetCompareManager = jest.fn();
const mockSetSettings = jest.fn();
const mockDataSource = jest.fn();
const mockStartRealtime = jest.fn();
const mockGetDataSource = jest.fn();
const mockRealtimeUnsubscribe = jest.fn();
const mockCompareManager = {
id: 'compare-manager',
};
const mockSnapshot: MockChartSnapshot = {
charts: [
{
symbol: 'OLD:SYMBOL',
symbolName: 'Old instrument',
timeframe: Timeframes['5m'],
chartSeriesType: 'Candlestick',
panes: [
{
indicators: [],
},
],
},
],
};
let hookResult: ReturnType<typeof useMoexChart> | null =
null;
let lastMoexChartConfig: MockMoexChartConfig | null =
null;
let mockMoexChartState:
| MockMoexChartState
| undefined;
const TestComponent = ({
symbol = 'MOEX:SBER',
symbolName = 'Сбербанк',
indicativeData,
}: TestComponentProps): React.ReactElement => {
hookResult = useMoexChart({
symbol,
symbolName,
indicativeData,
});
return <div ref={hookResult.containerRef} />;
};
beforeEach(() => {
jest.clearAllMocks();
jest.useFakeTimers();
hookResult = null;
lastMoexChartConfig = null;
mockMoexChartState = {
timeframe: Timeframes['1m'],
savedData: undefined,
};
mockUseSelectProperties.mockImplementation(
(
selector: (
state: MockPropertiesState,
) => unknown,
) =>
selector({
moexChartState: mockMoexChartState,
}),
);
mockUseChangeProperties.mockReturnValue({
updateProperties: mockUpdateProperties,
});
mockGetDataSource.mockImplementation(
(
_indicativeData?: ChartIndicativeData,
callback?: (
timeframe: TimeframeValue,
) => void,
) =>
(timeframe: TimeframeValue) => {
callback?.(timeframe);
return mockDataSource();
},
);
mockStartRealtime.mockReturnValue(
mockRealtimeUnsubscribe,
);
mockDataSourceProvider.mockImplementation(() => ({
getDataSource: mockGetDataSource,
startRealtime: mockStartRealtime,
}));
mockGetSnapshot.mockReturnValue(mockSnapshot);
mockGetCompareManager.mockReturnValue(
mockCompareManager,
);
mockMoexChart.mockImplementation(
(config: MockMoexChartConfig) => {
lastMoexChartConfig = config;
return {
destroy: mockDestroy,
getSnapshot: mockGetSnapshot,
setSnapshot: mockSetSnapshot,
setSymbol: mockSetSymbol,
setSettings: mockSetSettings,
getCompareManager: mockGetCompareManager,
};
},
);
});
afterEach(() => {
jest.clearAllTimers();
jest.useRealTimers();
});
it('should create moex chart with current symbol, symbol name and timeframe', () => {
// Arrange & Act
render(
<TestComponent
symbol="MOEX:SBER"
symbolName="Сбербанк"
/>,
);
// Assert
expect(mockMoexChart).toHaveBeenCalledTimes(1);
expect(mockDataSourceProvider).toHaveBeenCalledTimes(1);
expect(
lastMoexChartConfig?.container,
).toBeInstanceOf(HTMLDivElement);
expect(
lastMoexChartConfig?.snapshot.charts[0]?.symbol,
).toBe('MOEX:SBER');
expect(
lastMoexChartConfig?.snapshot.charts[0]
?.symbolName,
).toBe('Сбербанк');
expect(
lastMoexChartConfig?.snapshot.charts[0]
?.timeframe,
).toBe(Timeframes['1m']);
expect(
hookResult?.compareManagerRef.current,
).toBe(mockCompareManager);
expect(hookResult?.hasSavedSnapshot).toBe(false);
});
it('should create chart with saved snapshot and current instrument data', () => {
// Arrange
mockMoexChartState = {
timeframe: Timeframes['5m'],
savedData: JSON.stringify(mockSnapshot),
};
// Act
render(
<TestComponent
symbol="MOEX:GAZP"
symbolName="Газпром"
/>,
);
// Assert
expect(
lastMoexChartConfig?.snapshot.charts[0]?.symbol,
).toBe('MOEX:GAZP');
expect(
lastMoexChartConfig?.snapshot.charts[0]
?.symbolName,
).toBe('Газпром');
expect(
lastMoexChartConfig?.snapshot.charts[0]
?.timeframe,
).toBe(Timeframes['5m']);
expect(hookResult?.hasSavedSnapshot).toBe(true);
});
it('should save chart snapshot to widget properties', () => {
// Arrange
render(
<TestComponent
symbol="MOEX:SBER"
symbolName="Сбербанк"
/>,
);
// Act
act(() => {
hookResult?.saveSnapshot();
});
// Assert
expect(mockGetSnapshot).toHaveBeenCalledTimes(1);
expect(mockUpdateProperties).toHaveBeenCalledTimes(
1,
);
const updateCallback =
mockUpdateProperties.mock.calls[0]?.[0] as UpdatePropertiesCallback;
const mockState: MockPropertiesState = {
moexChartState: {
timeframe: Timeframes['1m'],
},
};
updateCallback(mockState);
expect(mockState.moexChartState).toEqual({
timeframe: Timeframes['1m'],
savedData: JSON.stringify(mockSnapshot),
});
});
it('should serialize compare symbol and display name in snapshot', () => {
// Arrange
const snapshotWithIndicators: MockChartSnapshot = {
charts: [
{
symbol: 'MOEX:SBER',
symbolName: 'Сбербанк',
timeframe: Timeframes['1m'],
chartSeriesType: 'Candlestick',
panes: [
{
indicators: [
{
id: 'compare-gazp',
indicatorType: undefined,
dataSource: {
subscription: {},
},
config: {
symbol: 'MOEX:GAZP',
label: 'Газпром',
newPane: false,
series: [
{
name: 'Line',
seriesOptions: {
priceScaleId: 'left',
color: '#FFFFFF',
},
},
],
},
},
{
id: 'rsi',
indicatorType: 'RSI',
dataSource: {
subscription: {},
},
config: {
label: 'RSI',
newPane: true,
series: [
{
name: 'Line',
seriesOptions: {
priceScaleId: 'right',
},
},
],
},
},
],
},
],
},
],
};
mockGetSnapshot.mockReturnValue(
snapshotWithIndicators,
);
render(
<TestComponent
symbol="MOEX:SBER"
symbolName="Сбербанк"
/>,
);
// Act
act(() => {
hookResult?.saveSnapshot();
});
const updateCallback =
mockUpdateProperties.mock.calls[0]?.[0] as UpdatePropertiesCallback;
const mockState: MockPropertiesState = {
moexChartState: {
timeframe: Timeframes['1m'],
},
};
updateCallback(mockState);
const savedSnapshot = JSON.parse(
mockState.moexChartState?.savedData ?? '{}',
) as MockChartSnapshot;
const [compareIndicator, regularIndicator] =
savedSnapshot.charts[0]?.panes[0]
?.indicators ?? [];
// Assert
expect(compareIndicator).toEqual({
id: 'compare-gazp',
config: {
symbol: 'MOEX:GAZP',
label: 'Газпром',
newPane: false,
series: [
{
name: 'Line',
seriesOptions: {
priceScaleId: 'left',
},
},
],
},
});
expect(regularIndicator).toEqual({
id: 'rsi',
indicatorType: 'RSI',
});
});
it('should not save snapshot when chart does not return snapshot', () => {
// Arrange
mockGetSnapshot.mockReturnValue(undefined);
render(<TestComponent />);
// Act
act(() => {
hookResult?.saveSnapshot();
});
// Assert
expect(mockGetSnapshot).toHaveBeenCalledTimes(1);
expect(
mockUpdateProperties,
).not.toHaveBeenCalled();
});
it('should apply saved snapshot with current symbol, symbol name and timeframe', () => {
// Arrange
mockMoexChartState = {
timeframe: Timeframes['5m'],
savedData: JSON.stringify(mockSnapshot),
};
render(
<TestComponent
symbol="MOEX:SBER"
symbolName="Сбербанк"
/>,
);
// Act
act(() => {
hookResult?.applySnapshot();
});
// Assert
expect(mockSetSnapshot).toHaveBeenCalledWith({
...mockSnapshot,
charts: [
{
...mockSnapshot.charts[0],
symbol: 'MOEX:SBER',
symbolName: 'Сбербанк',
timeframe: Timeframes['5m'],
},
],
});
expect(
hookResult?.compareManagerRef.current,
).toBe(mockCompareManager);
});
it('should update compare modal state', () => {
// Arrange
render(<TestComponent />);
// Act
act(() => {
hookResult?.setIsCompareOpen(true);
});
// Assert
expect(hookResult?.isCompareOpen).toBe(true);
});
it('should open compare modal from chart preset callback', () => {
// Arrange
render(<TestComponent />);
// Act
act(() => {
lastMoexChartConfig?.chartCollectionPreset.openCompareModal();
});
// Assert
expect(hookResult?.isCompareOpen).toBe(true);
});
it('should update symbol search modal state', () => {
// Arrange
render(<TestComponent />);
// Act
act(() => {
hookResult?.setIsSymbolSearchOpen(true);
});
// Assert
expect(hookResult?.isSymbolSearchOpen).toBe(true);
});
it('should open symbol search modal from chart preset callback', () => {
// Arrange
render(<TestComponent />);
// Act
act(() => {
lastMoexChartConfig?.chartCollectionPreset.openSymbolSearchModal();
});
// Assert
expect(hookResult?.isSymbolSearchOpen).toBe(true);
});
it('should update chart when external instrument changes', () => {
// Arrange
const { rerender } = render(
<TestComponent
symbol="MOEX:SBER"
symbolName="Сбербанк"
/>,
);
// Act
rerender(
<TestComponent
symbol="MOEX:GAZP"
symbolName="Газпром"
/>,
);
// Assert
expect(mockSetSymbol).toHaveBeenCalledTimes(1);
expect(mockSetSymbol).toHaveBeenCalledWith(
'MOEX:GAZP',
'Газпром',
);
});
it('should update chart when only external symbol name changes', () => {
// Arrange
const { rerender } = render(
<TestComponent
symbol="MOEX:SBER"
symbolName="Сбербанк"
/>,
);
// Act
rerender(
<TestComponent
symbol="MOEX:SBER"
symbolName="Сбербанк ПАО"
/>,
);
// Assert
expect(mockSetSymbol).toHaveBeenCalledTimes(1);
expect(mockSetSymbol).toHaveBeenCalledWith(
'MOEX:SBER',
'Сбербанк ПАО',
);
});
it('should use symbol as display name when symbol name is empty', () => {
// Arrange
const { rerender } = render(
<TestComponent
symbol="MOEX:SBER"
symbolName="Сбербанк"
/>,
);
// Act
rerender(
<TestComponent
symbol="MOEX:SBER"
symbolName=""
/>,
);
// Assert
expect(mockSetSymbol).toHaveBeenCalledTimes(1);
expect(mockSetSymbol).toHaveBeenCalledWith(
'MOEX:SBER',
'MOEX:SBER',
);
});
it('should not update chart when symbol and symbol name are unchanged', () => {
// Arrange
const { rerender } = render(
<TestComponent
symbol="MOEX:SBER"
symbolName="Сбербанк"
/>,
);
// Act
rerender(
<TestComponent
symbol="MOEX:SBER"
symbolName="Сбербанк"
/>,
);
// Assert
expect(mockSetSymbol).not.toHaveBeenCalled();
});
it('should not update chart when external symbol is empty', () => {
// Arrange
const { rerender } = render(
<TestComponent
symbol="MOEX:SBER"
symbolName="Сбербанк"
/>,
);
// Act
rerender(
<TestComponent
symbol=""
symbolName="Пустой инструмент"
/>,
);
// Assert
expect(mockSetSymbol).not.toHaveBeenCalled();
});
it('should not recreate chart when external instrument changes', () => {
// Arrange
const { rerender } = render(
<TestComponent
symbol="MOEX:SBER"
symbolName="Сбербанк"
/>,
);
// Act
rerender(
<TestComponent
symbol="MOEX:GAZP"
symbolName="Газпром"
/>,
);
// Assert
expect(mockMoexChart).toHaveBeenCalledTimes(1);
});
it('should apply saved snapshot with externally selected instrument', () => {
// Arrange
mockMoexChartState = {
timeframe: Timeframes['5m'],
savedData: JSON.stringify(mockSnapshot),
};
const { rerender } = render(
<TestComponent
symbol="MOEX:SBER"
symbolName="Сбербанк"
/>,
);
rerender(
<TestComponent
symbol="MOEX:GAZP"
symbolName="Газпром"
/>,
);
// Act
act(() => {
hookResult?.applySnapshot();
});
// Assert
expect(mockSetSnapshot).toHaveBeenCalledWith({
...mockSnapshot,
charts: [
{
...mockSnapshot.charts[0],
symbol: 'MOEX:GAZP',
symbolName: 'Газпром',
timeframe: Timeframes['5m'],
},
],
});
});
it('should initialize indicative instrument with id and display name', () => {
// Arrange
const indicativeData: ChartIndicativeData = {
id: 1,
title: 'Indicative instrument',
secId: 'INAV',
instrumentName: 'Индикатив',
settlement: 'Расчётный',
firmName: 'Тестовая фирма',
key: '2xOFZ:INAV',
};
// Act
render(
<TestComponent
symbol="2xOFZ:INAV"
symbolName="Индикатив Расчётный"
indicativeData={indicativeData}
/>,
);
// Assert
expect(mockGetDataSource).toHaveBeenCalledWith(
indicativeData,
expect.any(Function),
);
expect(mockSetSymbol).toHaveBeenCalledWith(
'2xOFZ:INAV',
'Индикатив Расчётный',
);
});
it('should pass realtime params to data source provider', () => {
// Arrange
render(<TestComponent />);
const getSymbols = jest.fn(() => [
'MOEX:SBER',
]);
const getTimeframe = jest.fn(
() => Timeframes['1m'],
);
const update = jest.fn();
// Act
const unsubscribe =
lastMoexChartConfig?.chartCollectionPreset.startRealtime(
getSymbols,
getTimeframe,
update,
);
// Assert
expect(mockStartRealtime).toHaveBeenCalledWith({
getSymbols,
getTimeframe,
update,
});
expect(unsubscribe).toBe(
mockRealtimeUnsubscribe,
);
});
it('should update timeframe from data source callback', () => {
// Arrange
render(<TestComponent />);
const timeframeChangeCallback =
mockGetDataSource.mock.calls[0]?.[1] as TimeframeChangeCallback;
// Act
act(() => {
timeframeChangeCallback(Timeframes['5m']);
});
// Assert
expect(mockUpdateProperties).toHaveBeenCalledTimes(
1,
);
const updateCallback =
mockUpdateProperties.mock.calls[0]?.[0] as UpdatePropertiesCallback;
const mockState: MockPropertiesState = {
moexChartState: {
timeframe: Timeframes['1m'],
},
};
updateCallback(mockState);
expect(
mockState.moexChartState?.timeframe,
).toBe(Timeframes['5m']);
});
it('should not update timeframe when it is the same as current timeframe', () => {
// Arrange
render(<TestComponent />);
const timeframeChangeCallback =
mockGetDataSource.mock.calls[0]?.[1] as TimeframeChangeCallback;
// Act
act(() => {
timeframeChangeCallback(Timeframes['1m']);
});
// Assert
expect(
mockUpdateProperties,
).not.toHaveBeenCalled();
});
it('should destroy chart on unmount', () => {
// Arrange
const { unmount } = render(<TestComponent />);
// Act
unmount();
// Assert
expect(mockDestroy).toHaveBeenCalledTimes(1);
});
});