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


import { fireEvent, render, screen } from '@testing-library/react';
import React from 'react';

import { Contract } from '@modules/contracts';
import '@testing-library/jest-dom';

import { MoexChartActions } from '../MoexChartActions';

describe('MoexChartActions', () => {
  const mockHandlers = {
    handleAbsolute: jest.fn(),
    handlePercent: jest.fn(),
    handleNewScale: jest.fn(),
    handleNewPanel: jest.fn(),
  };

  const mockContract = {
    issKey: 'RU000A0JQ0Y0',
  } as Contract;

  const renderComponent = (
    selectedInstrumentRows: Contract[] = [],
    isNewScaleDisabled = false,
  ) =>
    render(
      <MoexChartActions
        selectedInstrumentRows={selectedInstrumentRows}
        customActionsFooterHandlers={mockHandlers}
        isNewScaleDisabled={isNewScaleDisabled}
      />,
    );

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

  it('should render all action buttons', () => {
    renderComponent();

    expect(screen.getByText('Абсолютная шкала')).toBeInTheDocument();
    expect(screen.getByText('%')).toBeInTheDocument();
    expect(screen.getByText('Новая шкала')).toBeInTheDocument();
    expect(screen.getByText('Новая панель')).toBeInTheDocument();
  });

  it('should disable all action buttons when instrument is not selected', () => {
    renderComponent();

    expect(screen.getByText('Абсолютная шкала')).toBeDisabled();
    expect(screen.getByText('%')).toBeDisabled();
    expect(screen.getByText('Новая шкала')).toBeDisabled();
    expect(screen.getByText('Новая панель')).toBeDisabled();
  });

  it('should enable all action buttons when instrument is selected', () => {
    renderComponent([mockContract]);

    expect(screen.getByText('Абсолютная шкала')).not.toBeDisabled();
    expect(screen.getByText('%')).not.toBeDisabled();
    expect(screen.getByText('Новая шкала')).not.toBeDisabled();
    expect(screen.getByText('Новая панель')).not.toBeDisabled();
  });

  it('should disable only new scale button when new scale is unavailable', () => {
    renderComponent([mockContract], true);

    expect(screen.getByText('Абсолютная шкала')).not.toBeDisabled();
    expect(screen.getByText('%')).not.toBeDisabled();
    expect(screen.getByText('Новая шкала')).toBeDisabled();
    expect(screen.getByText('Новая панель')).not.toBeDisabled();
  });

  it.each([
    ['Абсолютная шкала', 'handleAbsolute'],
    ['%', 'handlePercent'],
    ['Новая шкала', 'handleNewScale'],
    ['Новая панель', 'handleNewPanel'],
  ] as const)('should call %s handler when button is clicked', (buttonText, handlerName) => {
    renderComponent([mockContract]);

    fireEvent.click(screen.getByText(buttonText));

    expect(mockHandlers[handlerName]).toHaveBeenCalledTimes(1);
    expect(mockHandlers[handlerName]).toHaveBeenCalledWith(mockContract);
  });

  it('should use first instrument when multiple instruments are selected', () => {
    const multipleContracts = [
      {
        issKey: 'RU000A0JQ0Y0',
      },
      {
        issKey: 'RU000A0JQ0Y1',
      },
    ] as Contract[];

    renderComponent(multipleContracts);

    fireEvent.click(screen.getByText('Абсолютная шкала'));
    fireEvent.click(screen.getByText('%'));
    fireEvent.click(screen.getByText('Новая шкала'));
    fireEvent.click(screen.getByText('Новая панель'));

    expect(mockHandlers.handleAbsolute).toHaveBeenCalledWith(multipleContracts[0]);
    expect(mockHandlers.handlePercent).toHaveBeenCalledWith(multipleContracts[0]);
    expect(mockHandlers.handleNewScale).toHaveBeenCalledWith(multipleContracts[0]);
    expect(mockHandlers.handleNewPanel).toHaveBeenCalledWith(multipleContracts[0]);
  });
});





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: {
    Absolute: 'ABS',
    Percentage: 'PCT',
    NewScale: 'SCALE',
    NewPane: 'PANE',
  },
}));

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

interface CompareActions {
  handleAbsolute: (instrument: Contract) => void;
  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
        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', () => {
    mockIsNewScaleDisabled.mockReturnValue(true);

    renderComponent();

    const instrumentSearchProps = getInstrumentSearchProps();

    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({
      handleAbsolute: expect.any(Function),
      handlePercent: expect.any(Function),
      handleNewScale: expect.any(Function),
      handleNewPanel: expect.any(Function),
    });
  });

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

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

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

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

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

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

    unmount();

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

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

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

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

    renderComponent();

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

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

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

    const handlers = getInstrumentSearchProps().customActionsFooterHandlers;

    handlers[handlerName](instrument);

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

  it('should delegate missing symbol metadata fallback to moex chart', () => {
    renderComponent();

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

    getInstrumentSearchProps().customActionsFooterHandlers.handlePercent(instrument);

    expect(mockSetSymbolMode).toHaveBeenCalledWith(
      'Line',
      {
        symbolId: 'MXSE:TQBR:SBER',
        symbol: '',
        symbolName: '',
      },
      CompareMode.Percentage,
    );
  });

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

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

    getInstrumentSearchProps().customActionsFooterHandlers.handleAbsolute(instrument);

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