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


import { act, renderHook } from '@testing-library/react';

import { useDispatch } from 'react-redux';

import { communicator } from '@core/comm';
import { useAppSelect } from '@hooks/useAppSelector';
import { CORPACTIONS_OPEN_EXESTED_WIDGET_EVENT, HIGHLIGHT_WIDGET_EVENT } from '@modules/widgets/shared';
import { addContentPropsToWidget, unbindWidgets } from '@store/slices/widgets';
import { getState } from '@store/store';
import { useWidgetsBind } from '@utils/hooks/useWidgetsBind';

import useChartComponentFacade from '../hooks/useChartComponentFacade';
import { useChartPublicContext } from '../hooks/useChartPublicContext';

import type { WidgetProperties } from '../properties/types';
import type { ChartContainerProps } from '../types';

jest.mock('@store/store', () => ({
  getState: jest.fn(),
}));

jest.mock('@store/slices/widgets', () => ({
  addContentPropsToWidget: jest.fn(),
  unbindWidgets: jest.fn(),
}));

jest.mock('react-redux', () => ({
  useDispatch: jest.fn(),
}));

jest.mock('@core/comm', () => ({
  communicator: {
    listen: jest.fn(),
  },
}));

jest.mock('@hooks/useAppSelector', () => ({
  useAppSelect: jest.fn(),
}));

jest.mock('@utils/hooks/useWidgetsBind', () => ({
  useWidgetsBind: jest.fn(),
}));

jest.mock('../hooks/useChartPublicContext', () => ({
  useChartPublicContext: jest.fn(),
}));

interface PublicContextMockParams {
  setCurrInstrument: (
    instrumentId: string,
    instrumentName?: string,
    instrumentTicker?: string,
  ) => void;
}

