Загрузка данных
import { BehaviorSubject } from 'rxjs';
import {
FibonacciRetracement,
FibonacciRetracementRenderData,
} from '@core/Drawings/fibonacciRetracement/fibonacciRetracement';
import { FibonacciRetracementPaneRenderer } from '@core/Drawings/fibonacciRetracement/paneRenderer';
import { FibonacciRetracementPaneView } from '@core/Drawings/fibonacciRetracement/paneView';
import {
cloneFibonacciRetracementSettings,
createDefaultSettings,
formatLevelLabel,
getFibonacciRetracementSettingsTabs,
getFibonacciRetracementSettingsValues,
getVisibleFibonacciLevels,
mergeFibonacciRetracementSettings,
} from '@core/Drawings/fibonacciRetracement/settings';
import type { DrawingHandle } from '@core/Drawings/handles';
import type { DrawingInteraction } from '@core/Drawings/SeriesDrawingBase';
import type { AxisLabel, AxisSegment, SeriesApi } from '@core/Drawings/types';
import type { CanvasRenderingTarget2D } from 'fancy-canvas';
import type { Coordinate, IChartApi, Logical, PrimitiveHoveredItem, Time } from 'lightweight-charts';
jest.mock('@src/theme', () => ({
getThemeStore: () => ({
colors: {
chartBackground: '#101010',
chartLineColor: '#2962ff',
chartCrosshairLine: '#999999',
axisMarkerAreaFill: '#2962ff44',
axisMarkerLabelFill: '#2962ff',
chartPriceLineText: '#ffffff',
},
}),
}));
jest.mock('@src/translations', () => ({
t: (key: string) => key,
}));
jest.mock('@src/types/defaults', () => ({
Defaults: {
dateFormat: 'date',
timeFormat: '24h',
showTime: true,
},
}));
jest.mock('@src/utils', () => ({
formatPrice: (value: number) => `price:${value}`,
}));
jest.mock('@src/utils/formatter', () => ({
formatDate: (value: number) => `date:${value}`,
}));
class TestFibonacciRetracement extends FibonacciRetracement {
public getHandles(): readonly DrawingHandle[] {
return this.getDrawingHandles();
}
public getTimeSegments(): AxisSegment[] {
return this.getTimeAxisSegments();
}
public getPriceSegments(): AxisSegment[] {
return this.getPriceAxisSegments();
}
public getTimeLabel(kind: string): AxisLabel | null {
return this.getTimeAxisLabel(kind);
}
public getPriceLabel(kind: string): AxisLabel | null {
return this.getPriceAxisLabel(kind);
}
public getHovered(x: number, y: number): PrimitiveHoveredItem | null {
return this.getHoveredItem(x, y);
}
public getGeometryForTest() {
return this.getGeometry();
}
public move(event: PointerEvent): void {
this.handlePointerMove(event);
}
public up(event: PointerEvent): void {
this.handlePointerUp(event);
}
}
function createEnvironment(selected = false) {
let selectedValue = selected;
let lockedValue = false;
const selected$ = new BehaviorSubject(selected);
const locked$ = new BehaviorSubject(false);
const interaction: DrawingInteraction = {
selected$,
locked$,
isSelected: jest.fn(() => selectedValue),
isLocked: jest.fn(() => lockedValue),
select: jest.fn(() => {
selectedValue = true;
selected$.next(true);
}),
deselect: jest.fn(() => {
selectedValue = false;
selected$.next(false);
}),
};
const container = document.createElement('div');
jest.spyOn(container, 'getBoundingClientRect').mockReturnValue({
x: 0,
y: 0,
left: 0,
top: 0,
right: 400,
bottom: 300,
width: 400,
height: 300,
toJSON: jest.fn(),
});
const timeScale = {
coordinateToTime: jest.fn((coordinate: Coordinate): Time | null => Number(coordinate) as Time),
timeToCoordinate: jest.fn((time: Time): Coordinate | null => Number(time) as Coordinate),
logicalToCoordinate: jest.fn((logical: Logical): Coordinate | null => Number(logical) as Coordinate),
};
const pane = {
paneIndex: jest.fn(() => 0),
getHeight: jest.fn(() => 300),
};
const chart = {
timeScale: jest.fn(() => timeScale),
applyOptions: jest.fn(),
panes: jest.fn(() => [pane]),
} as unknown as IChartApi;
const series = {
coordinateToPrice: jest.fn((coordinate: Coordinate): number | null => Number(coordinate)),
priceToCoordinate: jest.fn((price: number): Coordinate | null => price as Coordinate),
data: jest.fn(() => []),
attachPrimitive: jest.fn(),
detachPrimitive: jest.fn(),
getPane: jest.fn(() => pane),
} as unknown as SeriesApi;
return {
chart,
series,
container,
interaction,
setSelected(value: boolean) {
selectedValue = value;
selected$.next(value);
},
setLocked(value: boolean) {
lockedValue = value;
locked$.next(value);
},
};
}
function createDrawing(selected = false) {
const env = createEnvironment(selected);
const removeSelf = jest.fn();
const openSettings = jest.fn();
const drawing = new TestFibonacciRetracement({
chart: env.chart,
series: env.series,
container: env.container,
interaction: env.interaction,
removeSelf,
openSettings,
});
return { ...env, drawing, removeSelf, openSettings };
}
function setReady(drawing: TestFibonacciRetracement) {
drawing.setState({
mode: 'ready',
startTime: 50 as Time,
endTime: 150 as Time,
startPrice: 50,
endPrice: 150,
});
}
function createPointerEvent(
target: Node,
x: number,
y: number,
pointerId = 1,
button = 0,
): PointerEvent {
return {
target,
clientX: x,
clientY: y,
pointerId,
button,
preventDefault: jest.fn(),
stopPropagation: jest.fn(),
} as unknown as PointerEvent;
}
function createMouseEvent(target: Node, x: number, y: number): MouseEvent {
return {
target,
clientX: x,
clientY: y,
preventDefault: jest.fn(),
stopPropagation: jest.fn(),
} as unknown as MouseEvent;
}
function createRendererDrawing(data: FibonacciRetracementRenderData | null): FibonacciRetracement {
return {
getRenderData: () => data,
} as unknown as FibonacciRetracement;
}
function createRenderData(): FibonacciRetracementRenderData {
return {
startPoint: { x: 10, y: 20 },
endPoint: { x: 90, y: 80 },
left: 10,
right: 90,
top: 20,
bottom: 80,
width: 80,
height: 60,
levels: [
{
id: '0',
value: 0,
price: 100,
y: 20,
color: '#ff0000',
text: '0 (100)',
},
{
id: '1',
value: 1,
price: 50,
y: 80,
color: '#00ff00',
text: '1 (50)',
},
],
areas: [
{
top: 20,
bottom: 80,
color: '#00ff00',
},
],
showBackground: true,
backgroundOpacity: 0.18,
reverse: false,
labelsPosition: 'right',
showPrices: true,
showLevelValues: true,
fontSize: 12,
isBold: false,
isItalic: false,
};
}
function createCanvasTarget() {
const context = {
save: jest.fn(),
restore: jest.fn(),
beginPath: jest.fn(),
moveTo: jest.fn(),
lineTo: jest.fn(),
stroke: jest.fn(),
fillRect: jest.fn(),
fillText: jest.fn(),
setLineDash: jest.fn(),
fillStyle: '',
strokeStyle: '',
globalAlpha: 1,
lineWidth: 0,
font: '',
textAlign: '',
textBaseline: '',
};
const useBitmapCoordinateSpace = jest.fn(
(
callback: (scope: {
context: CanvasRenderingContext2D;
horizontalPixelRatio: number;
verticalPixelRatio: number;
}) => void,
) => {
callback({
context: context as unknown as CanvasRenderingContext2D,
horizontalPixelRatio: 2,
verticalPixelRatio: 2,
});
},
);
return {
context,
target: { useBitmapCoordinateSpace } as unknown as CanvasRenderingTarget2D,
useBitmapCoordinateSpace,
};
}
describe('FibonacciRetracement', () => {
it('должен прикреплять primitive при создании', () => {
const { drawing, series } = createDrawing();
expect(series.attachPrimitive).toHaveBeenCalledWith(drawing);
});
it('должен начинаться в idle', () => {
const { drawing } = createDrawing();
expect(drawing.isCreationPending()).toBe(true);
expect(drawing.getState()).toMatchObject({
hidden: false,
mode: 'idle',
startTime: null,
endTime: null,
startPrice: null,
endPrice: null,
});
});
it('должен создавать retracement двумя нажатиями', () => {
const { drawing, container } = createDrawing();
drawing.pointerDown(createPointerEvent(container, 50, 50));
drawing.pointerDown(createPointerEvent(container, 150, 150));
expect(drawing.getState()).toMatchObject({
mode: 'ready',
startTime: 50,
endTime: 150,
startPrice: 50,
endPrice: 150,
});
});
it('должен удалять слишком маленький retracement', () => {
const { drawing, container, removeSelf } = createDrawing();
drawing.pointerDown(createPointerEvent(container, 50, 50));
drawing.pointerDown(createPointerEvent(container, 53, 53));
expect(removeSelf).toHaveBeenCalledTimes(1);
});
it('должен возвращать start и end handles', () => {
const { drawing } = createDrawing();
setReady(drawing);
expect(drawing.getHandles()).toEqual([
{
id: 'end',
x: 150,
y: 150,
},
{
id: 'start',
x: 50,
y: 50,
},
]);
});
it('должен рассчитывать fibonacci levels', () => {
const { drawing } = createDrawing();
setReady(drawing);
const data = drawing.getRenderData();
expect(data?.levels).toHaveLength(11);
expect(data?.levels.find((level) => level.id === '0')?.price).toBe(150);
expect(data?.levels.find((level) => level.id === '1')?.price).toBe(50);
expect(data?.backgroundOpacity).toBe(0.18);
});
it('должен учитывать reverse', () => {
const { drawing } = createDrawing();
setReady(drawing);
drawing.updateSettings({ reverse: true });
const data = drawing.getRenderData();
expect(data?.levels.find((level) => level.id === '0')?.price).toBe(50);
expect(data?.levels.find((level) => level.id === '1')?.price).toBe(150);
});
it('должен возвращать axis segments для выбранного retracement', () => {
const { drawing } = createDrawing(true);
setReady(drawing);
const data = drawing.getRenderData();
expect(drawing.getTimeSegments()).toEqual([
{
from: 50,
to: 150,
color: '#2962ff44',
},
]);
expect(drawing.getPriceSegments()).toEqual([
{
from: data?.top,
to: data?.bottom,
color: '#2962ff44',
},
]);
});
it('не должен показывать axis segments у невыбранного ready retracement', () => {
const { drawing } = createDrawing();
setReady(drawing);
expect(drawing.getTimeSegments()).toEqual([]);
expect(drawing.getPriceSegments()).toEqual([]);
});
it('должен возвращать time axis labels', () => {
const { drawing } = createDrawing(true);
setReady(drawing);
expect(drawing.getTimeLabel('start')).toEqual({
coordinate: 50,
text: 'date:50',
textColor: '#ffffff',
backgroundColor: '#2962ff',
});
expect(drawing.getTimeLabel('end')).toEqual({
coordinate: 150,
text: 'date:150',
textColor: '#ffffff',
backgroundColor: '#2962ff',
});
});
it('не должен возвращать неизвестные axis labels', () => {
const { drawing } = createDrawing(true);
setReady(drawing);
expect(drawing.getTimeLabel('unknown')).toBeNull();
expect(drawing.getPriceLabel('unknown')).toBeNull();
});
it('должен возвращать pointer для выбранного handle', () => {
const { drawing } = createDrawing(true);
setReady(drawing);
expect(drawing.getHovered(50, 50)).toEqual({
cursorStyle: 'pointer',
externalId: 'fibonacci-retracement-position',
zOrder: 'top',
});
});
it('должен выбирать retracement при нажатии на него', () => {
const { drawing, container, interaction } = createDrawing();
setReady(drawing);
drawing.pointerDown(createPointerEvent(container, 100, 100));
expect(interaction.select).toHaveBeenCalledTimes(1);
});
it('должен перемещать start handle', () => {
const { drawing, container } = createDrawing(true);
setReady(drawing);
drawing.pointerDown(createPointerEvent(container, 50, 50, 1));
drawing.move(createPointerEvent(container, 70, 80, 1));
drawing.up(createPointerEvent(container, 70, 80, 1));
expect(drawing.getState()).toMatchObject({
mode: 'ready',
startTime: 70,
startPrice: 80,
endTime: 150,
endPrice: 150,
});
});
it('должен перемещать end handle', () => {
const { drawing, container } = createDrawing(true);
setReady(drawing);
drawing.pointerDown(createPointerEvent(container, 150, 150, 1));
drawing.move(createPointerEvent(container, 180, 170, 1));
drawing.up(createPointerEvent(container, 180, 170, 1));
expect(drawing.getState()).toMatchObject({
startTime: 50,
startPrice: 50,
endTime: 180,
endPrice: 170,
});
});
it('должен открывать настройки по double click', () => {
const { drawing, container, openSettings } = createDrawing();
setReady(drawing);
drawing.doubleClick(createMouseEvent(container, 100, 100));
expect(openSettings).toHaveBeenCalledTimes(1);
});
it('не должен возвращать render data когда скрыт', () => {
const { drawing } = createDrawing();
setReady(drawing);
drawing.hide();
expect(drawing.getRenderData()).toBeNull();
});
});
describe('FibonacciRetracementPaneRenderer', () => {
it('не должен рисовать без render data', () => {
const { target, useBitmapCoordinateSpace } = createCanvasTarget();
const renderer = new FibonacciRetracementPaneRenderer(createRendererDrawing(null));
renderer.draw(target);
expect(useBitmapCoordinateSpace).not.toHaveBeenCalled();
});
it('должен рисовать уровни, background и control line', () => {
const { context, target } = createCanvasTarget();
const renderer = new FibonacciRetracementPaneRenderer(createRendererDrawing(createRenderData()));
renderer.draw(target);
expect(context.fillRect).toHaveBeenCalled();
expect(context.stroke).toHaveBeenCalled();
expect(context.fillText).toHaveBeenCalledTimes(2);
expect(context.setLineDash).toHaveBeenCalled();
});
});
describe('FibonacciRetracementPaneView', () => {
it('должен возвращать renderer и top zOrder', () => {
const drawing = createRendererDrawing(null);
const view = new FibonacciRetracementPaneView(drawing);
expect(view.renderer()).toBeInstanceOf(FibonacciRetracementPaneRenderer);
expect(view.zOrder()).toBe('top');
});
});
describe('FibonacciRetracement settings', () => {
it('должен создавать независимые default settings', () => {
const first = createDefaultSettings();
const second = createDefaultSettings();
first.levels[0].visible = false;
expect(second.levels[0].visible).toBe(true);
});
it('должен глубоко клонировать settings', () => {
const settings = createDefaultSettings();
const clone = cloneFibonacciRetracementSettings(settings);
clone.levels[0].color = '#000000';
expect(settings.levels[0].color).not.toBe('#000000');
});
it('должен объединять обычные и level settings', () => {
const settings = createDefaultSettings();
const result = mergeFibonacciRetracementSettings(settings, {
reverse: true,
level_0_236_visible: false,
level_0_236_color: '#123456',
});
const level = result.levels.find((item) => item.id === '0.236');
expect(result.reverse).toBe(true);
expect(level?.visible).toBe(false);
expect(level?.color).toBe('#123456');
});
it('должен отдавать только visible levels', () => {
const settings = createDefaultSettings();
settings.levels[0].visible = false;
expect(getVisibleFibonacciLevels(settings)).toHaveLength(settings.levels.length - 1);
});
it('должен форматировать level label', () => {
expect(formatLevelLabel(0.618)).toBe('0,618');
expect(formatLevelLabel(1)).toBe('1');
});
it('должен преобразовывать settings в SettingsValues', () => {
const values = getFibonacciRetracementSettingsValues(createDefaultSettings());
expect(values).toMatchObject({
showBackground: true,
backgroundOpacity: 18,
reverse: false,
level_0_visible: true,
level_0_color: '#787B86',
});
});
it('должен создавать style, text и levels tabs', () => {
expect(getFibonacciRetracementSettingsTabs(createDefaultSettings()).map((tab) => tab.key)).toEqual([
'style',
'text',
'levels',
]);
});
});
import { BehaviorSubject } from 'rxjs';
import { LineDrawing, LineDrawingRenderData } from '@core/Drawings/line/lineDrawing';
import { LineDrawingPaneRenderer } from '@core/Drawings/line/paneRenderer';
import { LineDrawingPaneView } from '@core/Drawings/line/paneView';
import { createDefaultSettings, getLineDrawingSettingsTabs } from '@core/Drawings/line/settings';
import { LineMarker } from '@src/types';
import type { DrawingHandle } from '@core/Drawings/handles';
import type { DrawingInteraction } from '@core/Drawings/SeriesDrawingBase';
import type { AxisLabel, AxisSegment, SeriesApi } from '@core/Drawings/types';
import type { CanvasRenderingTarget2D } from 'fancy-canvas';
import type { Coordinate, IChartApi, Logical, PrimitiveHoveredItem, Time } from 'lightweight-charts';
jest.mock('@src/theme', () => ({
getThemeStore: () => ({
colors: {
chartLineColor: '#2962ff',
axisMarkerAreaFill: '#2962ff44',
axisMarkerLabelFill: '#2962ff',
chartPriceLineText: '#ffffff',
},
}),
}));
jest.mock('@src/translations', () => ({
t: (key: string) => key,
}));
jest.mock('@src/types/defaults', () => ({
Defaults: {
dateFormat: 'date',
timeFormat: '24h',
showTime: true,
},
}));
jest.mock('@src/utils', () => ({
formatPrice: (value: number) => `price:${value}`,
}));
jest.mock('@src/utils/formatter', () => ({
formatDate: (value: number) => `date:${value}`,
}));
class TestLineDrawing extends LineDrawing {
public getHandles(): readonly DrawingHandle[] {
return this.getDrawingHandles();
}
public getTimeSegments(): AxisSegment[] {
return this.getTimeAxisSegments();
}
public getPriceSegments(): AxisSegment[] {
return this.getPriceAxisSegments();
}
public getTimeLabel(kind: string): AxisLabel | null {
return this.getTimeAxisLabel(kind);
}
public getPriceLabel(kind: string): AxisLabel | null {
return this.getPriceAxisLabel(kind);
}
public getHovered(x: number, y: number): PrimitiveHoveredItem | null {
return this.getHoveredItem(x, y);
}
public move(event: PointerEvent): void {
this.handlePointerMove(event);
}
public up(event: PointerEvent): void {
this.handlePointerUp(event);
}
}
function createEnvironment(selected = false) {
let selectedValue = selected;
const selected$ = new BehaviorSubject(selected);
const locked$ = new BehaviorSubject(false);
const interaction: DrawingInteraction = {
selected$,
locked$,
isSelected: jest.fn(() => selectedValue),
isLocked: jest.fn(() => false),
select: jest.fn(() => {
selectedValue = true;
selected$.next(true);
}),
deselect: jest.fn(() => {
selectedValue = false;
selected$.next(false);
}),
};
const container = document.createElement('div');
jest.spyOn(container, 'getBoundingClientRect').mockReturnValue({
x: 0,
y: 0,
left: 0,
top: 0,
right: 400,
bottom: 300,
width: 400,
height: 300,
toJSON: jest.fn(),
});
const timeScale = {
coordinateToTime: jest.fn((coordinate: Coordinate): Time | null => Number(coordinate) as Time),
timeToCoordinate: jest.fn((time: Time): Coordinate | null => Number(time) as Coordinate),
logicalToCoordinate: jest.fn((logical: Logical): Coordinate | null => Number(logical) as Coordinate),
};
const pane = {
paneIndex: jest.fn(() => 0),
getHeight: jest.fn(() => 300),
};
const chart = {
timeScale: jest.fn(() => timeScale),
applyOptions: jest.fn(),
panes: jest.fn(() => [pane]),
} as unknown as IChartApi;
const series = {
coordinateToPrice: jest.fn((coordinate: Coordinate): number | null => Number(coordinate)),
priceToCoordinate: jest.fn((price: number): Coordinate | null => price as Coordinate),
data: jest.fn(() => []),
attachPrimitive: jest.fn(),
detachPrimitive: jest.fn(),
getPane: jest.fn(() => pane),
} as unknown as SeriesApi;
return { chart, series, container, interaction };
}
function createDrawing(selected = false) {
const env = createEnvironment(selected);
const removeSelf = jest.fn();
const openSettings = jest.fn();
const drawing = new TestLineDrawing({
chart: env.chart,
series: env.series,
container: env.container,
interaction: env.interaction,
removeSelf,
openSettings,
});
return { ...env, drawing, removeSelf, openSettings };
}
function setReady(drawing: TestLineDrawing) {
drawing.setState({
mode: 'ready',
startAnchor: {
time: 50 as Time,
price: 50,
},
endAnchor: {
time: 150 as Time,
price: 150,
},
});
}
function pointer(target: Node, x: number, y: number, pointerId = 1): PointerEvent {
return {
target,
clientX: x,
clientY: y,
pointerId,
button: 0,
preventDefault: jest.fn(),
stopPropagation: jest.fn(),
} as unknown as PointerEvent;
}
function mouse(target: Node, x: number, y: number): MouseEvent {
return {
target,
clientX: x,
clientY: y,
preventDefault: jest.fn(),
stopPropagation: jest.fn(),
} as unknown as MouseEvent;
}
function rendererDrawing(data: LineDrawingRenderData | null): LineDrawing {
return {
getRenderData: () => data,
} as unknown as LineDrawing;
}
function renderData(): LineDrawingRenderData {
return {
startPoint: { x: 10, y: 20 },
endPoint: { x: 90, y: 80 },
left: 10,
right: 90,
top: 20,
bottom: 80,
lineColor: '#ff0000',
startMarker: LineMarker.arrow,
endMarker: LineMarker.arrow,
fontSize: 14,
text: 'Test',
isBold: true,
isItalic: true,
textColor: '#ffffff',
};
}
function canvasTarget() {
const context = {
save: jest.fn(),
restore: jest.fn(),
beginPath: jest.fn(),
moveTo: jest.fn(),
lineTo: jest.fn(),
stroke: jest.fn(),
translate: jest.fn(),
rotate: jest.fn(),
fillText: jest.fn(),
lineWidth: 0,
strokeStyle: '',
fillStyle: '',
font: '',
textAlign: '',
textBaseline: '',
};
const useBitmapCoordinateSpace = jest.fn(
(
callback: (scope: {
context: CanvasRenderingContext2D;
horizontalPixelRatio: number;
verticalPixelRatio: number;
}) => void,
) =>
callback({
context: context as unknown as CanvasRenderingContext2D,
horizontalPixelRatio: 2,
verticalPixelRatio: 2,
}),
);
return {
context,
target: { useBitmapCoordinateSpace } as unknown as CanvasRenderingTarget2D,
useBitmapCoordinateSpace,
};
}
describe('LineDrawing', () => {
it('должен прикреплять primitive', () => {
const { drawing, series } = createDrawing();
expect(series.attachPrimitive).toHaveBeenCalledWith(drawing);
});
it('должен создаваться двумя нажатиями', () => {
const { drawing, container } = createDrawing();
drawing.pointerDown(pointer(container, 50, 50));
drawing.pointerDown(pointer(container, 150, 150));
expect(drawing.getState()).toMatchObject({
mode: 'ready',
startAnchor: { time: 50, price: 50 },
endAnchor: { time: 150, price: 150 },
});
});
it('должен удалять слишком короткую линию', () => {
const { drawing, container, removeSelf } = createDrawing();
drawing.pointerDown(pointer(container, 50, 50));
drawing.pointerDown(pointer(container, 52, 52));
expect(removeSelf).toHaveBeenCalledTimes(1);
});
it('должен возвращать два handles', () => {
const { drawing } = createDrawing();
setReady(drawing);
expect(drawing.getHandles()).toEqual([
{
id: 'start',
x: 50,
y: 50,
shape: 'circle',
borderWidth: 2,
},
{
id: 'end',
x: 150,
y: 150,
shape: 'circle',
borderWidth: 2,
},
]);
});
it('должен возвращать render data', () => {
const { drawing } = createDrawing();
setReady(drawing);
expect(drawing.getRenderData()).toMatchObject({
startPoint: { x: 50, y: 50 },
endPoint: { x: 150, y: 150 },
lineColor: '#2962ff',
fontSize: 14,
});
});
it('должен возвращать axis segments для выбранной линии', () => {
const { drawing } = createDrawing(true);
setReady(drawing);
expect(drawing.getTimeSegments()).toEqual([
{
from: 50,
to: 150,
color: '#2962ff44',
},
]);
expect(drawing.getPriceSegments()).toEqual([
{
from: 50,
to: 150,
color: '#2962ff44',
},
]);
});
it('должен возвращать axis labels', () => {
const { drawing } = createDrawing(true);
setReady(drawing);
expect(drawing.getTimeLabel('start')).toEqual({
coordinate: 50,
text: 'date:50',
textColor: '#ffffff',
backgroundColor: '#2962ff',
});
expect(drawing.getPriceLabel('end')).toEqual({
coordinate: 150,
text: 'price:150',
textColor: '#ffffff',
backgroundColor: '#2962ff',
});
});
it('должен определять handle и body', () => {
const { drawing } = createDrawing(true);
setReady(drawing);
expect(drawing.getHovered(50, 50)?.cursorStyle).toBe('move');
expect(drawing.getHovered(100, 100)?.cursorStyle).toBe('grab');
expect(drawing.getHovered(300, 100)).toBeNull();
});
it('должен выбирать линию при первом нажатии', () => {
const { drawing, container, interaction } = createDrawing();
setReady(drawing);
drawing.pointerDown(pointer(container, 100, 100));
expect(interaction.select).toHaveBeenCalledTimes(1);
});
it('должен двигать start handle', () => {
const { drawing, container } = createDrawing(true);
setReady(drawing);
drawing.pointerDown(pointer(container, 50, 50));
drawing.move(pointer(container, 70, 80));
drawing.up(pointer(container, 70, 80));
expect(drawing.getState()).toMatchObject({
mode: 'ready',
startAnchor: {
time: 70,
price: 80,
},
endAnchor: {
time: 150,
price: 150,
},
});
});
it('должен двигать end handle', () => {
const { drawing, container } = createDrawing(true);
setReady(drawing);
drawing.pointerDown(pointer(container, 150, 150));
drawing.move(pointer(container, 170, 180));
drawing.up(pointer(container, 170, 180));
expect(drawing.getState()).toMatchObject({
endAnchor: {
time: 170,
price: 180,
},
});
});
it('должен двигать всю линию', () => {
const { drawing, container } = createDrawing(true);
setReady(drawing);
drawing.pointerDown(pointer(container, 100, 100));
drawing.move(pointer(container, 120, 130));
expect(drawing.getState()).toMatchObject({
startAnchor: {
time: 70,
price: 80,
},
endAnchor: {
time: 170,
price: 180,
},
});
drawing.up(pointer(container, 120, 130));
expect(drawing.getState()).toMatchObject({ mode: 'ready' });
});
it('должен открывать настройки по double click', () => {
const { drawing, container, openSettings } = createDrawing();
setReady(drawing);
drawing.doubleClick(mouse(container, 100, 100));
expect(openSettings).toHaveBeenCalledTimes(1);
});
it('не должен возвращать render data когда скрыт', () => {
const { drawing } = createDrawing();
setReady(drawing);
drawing.hide();
expect(drawing.getRenderData()).toBeNull();
});
});
describe('LineDrawingPaneRenderer', () => {
it('не должен рисовать без данных', () => {
const { target, useBitmapCoordinateSpace } = canvasTarget();
const renderer = new LineDrawingPaneRenderer(rendererDrawing(null));
renderer.draw(target);
expect(useBitmapCoordinateSpace).not.toHaveBeenCalled();
});
it('должен рисовать line, markers и text', () => {
const { context, target } = canvasTarget();
const renderer = new LineDrawingPaneRenderer(rendererDrawing(renderData()));
renderer.draw(target);
expect(context.stroke).toHaveBeenCalled();
expect(context.fillText).toHaveBeenCalledWith('Test', 0, expect.any(Number));
expect(context.translate).toHaveBeenCalled();
expect(context.rotate).toHaveBeenCalled();
});
});
describe('LineDrawingPaneView', () => {
it('должен возвращать renderer и top zOrder', () => {
const drawing = rendererDrawing(null);
const view = new LineDrawingPaneView(drawing);
expect(view.renderer()).toBeInstanceOf(LineDrawingPaneRenderer);
expect(view.zOrder()).toBe('top');
});
});
describe('LineDrawing settings', () => {
it('должен создавать настройки по умолчанию', () => {
expect(createDefaultSettings()).toEqual({
startMarker: LineMarker.normal,
endMarker: LineMarker.normal,
lineColor: '#2962ff',
fontSize: 14,
text: '',
isBold: false,
isItalic: false,
textColor: '#2962ff',
});
});
it('должен принимать default markers', () => {
const settings = createDefaultSettings({
startMarker: LineMarker.arrow,
});
expect(settings.startMarker).toBe(LineMarker.arrow);
expect(settings.endMarker).toBe(LineMarker.normal);
});
it('должен создавать style и text tabs', () => {
expect(getLineDrawingSettingsTabs(createDefaultSettings()).map((tab) => tab.key)).toEqual(['style', 'text']);
});
});
import { BehaviorSubject } from 'rxjs';
import { ParallelChannel, ParallelChannelRenderData } from '@core/Drawings/parallelChannel/parallelChannel';
import { ParallelChannelPaneRenderer } from '@core/Drawings/parallelChannel/paneRenderer';
import { ParallelChannelPaneView } from '@core/Drawings/parallelChannel/paneView';
import { createDefaultSettings, getParallelChannelSettingsTabs } from '@core/Drawings/parallelChannel/settings';
import type { DrawingHandle } from '@core/Drawings/handles';
import type { DrawingInteraction } from '@core/Drawings/SeriesDrawingBase';
import type { AxisLabel, AxisSegment, SeriesApi } from '@core/Drawings/types';
import type { CanvasRenderingTarget2D } from 'fancy-canvas';
import type { Coordinate, IChartApi, Logical, PrimitiveHoveredItem, Time } from 'lightweight-charts';
jest.mock('@src/theme', () => ({
getThemeStore: () => ({
colors: {
chartLineColor: '#2962ff',
axisMarkerAreaFill: '#2962ff44',
axisMarkerLabelFill: '#2962ff',
chartPriceLineText: '#ffffff',
},
}),
}));
jest.mock('@src/translations', () => ({
t: (key: string) => key,
}));
jest.mock('@src/types/defaults', () => ({
Defaults: {
dateFormat: 'date',
timeFormat: '24h',
showTime: true,
},
}));
jest.mock('@src/utils', () => ({
formatPrice: (value: number) => `price:${value}`,
}));
jest.mock('@src/utils/formatter', () => ({
formatDate: (value: number) => `date:${value}`,
}));
class TestParallelChannel extends ParallelChannel {
public getHandles(): readonly DrawingHandle[] {
return this.getDrawingHandles();
}
public getTimeSegments(): AxisSegment[] {
return this.getTimeAxisSegments();
}
public getPriceSegments(): AxisSegment[] {
return this.getPriceAxisSegments();
}
public getTimeLabel(kind: string): AxisLabel | null {
return this.getTimeAxisLabel(kind);
}
public getPriceLabel(kind: string): AxisLabel | null {
return this.getPriceAxisLabel(kind);
}
public getHovered(x: number, y: number): PrimitiveHoveredItem | null {
return this.getHoveredItem(x, y);
}
public move(event: PointerEvent): void {
this.handlePointerMove(event);
}
public up(event: PointerEvent): void {
this.handlePointerUp(event);
}
}
function createEnvironment(selected = false) {
let selectedValue = selected;
const selected$ = new BehaviorSubject(selected);
const locked$ = new BehaviorSubject(false);
const interaction: DrawingInteraction = {
selected$,
locked$,
isSelected: jest.fn(() => selectedValue),
isLocked: jest.fn(() => false),
select: jest.fn(() => {
selectedValue = true;
selected$.next(true);
}),
deselect: jest.fn(() => {
selectedValue = false;
selected$.next(false);
}),
};
const container = document.createElement('div');
jest.spyOn(container, 'getBoundingClientRect').mockReturnValue({
x: 0,
y: 0,
left: 0,
top: 0,
right: 400,
bottom: 300,
width: 400,
height: 300,
toJSON: jest.fn(),
});
const timeScale = {
coordinateToTime: jest.fn((coordinate: Coordinate): Time | null => Number(coordinate) as Time),
timeToCoordinate: jest.fn((time: Time): Coordinate | null => Number(time) as Coordinate),
logicalToCoordinate: jest.fn((logical: Logical): Coordinate | null => Number(logical) as Coordinate),
};
const pane = {
paneIndex: jest.fn(() => 0),
getHeight: jest.fn(() => 300),
};
const chart = {
timeScale: jest.fn(() => timeScale),
applyOptions: jest.fn(),
panes: jest.fn(() => [pane]),
} as unknown as IChartApi;
const series = {
coordinateToPrice: jest.fn((coordinate: Coordinate): number | null => Number(coordinate)),
priceToCoordinate: jest.fn((price: number): Coordinate | null => price as Coordinate),
data: jest.fn(() => []),
attachPrimitive: jest.fn(),
detachPrimitive: jest.fn(),
getPane: jest.fn(() => pane),
} as unknown as SeriesApi;
return { chart, series, container, interaction };
}
function createDrawing(selected = false) {
const env = createEnvironment(selected);
const openSettings = jest.fn();
const drawing = new TestParallelChannel({
chart: env.chart,
series: env.series,
container: env.container,
interaction: env.interaction,
openSettings,
});
return { ...env, drawing, openSettings };
}
function setReady(drawing: TestParallelChannel) {
drawing.setState({
mode: 'ready',
startAnchor: {
time: 50 as Time,
price: 50,
},
endAnchor: {
time: 150 as Time,
price: 80,
},
priceOffset: 60,
});
}
function pointer(target: Node, x: number, y: number, pointerId = 1): PointerEvent {
return {
target,
clientX: x,
clientY: y,
pointerId,
button: 0,
preventDefault: jest.fn(),
stopPropagation: jest.fn(),
} as unknown as PointerEvent;
}
function mouse(target: Node, x: number, y: number): MouseEvent {
return {
target,
clientX: x,
clientY: y,
preventDefault: jest.fn(),
stopPropagation: jest.fn(),
} as unknown as MouseEvent;
}
function rendererDrawing(data: ParallelChannelRenderData | null): ParallelChannel {
return {
getRenderData: () => data,
} as unknown as ParallelChannel;
}
function renderData(): ParallelChannelRenderData {
return {
startPoint: { x: 10, y: 20 },
mainMiddlePoint: { x: 50, y: 30 },
endPoint: { x: 90, y: 40 },
parallelStartPoint: { x: 10, y: 80 },
parallelMiddlePoint: { x: 50, y: 90 },
parallelEndPoint: { x: 90, y: 100 },
middleStartPoint: { x: 10, y: 50 },
middleEndPoint: { x: 90, y: 70 },
left: 10,
right: 90,
top: 20,
bottom: 100,
lineColor: '#ff0000',
backgroundColor: '#00ff0033',
showMiddleLine: true,
fontSize: 14,
text: 'Channel',
isBold: true,
isItalic: false,
textColor: '#ffffff',
};
}
function canvasTarget() {
const context = {
save: jest.fn(),
restore: jest.fn(),
beginPath: jest.fn(),
moveTo: jest.fn(),
lineTo: jest.fn(),
closePath: jest.fn(),
fill: jest.fn(),
stroke: jest.fn(),
setLineDash: jest.fn(),
translate: jest.fn(),
rotate: jest.fn(),
fillText: jest.fn(),
measureText: jest.fn((text: string) => ({ width: text.length * 5 })),
fillStyle: '',
strokeStyle: '',
lineWidth: 0,
font: '',
textAlign: '',
textBaseline: '',
};
const useBitmapCoordinateSpace = jest.fn(
(
callback: (scope: {
context: CanvasRenderingContext2D;
horizontalPixelRatio: number;
verticalPixelRatio: number;
}) => void,
) =>
callback({
context: context as unknown as CanvasRenderingContext2D,
horizontalPixelRatio: 2,
verticalPixelRatio: 2,
}),
);
return {
context,
target: { useBitmapCoordinateSpace } as unknown as CanvasRenderingTarget2D,
useBitmapCoordinateSpace,
};
}
describe('ParallelChannel', () => {
it('должен создаваться за три этапа', () => {
const { drawing, container } = createDrawing();
drawing.pointerDown(pointer(container, 50, 50));
expect(drawing.getState().mode).toBe('drawing-line');
drawing.pointerDown(pointer(container, 150, 80));
expect(drawing.getState().mode).toBe('drawing-channel');
drawing.pointerDown(pointer(container, 100, 130));
expect(drawing.getState().mode).toBe('ready');
expect(drawing.isCreationPending()).toBe(false);
});
it('должен восстанавливать dragging state как ready', () => {
const { drawing } = createDrawing();
drawing.setState({
mode: 'dragging',
});
expect(drawing.getState().mode).toBe('ready');
});
it('должен возвращать шесть handles', () => {
const { drawing } = createDrawing();
setReady(drawing);
expect(drawing.getHandles()).toEqual([
{
id: 'parallel-end',
x: 150,
y: 140,
shape: 'circle',
borderWidth: 2,
},
{
id: 'parallel-middle',
x: 100,
y: 125,
shape: 'circle',
borderWidth: 2,
},
{
id: 'parallel-start',
x: 50,
y: 110,
shape: 'circle',
borderWidth: 2,
},
{
id: 'main-end',
x: 150,
y: 80,
shape: 'circle',
borderWidth: 2,
},
{
id: 'main-middle',
x: 100,
y: 65,
shape: 'circle',
borderWidth: 2,
},
{
id: 'main-start',
x: 50,
y: 50,
shape: 'circle',
borderWidth: 2,
},
]);
});
it('должен возвращать axis segments', () => {
const { drawing } = createDrawing(true);
setReady(drawing);
expect(drawing.getTimeSegments()).toEqual([
{
from: 50,
to: 150,
color: '#2962ff44',
},
]);
expect(drawing.getPriceSegments()).toEqual([
{
from: 50,
to: 140,
color: '#2962ff44',
},
]);
});
it('должен возвращать time labels', () => {
const { drawing } = createDrawing(true);
setReady(drawing);
expect(drawing.getTimeLabel('start')).toEqual({
coordinate: 50,
text: 'date:50',
textColor: '#ffffff',
backgroundColor: '#2962ff',
});
expect(drawing.getTimeLabel('end')).toEqual({
coordinate: 150,
text: 'date:150',
textColor: '#ffffff',
backgroundColor: '#2962ff',
});
});
it('должен возвращать четыре price labels', () => {
const { drawing } = createDrawing(true);
setReady(drawing);
expect(drawing.getPriceLabel('main-start')?.text).toBe('price:50');
expect(drawing.getPriceLabel('main-end')?.text).toBe('price:80');
expect(drawing.getPriceLabel('parallel-start')?.text).toBe('price:110');
expect(drawing.getPriceLabel('parallel-end')?.text).toBe('price:140');
});
it('должен возвращать pointer для невыбранного channel', () => {
const { drawing } = createDrawing();
setReady(drawing);
expect(drawing.getHovered(100, 90)?.cursorStyle).toBe('pointer');
});
it('должен возвращать move для selected handle', () => {
const { drawing } = createDrawing(true);
setReady(drawing);
expect(drawing.getHovered(50, 50)?.cursorStyle).toBe('move');
});
it('должен возвращать grab внутри selected channel', () => {
const { drawing } = createDrawing(true);
setReady(drawing);
expect(drawing.getHovered(100, 90)?.cursorStyle).toBe('grab');
});
it('должен выбирать channel первым нажатием', () => {
const { drawing, container, interaction } = createDrawing();
setReady(drawing);
drawing.pointerDown(pointer(container, 100, 90));
expect(interaction.select).toHaveBeenCalledTimes(1);
});
it('должен двигать main-start handle', () => {
const { drawing, container } = createDrawing(true);
setReady(drawing);
drawing.pointerDown(pointer(container, 50, 50));
drawing.move(pointer(container, 60, 60));
drawing.up(pointer(container, 60, 60));
expect(drawing.getState()).toMatchObject({
mode: 'ready',
startAnchor: {
time: 60,
price: 60,
},
endAnchor: {
time: 150,
price: 80,
},
priceOffset: 60,
});
});
it('должен двигать parallel-middle handle', () => {
const { drawing, container } = createDrawing(true);
setReady(drawing);
drawing.pointerDown(pointer(container, 100, 125));
drawing.move(pointer(container, 100, 145));
expect(drawing.getState().priceOffset).toBe(80);
});
it('должен двигать channel целиком', () => {
const { drawing, container } = createDrawing(true);
setReady(drawing);
drawing.pointerDown(pointer(container, 100, 90));
drawing.move(pointer(container, 120, 100));
expect(drawing.getState()).toMatchObject({
startAnchor: {
time: 70,
price: 60,
},
endAnchor: {
time: 170,
price: 90,
},
priceOffset: 60,
});
});
it('должен открывать настройки только у выбранного channel', () => {
const { drawing, container, openSettings } = createDrawing(true);
setReady(drawing);
drawing.doubleClick(mouse(container, 100, 90));
expect(openSettings).toHaveBeenCalledTimes(1);
});
it('не должен возвращать render data когда скрыт', () => {
const { drawing } = createDrawing();
setReady(drawing);
drawing.hide();
expect(drawing.getRenderData()).toBeNull();
});
});
describe('ParallelChannelPaneRenderer', () => {
it('не должен рисовать без данных', () => {
const { target, useBitmapCoordinateSpace } = canvasTarget();
const renderer = new ParallelChannelPaneRenderer(rendererDrawing(null));
renderer.draw(target);
expect(useBitmapCoordinateSpace).not.toHaveBeenCalled();
});
it('должен рисовать channel, middle line и text', () => {
const { context, target } = canvasTarget();
const renderer = new ParallelChannelPaneRenderer(rendererDrawing(renderData()));
renderer.draw(target);
expect(context.fill).toHaveBeenCalled();
expect(context.stroke).toHaveBeenCalled();
expect(context.setLineDash).toHaveBeenCalled();
expect(context.fillText).toHaveBeenCalledWith('Channel', 0, expect.any(Number));
});
});
describe('ParallelChannelPaneView', () => {
it('должен возвращать renderer и top zOrder', () => {
const drawing = rendererDrawing(null);
const view = new ParallelChannelPaneView(drawing);
expect(view.renderer()).toBeInstanceOf(ParallelChannelPaneRenderer);
expect(view.zOrder()).toBe('top');
});
});
describe('ParallelChannel settings', () => {
it('должен создавать настройки по умолчанию', () => {
expect(createDefaultSettings()).toEqual({
lineColor: '#2962ff',
backgroundColor: '#2962ff44',
showMiddleLine: true,
fontSize: 14,
text: '',
isBold: false,
isItalic: false,
textColor: '#2962ff',
});
});
it('должен создавать style и text tabs', () => {
expect(getParallelChannelSettingsTabs(createDefaultSettings()).map((tab) => tab.key)).toEqual(['style', 'text']);
});
});
import { BehaviorSubject } from 'rxjs';
import { Ray, RayRenderData } from '@core/Drawings/ray/ray';
import { RayPaneRenderer } from '@core/Drawings/ray/paneRenderer';
import { RayPaneView } from '@core/Drawings/ray/paneView';
import { createDefaultSettings, getRaySettingTabs } from '@core/Drawings/ray/settings';
import type { DrawingHandle } from '@core/Drawings/handles';
import type { DrawingInteraction } from '@core/Drawings/SeriesDrawingBase';
import type { AxisLabel, AxisSegment, SeriesApi } from '@core/Drawings/types';
import type { CanvasRenderingTarget2D } from 'fancy-canvas';
import type { Coordinate, IChartApi, Logical, PrimitiveHoveredItem, Time } from 'lightweight-charts';
jest.mock('@src/theme', () => ({
getThemeStore: () => ({
colors: {
chartLineColor: '#2962ff',
axisMarkerAreaFill: '#2962ff44',
axisMarkerLabelFill: '#2962ff',
chartPriceLineText: '#ffffff',
},
}),
}));
jest.mock('@src/translations', () => ({
t: (key: string) => key,
}));
jest.mock('@src/types/defaults', () => ({
Defaults: {
dateFormat: 'date',
timeFormat: '24h',
showTime: true,
},
}));
jest.mock('@src/utils', () => ({
formatPrice: (value: number) => `price:${value}`,
}));
jest.mock('@src/utils/formatter', () => ({
formatDate: (value: number) => `date:${value}`,
}));
class TestRay extends Ray {
public getHandles(): readonly DrawingHandle[] {
return this.getDrawingHandles();
}
public getTimeSegments(): AxisSegment[] {
return this.getTimeAxisSegments();
}
public getPriceSegments(): AxisSegment[] {
return this.getPriceAxisSegments();
}
public getTimeLabel(kind: string): AxisLabel | null {
return this.getTimeAxisLabel(kind);
}
public getPriceLabel(kind: string): AxisLabel | null {
return this.getPriceAxisLabel(kind);
}
public getHovered(x: number, y: number): PrimitiveHoveredItem | null {
return this.getHoveredItem(x, y);
}
public move(event: PointerEvent): void {
this.handlePointerMove(event);
}
public up(event: PointerEvent): void {
this.handlePointerUp(event);
}
}
function createEnvironment(selected = false) {
let selectedValue = selected;
const selected$ = new BehaviorSubject(selected);
const locked$ = new BehaviorSubject(false);
const interaction: DrawingInteraction = {
selected$,
locked$,
isSelected: jest.fn(() => selectedValue),
isLocked: jest.fn(() => false),
select: jest.fn(() => {
selectedValue = true;
selected$.next(true);
}),
deselect: jest.fn(() => {
selectedValue = false;
selected$.next(false);
}),
};
const container = document.createElement('div');
jest.spyOn(container, 'getBoundingClientRect').mockReturnValue({
x: 0,
y: 0,
left: 0,
top: 0,
right: 400,
bottom: 300,
width: 400,
height: 300,
toJSON: jest.fn(),
});
const timeScale = {
coordinateToTime: jest.fn((coordinate: Coordinate): Time | null => Number(coordinate) as Time),
timeToCoordinate: jest.fn((time: Time): Coordinate | null => Number(time) as Coordinate),
logicalToCoordinate: jest.fn((logical: Logical): Coordinate | null => Number(logical) as Coordinate),
};
const pane = {
paneIndex: jest.fn(() => 0),
getHeight: jest.fn(() => 300),
};
const chart = {
timeScale: jest.fn(() => timeScale),
applyOptions: jest.fn(),
panes: jest.fn(() => [pane]),
} as unknown as IChartApi;
const series = {
coordinateToPrice: jest.fn((coordinate: Coordinate): number | null => Number(coordinate)),
priceToCoordinate: jest.fn((price: number): Coordinate | null => price as Coordinate),
data: jest.fn(() => []),
attachPrimitive: jest.fn(),
detachPrimitive: jest.fn(),
getPane: jest.fn(() => pane),
} as unknown as SeriesApi;
return { chart, series, container, interaction };
}
function createDrawing(selected = false) {
const env = createEnvironment(selected);
const removeSelf = jest.fn();
const openSettings = jest.fn();
const drawing = new TestRay({
chart: env.chart,
series: env.series,
container: env.container,
interaction: env.interaction,
removeSelf,
openSettings,
});
return { ...env, drawing, removeSelf, openSettings };
}
function setReady(drawing: TestRay) {
drawing.setState({
mode: 'ready',
startAnchor: {
time: 50 as Time,
price: 50,
},
directionAnchor: {
time: 150 as Time,
price: 100,
},
});
}
function pointer(target: Node, x: number, y: number, pointerId = 1): PointerEvent {
return {
target,
clientX: x,
clientY: y,
pointerId,
button: 0,
preventDefault: jest.fn(),
stopPropagation: jest.fn(),
} as unknown as PointerEvent;
}
function mouse(target: Node, x: number, y: number): MouseEvent {
return {
target,
clientX: x,
clientY: y,
preventDefault: jest.fn(),
stopPropagation: jest.fn(),
} as unknown as MouseEvent;
}
function rendererDrawing(data: RayRenderData | null): Ray {
return {
getRenderData: () => data,
} as unknown as Ray;
}
function renderData(): RayRenderData {
return {
startPoint: { x: 10, y: 20 },
directionPoint: { x: 50, y: 40 },
rayEndPoint: { x: 100, y: 65 },
left: 10,
right: 100,
top: 20,
bottom: 65,
lineColor: '#ff0000',
text: 'Ray',
fontSize: 14,
isBold: true,
isItalic: true,
textColor: '#ffffff',
};
}
function canvasTarget() {
const context = {
save: jest.fn(),
restore: jest.fn(),
beginPath: jest.fn(),
moveTo: jest.fn(),
lineTo: jest.fn(),
stroke: jest.fn(),
translate: jest.fn(),
rotate: jest.fn(),
fillText: jest.fn(),
lineWidth: 0,
strokeStyle: '',
fillStyle: '',
font: '',
textAlign: '',
textBaseline: '',
};
const useBitmapCoordinateSpace = jest.fn(
(
callback: (scope: {
context: CanvasRenderingContext2D;
horizontalPixelRatio: number;
verticalPixelRatio: number;
}) => void,
) =>
callback({
context: context as unknown as CanvasRenderingContext2D,
horizontalPixelRatio: 2,
verticalPixelRatio: 2,
}),
);
return {
context,
target: { useBitmapCoordinateSpace } as unknown as CanvasRenderingTarget2D,
useBitmapCoordinateSpace,
};
}
describe('Ray', () => {
it('должен прикреплять primitive', () => {
const { drawing, series } = createDrawing();
expect(series.attachPrimitive).toHaveBeenCalledWith(drawing);
});
it('должен создаваться двумя нажатиями', () => {
const { drawing, container } = createDrawing();
drawing.pointerDown(pointer(container, 50, 50));
drawing.pointerDown(pointer(container, 150, 100));
expect(drawing.getState()).toMatchObject({
mode: 'ready',
startAnchor: {
time: 50,
price: 50,
},
directionAnchor: {
time: 150,
price: 100,
},
});
});
it('должен удалять слишком короткий ray', () => {
const { drawing, container, removeSelf } = createDrawing();
drawing.pointerDown(pointer(container, 50, 50));
drawing.pointerDown(pointer(container, 52, 52));
expect(removeSelf).toHaveBeenCalledTimes(1);
});
it('должен продолжать ray до границы контейнера', () => {
const { drawing } = createDrawing();
setReady(drawing);
expect(drawing.getRenderData()).toMatchObject({
startPoint: {
x: 50,
y: 50,
},
directionPoint: {
x: 150,
y: 100,
},
rayEndPoint: {
x: 400,
y: 225,
},
});
});
it('должен возвращать handles только для start и direction', () => {
const { drawing } = createDrawing();
setReady(drawing);
expect(drawing.getHandles()).toEqual([
{
id: 'start',
x: 50,
y: 50,
shape: 'circle',
borderWidth: 2,
strokeColor: '#2962ff',
},
{
id: 'direction',
x: 150,
y: 100,
shape: 'circle',
borderWidth: 2,
strokeColor: '#2962ff',
},
]);
});
it('должен возвращать axis segments по управляющим точкам', () => {
const { drawing } = createDrawing(true);
setReady(drawing);
expect(drawing.getTimeSegments()).toEqual([
{
from: 50,
to: 150,
color: '#2962ff44',
},
]);
expect(drawing.getPriceSegments()).toEqual([
{
from: 50,
to: 100,
color: '#2962ff44',
},
]);
});
it('должен возвращать axis labels', () => {
const { drawing } = createDrawing(true);
setReady(drawing);
expect(drawing.getTimeLabel('direction')).toEqual({
coordinate: 150,
text: 'date:150',
textColor: '#ffffff',
backgroundColor: '#2962ff',
});
expect(drawing.getPriceLabel('start')).toEqual({
coordinate: 50,
text: 'price:50',
textColor: '#ffffff',
backgroundColor: '#2962ff',
});
});
it('должен определять handle и тело ray', () => {
const { drawing } = createDrawing(true);
setReady(drawing);
expect(drawing.getHovered(50, 50)?.cursorStyle).toBe('move');
expect(drawing.getHovered(200, 125)?.cursorStyle).toBe('grab');
expect(drawing.getHovered(200, 200)).toBeNull();
});
it('должен выбирать ray первым нажатием', () => {
const { drawing, container, interaction } = createDrawing();
setReady(drawing);
drawing.pointerDown(pointer(container, 200, 125));
expect(interaction.select).toHaveBeenCalledTimes(1);
});
it('должен двигать start handle', () => {
const { drawing, container } = createDrawing(true);
setReady(drawing);
drawing.pointerDown(pointer(container, 50, 50));
drawing.move(pointer(container, 70, 80));
drawing.up(pointer(container, 70, 80));
expect(drawing.getState()).toMatchObject({
mode: 'ready',
startAnchor: {
time: 70,
price: 80,
},
directionAnchor: {
time: 150,
price: 100,
},
});
});
it('должен двигать direction handle', () => {
const { drawing, container } = createDrawing(true);
setReady(drawing);
drawing.pointerDown(pointer(container, 150, 100));
drawing.move(pointer(container, 180, 130));
drawing.up(pointer(container, 180, 130));
expect(drawing.getState()).toMatchObject({
directionAnchor: {
time: 180,
price: 130,
},
});
});
it('должен двигать ray целиком', () => {
const { drawing, container } = createDrawing(true);
setReady(drawing);
drawing.pointerDown(pointer(container, 200, 125));
drawing.move(pointer(container, 220, 155));
expect(drawing.getState()).toMatchObject({
startAnchor: {
time: 70,
price: 80,
},
directionAnchor: {
time: 170,
price: 130,
},
});
});
it('должен открывать настройки по double click', () => {
const { drawing, container, openSettings } = createDrawing();
setReady(drawing);
drawing.doubleClick(mouse(container, 200, 125));
expect(openSettings).toHaveBeenCalledTimes(1);
});
it('не должен возвращать render data когда скрыт', () => {
const { drawing } = createDrawing();
setReady(drawing);
drawing.hide();
expect(drawing.getRenderData()).toBeNull();
});
});
describe('RayPaneRenderer', () => {
it('не должен рисовать без данных', () => {
const { target, useBitmapCoordinateSpace } = canvasTarget();
const renderer = new RayPaneRenderer(rendererDrawing(null));
renderer.draw(target);
expect(useBitmapCoordinateSpace).not.toHaveBeenCalled();
});
it('должен рисовать ray и text', () => {
const { context, target } = canvasTarget();
const renderer = new RayPaneRenderer(rendererDrawing(renderData()));
renderer.draw(target);
expect(context.moveTo).toHaveBeenCalledWith(20, 40);
expect(context.lineTo).toHaveBeenCalledWith(200, 130);
expect(context.fillText).toHaveBeenCalledWith('Ray', 0, expect.any(Number));
expect(context.translate).toHaveBeenCalled();
expect(context.rotate).toHaveBeenCalled();
});
});
describe('RayPaneView', () => {
it('должен возвращать renderer и top zOrder', () => {
const drawing = rendererDrawing(null);
const view = new RayPaneView(drawing);
expect(view.renderer()).toBeInstanceOf(RayPaneRenderer);
expect(view.zOrder()).toBe('top');
});
});
describe('Ray settings', () => {
it('должен создавать настройки по умолчанию', () => {
expect(createDefaultSettings()).toEqual({
lineColor: '#2962ff',
text: '',
fontSize: 14,
isBold: false,
isItalic: false,
textColor: '#2962ff',
});
});
it('должен создавать style и text tabs', () => {
expect(getRaySettingTabs(createDefaultSettings()).map((tab) => tab.key)).toEqual(['style', 'text']);
});
});