Загрузка данных
import { act, render } from '@testing-library/react';
import { ChartIndicativeData } from '@widgets/Chart/types';
import { MoexChart, Timeframes } from 'moex-chart';
import React from 'react';
import { useChangeProperties, useSelectProperties } from '@modules/widgetProperties';
import { useMoexChart } from '../components/MoexChart/hooks';
// Mock the dependencies
jest.mock('@modules/widgetProperties');
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',
},
}));
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(),
},
}));
type TimeframeValue = (typeof Timeframes)[keyof typeof Timeframes];
interface MockPaneSnapshot {
indicators: unknown[];
}
interface MockChartSnapshot {
charts: {
symbol: string;
timeframe: TimeframeValue;
chartSeriesType: string;
panes: MockPaneSnapshot[];
}[];
}
interface MockMoexChartConfig {
container: HTMLElement;
snapshot: MockChartSnapshot;
chartCollectionPreset: {
openCompareModal: () => void;
getDataSource: jest.Mock;
startRealtime: (
getSymbols: () => string[],
getTimeframe: () => TimeframeValue,
update: jest.Mock,
) => (() => void) | undefined;
};
}
interface MockMoexChartState {
tf?: TimeframeValue;
savedData?: string;
}
interface MockPropertiesState {
moexChartState?: MockMoexChartState;
}
type UpdatePropertiesCallback = (state: MockPropertiesState) => void;
type TimeframeChangeCallback = (timeframe: TimeframeValue) => void;
interface TestComponentProps {
symbol?: string;
}
describe('useMoexChart', () => {
const mockUseSelectProperties = useSelectProperties as jest.Mock;
const mockUseChangeProperties = useChangeProperties as jest.Mock;
const mockMoexChart = MoexChart as jest.Mock;
const mockUpdateProperties = jest.fn();
const mockDestroy = jest.fn();
const mockGetSnapshot = jest.fn();
const mockSetSnapshot = jest.fn();
const mockGetCompareManager = 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',
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' }: TestComponentProps): React.ReactElement => {
hookResult = useMoexChart({
symbol,
indicativeData: undefined,
isMoexChartShow: true,
});
return <div ref={hookResult.containerRef} />;
};
beforeEach(() => {
jest.clearAllMocks();
hookResult = null;
lastMoexChartConfig = null;
mockMoexChartState = {
tf: Timeframes['1m'],
savedData: undefined,
};
mockUseSelectProperties.mockImplementation((selector: (state: MockPropertiesState) => unknown) =>
selector({
moexChartState: mockMoexChartState,
}),
);
mockUseChangeProperties.mockReturnValue({
updateProperties: mockUpdateProperties,
});
mockGetDataSource.mockReturnValue(mockDataSource);
mockGetDataSource.mockImplementation(
(indicativeData?: ChartIndicativeData | undefined, cb?: (tf: Timeframes) => void) => (timeframe: Timeframes) => {
cb?.(timeframe);
},
);
mockStartRealtime.mockReturnValue(mockRealtimeUnsubscribe);
mockGetSnapshot.mockReturnValue(mockSnapshot);
mockGetCompareManager.mockReturnValue(mockCompareManager);
mockMoexChart.mockImplementation((config: MockMoexChartConfig) => {
lastMoexChartConfig = config;
return {
destroy: mockDestroy,
getSnapshot: mockGetSnapshot,
setSnapshot: mockSetSnapshot,
getCompareManager: mockGetCompareManager,
};
});
});
it('should create moex chart with current symbol and timeframe', () => {
// Arrange & Act
render(<TestComponent symbol="MOEX:SBER" />);
// Assert
expect(mockMoexChart).toHaveBeenCalledTimes(1);
expect(lastMoexChartConfig?.container).toBeInstanceOf(HTMLDivElement);
expect(lastMoexChartConfig?.snapshot.charts[0]?.symbol).toBe('MOEX:SBER');
expect(lastMoexChartConfig?.snapshot.charts[0]?.timeframe).toBe(Timeframes['1m']);
expect(hookResult?.compareManagerRef.current).toBe(mockCompareManager);
});
it('should create chart with saved snapshot when saved data exists', () => {
// Arrange
mockMoexChartState = {
tf: Timeframes['5m'],
savedData: JSON.stringify(mockSnapshot),
};
mockUseSelectProperties.mockImplementation((selector: (state: MockPropertiesState) => unknown) =>
selector({
moexChartState: mockMoexChartState,
}),
);
// Act
render(<TestComponent symbol="MOEX:GAZP" />);
// Assert
expect(lastMoexChartConfig?.snapshot.charts[0]?.symbol).toBe('MOEX:GAZP');
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" />);
// Act
act(() => {
hookResult?.saveSnapshot();
});
// Assert
expect(mockGetSnapshot).toHaveBeenCalled();
expect(mockUpdateProperties).toHaveBeenCalled();
const updateCallback = mockUpdateProperties.mock.calls[0]?.[0] as UpdatePropertiesCallback;
const mockState: MockPropertiesState = {
moexChartState: {
tf: Timeframes['1m'],
},
};
updateCallback(mockState);
expect(mockState.moexChartState).toEqual({
tf: Timeframes['1m'],
savedData: JSON.stringify(mockSnapshot),
});
});
it('should not save snapshot when chart does not return snapshot', () => {
// Arrange
mockGetSnapshot.mockReturnValue(undefined);
render(<TestComponent symbol="MOEX:SBER" />);
// Act
act(() => {
hookResult?.saveSnapshot();
});
// Assert
expect(mockUpdateProperties).not.toHaveBeenCalled();
});
it('should apply saved snapshot with current symbol and timeframe', () => {
// Arrange
mockMoexChartState = {
tf: Timeframes['5m'],
savedData: JSON.stringify(mockSnapshot),
};
mockUseSelectProperties.mockImplementation((selector: (state: MockPropertiesState) => unknown) =>
selector({
moexChartState: mockMoexChartState,
}),
);
render(<TestComponent symbol="MOEX:SBER" />);
// Act
act(() => {
hookResult?.applySnapshot();
});
// Assert
expect(mockSetSnapshot).toHaveBeenCalledWith({
...mockSnapshot,
charts: [
{
...mockSnapshot.charts[0],
symbol: 'MOEX:SBER',
timeframe: Timeframes['5m'],
},
],
});
expect(hookResult?.compareManagerRef.current).toBe(mockCompareManager);
});
it('should update compare modal state', () => {
// Arrange
render(<TestComponent symbol="MOEX:SBER" />);
// Act
act(() => {
hookResult?.setIsCompareOpen(true);
});
// Assert
expect(hookResult?.isCompareOpen).toBe(true);
});
it('should open compare modal from chart preset callback', () => {
// Arrange
render(<TestComponent symbol="MOEX:SBER" />);
// Act
act(() => {
lastMoexChartConfig?.chartCollectionPreset.openCompareModal();
});
// Assert
expect(hookResult?.isCompareOpen).toBe(true);
});
// it('should pass realtime params to data source provider', () => {
// // Arrange
// render(<TestComponent symbol="MOEX:SBER" />);
//
// 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 symbol="MOEX:SBER" />);
//
// const timeframeChangeCallback = mockGetDataSource.mock.calls[0]?.[1] as TimeframeChangeCallback;
//
// // Act
// act(() => {
// timeframeChangeCallback(Timeframes['5m']);
// });
//
// // Assert
// expect(mockUpdateProperties).toHaveBeenCalled();
//
// const updateCallback = mockUpdateProperties.mock.calls[0]?.[0] as UpdatePropertiesCallback;
//
// const mockState: MockPropertiesState = {
// moexChartState: {
// tf: Timeframes['1m'],
// },
// };
//
// updateCallback(mockState);
//
// expect(mockState.moexChartState?.tf).toBe(Timeframes['5m']);
// });
// it('should not update timeframe when it is the same as current timeframe', () => {
// // Arrange
// render(<TestComponent symbol="MOEX:SBER" />);
//
// 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 symbol="MOEX:SBER" />);
// Act
unmount();
// Assert
expect(mockDestroy).toHaveBeenCalled();
});
});
import { renderHook } from '@testing-library/react';
import { useAppSelect } from '@hooks/useAppSelector';
import { useContracts } from '@modules/contracts';
import { filterByUniqIssKey } from '@utils/filterByUniqIssKey';
import { DEFAULT_SYMBOL } from '@widgets/Chart/const';
import { useChartPublicContext } from '../hooks/useChartPublicContext';
// Mock the dependencies
jest.mock('@hooks/useAppSelector');
jest.mock('@modules/contracts');
jest.mock('@utils/filterByUniqIssKey');
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(),
},
}));
describe('useChartPublicContext', () => {
const mockUseAppSelect = useAppSelect as jest.Mock;
const mockUseContracts = useContracts as jest.Mock;
const mockFilterByUniqIssKey = filterByUniqIssKey as jest.Mock;
const mockContracts = [
{
issKey: 'MOEX:TEST1',
instrName: 'Test Instrument 1',
symbol: 'TEST1',
// ... other contract properties
},
{
issKey: 'MOEX:TEST2',
instrName: 'Test Instrument 2',
symbol: 'TEST2',
// ... other contract properties
},
{
issKey: 'MOEX:TEST3',
instrName: 'Test Instrument 3',
symbol: 'TEST3',
// ... other contract properties
},
];
const mockWidget = {
id: 1,
name: 'Test Widget',
type: 'graphic',
master: 123,
externalProperties: [{ key: 'instrument', value: 'MOEX:TEST1' }],
// ... other widget properties
};
const mockPublicContext = {
instrument: 'MOEX:TEST1',
};
beforeEach(() => {
jest.clearAllMocks();
// Mock the useAppSelect to return our test data
mockUseAppSelect.mockImplementation((selector) => {
if (selector.toString().includes('publicContext')) {
return mockPublicContext;
}
if (selector.toString().includes('widgets')) {
return mockWidget;
}
return {};
});
// Mock useContracts to return our test contracts
mockUseContracts.mockReturnValue({
contracts: mockContracts,
});
// Mock filterByUniqIssKey to return the same contracts
mockFilterByUniqIssKey.mockImplementation((contracts, keys) => contracts);
});
it('should return the correct issKey when a matching instrument is found', () => {
// Arrange
const mockSetCurrInstrument = jest.fn();
const mockGetMasterInstrumentFromPublicContext = jest.fn().mockReturnValue('MOEX:TEST1');
// Act
const { result } = renderHook(() =>
useChartPublicContext({
widgetId: 1,
setCurrInstrument: mockSetCurrInstrument,
getMasterInstrumentFromPublicContext: mockGetMasterInstrumentFromPublicContext,
}),
);
// Assert
expect(result.current.issKey).toBe('MOEX:TEST1');
});
it('should return undefined when no matching instrument is found', () => {
// Arrange
const mockSetCurrInstrument = jest.fn();
const mockGetMasterInstrumentFromPublicContext = jest.fn().mockReturnValue('MOEX:NONEXISTENT');
// Act
const { result } = renderHook(() =>
useChartPublicContext({
widgetId: 1,
setCurrInstrument: mockSetCurrInstrument,
getMasterInstrumentFromPublicContext: mockGetMasterInstrumentFromPublicContext,
}),
);
// Assert
expect(result.current.issKey).toBeUndefined();
});
it('should return undefined when getMasterInstrumentFromPublicContext returns null', () => {
// Arrange
const mockSetCurrInstrument = jest.fn();
const mockGetMasterInstrumentFromPublicContext = jest.fn().mockReturnValue(null);
// Act
const { result } = renderHook(() =>
useChartPublicContext({
widgetId: 1,
setCurrInstrument: mockSetCurrInstrument,
getMasterInstrumentFromPublicContext: mockGetMasterInstrumentFromPublicContext,
}),
);
// Assert
expect(result.current.issKey).toBeUndefined();
});
it('should return undefined when getMasterInstrumentFromPublicContext returns undefined', () => {
// Arrange
const mockSetCurrInstrument = jest.fn();
const mockGetMasterInstrumentFromPublicContext = jest.fn().mockReturnValue(undefined);
// Act
const { result } = renderHook(() =>
useChartPublicContext({
widgetId: 1,
setCurrInstrument: mockSetCurrInstrument,
getMasterInstrumentFromPublicContext: mockGetMasterInstrumentFromPublicContext,
}),
);
// Assert
expect(result.current.issKey).toBeUndefined();
});
it('should set current instrument to DEFAULT_SYMBOL when no field value and widget has master', () => {
// Arrange
const mockSetCurrInstrument = jest.fn();
const mockGetMasterInstrumentFromPublicContext = jest.fn().mockReturnValue(null);
// Act
renderHook(() =>
useChartPublicContext({
widgetId: 1,
setCurrInstrument: mockSetCurrInstrument,
getMasterInstrumentFromPublicContext: mockGetMasterInstrumentFromPublicContext,
}),
);
// Assert
expect(mockSetCurrInstrument).toHaveBeenCalledWith(DEFAULT_SYMBOL);
});
it('should set current instrument to the found issKey when a matching instrument exists', () => {
// Arrange
const mockSetCurrInstrument = jest.fn();
const mockGetMasterInstrumentFromPublicContext = jest.fn().mockReturnValue('MOEX:TEST2');
// Act
renderHook(() =>
useChartPublicContext({
widgetId: 1,
setCurrInstrument: mockSetCurrInstrument,
getMasterInstrumentFromPublicContext: mockGetMasterInstrumentFromPublicContext,
}),
);
// Assert
expect(mockSetCurrInstrument).toHaveBeenCalledWith('MOEX:TEST2');
});
it('should handle case when widget is not found in widgets array', () => {
// Arrange
const mockSetCurrInstrument = jest.fn();
const mockGetMasterInstrumentFromPublicContext = jest.fn().mockReturnValue('MOEX:TEST1');
// Mock useAppSelect to return null for widget (widget not found)
mockUseAppSelect.mockImplementation((selector) => {
if (selector.toString().includes('publicContext')) {
return mockPublicContext;
}
if (selector.toString().includes('widgets')) {
return null; // Widget not found
}
return {};
});
// Act
const { result } = renderHook(() =>
useChartPublicContext({
widgetId: 999, // Non-existent widget ID
setCurrInstrument: mockSetCurrInstrument,
getMasterInstrumentFromPublicContext: mockGetMasterInstrumentFromPublicContext,
}),
);
// Assert
expect(result.current.issKey).toBe('MOEX:TEST1');
});
it('should handle case when contracts array is empty', () => {
// Arrange
const mockSetCurrInstrument = jest.fn();
const mockGetMasterInstrumentFromPublicContext = jest.fn().mockReturnValue('MOEX:TEST1');
// Mock useContracts to return empty contracts
mockUseContracts.mockReturnValue({
contracts: [],
});
// Act
const { result } = renderHook(() =>
useChartPublicContext({
widgetId: 1,
setCurrInstrument: mockSetCurrInstrument,
getMasterInstrumentFromPublicContext: mockGetMasterInstrumentFromPublicContext,
}),
);
// Assert
expect(result.current.issKey).toBeUndefined();
});
});
import { indicativeQuotesController } from '@api/controllers/indicativeQuotesController';
import api from '@api/index';
import { candleToBar } from '@utils/candleToBar';
import { requestBars, requestRealtimeBars } from '../requestBars';
import { isIndicativeTicker } from '../utils/isIndicativeTicker';
// Mock the dependencies
jest.mock('../utils/isIndicativeTicker');
jest.mock('@utils/candleToBar');
// Mock the api module
jest.mock('@api/index', () => ({
__esModule: true,
default: {
getBars: jest.fn(),
},
}));
// Mock the indicativeQuotesController module
jest.mock('@api/controllers/indicativeQuotesController', () => ({
indicativeQuotesController: {
getCandles: jest.fn(),
},
}));
describe('requestBars', () => {
const mockPeriodParams = {
countBack: 100,
from: 1640195200,
to: 1640995200, // 2022-01-01T00:00:00Z
firstDataRequest: true,
};
const mockCandle = {
open: 100,
close: 110,
high: 120,
low: 90,
ticker: 'TEST',
volume: 1000,
end: '2022-01-01T10:00:00Z',
interval: '1',
};
const mockBar = {
open: 100,
close: 110,
high: 120,
low: 90,
volume: 1000,
time: 1640995200000,
};
beforeEach(() => {
jest.clearAllMocks();
});
it('should handle indicative instrument correctly when indicativeData matches ticker', async () => {
// Arrange
(isIndicativeTicker as jest.Mock).mockReturnValue(false);
(candleToBar as jest.Mock).mockReturnValue(mockBar);
const mockIndicativeData = {
key: 'test-ticker',
secId: 'string',
instrumentName: 'string',
settlement: 'string',
firmName: 'string',
};
const mockResponse = {
data: {
indicativeCandles: [mockCandle],
},
};
(
indicativeQuotesController.getCandles as unknown as { mockResolvedValue: (mockResponse: object) => void }
).mockResolvedValue(mockResponse);
// Act
const result = await requestBars({
currencyPair: 'USD/RUB',
interval: '1',
periodParams: mockPeriodParams,
ticker: 'test-ticker',
indicativeData: mockIndicativeData,
});
// Assert
expect(indicativeQuotesController.getCandles).toHaveBeenCalledWith({
count: mockPeriodParams.countBack,
key: 'USD/RUB',
date: '2022-01-01T00:00:00',
interval: '1',
});
expect(result).toEqual([mockBar]);
});
it('should handle indicative instrument correctly when ticker contains indicative board', async () => {
// Arrange
(isIndicativeTicker as jest.Mock).mockReturnValue(true);
(candleToBar as jest.Mock).mockReturnValue(mockBar);
const mockResponse = {
data: {
indicativeCandles: [mockCandle],
},
};
(
indicativeQuotesController.getCandles as unknown as { mockResolvedValue: (mockResponse: object) => void }
).mockResolvedValue(mockResponse);
// Act
const result = await requestBars({
currencyPair: 'USD/RUB',
interval: '1',
periodParams: mockPeriodParams,
ticker: 'TEST.indicative_spot',
});
// Assert
expect(indicativeQuotesController.getCandles).toHaveBeenCalledWith({
count: mockPeriodParams.countBack,
key: 'USD/RUB',
date: '2022-01-01T00:00:00',
interval: '1',
});
expect(result).toEqual([mockBar]);
});
it('should handle non-indicative instrument correctly', async () => {
// Arrange
(isIndicativeTicker as jest.Mock).mockReturnValue(false);
(candleToBar as jest.Mock).mockReturnValue(mockBar);
const mockResponse = {
data: [mockCandle],
};
(api.getBars as unknown as { mockResolvedValue: (mockResponse: object) => void }).mockResolvedValue(mockResponse);
// Act
const result = await requestBars({
currencyPair: 'USD/RUB',
interval: '1',
periodParams: mockPeriodParams,
ticker: 'TEST',
});
// Assert
expect(api.getBars).toHaveBeenCalledWith({
currencyPair: 'USD/RUB',
date: '2022-01-01%2000:00:00',
interval: '1',
count: mockPeriodParams.countBack,
ticker: 'TEST',
});
expect(result).toEqual([mockBar]);
});
it('should call onHistoryCallback with data when indicative instrument succeeds', async () => {
// Arrange
(isIndicativeTicker as jest.Mock).mockReturnValue(true);
(candleToBar as jest.Mock).mockReturnValue(mockBar);
const mockResponse = {
data: {
indicativeCandles: [mockCandle],
},
};
(
indicativeQuotesController.getCandles as unknown as { mockResolvedValue: (mockResponse: object) => void }
).mockResolvedValue(mockResponse);
const mockHistoryCallback = jest.fn();
// Act
await requestBars({
currencyPair: 'USD/RUB',
interval: '1',
periodParams: mockPeriodParams,
onHistoryCallback: mockHistoryCallback,
ticker: 'TEST.indicative_spot',
});
// Assert
expect(mockHistoryCallback).toHaveBeenCalledWith([mockBar], { noData: false });
});
it('should call onHistoryCallback with empty array and noData flag when indicative instrument fails', async () => {
// Arrange
(isIndicativeTicker as jest.Mock).mockReturnValue(true);
(
indicativeQuotesController.getCandles as unknown as { mockRejectedValue: (mockResponse: object) => void }
).mockRejectedValue(new Error('API Error'));
const mockHistoryCallback = jest.fn();
// Act
const result = await requestBars({
currencyPair: 'USD/RUB',
interval: '1',
periodParams: mockPeriodParams,
onHistoryCallback: mockHistoryCallback,
ticker: 'TEST.indicative_spot',
});
// Assert
expect(mockHistoryCallback).toHaveBeenCalledWith([], { noData: true });
expect(result).toEqual([]);
});
it('should call onHistoryCallback with empty array and noData flag when non-indicative instrument fails', async () => {
// Arrange
(isIndicativeTicker as jest.Mock).mockReturnValue(false);
(api.getBars as unknown as { mockRejectedValue: (mockResponse: object) => void }).mockRejectedValue(
new Error('API Error'),
);
const mockHistoryCallback = jest.fn();
// Act
const result = await requestBars({
currencyPair: 'USD/RUB',
interval: '1',
periodParams: mockPeriodParams,
onHistoryCallback: mockHistoryCallback,
ticker: 'TEST',
});
// Assert
expect(mockHistoryCallback).toHaveBeenCalledWith([], { noData: true });
expect(result).toEqual([]);
});
it('should handle multiple candles for indicative instrument', async () => {
// Arrange
(isIndicativeTicker as jest.Mock).mockReturnValue(true);
(candleToBar as jest.Mock).mockReturnValue(mockBar);
const mockCandles = [
{ ...mockCandle, end: '2022-01-01T10:00:00Z' },
{ ...mockCandle, end: '2022-01-01T11:00:00Z' },
{ ...mockCandle, end: '2022-01-01T12:00:00Z' },
];
const mockResponse = {
data: {
indicativeCandles: mockCandles,
},
};
(
indicativeQuotesController.getCandles as unknown as { mockResolvedValue: (mockResponse: object) => void }
).mockResolvedValue(mockResponse);
// Act
const result = await requestBars({
currencyPair: 'USD/RUB',
interval: '1',
periodParams: mockPeriodParams,
ticker: 'TEST.indicative_spot',
});
// Assert
expect(indicativeQuotesController.getCandles).toHaveBeenCalledWith({
count: mockPeriodParams.countBack,
key: 'USD/RUB',
date: '2022-01-01T00:00:00',
interval: '1',
});
expect(candleToBar).toHaveBeenCalledTimes(3);
expect(result).toEqual([mockBar, mockBar, mockBar]);
});
it('should handle multiple candles for non-indicative instrument', async () => {
// Arrange
(isIndicativeTicker as jest.Mock).mockReturnValue(false);
(candleToBar as jest.Mock).mockReturnValue(mockBar);
const mockCandles = [
{ ...mockCandle, end: '2022-01-01T10:00:00Z' },
{ ...mockCandle, end: '2022-01-01T11:00:00Z' },
{ ...mockCandle, end: '2022-01-01T12:00:00Z' },
];
const mockResponse = {
data: mockCandles,
};
(api.getBars as unknown as { mockResolvedValue: (mockResponse: object) => void }).mockResolvedValue(mockResponse);
// Act
const result = await requestBars({
currencyPair: 'USD/RUB',
interval: '1',
periodParams: mockPeriodParams,
ticker: 'TEST',
});
// Assert
expect(api.getBars).toHaveBeenCalledWith({
currencyPair: 'USD/RUB',
date: '2022-01-01%2000:00:00',
interval: '1',
count: mockPeriodParams.countBack,
ticker: 'TEST',
});
expect(candleToBar).toHaveBeenCalledTimes(3);
expect(result).toEqual([mockBar, mockBar, mockBar]);
});
it('should handle empty indicativeCandles array', async () => {
// Arrange
(isIndicativeTicker as jest.Mock).mockReturnValue(true);
const mockResponse = {
data: {
indicativeCandles: [],
},
};
(
indicativeQuotesController.getCandles as unknown as { mockResolvedValue: (mockResponse: object) => void }
).mockResolvedValue(mockResponse);
const mockHistoryCallback = jest.fn();
// Act
const result = await requestBars({
currencyPair: 'USD/RUB',
interval: '1',
periodParams: mockPeriodParams,
onHistoryCallback: mockHistoryCallback,
ticker: 'TEST.indicative_spot',
});
// Assert
expect(mockHistoryCallback).toHaveBeenCalledWith([], { noData: true });
expect(result).toEqual([]);
});
it('should handle empty bars array for non-indicative instrument', async () => {
// Arrange
(isIndicativeTicker as jest.Mock).mockReturnValue(false);
const mockResponse = {
data: [],
};
(api.getBars as unknown as { mockResolvedValue: (mockResponse: object) => void }).mockResolvedValue(mockResponse);
const mockHistoryCallback = jest.fn();
// Act
const result = await requestBars({
currencyPair: 'USD/RUB',
interval: '1',
periodParams: mockPeriodParams,
onHistoryCallback: mockHistoryCallback,
ticker: 'TEST',
});
// Assert
expect(mockHistoryCallback).toHaveBeenCalledWith([], { noData: true });
expect(result).toEqual([]);
});
});
describe('requestRealtimeBars', () => {
const mockCandle = {
open: 100,
close: 110,
high: 120,
low: 90,
ticker: 'TEST',
volume: 1000,
end: '2022-01-01T10:00:00Z',
interval: '1',
};
const mockBar = {
open: 100,
close: 110,
high: 120,
low: 90,
volume: 1000,
time: 1640995200000,
};
beforeEach(() => {
jest.clearAllMocks();
});
it('should handle indicative instrument correctly', async () => {
// Arrange
(isIndicativeTicker as jest.Mock).mockReturnValue(true);
(candleToBar as jest.Mock).mockReturnValue(mockBar);
const mockResponse = {
data: {
indicativeCandles: [mockCandle],
},
};
(
indicativeQuotesController.getCandles as unknown as { mockResolvedValue: (mockResponse: object) => void }
).mockResolvedValue(mockResponse);
// Act
const result = await requestRealtimeBars({
currencyPair: 'USD/RUB',
interval: '1',
ticker: 'TEST.indicative_spot',
});
// Assert
expect(indicativeQuotesController.getCandles).toHaveBeenCalledWith({
count: 1,
key: 'USD/RUB',
date: expect.any(String), // We can't predict the exact date string
interval: '1',
});
expect(candleToBar).toHaveBeenCalledWith(mockCandle);
expect(result).toEqual(mockBar);
});
it('should handle non-indicative instrument correctly', async () => {
// Arrange
(isIndicativeTicker as jest.Mock).mockReturnValue(false);
(candleToBar as jest.Mock).mockReturnValue(mockBar);
const mockResponse = {
data: [mockCandle],
};
(api.getBars as unknown as { mockResolvedValue: (mockResponse: object) => void }).mockResolvedValue(mockResponse);
// Act
const result = await requestRealtimeBars({
currencyPair: 'USD/RUB',
interval: '1',
ticker: 'TEST',
});
// Assert
expect(api.getBars).toHaveBeenCalledWith({
currencyPair: 'USD/RUB',
date: expect.any(String), // We can't predict the exact date string
interval: '1',
count: 1,
ticker: 'TEST',
});
expect(candleToBar).toHaveBeenCalledWith(mockCandle);
expect(result).toEqual(mockBar);
});
it('should handle empty indicativeCandles array in realtime request', async () => {
// Arrange
(isIndicativeTicker as jest.Mock).mockReturnValue(true);
const mockResponse = {
data: {
indicativeCandles: [],
},
};
(
indicativeQuotesController.getCandles as unknown as { mockResolvedValue: (mockResponse: object) => void }
).mockResolvedValue(mockResponse);
// Act
const result = await requestRealtimeBars({
currencyPair: 'USD/RUB',
interval: '1',
ticker: 'TEST.indicative_spot',
});
// Assert
expect(result).toBeUndefined();
});
it('should handle empty bars array in realtime request', async () => {
// Arrange
(isIndicativeTicker as jest.Mock).mockReturnValue(false);
const mockResponse = {
data: [],
};
(api.getBars as unknown as { mockResolvedValue: (mockResponse: object) => void }).mockResolvedValue(mockResponse);
// Act
const result = await requestRealtimeBars({
currencyPair: 'USD/RUB',
interval: '1',
ticker: 'TEST',
});
// Assert
expect(result).toBeUndefined();
});
it('should call onRealtimeCallback with bar when indicative instrument succeeds', async () => {
// Arrange
(isIndicativeTicker as jest.Mock).mockReturnValue(true);
(candleToBar as jest.Mock).mockReturnValue(mockBar);
const mockResponse = {
data: {
indicativeCandles: [mockCandle],
},
};
(
indicativeQuotesController.getCandles as unknown as { mockResolvedValue: (mockResponse: object) => void }
).mockResolvedValue(mockResponse);
const mockRealtimeCallback = jest.fn();
// Act
await requestRealtimeBars({
currencyPair: 'USD/RUB',
interval: '1',
ticker: 'TEST.indicative_spot',
onRealtimeCallback: mockRealtimeCallback,
});
// Assert
expect(mockRealtimeCallback).toHaveBeenCalledWith(mockBar);
});
it('should call onRealtimeCallback with bar when non-indicative instrument succeeds', async () => {
// Arrange
(isIndicativeTicker as jest.Mock).mockReturnValue(false);
(candleToBar as jest.Mock).mockReturnValue(mockBar);
const mockResponse = {
data: [mockCandle],
};
(api.getBars as unknown as { mockResolvedValue: (mockResponse: object) => void }).mockResolvedValue(mockResponse);
const mockRealtimeCallback = jest.fn();
// Act
await requestRealtimeBars({
currencyPair: 'USD/RUB',
interval: '1',
ticker: 'TEST',
onRealtimeCallback: mockRealtimeCallback,
});
// Assert
expect(mockRealtimeCallback).toHaveBeenCalledWith(mockBar);
});
it('should handle error in indicative instrument request', async () => {
// Arrange
(isIndicativeTicker as jest.Mock).mockReturnValue(true);
(
indicativeQuotesController.getCandles as unknown as { mockRejectedValue: (mockResponse: object) => void }
).mockRejectedValue(new Error('API Error'));
const mockRealtimeCallback = jest.fn();
// Act & Assert
await expect(
requestRealtimeBars({
currencyPair: 'USD/RUB',
interval: '1',
ticker: 'TEST.indicative_spot',
onRealtimeCallback: mockRealtimeCallback,
}),
).resolves.toBeUndefined();
});
it('should handle error in non-indicative instrument request', async () => {
// Arrange
(isIndicativeTicker as jest.Mock).mockReturnValue(false);
(api.getBars as unknown as { mockRejectedValue: (mockResponse: object) => void }).mockRejectedValue(
new Error('API Error'),
);
const mockRealtimeCallback = jest.fn();
// Act & Assert
await expect(
requestRealtimeBars({
currencyPair: 'USD/RUB',
interval: '1',
ticker: 'TEST',
onRealtimeCallback: mockRealtimeCallback,
}),
).resolves.toBeUndefined();
});
});
import type { Timeframes as TimeframesType } from 'moex-chart';
type DataSourceProvideModule = typeof import('@widgets/Chart/components/MoexChart/dataSourceProvide');
const mockRequestBars = jest.fn();
const mockRequestRealtimeBars = jest.fn();
const mockMoexChartTimeConverter = jest.fn();
const mockParseTimeframe = jest.fn();
const mockGetStartTime = jest.fn();
const mockMoexChartToIssTimeframe = jest.fn();
const mockGetISSOddTimeframePart = jest.fn();
// Mock the dependencies
jest.mock('moex-chart', () => ({
Timeframes: {
'10s': '10s',
'1m': '1m',
'5m': '5m',
},
parseTimeframe: mockParseTimeframe,
getStartTime: mockGetStartTime,
}));
jest.mock('@utils/chartToReqTimeConverter', () => ({
moexChartTimeConverter: mockMoexChartTimeConverter,
moexChartToIssTimeframe: mockMoexChartToIssTimeframe,
getISSOddTimeframePart: mockGetISSOddTimeframePart,
}));
jest.mock('../requestBars', () => ({
requestBars: mockRequestBars,
requestRealtimeBars: mockRequestRealtimeBars,
}));
jest.mock('@widgets/Chart/requestBars', () => ({
requestBars: mockRequestBars,
requestRealtimeBars: mockRequestRealtimeBars,
}));
const { DataSourceProvider } = jest.requireActual(
'@widgets/Chart/components/MoexChart/dataSourceProvide',
) as DataSourceProvideModule;
const Timeframes = {
'10s': '10s' as TimeframesType,
'1m': '1m' as TimeframesType,
'5m': '5m' as TimeframesType,
};
const flushPromises = async (): Promise<void> => {
await Promise.resolve();
await Promise.resolve();
};
describe('DataSourceProvider', () => {
const mockBar = {
time: 1640995200,
open: 100,
close: 110,
high: 120,
low: 90,
volume: 1000,
};
beforeEach(() => {
jest.clearAllMocks();
jest.useFakeTimers();
jest.setSystemTime(new Date('2026-05-19T10:00:00Z'));
mockMoexChartTimeConverter.mockReturnValue('1');
mockMoexChartToIssTimeframe.mockImplementation((timeframe: TimeframesType) => timeframe);
mockGetISSOddTimeframePart.mockReturnValue({
value: 0,
unit: 'minute',
});
mockParseTimeframe.mockReturnValue({
candleWidth: 1,
dayjsUnit: 'minute',
});
mockGetStartTime.mockImplementation((_timeframe: TimeframesType, time: number) => Math.floor(time / 1000));
});
afterEach(() => {
jest.useRealTimers();
});
it('should request chart history data with converted timeframe', async () => {
// Arrange
const mockTimeframeCallback = jest.fn();
// mockMoexChartTimeConverter.mockReturnValue('1');
mockRequestBars.mockResolvedValue([mockBar]);
const provider = new DataSourceProvider();
const dataSource = provider.getDataSource(undefined, mockTimeframeCallback);
// Act
const result = await dataSource(Timeframes['1m'], 'MOEX:SBER');
// Assert
expect(mockTimeframeCallback).toHaveBeenCalledWith(Timeframes['1m']);
expect(mockMoexChartTimeConverter).toHaveBeenCalledWith(Timeframes['1m']);
expect(mockRequestBars).toHaveBeenCalledWith({
currencyPair: 'MOEX.SBER',
interval: '1',
periodParams: {
firstDataRequest: true,
to: Math.round(Date.now() / 1000),
from: Date.now(),
countBack: 2000,
},
ticker: 'MOEX:SBER',
indicativeData: undefined,
});
expect(result).toEqual([mockBar]);
});
it('should request chart history data with indicative data', async () => {
// Arrange
const indicativeData = {
id: 1,
title: 'Test instrument',
secId: 'SBER',
instrumentName: 'SBER',
settlement: 'TQBR',
firmName: 'Test firm',
key: 'SBER_TBQR',
};
// mockMoexChartTimeConverter.mockReturnValue('1');
mockRequestBars.mockResolvedValue([mockBar]);
const provider = new DataSourceProvider();
const dataSource = provider.getDataSource(indicativeData);
// Act
await dataSource(Timeframes['1m'], 'MOEX:SBER');
// Assert
expect(mockRequestBars).toHaveBeenCalledWith(
expect.objectContaining({
indicativeData,
}),
);
});
it('should use until time when it is provided', async () => {
// Arrange
mockMoexChartTimeConverter.mockReturnValue('5');
mockRequestBars.mockResolvedValue([mockBar]);
const provider = new DataSourceProvider();
const dataSource = provider.getDataSource();
const until = { time: 111 } as NonNullable<Parameters<typeof dataSource>[2]>;
// Act
await dataSource(Timeframes['5m'], 'MOEX:GAZP', until);
// Assert
expect(mockRequestBars).toHaveBeenCalledWith(
expect.objectContaining({
periodParams: expect.objectContaining({
to: 111,
}),
}),
);
});
it('should return null when history data is empty', async () => {
// Arrange
// mockMoexChartTimeConverter.mockReturnValue('1');
mockRequestBars.mockResolvedValue([]);
const provider = new DataSourceProvider();
const dataSource = provider.getDataSource();
// Act
const result = await dataSource(Timeframes['1m'], 'MOEX:SBER');
// Assert
expect(result).toBeNull();
});
it('should request realtime data and update normalized symbol', async () => {
// Arrange
const provider = new DataSourceProvider();
const mockUpdate = jest.fn();
// mockMoexChartTimeConverter.mockReturnValue('1');
const localMockedBar = {
time: 1640995200,
open: 100,
close: 110,
high: 120,
low: 90,
volume: Math.floor(Math.random() * 1000),
};
mockRequestRealtimeBars.mockImplementation(() => localMockedBar);
provider.startRealtime({
getSymbols: () => [' moex:sber '],
getTimeframe: () => Timeframes['1m'],
update: mockUpdate,
periodMs: 1000,
});
// Act
jest.advanceTimersByTime(1000);
await flushPromises();
// Assert
expect(mockRequestRealtimeBars).toHaveBeenCalledWith({
currencyPair: 'MOEX.SBER',
interval: '1',
ticker: 'MOEX:SBER',
indicativeData: undefined,
});
expect(mockUpdate).toHaveBeenCalledWith('MOEX:SBER', localMockedBar);
});
it('should request realtime data with indicative data', async () => {
// Arrange
const provider = new DataSourceProvider();
const mockUpdate = jest.fn();
const indicativeData = {
id: 1,
title: 'Test instrument',
secId: 'SBER',
instrumentName: 'SBER',
settlement: 'TQBR',
firmName: 'Test firm',
key: 'SBER_TBQR',
};
// mockMoexChartTimeConverter.mockReturnValue('1');
mockRequestRealtimeBars.mockResolvedValue(mockBar);
provider.startRealtime({
getSymbols: () => ['MOEX:SBER'],
getTimeframe: () => Timeframes['1m'],
update: mockUpdate,
periodMs: 1000,
indicativeData,
});
// Act
jest.advanceTimersByTime(1000);
await flushPromises();
// Assert
expect(mockRequestRealtimeBars).toHaveBeenCalledWith(
expect.objectContaining({
indicativeData,
}),
);
});
it('should not call update when realtime data is empty', async () => {
// Arrange
const provider = new DataSourceProvider();
const mockUpdate = jest.fn();
// mockMoexChartTimeConverter.mockReturnValue('1');
mockRequestRealtimeBars.mockResolvedValue(undefined);
provider.startRealtime({
getSymbols: () => ['MOEX:SBER'],
getTimeframe: () => Timeframes['1m'],
update: mockUpdate,
periodMs: 1000,
});
// Act
jest.advanceTimersByTime(1000);
await flushPromises();
// Assert
expect(mockUpdate).not.toHaveBeenCalled();
});
it('should not request realtime data when symbols list is empty', async () => {
// Arrange
const provider = new DataSourceProvider();
// mockMoexChartTimeConverter.mockReturnValue('1');
mockRequestRealtimeBars.mockResolvedValue(mockBar);
provider.startRealtime({
getSymbols: () => [],
getTimeframe: () => Timeframes['1m'],
update: jest.fn(),
periodMs: 1000,
});
// Act
jest.advanceTimersByTime(1000);
await flushPromises();
// Assert
expect(mockRequestRealtimeBars).not.toHaveBeenCalled();
});
it('should clear realtime timer on unsubscribe', async () => {
// Arrange
const provider = new DataSourceProvider();
// mockMoexChartTimeConverter.mockReturnValue('1');
mockRequestRealtimeBars.mockResolvedValue(mockBar);
const unsubscribe = provider.startRealtime({
getSymbols: () => ['MOEX:SBER'],
getTimeframe: () => Timeframes['1m'],
update: jest.fn(),
periodMs: 1000,
});
// Act
unsubscribe();
jest.advanceTimersByTime(1000);
await flushPromises();
// Assert
expect(mockRequestRealtimeBars).not.toHaveBeenCalled();
});
});