Загрузка данных
import { IChartApi } from 'lightweight-charts';
import { of } from 'rxjs';
import { DataSource } from '@core/DataSource';
import { DOMModel } from '@core/DOMModel';
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 { IndicatorsIds } from '@src/constants';
jest.mock('@core/Indicator');
jest.mock('@src/core/Indicators', () => ({
indicatorsMap: jest.fn(
() =>
new Proxy(
{},
{
get: () => ({
newPane: false,
series: [],
}),
},
),
),
}));
jest.mock('@src/utils', () => ({
applyNextIndicatorColors: jest.fn((config) => config),
getIndicatorColors: jest.fn(() => []),
}));
type EntityFactory = (
zIndex: number,
moveUp: (id: string) => void,
moveDown: (id: string) => void,
) => Indicator;
describe('IndicatorManager', () => {
const IndicatorMock = Indicator as jest.MockedClass<typeof Indicator>;
let manager: IndicatorManager;
let mainPane: Pane;
let setEntity: jest.Mock;
let removeEntity: jest.Mock;
let getMainPane: jest.Mock;
let getPaneById: jest.Mock;
let addPane: jest.Mock;
let symbolId: jest.Mock;
let symbol: jest.Mock;
beforeEach(() => {
jest.clearAllMocks();
IndicatorMock.mockImplementation(
(params) =>
({
id: params.id,
destroy: jest.fn(),
getConfig: jest.fn(() => params.config),
getIndicatorType: jest.fn(() => params.type),
}) as unknown as Indicator,
);
mainPane = {
getId: jest.fn(() => 0),
} as unknown as Pane;
getMainPane = jest.fn(() => mainPane);
getPaneById = jest.fn();
addPane = jest.fn(() => mainPane);
symbolId = jest.fn(() => of('SBER'));
symbol = jest.fn(() => of('SBER'));
setEntity = jest.fn((factory: EntityFactory) => factory(1, jest.fn(), jest.fn()));
removeEntity = jest.fn();
const paneManager = {
getMainPane,
getPaneById,
addPane,
} as unknown as PaneManager;
const eventManager = {
symbolId,
symbol,
} as unknown as EventManager;
const DOM = {
setEntity,
removeEntity,
} as unknown as DOMModel;
manager = new IndicatorManager({
eventManager,
dataSource: {} as DataSource,
lwcChart: {} as IChartApi,
paneManager,
DOM,
});
});
it('не должен добавлять индикатор без типа', () => {
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
manager.addIndicator({});
expect(setEntity).not.toHaveBeenCalled();
expect(manager.getIndicators().value.size).toBe(0);
consoleSpy.mockRestore();
});
it('должен добавлять индикатор', () => {
manager.addIndicator({
id: 'ema-1',
indicatorType: IndicatorsIds.EMA,
});
expect(manager.getIndicators().value.has('ema-1')).toBe(true);
expect(getMainPane).toHaveBeenCalledTimes(1);
expect(IndicatorMock).toHaveBeenCalledTimes(1);
});
it('должен публиковать добавленные индикаторы', () => {
let entities: Indicator[] = [];
const subscription = manager.entities().subscribe((value) => {
entities = value;
});
manager.addIndicator({
id: 'ema-1',
indicatorType: IndicatorsIds.EMA,
});
expect(entities).toHaveLength(1);
expect(entities[0]).toBe(manager.getIndicators().value.get('ema-1'));
subscription.unsubscribe();
});
it('должен использовать pane из snapshot', () => {
const pane = {
getId: jest.fn(() => 3),
} as unknown as Pane;
getPaneById.mockReturnValue(pane);
manager.addIndicator({
id: 'ema-1',
indicatorType: IndicatorsIds.EMA,
paneId: 3,
});
expect(getPaneById).toHaveBeenCalledWith(3);
expect(addPane).not.toHaveBeenCalled();
});
it('должен создать pane если pane из snapshot отсутствует', () => {
getPaneById.mockReturnValue(undefined);
manager.addIndicator({
id: 'ema-1',
indicatorType: IndicatorsIds.EMA,
paneId: 3,
});
expect(getPaneById).toHaveBeenCalledWith(3);
expect(addPane).toHaveBeenCalledTimes(1);
});
it('должен удалить индикатор из состояния и DOM', () => {
manager.addIndicator({
id: 'ema-1',
indicatorType: IndicatorsIds.EMA,
});
const indicator = manager.getIndicators().value.get('ema-1');
if (!indicator) {
throw new Error('Индикатор не был добавлен');
}
manager.deleteIndicator('ema-1');
expect(manager.getIndicators().value.has('ema-1')).toBe(false);
expect(removeEntity).toHaveBeenCalledWith(indicator);
expect(indicator.destroy).toHaveBeenCalledTimes(1);
});
it('не должен удалять несуществующий индикатор', () => {
manager.deleteIndicator('unknown');
expect(removeEntity).not.toHaveBeenCalled();
});
});
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;
beforeEach(() => {
jest.clearAllMocks();
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.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);
});
});