Загрузка данных
- Исправлено применение начального интервала при открытии графика
import { BehaviorSubject, Subscription } from 'rxjs';
import { Chart } from '@core/Chart';
import { Intervals } from '@src/types/intervals';
interface ChartInternals {
eventManager: {
symbolId: () => BehaviorSubject<string>;
getInterval: () => BehaviorSubject<Intervals | null>;
};
compareManager: {
itemsObs: () => BehaviorSubject<{ symbolId: string }[]>;
};
dataSource: {
setSymbols: jest.Mock;
loadTill: jest.Mock;
loadAllHistory: jest.Mock;
getOldestTime: jest.Mock;
getLastCandle: jest.Mock;
};
lwcChart: {
timeScale: () => {
getVisibleRange: jest.Mock;
setVisibleRange: jest.Mock;
};
};
subscriptions: Subscription;
activeSymbolIds: string[];
currentInterval: Intervals | null;
setupDataSourceSubs: () => void;
}
interface ChartTestContext {
chart: ChartInternals;
symbolId$: BehaviorSubject<string>;
interval$: BehaviorSubject<Intervals | null>;
compareItems$: BehaviorSubject<{ symbolId: string }[]>;
setSymbols: jest.Mock;
loadTill: jest.Mock;
loadAllHistory: jest.Mock;
getOldestTime: jest.Mock;
getLastCandle: jest.Mock;
getVisibleRange: jest.Mock;
setVisibleRange: jest.Mock;
}
interface CreateChartOptions {
symbolId?: string;
interval?: Intervals | null;
compareSymbolIds?: string[];
}
interface Deferred<T> {
promise: Promise<T>;
resolve: (value: T | PromiseLike<T>) => void;
}
function createDeferred<T>(): Deferred<T> {
let resolve!: Deferred<T>['resolve'];
const promise = new Promise<T>((resolver) => {
resolve = resolver;
});
return {
promise,
resolve,
};
}
async function flushAsyncOperations(): Promise<void> {
await Promise.resolve();
await Promise.resolve();
jest.runOnlyPendingTimers();
await Promise.resolve();
}
describe('Chart', () => {
function createChartInternals({
symbolId = '',
interval = null,
compareSymbolIds = ['SBER', 'SBER'],
}: CreateChartOptions = {}): ChartTestContext {
const symbolId$ = new BehaviorSubject(symbolId);
const interval$ = new BehaviorSubject<Intervals | null>(interval);
const compareItems$ = new BehaviorSubject(compareSymbolIds.map((value) => ({ symbolId: value })));
const setSymbols = jest.fn();
const loadTill = jest.fn(() => Promise.resolve());
const loadAllHistory = jest.fn(() => Promise.resolve());
const getOldestTime = jest.fn(() => 100);
const getLastCandle = jest.fn(() => ({ time: 200 }));
const getVisibleRange = jest.fn(() => null);
const setVisibleRange = jest.fn();
const chart = Object.assign(Object.create(Chart.prototype), {
eventManager: {
symbolId: () => symbolId$,
getInterval: () => interval$,
},
compareManager: {
itemsObs: () => compareItems$,
},
dataSource: {
setSymbols,
loadTill,
loadAllHistory,
getOldestTime,
getLastCandle,
},
lwcChart: {
timeScale: () => ({
getVisibleRange,
setVisibleRange,
}),
},
subscriptions: new Subscription(),
activeSymbolIds: [],
currentInterval: null,
}) as ChartInternals;
return {
chart,
symbolId$,
interval$,
compareItems$,
setSymbols,
loadTill,
loadAllHistory,
getOldestTime,
getLastCandle,
getVisibleRange,
setVisibleRange,
};
}
beforeEach(() => {
jest.useFakeTimers();
jest.spyOn(window, 'requestAnimationFrame').mockImplementation((callback) => {
return window.setTimeout(() => callback(performance.now()), 16);
});
jest.spyOn(window, 'cancelAnimationFrame').mockImplementation((id) => {
window.clearTimeout(id);
});
});
afterEach(() => {
jest.clearAllTimers();
jest.useRealTimers();
jest.restoreAllMocks();
});
it('должен исключать пустой основной symbolId и удалять дубликаты', () => {
const { chart, setSymbols } = createChartInternals();
chart.setupDataSourceSubs();
expect(setSymbols).toHaveBeenLastCalledWith(['SBER']);
chart.subscriptions.unsubscribe();
});
it('должен зарегистрировать символ до загрузки данных начального интервала', () => {
const { chart, setSymbols, loadTill } = createChartInternals({
symbolId: 'SBER',
compareSymbolIds: [],
interval: Intervals['1Y'],
});
chart.setupDataSourceSubs();
expect(setSymbols).toHaveBeenCalledWith(['SBER']);
expect(loadTill).toHaveBeenCalledTimes(1);
expect(setSymbols.mock.invocationCallOrder[0]).toBeLessThan(loadTill.mock.invocationCallOrder[0]);
chart.subscriptions.unsubscribe();
});
it('должен применить начальный интервал после загрузки данных', async () => {
const { chart, setVisibleRange } = createChartInternals({
symbolId: 'SBER',
compareSymbolIds: [],
interval: Intervals['1Y'],
});
chart.setupDataSourceSubs();
await flushAsyncOperations();
expect(setVisibleRange).toHaveBeenCalledTimes(1);
expect(setVisibleRange).toHaveBeenCalledWith({
from: expect.any(Number),
to: expect.any(Number),
});
chart.subscriptions.unsubscribe();
});
it('не должен применять интервал, если для инструментов нет данных', async () => {
const { chart, getLastCandle, setVisibleRange } = createChartInternals({
symbolId: 'SBER',
compareSymbolIds: [],
interval: Intervals['1Y'],
});
getLastCandle.mockReturnValue(null);
chart.setupDataSourceSubs();
await flushAsyncOperations();
expect(setVisibleRange).not.toHaveBeenCalled();
chart.subscriptions.unsubscribe();
});
it('не должен применять устаревший интервал после выбора нового', async () => {
const firstLoad = createDeferred<void>();
const secondLoad = createDeferred<void>();
const { chart, interval$, loadTill, setVisibleRange } = createChartInternals({
symbolId: 'SBER',
compareSymbolIds: [],
interval: Intervals['1Y'],
});
loadTill
.mockImplementationOnce(() => firstLoad.promise)
.mockImplementationOnce(() => secondLoad.promise);
chart.setupDataSourceSubs();
interval$.next(Intervals['1M']);
secondLoad.resolve();
await flushAsyncOperations();
expect(setVisibleRange).toHaveBeenCalledTimes(1);
firstLoad.resolve();
await flushAsyncOperations();
expect(setVisibleRange).toHaveBeenCalledTimes(1);
chart.subscriptions.unsubscribe();
});
it('должен загрузить всю историю и применить диапазон для интервала All', async () => {
const {
chart,
loadAllHistory,
getOldestTime,
getLastCandle,
setVisibleRange,
} = createChartInternals({
symbolId: 'SBER',
compareSymbolIds: ['GAZP'],
interval: Intervals.All,
});
getOldestTime.mockImplementation((symbolId: string) => {
return symbolId === 'SBER' ? 100 : 50;
});
getLastCandle.mockImplementation((symbolId: string) => {
return {
time: symbolId === 'SBER' ? 200 : 300,
};
});
chart.setupDataSourceSubs();
await flushAsyncOperations();
expect(loadAllHistory).toHaveBeenCalledTimes(2);
expect(loadAllHistory).toHaveBeenCalledWith('SBER');
expect(loadAllHistory).toHaveBeenCalledWith('GAZP');
expect(setVisibleRange).toHaveBeenCalledWith({
from: 50,
to: 300,
});
chart.subscriptions.unsubscribe();
});
it('не должен применять диапазон после отписки', async () => {
const loading = createDeferred<void>();
const { chart, loadTill, setVisibleRange } = createChartInternals({
symbolId: 'SBER',
compareSymbolIds: [],
interval: Intervals['1Y'],
});
loadTill.mockReturnValue(loading.promise);
chart.setupDataSourceSubs();
chart.subscriptions.unsubscribe();
loading.resolve();
await flushAsyncOperations();
expect(setVisibleRange).not.toHaveBeenCalled();
});
});