describe('useChartComponentFacade', () => {
  const mockUseDispatch = useDispatch as jest.Mock;
  const mockUseAppSelect = useAppSelect as jest.Mock;
  const mockAddContentPropsToWidget = addContentPropsToWidget as unknown as jest.Mock;
  const mockUnbindWidgets = unbindWidgets as unknown as jest.Mock;
  const mockGetState = getState as jest.Mock;
  const mockUseWidgetsBind = useWidgetsBind as jest.Mock;
  const mockUseChartPublicContext = useChartPublicContext as jest.Mock;
  const mockCommunicatorListen = communicator.listen as jest.Mock;

  const mockDispatch = jest.fn();
  const mockTriggerRelatedWidgetsToUpdate = jest.fn();
  const mockGetMasterInstrumentFromPublicContext = jest.fn();

  const mockUnsubscribeCorpActions = jest.fn();
  const mockUnsubscribeHighlighter = jest.fn();

  const widgetProperties = {
    chartState: {
      savedInstrument: 'MOEX:SBER',
      savedInstrumentName: 'Сбербанк',
      savedInstrumentTicker: 'SBER',
      interval: '1m',
    },
    indicativeData: {
      id: 1,
      key: 'INDICATIVE',
      secId: 'INAV',
      instrumentName: 'Индикатив',
      settlement: 'Расчётный',
      firmName: 'Тестовая фирма',
    },
  } as WidgetProperties;

  const renderFacade = () =>
    renderHook(() =>
      useChartComponentFacade({
        widgetId: 42,
      } as ChartContainerProps),
    );

  beforeEach(() => {
    jest.clearAllMocks();

    mockUseDispatch.mockReturnValue(mockDispatch);
    mockUseAppSelect.mockReturnValue(widgetProperties);

    mockGetState.mockReturnValue({
      widgets: {
        widgets: [
          {
            id: 42,
            widgetContentProps: widgetProperties,
          },
        ],
      },
    });

    mockUseWidgetsBind.mockReturnValue({
      triggerRelatedWidgetsToUpdate: mockTriggerRelatedWidgetsToUpdate,
      getMasterInstrumentFromPublicContext: mockGetMasterInstrumentFromPublicContext,
    });

    mockUseChartPublicContext.mockReturnValue({
      issKey: undefined,
    });

    mockAddContentPropsToWidget.mockImplementation((payload) => ({
      type: 'widgets/addContentPropsToWidget',
      payload,
    }));

    mockUnbindWidgets.mockImplementation((payload) => ({
      type: 'widgets/unbindWidgets',
      payload,
    }));

    mockCommunicatorListen.mockImplementation(({ messageType }, listener) => {
      if (messageType === CORPACTIONS_OPEN_EXESTED_WIDGET_EVENT) {
        return mockUnsubscribeCorpActions;
      }

      if (messageType === HIGHLIGHT_WIDGET_EVENT) {
        return mockUnsubscribeHighlighter;
      }

      return jest.fn();
    });
  });

  it('should initialize symbol, display name and ticker from widget properties', () => {
    // Arrange & Act
    const { result } = renderFacade();

    // Assert
    expect(result.current.currentInstrument).toBe('MOEX:SBER');
    expect(result.current.currentInstrumentName).toBe('Сбербанк');
    expect(result.current.currentInstrumentTicker).toBe('SBER');
  });

  it('should update and save instrument selected from modal', () => {
    // Arrange
    const { result } = renderFacade();

    jest.clearAllMocks();

    // Act
    act(() => {
      result.current.addInstrumentFromModal([
        {
          issKey: 'MOEX:GAZP',
          displayName: 'Газпром',
          symbol: 'GAZP',
        },
      ]);
    });

    // Assert
    expect(result.current.currentInstrument).toBe('MOEX:GAZP');
    expect(result.current.currentInstrumentName).toBe('Газпром');
    expect(result.current.currentInstrumentTicker).toBe('GAZP');

    expect(mockUnbindWidgets).toHaveBeenCalledWith({
      widgetId: 42,
    });

    expect(mockAddContentPropsToWidget).toHaveBeenCalledWith({
      id: 42,
      withoutSend: true,
      widgetContentProps: {
        chartState: {
          ...widgetProperties.chartState,
          savedInstrument: 'MOEX:GAZP',
          savedInstrumentName: 'Газпром',
          savedInstrumentTicker: 'GAZP',
        },
        indicativeData: undefined,
      },
    });

    expect(mockTriggerRelatedWidgetsToUpdate).toHaveBeenCalledWith('MOEX:GAZP');
  });

  it('should update name and ticker without unbinding when symbol is unchanged', () => {
    // Arrange
    const { result } = renderFacade();

    jest.clearAllMocks();

    // Act
    act(() => {
      result.current.addInstrumentFromModal([
        {
          issKey: 'MOEX:SBER',
          displayName: 'Сбербанк ПАО',
          symbol: 'SBERP',
        },
      ]);
    });

    // Assert
    expect(result.current.currentInstrument).toBe('MOEX:SBER');
    expect(result.current.currentInstrumentName).toBe('Сбербанк ПАО');
    expect(result.current.currentInstrumentTicker).toBe('SBERP');

    expect(mockUnbindWidgets).not.toHaveBeenCalled();
    expect(mockTriggerRelatedWidgetsToUpdate).not.toHaveBeenCalled();

    expect(mockAddContentPropsToWidget).toHaveBeenCalledWith({
      id: 42,
      withoutSend: true,
      widgetContentProps: {
        chartState: {
          ...widgetProperties.chartState,
          savedInstrument: 'MOEX:SBER',
          savedInstrumentName: 'Сбербанк ПАО',
          savedInstrumentTicker: 'SBERP',
        },
        indicativeData: widgetProperties.indicativeData,
      },
    });
  });

  it('should send dropped instrument update immediately', () => {
    // Arrange
    const { result } = renderFacade();

    jest.clearAllMocks();

    // Act
    act(() => {
      result.current.onDropInstruments(
        {
          issKey: 'MOEX:LKOH',
          displayName: 'Лукойл',
          symbol: 'LKOH',
        },
        true,
      );
    });

    // Assert
    expect(mockAddContentPropsToWidget).toHaveBeenCalledWith(
      expect.objectContaining({
        id: 42,
        withoutSend: false,
        widgetContentProps: expect.objectContaining({
          chartState: expect.objectContaining({
            savedInstrument: 'MOEX:LKOH',
            savedInstrumentName: 'Лукойл',
            savedInstrumentTicker: 'LKOH',
          }),
        }),
      }),
    );
  });

  it('should update instrument from public context without unbinding widget', () => {
    // Arrange
    renderFacade();

    const publicContextParams = mockUseChartPublicContext.mock.calls[0]?.[0] as PublicContextMockParams;

    jest.clearAllMocks();

    // Act
    act(() => {
      publicContextParams.setCurrInstrument('MOEX:ROSN', 'Роснефть', 'ROSN');
    });

    // Assert
    expect(mockUnbindWidgets).not.toHaveBeenCalled();

    expect(mockAddContentPropsToWidget).toHaveBeenCalledWith(
      expect.objectContaining({
        withoutSend: false,
        widgetContentProps: expect.objectContaining({
          chartState: expect.objectContaining({
            savedInstrument: 'MOEX:ROSN',
            savedInstrumentName: 'Роснефть',
            savedInstrumentTicker: 'ROSN',
          }),
        }),
      }),
    );
  });

  it('should use ticker extracted from instrument id when metadata is unavailable', () => {
    // Arrange
    renderFacade();

    const corpActionsListener = mockCommunicatorListen.mock.calls.find(
      ([options]) => options.messageType === CORPACTIONS_OPEN_EXESTED_WIDGET_EVENT,
    )?.[1] as (message: Record<number, string>) => void;

    // Act
    act(() => {
      corpActionsListener({
        42: 'MOEX:UNKNOWN',
      });
    });

    // Assert
    expect(mockAddContentPropsToWidget).toHaveBeenCalledWith(
      expect.objectContaining({
        widgetContentProps: expect.objectContaining({
          chartState: expect.objectContaining({
            savedInstrument: 'MOEX:UNKNOWN',
            savedInstrumentName: 'UNKNOWN',
            savedInstrumentTicker: 'UNKNOWN',
          }),
        }),
      }),
    );
  });

  it('should not update instrument when modal selection is empty', () => {
    // Arrange
    const { result } = renderFacade();

    jest.clearAllMocks();

    // Act
    act(() => {
      result.current.addInstrumentFromModal([]);
    });

    // Assert
    expect(mockAddContentPropsToWidget).not.toHaveBeenCalled();
    expect(mockUnbindWidgets).not.toHaveBeenCalled();
    expect(mockTriggerRelatedWidgetsToUpdate).not.toHaveBeenCalled();
  });

  it('should update widget highlight state', () => {
    // Arrange
    const { result } = renderFacade();

    const highlighterListener = mockCommunicatorListen.mock.calls.find(
      ([options]) => options.messageType === HIGHLIGHT_WIDGET_EVENT,
    )?.[1] as (message: Record<number, boolean>) => void;

    // Act
    act(() => {
      highlighterListener({
        42: true,
      });
    });

    // Assert
    expect(result.current.isOver).toBe(true);
  });

  it('should unsubscribe communicator listeners on unmount', () => {
    // Arrange
    const { unmount } = renderFacade();

    // Act
    unmount();

    // Assert
    expect(mockUnsubscribeCorpActions).toHaveBeenCalledTimes(1);
    expect(mockUnsubscribeHighlighter).toHaveBeenCalledTimes(1);
  });
});