Загрузка данных


import React from 'react';

import { act, render } from '@testing-library/react';
import { MoexChart, Timeframes } from 'moex-chart';

import { useChangeProperties, useSelectProperties } from '@modules/widgetProperties';

import { useMoexChart } from '../components/MoexChart/hooks';

const mockGetDataSource = jest.fn();
const mockStartRealtime = jest.fn();

interface MockDataSourceProviderInstance {
  startRealtime: jest.Mock;
}

interface MockDataSourceProviderConstructor extends jest.Mock<MockDataSourceProviderInstance, []> {
  getDataSource: jest.Mock;
}

const mockDataSourceProviderConstructor = jest.fn(() => ({
  startRealtime: mockStartRealtime,
})) as MockDataSourceProviderConstructor;

mockDataSourceProviderConstructor.getDataSource = mockGetDataSource;

// Mock the dependencies
jest.mock('@modules/widgetProperties');

jest.mock('../components/MoexChart/dataSourceProvide', () => ({
  DataSourceProvider: mockDataSourceProviderConstructor,
}));

jest.mock('@widgets/Chart/components/MoexChart/dataSourceProvide', () => ({
  DataSourceProvider: mockDataSourceProviderConstructor,
}));

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',
  },
}));

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 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,
    });

    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);
    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(mockDataSourceProviderConstructor).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 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 mockMoexChartToIssTimeframe = jest.fn();
const mockGetISSOddTimeframePart = jest.fn();
const mockParseTimeframe = jest.fn();
const mockGetStartTime = 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();

    mockRequestBars.mockResolvedValue([mockBar]);

    const dataSource = DataSourceProvider.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',
    };

    mockRequestBars.mockResolvedValue([mockBar]);

    const dataSource = DataSourceProvider.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 dataSource = DataSourceProvider.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({
        interval: '5',
        periodParams: expect.objectContaining({
          to: 111,
        }),
      }),
    );
  });

  it('should return null when history data is empty', async () => {
    // Arrange
    mockRequestBars.mockResolvedValue([]);

    const dataSource = DataSourceProvider.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();

    mockRequestRealtimeBars.mockResolvedValue(mockBar);

    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', mockBar);
  });

  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',
    };

    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();

    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();

    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();

    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();
  });
});