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


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

import { CompareMode } from 'moex-chart';
import React from 'react';

import { InstrumentSearch } from '@components/InstrumentSearch';

import { CompareModal } from '../components/MoexChart/components/CompareModal';

import type { Contract } from '@modules/contracts';
import type { __CompareManager__ } from 'moex-chart';
import type { MutableRefObject } from 'react';

jest.mock('moex-chart', () => ({
  __esModule: true,
  CompareMode: {
    Percentage: 'PCT',
    NewScale: 'SCALE',
    NewPane: 'PANE',
  },
}));

jest.mock('@components/InstrumentSearch', () => ({
  InstrumentSearch: jest.fn(() => null),
}));

interface CompareActions {
  handlePercent: (instrument: Contract) => void;
  handleNewScale: (instrument: Contract) => void;
  handleNewPanel: (instrument: Contract) => void;
}

interface InstrumentSearchMockProps {
  widgetId: number;
  variant: string;
  isOpen: boolean;
  setOpen: (isOpen: boolean) => void;
  isNewScaleDisabled: boolean;
  customActionsFooterHandlers: CompareActions;
}

describe('CompareModal', () => {
  const mockInstrumentSearch = InstrumentSearch as jest.Mock;

  const mockSetOpen = jest.fn();
  const mockSetSymbolMode = jest.fn();
  const mockIsNewScaleDisabled = jest.fn();
  const mockIsNewScaleDisabledObservable = jest.fn();
  const mockSubscribe = jest.fn();
  const mockUnsubscribe = jest.fn();

  let newScaleDisabledListener: ((disabled: boolean) => void) | null;
  let compareManager: __CompareManager__;
  let compareManagerRef: MutableRefObject<__CompareManager__ | null>;

  const getInstrumentSearchProps = (): InstrumentSearchMockProps => {
    const lastCall = mockInstrumentSearch.mock.calls[mockInstrumentSearch.mock.calls.length - 1];

    return lastCall?.[0] as InstrumentSearchMockProps;
  };

  const renderComponent = (isOpen = true) =>
    render(
      <CompareModal
        onClose={jest.fn()}
        widgetId={42}
        compareManager={compareManagerRef}
        isOpen={isOpen}
        setOpen={mockSetOpen}
      />,
    );

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

    newScaleDisabledListener = null;

    mockIsNewScaleDisabled.mockReturnValue(false);
    mockSetSymbolMode.mockResolvedValue(undefined);

    mockSubscribe.mockImplementation((listener: (disabled: boolean) => void) => {
      newScaleDisabledListener = listener;

      return {
        unsubscribe: mockUnsubscribe,
      };
    });

    mockIsNewScaleDisabledObservable.mockReturnValue({
      subscribe: mockSubscribe,
    });

    compareManager = {
      setSymbolMode: mockSetSymbolMode,
      isNewScaleDisabled: mockIsNewScaleDisabled,
      isNewScaleDisabledObservable: mockIsNewScaleDisabledObservable,
    } as unknown as __CompareManager__;

    compareManagerRef = {
      current: compareManager,
    };
  });

  it('should pass modal properties and current scale state to instrument search', () => {
    // Arrange
    mockIsNewScaleDisabled.mockReturnValue(true);

    // Act
    renderComponent();

    const instrumentSearchProps = getInstrumentSearchProps();

    // Assert
    expect(instrumentSearchProps.widgetId).toBe(42);
    expect(instrumentSearchProps.variant).toBe('single');
    expect(instrumentSearchProps.isOpen).toBe(true);
    expect(instrumentSearchProps.setOpen).toBe(mockSetOpen);
    expect(instrumentSearchProps.isNewScaleDisabled).toBe(true);
    expect(instrumentSearchProps.customActionsFooterHandlers).toEqual({
      handlePercent: expect.any(Function),
      handleNewScale: expect.any(Function),
      handleNewPanel: expect.any(Function),
    });
  });

  it('should subscribe to new scale disabled state', () => {
    // Arrange & Act
    renderComponent();

    // Assert
    expect(mockIsNewScaleDisabled).toHaveBeenCalledTimes(1);
    expect(mockIsNewScaleDisabledObservable).toHaveBeenCalledTimes(1);
    expect(mockSubscribe).toHaveBeenCalledTimes(1);
  });

  it('should update new scale disabled state from manager observable', () => {
    // Arrange
    renderComponent();

    // Act
    act(() => {
      newScaleDisabledListener?.(true);
    });

    // Assert
    expect(getInstrumentSearchProps().isNewScaleDisabled).toBe(true);
  });

  it('should unsubscribe from manager observable on unmount', () => {
    // Arrange
    const { unmount } = renderComponent();

    // Act
    unmount();

    // Assert
    expect(mockUnsubscribe).toHaveBeenCalledTimes(1);
  });

  it('should not subscribe when modal is closed', () => {
    // Arrange & Act
    renderComponent(false);

    // Assert
    expect(mockIsNewScaleDisabled).not.toHaveBeenCalled();
    expect(mockIsNewScaleDisabledObservable).not.toHaveBeenCalled();
    expect(getInstrumentSearchProps().isNewScaleDisabled).toBe(false);
  });

  it('should not subscribe when compare manager is unavailable', () => {
    // Arrange
    compareManagerRef.current = null;

    // Act
    renderComponent();

    // Assert
    expect(mockIsNewScaleDisabled).not.toHaveBeenCalled();
    expect(mockIsNewScaleDisabledObservable).not.toHaveBeenCalled();
    expect(getInstrumentSearchProps().isNewScaleDisabled).toBe(false);
  });

  it.each([
    ['handlePercent', CompareMode.Percentage],
    ['handleNewScale', CompareMode.NewScale],
    ['handleNewPanel', CompareMode.NewPane],
  ] as const)('should add compare instrument using %s action', (handlerName, mode) => {
    // Arrange
    renderComponent();

    const instrument = {
      issKey: 'MXSE:TQBR:SBER',
      displayName: 'Сбербанк',
      symbol: 'SBER',
    } as Contract;

    const handlers = getInstrumentSearchProps().customActionsFooterHandlers;

    // Act
    handlers[handlerName](instrument);

    // Assert
    expect(mockSetSymbolMode).toHaveBeenCalledTimes(1);
    expect(mockSetSymbolMode).toHaveBeenCalledWith(
      'Line',
      {
        symbol: 'MXSE:TQBR:SBER',
        instrumentName: 'Сбербанк',
        instrumentTicker: 'SBER',
      },
      mode,
    );
  });

  it('should delegate missing instrument metadata fallback to moex-chart', () => {
    // Arrange
    renderComponent();

    const instrument = {
      issKey: 'MXSE:TQBR:SBER',
      displayName: '',
      symbol: '',
    } as Contract;

    // Act
    getInstrumentSearchProps().customActionsFooterHandlers.handlePercent(instrument);

    // Assert
    expect(mockSetSymbolMode).toHaveBeenCalledWith(
      'Line',
      {
        symbol: 'MXSE:TQBR:SBER',
        instrumentName: undefined,
        instrumentTicker: undefined,
      },
      CompareMode.Percentage,
    );
  });

  it('should not add compare instrument without issKey', () => {
    // Arrange
    renderComponent();

    const instrument = {
      issKey: '',
      displayName: 'Сбербанк',
      symbol: 'SBER',
    } as Contract;

    // Act
    getInstrumentSearchProps().customActionsFooterHandlers.handlePercent(instrument);

    // Assert
    expect(mockSetSymbolMode).not.toHaveBeenCalled();
  });
});