Загрузка данных
import { SliderPaneRenderer } from '@core/Drawings/sliderPosition/paneRenderer';
import { SliderPaneView } from '@core/Drawings/sliderPosition/paneView';
import { createDefaultSettings, getSliderPositionSettingsTabs } from '@core/Drawings/sliderPosition/settings';
import { SliderPosition, type SliderRenderData } from '@core/Drawings/sliderPosition/sliderPosition';
import { createCanvasTarget, createDrawingTestEnvironment, createPointerEvent } from '../drawingTestUtils';
import type { DrawingHandle } from '@core/Drawings/handles';
import type { AxisLabel, AxisSegment } from '@core/Drawings/types';
import type { PrimitiveHoveredItem, Time } from 'lightweight-charts';
jest.mock('@src/theme', () => ({
getThemeStore: () => ({
colors: {
chartCrosshairLine: '#999999',
sliderPositiveFill: '#00aa0044',
sliderNegativeFill: '#aa000044',
chartPriceLineText: '#ffffff',
chartCandleUp: '#00aa00',
chartCandleDown: '#aa0000',
axisMarkerAreaFill: '#2962ff44',
axisMarkerLabelFill: '#2962ff',
axisMarkerLabelDefaultFill: '#777777',
axisMarkerLabelPositiveFill: '#00aa00',
axisMarkerLabelNegativeFill: '#aa0000',
},
}),
}));
jest.mock('@src/translations', () => ({ t: (value: string) => value }));
jest.mock('@src/types/defaults', () => ({
Defaults: {
dateFormat: 'date',
timeFormat: '24h',
showTime: true,
},
}));
jest.mock('@src/utils', () => ({
formatPrice: (value: number) => String(value),
formatPercent: (value: number) => `${value}%`,
formatSignedNumber: (value: number) => String(value),
}));
jest.mock('@src/utils/formatter', () => ({
formatDate: (value: number) => `date:${value}`,
}));
class TestSliderPosition extends SliderPosition {
public handles(): readonly DrawingHandle[] {
return this.getDrawingHandles();
}
public timeSegments(): AxisSegment[] {
return this.getTimeAxisSegments();
}
public priceSegments(): AxisSegment[] {
return this.getPriceAxisSegments();
}
public timeLabel(kind: string): AxisLabel | null {
return this.getTimeAxisLabel(kind);
}
public priceLabel(kind: string): AxisLabel | null {
return this.getPriceAxisLabel(kind);
}
public hovered(x: number, y: number): PrimitiveHoveredItem | null {
return this.getHoveredItem(x, y);
}
public pointerDown(event: PointerEvent): void {
this.handlePointerDown(event);
}
public pointerMove(event: PointerEvent): void {
this.handlePointerMove(event);
}
public pointerUp(event: PointerEvent): void {
this.handlePointerUp(event);
}
}
function createSlider(side: 'long' | 'short' = 'long', selected = false) {
const seriesData = [
{ time: 10 as Time, close: 100 },
{ time: 100 as Time, close: 110 },
];
const env = createDrawingTestEnvironment({ selected, seriesData });
const removeSelf = jest.fn();
const openSettings = jest.fn();
const slider = new TestSliderPosition({
...env,
side,
removeSelf,
openSettings,
});
return { ...env, slider, removeSelf, openSettings };
}
function setReady(slider: TestSliderPosition): void {
slider.setState({
mode: 'ready',
startTime: 10 as Time,
endTime: 100 as Time,
entryPrice: 100,
stopPrice: 90,
targetPrice: 120,
amount: 1000,
tickSize: 1,
});
}
describe('SliderPosition', () => {
it('считает Risk/Reward Ratio', () => {
const { slider } = createSlider();
setReady(slider);
expect(slider.getState().riskRewardRatio).toBe(2);
});
it('поддерживает RR = 0', () => {
const { slider } = createSlider();
slider.setState({
mode: 'ready',
startTime: 10 as Time,
endTime: 100 as Time,
entryPrice: 100,
stopPrice: 90,
riskRewardRatio: 0,
});
expect(slider.getState()).toMatchObject({
targetPrice: 100,
riskRewardRatio: 0,
});
});
it('возвращает RR = 0 когда Target совпадает с Entry', () => {
const { slider } = createSlider();
slider.setState({
mode: 'ready',
startTime: 10 as Time,
endTime: 100 as Time,
entryPrice: 100,
stopPrice: 90,
targetPrice: 100,
});
expect(slider.getState().riskRewardRatio).toBe(0);
});
it('возвращает RR = 0 когда Stop совпадает с Entry', () => {
const { slider } = createSlider();
slider.setState({
mode: 'ready',
startTime: 10 as Time,
endTime: 100 as Time,
entryPrice: 100,
stopPrice: 100,
targetPrice: 120,
});
expect(slider.getState().riskRewardRatio).toBe(0);
});
it('возвращает четыре handles в правильном приоритетном порядке', () => {
const { slider } = createSlider();
setReady(slider);
expect(slider.handles()).toEqual([
{ id: 'target', x: 10, y: 120 },
{ id: 'stop', x: 10, y: 90 },
{ id: 'end', x: 100, y: 100 },
{ id: 'entry', x: 10, y: 100, shape: 'circle' },
]);
});
it('Entry handle имеет приоритет над Target при совпадении', () => {
const { slider, container } = createSlider('long', true);
slider.setState({
mode: 'ready',
startTime: 10 as Time,
endTime: 100 as Time,
entryPrice: 100,
stopPrice: 90,
targetPrice: 100,
});
slider.pointerDown(createPointerEvent(container, 10, 100));
slider.pointerMove(createPointerEvent(container, 10, 95));
expect(slider.getState()).toMatchObject({
entryPrice: 95,
targetPrice: 100,
});
});
it('Entry handle имеет приоритет над Stop при совпадении', () => {
const { slider, container } = createSlider('long', true);
slider.setState({
mode: 'ready',
startTime: 10 as Time,
endTime: 100 as Time,
entryPrice: 100,
stopPrice: 100,
targetPrice: 120,
});
slider.pointerDown(createPointerEvent(container, 10, 100));
slider.pointerMove(createPointerEvent(container, 10, 110));
expect(slider.getState()).toMatchObject({
entryPrice: 110,
stopPrice: 100,
});
});
it('Entry нельзя вывести за Stop и Target', () => {
const { slider, container } = createSlider('long', true);
setReady(slider);
slider.pointerDown(createPointerEvent(container, 10, 100));
slider.pointerMove(createPointerEvent(container, 10, 200));
expect(slider.getState().entryPrice).toBe(120);
});
it('Stop можно схлопнуть с Entry', () => {
const { slider, container } = createSlider('long', true);
setReady(slider);
slider.pointerDown(createPointerEvent(container, 10, 90));
slider.pointerMove(createPointerEvent(container, 10, 100));
expect(slider.getState().stopPrice).toBe(100);
});
it('Target можно схлопнуть с Entry', () => {
const { slider, container } = createSlider('long', true);
setReady(slider);
slider.pointerDown(createPointerEvent(container, 10, 120));
slider.pointerMove(createPointerEvent(container, 10, 100));
expect(slider.getState().targetPrice).toBe(100);
});
it('работает для short позиции', () => {
const { slider } = createSlider('short');
slider.setState({
mode: 'ready',
startTime: 10 as Time,
endTime: 100 as Time,
entryPrice: 100,
stopPrice: 110,
targetPrice: 80,
});
expect(slider.getState().riskRewardRatio).toBe(2);
expect(slider.getRenderData()).toMatchObject({
targetLabelDirection: 'down',
stopLabelDirection: 'up',
});
});
it('возвращает selected axis areas', () => {
const { slider } = createSlider('long', true);
setReady(slider);
expect(slider.timeSegments()).toEqual([{ from: 10, to: 100, color: '#2962ff44' }]);
expect(slider.priceSegments()).toEqual([
{ from: 100, to: 120, color: '#2962ff44' },
{ from: 90, to: 100, color: '#2962ff44' },
]);
});
it('использует normal zOrder для selected axis pane areas', () => {
const { slider } = createSlider();
expect(slider.timeAxisPaneViews()[0].zOrder?.()).toBe('normal');
expect(slider.priceAxisPaneViews()[0].zOrder?.()).toBe('normal');
});
it('time labels показываются только selected drawing', () => {
const { slider } = createSlider();
setReady(slider);
expect(slider.timeLabel('start')).toBeNull();
});
it('price labels доступны независимо от selection', () => {
const { slider } = createSlider();
setReady(slider);
expect(slider.priceLabel('target')).toMatchObject({
coordinate: 120,
text: '120',
backgroundColor: '#00aa00',
});
expect(slider.priceLabel('entry')).toMatchObject({
coordinate: 100,
backgroundColor: '#777777',
});
expect(slider.priceLabel('stop')).toMatchObject({
coordinate: 90,
backgroundColor: '#aa0000',
});
});
it('возвращает правильные cursors', () => {
const { slider } = createSlider('long', true);
setReady(slider);
expect(slider.hovered(10, 120)?.cursorStyle).toBe('ns-resize');
expect(slider.hovered(10, 90)?.cursorStyle).toBe('ns-resize');
expect(slider.hovered(100, 100)?.cursorStyle).toBe('ew-resize');
expect(slider.hovered(10, 100)?.cursorStyle).toBe('grab');
});
it('выбирает unselected slider по body', () => {
const { slider, interaction, container } = createSlider();
setReady(slider);
slider.pointerDown(createPointerEvent(container, 50, 100));
expect(interaction.select).toHaveBeenCalledTimes(1);
});
it('изменяет end time', () => {
const { slider, container } = createSlider('long', true);
setReady(slider);
slider.pointerDown(createPointerEvent(container, 100, 100));
slider.pointerMove(createPointerEvent(container, 150, 100));
expect(slider.getState().endTime).toBe(150);
});
});
describe('SliderPaneRenderer', () => {
it('не рисует без data', () => {
const { target, useBitmapCoordinateSpace } = createCanvasTarget();
new SliderPaneRenderer({
getRenderData: () => null,
} as unknown as SliderPosition).draw(target);
expect(useBitmapCoordinateSpace).not.toHaveBeenCalled();
});
it('рисует profit/loss и labels', () => {
const data: SliderRenderData = {
startX: 10,
endX: 100,
leftX: 10,
rightX: 100,
entryY: 100,
stopY: 120,
targetY: 80,
entryPrice: 100,
stopPrice: 90,
targetPrice: 120,
profitTop: 80,
profitBottom: 100,
lossTop: 100,
lossBottom: 120,
targetText: 'target',
centerText: 'center',
stopText: 'stop',
centerBoxColor: '#0a0',
targetLabelDirection: 'up',
stopLabelDirection: 'down',
showLabels: true,
lineColor: '#fff',
positiveFillColor: '#0f03',
negativeFillColor: '#f003',
textColor: '#fff',
fontSize: 10,
isBold: false,
isItalic: false,
};
const { target, context } = createCanvasTarget();
new SliderPaneRenderer({
getRenderData: () => data,
} as unknown as SliderPosition).draw(target);
expect(context.fillRect).toHaveBeenCalledTimes(2);
expect(context.fillText).toHaveBeenCalled();
expect(context.stroke).toHaveBeenCalled();
});
});
describe('SliderPaneView', () => {
it('возвращает renderer и top', () => {
const view = new SliderPaneView({} as SliderPosition);
expect(view.renderer()).toBeInstanceOf(SliderPaneRenderer);
expect(view.zOrder()).toBe('top');
});
});
describe('SliderPosition settings', () => {
it('создаёт defaults', () => {
expect(createDefaultSettings()).toEqual({
lineColor: '#999999',
positiveFillColor: '#00aa0044',
negativeFillColor: '#aa000044',
textColor: '#ffffff',
fontSize: 10,
isBold: false,
isItalic: false,
});
});
it('создаёт style и text tabs', () => {
expect(getSliderPositionSettingsTabs(createDefaultSettings()).map(({ key }) => key)).toEqual(['style', 'text']);
});
});