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


import { IChartApi, PriceScaleMode, SeriesType } from 'lightweight-charts';
import { BehaviorSubject } from 'rxjs';

import { DataSource } from '@core/DataSource';
import { EventManager } from '@core/EventManager';
import { Indicator } from '@core/Indicator';
import { IndicatorManager } from '@core/IndicatorManager';
import { Pane } from '@core/Pane';
import { PaneManager } from '@core/PaneManager';
import { PriceScale } from '@core/PriceScale';

import { CompareManager } from '@src/core/CompareManager';
import { CompareMode, Direction } from '@src/types';
import { PriceScaleSide } from '@src/types/snapshot';
import { Timeframes } from '@src/types/timeframes';

jest.mock('@core/Indicator');

type IndicatorFactory = (
  zIndex: number,
  moveUp: (id: string) => void,
  moveDown: (id: string) => void,
) => Indicator;

describe('CompareManager', () => {
  const IndicatorMock = Indicator as jest.MockedClass<typeof Indicator>;

  let manager: CompareManager;
  let mainPane: Pane;
  let secondaryPane: Pane;
  let rightPriceScale: PriceScale;

  let addEntity: jest.Mock;
  let getIndicators: jest.Mock;
  let removeEntity: jest.Mock;

  let getMainPane: jest.Mock;
  let getPaneById: jest.Mock;
  let addPane: jest.Mock;
  let setPriceScaleSideVisible: jest.Mock;
  let invalidate: jest.Mock;

  let waitUntilReady: jest.Mock;

  beforeAll(() => {
    Object.defineProperty(globalThis, 'crypto', {
      configurable: true,
      value: {
        randomUUID: () => 'test-uuid',
      },
    });
  });

  beforeEach(() => {
    IndicatorMock.mockImplementation(
      (params) =>
        ({
          id: params.id,
          destroy: jest.fn(),
          getConfig: jest.fn(() => params.config),
          getIndicatorType: jest.fn(() => params.type),
        }) as unknown as Indicator,
    );

    rightPriceScale = {
      getMode: jest.fn(() => PriceScaleMode.Normal),
      setMode: jest.fn(),
    } as unknown as PriceScale;

    mainPane = {
      getId: jest.fn(() => 0),
      getPriceScale: jest.fn((_side: PriceScaleSide) => rightPriceScale),
    } as unknown as Pane;

    secondaryPane = {
      getId: jest.fn(() => 1),
    } as unknown as Pane;

    addEntity = jest.fn((factory: IndicatorFactory) => factory(1, jest.fn(), jest.fn()));
    getIndicators = jest.fn(() => new BehaviorSubject<Map<string, Indicator>>(new Map()));
    removeEntity = jest.fn();

    getMainPane = jest.fn(() => mainPane);
    getPaneById = jest.fn();
    addPane = jest.fn(() => secondaryPane);
    setPriceScaleSideVisible = jest.fn();
    invalidate = jest.fn();

    waitUntilReady = jest.fn((_symbolRaw: string) => Promise.resolve());

    const indicatorManager = {
      addEntity,
      getIndicators,
      removeEntity,
    } as unknown as IndicatorManager;

    const paneManager = {
      getMainPane,
      getPaneById,
      addPane,
      setPriceScaleSideVisible,
      invalidate,
    } as unknown as PaneManager;

    const dataSource = {
      waitUntilReady,
    } as unknown as DataSource;

    const eventManager = {
      timeframe: jest.fn(() => new BehaviorSubject(Timeframes['10s'])),
    } as unknown as EventManager;

    manager = new CompareManager({
      chart: {} as IChartApi,
      eventManager,
      dataSource,
      indicatorManager,
      paneManager,
    });
  });

  afterEach(() => {
    manager.destroy();
  });

  it('должен добавлять compare-серию типа Line', async () => {
    await manager.setSymbolMode(
      'Line' as SeriesType,
      {
        symbolId: 'SBER',
        symbol: 'SBER',
        symbolName: 'Sberbank',
      },
      CompareMode.Absolute,
    );

    expect(IndicatorMock).toHaveBeenCalledTimes(1);

    const params = IndicatorMock.mock.calls[0][0];

    expect(params.config.series[0]?.name).toBe('Line');
    expect(params.config.series[0]?.id).toBe('compare-test-uuid');
    expect(params.associatedPane).toBe(mainPane);
    expect(waitUntilReady).toHaveBeenCalledWith('SBER');
    expect(manager.getAllEntities()).toHaveLength(1);
  });

  it('не должен добавлять невалидный инструмент', async () => {
    await manager.setSymbolMode(
      'Line' as SeriesType,
      {
        symbolId: '',
      },
      CompareMode.Absolute,
    );

    expect(addEntity).not.toHaveBeenCalled();
    expect(manager.getAllEntities()).toHaveLength(0);
  });

  it('должен создавать отдельный pane для режима NewPane', async () => {
    await manager.setSymbolMode(
      'Line' as SeriesType,
      {
        symbolId: 'SBER',
      },
      CompareMode.NewPane,
    );

    expect(addPane).toHaveBeenCalledTimes(1);
    expect(IndicatorMock.mock.calls[0][0].associatedPane).toBe(secondaryPane);
  });

  it('должен использовать существующий pane при восстановлении NewPane', async () => {
    getPaneById.mockReturnValue(secondaryPane);

    await manager.setSymbolMode(
      'Line' as SeriesType,
      {
        symbolId: 'SBER',
      },
      CompareMode.NewPane,
      1,
    );

    expect(getPaneById).toHaveBeenCalledWith(1);
    expect(addPane).not.toHaveBeenCalled();
  });

  it('не должен повторно добавлять одинаковый инструмент в одном режиме', async () => {
    await manager.setSymbolMode(
      'Line' as SeriesType,
      {
        symbolId: 'SBER',
      },
      CompareMode.Absolute,
    );

    await manager.setSymbolMode(
      'Line' as SeriesType,
      {
        symbolId: 'SBER',
      },
      CompareMode.Absolute,
    );

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

  it('не должен добавлять NewScale если compare уже существует', async () => {
    await manager.setSymbolMode(
      'Line' as SeriesType,
      {
        symbolId: 'SBER',
      },
      CompareMode.Absolute,
    );

    await manager.setSymbolMode(
      'Line' as SeriesType,
      {
        symbolId: 'GAZP',
      },
      CompareMode.NewScale,
    );

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

  it('должен включать левую шкалу для NewScale', async () => {
    await manager.setSymbolMode(
      'Line' as SeriesType,
      {
        symbolId: 'SBER',
      },
      CompareMode.NewScale,
    );

    expect(setPriceScaleSideVisible).toHaveBeenCalledWith(Direction.Left, true);
  });

  it('должен удалять compare-сущность', async () => {
    await manager.setSymbolMode(
      'Line' as SeriesType,
      {
        symbolId: 'SBER',
      },
      CompareMode.Absolute,
    );

    const [entry] = manager.getAllEntities();

    if (!entry) {
      throw new Error('Compare-сущность не была добавлена');
    }

    manager.removeSymbol('SBER');

    expect(removeEntity).toHaveBeenCalledWith(entry.entity);
    expect(entry.entity.destroy).toHaveBeenCalledTimes(1);
    expect(manager.getAllEntities()).toHaveLength(0);
  });

  it('должен очищать все compare-сущности', async () => {
    await manager.setSymbolMode(
      'Line' as SeriesType,
      {
        symbolId: 'SBER',
      },
      CompareMode.Absolute,
    );

    await manager.setSymbolMode(
      'Line' as SeriesType,
      {
        symbolId: 'GAZP',
      },
      CompareMode.NewPane,
    );

    expect(manager.getAllEntities()).toHaveLength(2);

    manager.clear();

    expect(manager.getAllEntities()).toHaveLength(0);
    expect(removeEntity).toHaveBeenCalledTimes(2);
  });
});