Загрузка данных
import { CustomPriceAxisPaneView, CustomPriceAxisView } from '@core/Drawings/axis';
import type { CanvasRenderingTarget2D } from 'fancy-canvas';
function createTarget(verticalPixelRatio = 2) {
const context = {
save: jest.fn(),
restore: jest.fn(),
fillRect: jest.fn(),
fillStyle: '',
} as unknown as CanvasRenderingContext2D;
const useBitmapCoordinateSpace = jest.fn(
(
callback: (scope: {
context: CanvasRenderingContext2D;
horizontalPixelRatio: number;
verticalPixelRatio: number;
bitmapSize: { width: number; height: number };
}) => void,
) => {
callback({
context,
horizontalPixelRatio: 2,
verticalPixelRatio,
bitmapSize: { width: 300, height: 200 },
});
},
);
return {
context,
target: { useBitmapCoordinateSpace } as unknown as CanvasRenderingTarget2D,
useBitmapCoordinateSpace,
};
}
describe('CustomPriceAxisPaneView', () => {
it('должен использовать bottom zOrder по умолчанию', () => {
const view = new CustomPriceAxisPaneView({
getAxisSegments: () => [],
});
expect(view.zOrder()).toBe('bottom');
});
it('должен использовать переданный zOrder', () => {
const view = new CustomPriceAxisPaneView({
getAxisSegments: () => [],
zOrder: 'top',
});
expect(view.zOrder()).toBe('top');
});
it('должен возвращать один экземпляр renderer', () => {
const view = new CustomPriceAxisPaneView({
getAxisSegments: () => [],
});
expect(view.renderer()).toBe(view.renderer());
});
it('не должен рисовать при отсутствии сегментов', () => {
const { target, useBitmapCoordinateSpace } = createTarget();
const view = new CustomPriceAxisPaneView({
getAxisSegments: () => [],
});
view.renderer().draw(target);
expect(useBitmapCoordinateSpace).not.toHaveBeenCalled();
});
it('должен рисовать сегмент по вертикали', () => {
const { context, target } = createTarget();
const view = new CustomPriceAxisPaneView({
getAxisSegments: () => [
{
from: 10,
to: 30,
color: '#ff0000',
},
],
});
view.renderer().draw(target);
expect(context.fillStyle).toBe('#ff0000');
expect(context.fillRect).toHaveBeenCalledWith(0, 20, 300, 40);
expect(context.save).toHaveBeenCalledTimes(1);
expect(context.restore).toHaveBeenCalledTimes(1);
});
it('должен корректно рисовать сегмент при обратном порядке координат', () => {
const { context, target } = createTarget();
const view = new CustomPriceAxisPaneView({
getAxisSegments: () => [
{
from: 30,
to: 10,
color: '#00ff00',
},
],
});
view.renderer().draw(target);
expect(context.fillRect).toHaveBeenCalledWith(0, 20, 300, 40);
});
it('должен рисовать несколько сегментов', () => {
const { context, target } = createTarget();
const view = new CustomPriceAxisPaneView({
getAxisSegments: () => [
{
from: 10,
to: 20,
color: '#ff0000',
},
{
from: 30,
to: 50,
color: '#00ff00',
},
],
});
view.renderer().draw(target);
expect(context.fillRect).toHaveBeenNthCalledWith(1, 0, 20, 300, 20);
expect(context.fillRect).toHaveBeenNthCalledWith(2, 0, 60, 300, 40);
});
});
describe('CustomPriceAxisView', () => {
it('должен возвращать данные label', () => {
const getAxisLabel = jest.fn(() => ({
coordinate: 125,
text: '100.50',
textColor: '#ffffff',
backgroundColor: '#000000',
}));
const view = new CustomPriceAxisView({
getAxisLabel,
labelKind: 'entry',
});
expect(view.visible()).toBe(true);
expect(view.tickVisible()).toBe(false);
expect(view.coordinate()).toBe(125);
expect(view.text()).toBe('100.50');
expect(view.textColor()).toBe('#ffffff');
expect(view.backColor()).toBe('#000000');
expect(getAxisLabel).toHaveBeenCalledWith('entry');
});
it('должен возвращать значения по умолчанию при отсутствии label', () => {
const view = new CustomPriceAxisView({
getAxisLabel: () => null,
labelKind: 'entry',
});
expect(view.visible()).toBe(false);
expect(view.tickVisible()).toBe(false);
expect(view.coordinate()).toBe(0);
expect(view.text()).toBe('');
expect(view.textColor()).toBe('');
expect(view.backColor()).toBe('');
});
it('не должен быть видимым при пустом тексте', () => {
const view = new CustomPriceAxisView({
getAxisLabel: () => ({
coordinate: 100,
text: '',
textColor: '#ffffff',
backgroundColor: '#000000',
}),
labelKind: 'entry',
});
expect(view.visible()).toBe(false);
});
});
import { CustomTimeAxisPaneView, CustomTimeAxisView } from '@core/Drawings/axis';
import type { CanvasRenderingTarget2D } from 'fancy-canvas';
function createTarget(horizontalPixelRatio = 2) {
const context = {
save: jest.fn(),
restore: jest.fn(),
fillRect: jest.fn(),
fillStyle: '',
} as unknown as CanvasRenderingContext2D;
const useBitmapCoordinateSpace = jest.fn(
(
callback: (scope: {
context: CanvasRenderingContext2D;
horizontalPixelRatio: number;
verticalPixelRatio: number;
bitmapSize: { width: number; height: number };
}) => void,
) => {
callback({
context,
horizontalPixelRatio,
verticalPixelRatio: 2,
bitmapSize: { width: 300, height: 200 },
});
},
);
return {
context,
target: { useBitmapCoordinateSpace } as unknown as CanvasRenderingTarget2D,
useBitmapCoordinateSpace,
};
}
describe('CustomTimeAxisPaneView', () => {
it('должен использовать bottom zOrder по умолчанию', () => {
const view = new CustomTimeAxisPaneView({
getAxisSegments: () => [],
});
expect(view.zOrder()).toBe('bottom');
});
it('должен использовать переданный zOrder', () => {
const view = new CustomTimeAxisPaneView({
getAxisSegments: () => [],
zOrder: 'normal',
});
expect(view.zOrder()).toBe('normal');
});
it('должен возвращать один экземпляр renderer', () => {
const view = new CustomTimeAxisPaneView({
getAxisSegments: () => [],
});
expect(view.renderer()).toBe(view.renderer());
});
it('не должен рисовать при отсутствии сегментов', () => {
const { target, useBitmapCoordinateSpace } = createTarget();
const view = new CustomTimeAxisPaneView({
getAxisSegments: () => [],
});
view.renderer().draw(target);
expect(useBitmapCoordinateSpace).not.toHaveBeenCalled();
});
it('должен рисовать сегмент по горизонтали', () => {
const { context, target } = createTarget();
const view = new CustomTimeAxisPaneView({
getAxisSegments: () => [
{
from: 10,
to: 30,
color: '#ff0000',
},
],
});
view.renderer().draw(target);
expect(context.fillStyle).toBe('#ff0000');
expect(context.fillRect).toHaveBeenCalledWith(20, 0, 40, 200);
expect(context.save).toHaveBeenCalledTimes(1);
expect(context.restore).toHaveBeenCalledTimes(1);
});
it('должен корректно рисовать сегмент при обратном порядке координат', () => {
const { context, target } = createTarget();
const view = new CustomTimeAxisPaneView({
getAxisSegments: () => [
{
from: 30,
to: 10,
color: '#00ff00',
},
],
});
view.renderer().draw(target);
expect(context.fillRect).toHaveBeenCalledWith(20, 0, 40, 200);
});
it('должен рисовать несколько сегментов', () => {
const { context, target } = createTarget();
const view = new CustomTimeAxisPaneView({
getAxisSegments: () => [
{
from: 10,
to: 20,
color: '#ff0000',
},
{
from: 30,
to: 50,
color: '#00ff00',
},
],
});
view.renderer().draw(target);
expect(context.fillRect).toHaveBeenNthCalledWith(1, 20, 0, 20, 200);
expect(context.fillRect).toHaveBeenNthCalledWith(2, 60, 0, 40, 200);
});
});
describe('CustomTimeAxisView', () => {
it('должен возвращать данные label', () => {
const getAxisLabel = jest.fn(() => ({
coordinate: 150,
text: '09:30',
textColor: '#ffffff',
backgroundColor: '#000000',
}));
const view = new CustomTimeAxisView({
getAxisLabel,
labelKind: 'start',
});
expect(view.visible()).toBe(true);
expect(view.tickVisible()).toBe(false);
expect(view.coordinate()).toBe(150);
expect(view.text()).toBe('09:30');
expect(view.textColor()).toBe('#ffffff');
expect(view.backColor()).toBe('#000000');
expect(getAxisLabel).toHaveBeenCalledWith('start');
});
it('должен возвращать значения по умолчанию при отсутствии label', () => {
const view = new CustomTimeAxisView({
getAxisLabel: () => null,
labelKind: 'start',
});
expect(view.visible()).toBe(false);
expect(view.tickVisible()).toBe(false);
expect(view.coordinate()).toBe(0);
expect(view.text()).toBe('');
expect(view.textColor()).toBe('');
expect(view.backColor()).toBe('');
});
it('не должен быть видимым при пустом тексте', () => {
const view = new CustomTimeAxisView({
getAxisLabel: () => ({
coordinate: 150,
text: '',
textColor: '#ffffff',
backgroundColor: '#000000',
}),
labelKind: 'start',
});
expect(view.visible()).toBe(false);
});
});
import { BehaviorSubject } from 'rxjs';
import { AxisLine } from '@core/Drawings/axisLine/axisLine';
import { AxisLinePaneRenderer } from '@core/Drawings/axisLine/paneRenderer';
import { AxisLinePaneView } from '@core/Drawings/axisLine/paneView';
import { createDefaultSettings, getAxisLineSettingsTabs } from '@core/Drawings/axisLine/settings';
import type { DrawingInteraction } from '@core/Drawings/SeriesDrawingBase';
import type { AxisLabel, DrawingHandle, Point, SeriesApi } from '@core/Drawings/types';
import type { CanvasRenderingTarget2D } from 'fancy-canvas';
import type { IChartApi, PrimitiveHoveredItem } from 'lightweight-charts';
jest.mock('@src/theme', () => ({
getThemeStore: () => ({
colors: {
chartBackground: '#ffffff',
chartLineColor: '#3366ff',
chartPriceLineText: '#ffffff',
axisMarkerLabelFill: '#112233',
},
}),
}));
jest.mock('@src/types/defaults', () => ({
Defaults: {
dateFormat: 'date-format',
timeFormat: '24h',
showTime: true,
},
}));
jest.mock('@src/utils', () => ({
formatPrice: (value: number) => String(value),
}));
jest.mock('@src/utils/formatter', () => ({
formatDate: () => 'formatted-date',
}));
jest.mock('@src/translations', () => ({
t: (value: string) => value,
}));
class TestAxisLine extends AxisLine {
public getHandles(): readonly DrawingHandle<'main'>[] {
return this.getDrawingHandles();
}
public getHovered(x: number, y: number): PrimitiveHoveredItem | null {
return this.getHoveredItem(x, y);
}
public getTimeLabel(kind: string): AxisLabel | null {
return this.getTimeAxisLabel(kind);
}
public getPriceLabel(kind: string): AxisLabel | null {
return this.getPriceAxisLabel(kind);
}
public handlePointerDownForTest(event: PointerEvent): void {
this.handlePointerDown(event);
}
public handlePointerMoveForTest(event: PointerEvent): void {
this.handlePointerMove(event);
}
public handlePointerUpForTest(event: PointerEvent): void {
this.handlePointerUp(event);
}
public handleDoubleClickForTest(event: MouseEvent): void {
this.handleDoubleClick(event);
}
}
function createContainer() {
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(),
});
return container;
}
function createChart() {
const timeScale = {
coordinateToTime: jest.fn((coordinate: number) => coordinate),
timeToCoordinate: jest.fn((time: number) => time),
logicalToCoordinate: jest.fn((logical: number) => logical),
};
const chart = {
timeScale: jest.fn(() => timeScale),
applyOptions: jest.fn(),
panes: jest.fn(() => []),
} as unknown as IChartApi;
return {
chart,
timeScale,
};
}
function createSeries() {
const pane = {
paneIndex: jest.fn(() => 0),
getHeight: jest.fn(() => 300),
};
const series = {
attachPrimitive: jest.fn(),
detachPrimitive: jest.fn(),
priceToCoordinate: jest.fn((price: number) => price),
coordinateToPrice: jest.fn((coordinate: number) => coordinate),
getPane: jest.fn(() => pane),
data: jest.fn(() => []),
} as unknown as SeriesApi;
return {
series,
pane,
};
}
function createInteraction(selected = false, locked = false) {
let selectedValue = selected;
let lockedValue = locked;
const selected$ = new BehaviorSubject(selected);
const locked$ = new BehaviorSubject(locked);
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);
}),
};
return {
interaction,
setSelected(value: boolean) {
selectedValue = value;
selected$.next(value);
},
setLocked(value: boolean) {
lockedValue = value;
locked$.next(value);
},
};
}
function createPointerEvent(x: number, y: number, pointerId = 1, button = 0) {
return {
button,
pointerId,
clientX: x,
clientY: y,
preventDefault: jest.fn(),
stopPropagation: jest.fn(),
} as unknown as PointerEvent;
}
function createMouseEvent(x: number, y: number) {
return {
clientX: x,
clientY: y,
preventDefault: jest.fn(),
stopPropagation: jest.fn(),
} as unknown as MouseEvent;
}
function createAxisLine(direction: 'vertical' | 'horizontal', selected = false) {
const container = createContainer();
const { chart, timeScale } = createChart();
const { series } = createSeries();
const { interaction, setSelected, setLocked } = createInteraction(selected);
const openSettings = jest.fn();
const drawing = new TestAxisLine({
chart,
series,
container,
direction,
interaction,
openSettings,
});
return {
drawing,
chart,
timeScale,
series,
interaction,
setSelected,
setLocked,
openSettings,
};
}
function createCanvasTarget() {
const context = {
save: jest.fn(),
restore: jest.fn(),
fillRect: jest.fn(),
fillText: jest.fn(),
translate: jest.fn(),
rotate: jest.fn(),
fillStyle: '',
font: '',
textAlign: '',
textBaseline: '',
} as unknown as CanvasRenderingContext2D;
const useBitmapCoordinateSpace = jest.fn(
(
callback: (scope: {
context: CanvasRenderingContext2D;
horizontalPixelRatio: number;
verticalPixelRatio: number;
bitmapSize: { width: number; height: number };
}) => void,
) => {
callback({
context,
horizontalPixelRatio: 2,
verticalPixelRatio: 2,
bitmapSize: { width: 300, height: 200 },
});
},
);
return {
context,
target: { useBitmapCoordinateSpace } as unknown as CanvasRenderingTarget2D,
useBitmapCoordinateSpace,
};
}
describe('AxisLine', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('creation', () => {
it('должен находиться в idle после создания', () => {
const { drawing } = createAxisLine('vertical');
expect(drawing.isCreationPending()).toBe(true);
expect(drawing.shouldShowInObjectTree()).toBe(false);
expect(drawing.getState()).toMatchObject({
mode: 'idle',
time: null,
price: null,
});
});
it('должен создавать вертикальную линию по координате времени', () => {
const { drawing, timeScale } = createAxisLine('vertical');
drawing.handlePointerDownForTest(createPointerEvent(80, 120));
expect(timeScale.coordinateToTime).toHaveBeenCalledWith(80);
expect(drawing.getState()).toMatchObject({
mode: 'ready',
time: 80,
price: null,
});
expect(drawing.isCreationPending()).toBe(false);
expect(drawing.shouldShowInObjectTree()).toBe(true);
});
it('должен создавать горизонтальную линию по координате цены', () => {
const { drawing, series } = createAxisLine('horizontal');
drawing.handlePointerDownForTest(createPointerEvent(80, 120));
expect(series.coordinateToPrice).toHaveBeenCalledWith(120);
expect(drawing.getState()).toMatchObject({
mode: 'ready',
time: null,
price: 120,
});
});
it('не должен начинать создание правой кнопкой мыши', () => {
const { drawing } = createAxisLine('vertical');
drawing.handlePointerDownForTest(createPointerEvent(80, 120, 1, 2));
expect(drawing.getState()).toMatchObject({
mode: 'idle',
time: null,
});
});
});
describe('state', () => {
it('должен восстанавливать состояние', () => {
const { drawing } = createAxisLine('horizontal');
drawing.setState({
hidden: true,
mode: 'ready',
price: 150,
settings: {
lineColor: '#ff0000',
text: 'Test',
},
});
expect(drawing.getState()).toMatchObject({
hidden: true,
mode: 'ready',
price: 150,
settings: {
lineColor: '#ff0000',
text: 'Test',
},
});
});
it('должен сохранять дефолтные настройки при частичном setState', () => {
const { drawing } = createAxisLine('vertical');
drawing.setState({
settings: {
text: 'Test',
},
});
expect(drawing.getSettings()).toMatchObject({
lineColor: '#3366ff',
text: 'Test',
fontSize: 14,
isBold: false,
isItalic: false,
textColor: '#3366ff',
});
});
it('не должен возвращать render data для скрытой линии', () => {
const { drawing } = createAxisLine('horizontal');
drawing.setState({
hidden: true,
mode: 'ready',
price: 100,
});
expect(drawing.getRenderData()).toBeNull();
});
it('не должен возвращать render data без координаты', () => {
const { drawing } = createAxisLine('vertical');
expect(drawing.getRenderData()).toBeNull();
});
it('должен возвращать render data вертикальной линии', () => {
const { drawing } = createAxisLine('vertical');
drawing.setState({
mode: 'ready',
time: 100,
});
expect(drawing.getRenderData()).toEqual({
direction: 'vertical',
coordinate: 100,
lineColor: '#3366ff',
text: '',
fontSize: 14,
isBold: false,
isItalic: false,
textColor: '#3366ff',
});
});
it('должен возвращать render data горизонтальной линии', () => {
const { drawing } = createAxisLine('horizontal');
drawing.setState({
mode: 'ready',
price: 120,
});
expect(drawing.getRenderData()).toEqual({
direction: 'horizontal',
coordinate: 120,
lineColor: '#3366ff',
text: '',
fontSize: 14,
isBold: false,
isItalic: false,
textColor: '#3366ff',
});
});
});
describe('handles', () => {
it('должен размещать handle вертикальной линии по центру высоты', () => {
const { drawing } = createAxisLine('vertical');
drawing.setState({
mode: 'ready',
time: 80,
});
expect(drawing.getHandles()).toEqual([
{
id: 'main',
x: 80,
y: 150,
shape: 'rounded',
},
]);
});
it('должен размещать handle горизонтальной линии по центру ширины', () => {
const { drawing } = createAxisLine('horizontal');
drawing.setState({
mode: 'ready',
price: 120,
});
expect(drawing.getHandles()).toEqual([
{
id: 'main',
x: 200,
y: 120,
shape: 'rounded',
},
]);
});
it('не должен возвращать handle без координаты', () => {
const { drawing } = createAxisLine('vertical');
expect(drawing.getHandles()).toEqual([]);
});
it('должен определять hover по handle через реальную инфраструктуру Base', () => {
const { drawing } = createAxisLine('vertical', true);
drawing.setState({
mode: 'ready',
time: 80,
});
expect(drawing.getHovered(80, 150)).toEqual({
cursorStyle: 'ew-resize',
externalId: 'axis-line',
zOrder: 'top',
});
});
});
describe('axis views', () => {
it('должен возвращать time axis view только для вертикальной линии', () => {
const { drawing } = createAxisLine('vertical');
expect(drawing.timeAxisViews()).toHaveLength(1);
expect(drawing.priceAxisViews()).toHaveLength(0);
});
it('должен возвращать price axis view только для горизонтальной линии', () => {
const { drawing } = createAxisLine('horizontal');
expect(drawing.timeAxisViews()).toHaveLength(0);
expect(drawing.priceAxisViews()).toHaveLength(1);
});
it('не должен добавлять area pane view на оси', () => {
const { drawing } = createAxisLine('vertical');
expect(drawing.timeAxisPaneViews()).toEqual([]);
expect(drawing.priceAxisPaneViews()).toEqual([]);
});
});
describe('axis labels', () => {
it('должен возвращать time label для выбранной вертикальной линии', () => {
const { drawing } = createAxisLine('vertical', true);
drawing.setState({
mode: 'ready',
time: 80,
});
expect(drawing.getTimeLabel('main')).toEqual({
coordinate: 80,
text: 'formatted-date',
textColor: '#ffffff',
backgroundColor: '#112233',
});
});
it('должен возвращать price label для выбранной горизонтальной линии', () => {
const { drawing } = createAxisLine('horizontal', true);
drawing.setState({
mode: 'ready',
price: 120,
});
expect(drawing.getPriceLabel('main')).toEqual({
coordinate: 120,
text: '120',
textColor: '#ffffff',
backgroundColor: '#112233',
});
});
it('не должен возвращать label для невыбранной линии', () => {
const { drawing } = createAxisLine('vertical');
drawing.setState({
mode: 'ready',
time: 80,
});
expect(drawing.getTimeLabel('main')).toBeNull();
});
it('не должен возвращать time label для горизонтальной линии', () => {
const { drawing } = createAxisLine('horizontal', true);
drawing.setState({
mode: 'ready',
price: 120,
});
expect(drawing.getTimeLabel('main')).toBeNull();
});
it('не должен возвращать price label для вертикальной линии', () => {
const { drawing } = createAxisLine('vertical', true);
drawing.setState({
mode: 'ready',
time: 120,
});
expect(drawing.getPriceLabel('main')).toBeNull();
});
it('не должен возвращать label для неизвестного kind', () => {
const { drawing } = createAxisLine('vertical', true);
drawing.setState({
mode: 'ready',
time: 120,
});
expect(drawing.getTimeLabel('unknown')).toBeNull();
});
});
describe('hover', () => {
it('должен возвращать ew-resize для вертикальной линии', () => {
const { drawing } = createAxisLine('vertical');
drawing.setState({
mode: 'ready',
time: 50,
});
expect(drawing.getHovered(54, 20)).toEqual({
cursorStyle: 'ew-resize',
externalId: 'axis-line',
zOrder: 'top',
});
});
it('должен возвращать ns-resize для горизонтальной линии', () => {
const { drawing } = createAxisLine('horizontal');
drawing.setState({
mode: 'ready',
price: 50,
});
expect(drawing.getHovered(20, 54)).toEqual({
cursorStyle: 'ns-resize',
externalId: 'axis-line',
zOrder: 'top',
});
});
it('не должен определять hover вдали от линии', () => {
const { drawing } = createAxisLine('vertical');
drawing.setState({
mode: 'ready',
time: 50,
});
expect(drawing.getHovered(70, 20)).toBeNull();
});
it('не должен определять hover для idle', () => {
const { drawing } = createAxisLine('vertical');
expect(drawing.getHovered(50, 20)).toBeNull();
});
it('не должен определять hover для скрытой линии', () => {
const { drawing } = createAxisLine('vertical');
drawing.setState({
hidden: true,
mode: 'ready',
time: 50,
});
expect(drawing.getHovered(50, 20)).toBeNull();
});
});
describe('interaction', () => {
it('должен выбирать невыбранную линию при нажатии на неё', () => {
const { drawing, interaction } = createAxisLine('vertical');
drawing.setState({
mode: 'ready',
time: 50,
});
const event = createPointerEvent(50, 20);
drawing.handlePointerDownForTest(event);
expect(interaction.select).toHaveBeenCalledTimes(1);
expect(event.preventDefault).toHaveBeenCalled();
expect(event.stopPropagation).toHaveBeenCalled();
expect(drawing.getState()).toMatchObject({
mode: 'ready',
});
});
it('не должен выбирать линию при нажатии вне неё', () => {
const { drawing, interaction } = createAxisLine('vertical');
drawing.setState({
mode: 'ready',
time: 50,
});
drawing.handlePointerDownForTest(createPointerEvent(100, 20));
expect(interaction.select).not.toHaveBeenCalled();
});
it('должен переходить в dragging при нажатии на выбранную линию', () => {
const { drawing } = createAxisLine('vertical', true);
drawing.setState({
mode: 'ready',
time: 50,
});
drawing.handlePointerDownForTest(createPointerEvent(50, 20));
expect(drawing.getState()).toMatchObject({
mode: 'dragging',
});
});
it('должен перемещать вертикальную линию', () => {
const { drawing } = createAxisLine('vertical', true);
drawing.setState({
mode: 'ready',
time: 50,
});
drawing.handlePointerDownForTest(createPointerEvent(50, 20, 1));
drawing.handlePointerMoveForTest(createPointerEvent(90, 20, 1));
expect(drawing.getState()).toMatchObject({
mode: 'dragging',
time: 90,
});
drawing.handlePointerUpForTest(createPointerEvent(90, 20, 1));
expect(drawing.getState()).toMatchObject({
mode: 'ready',
time: 90,
});
});
it('должен перемещать горизонтальную линию', () => {
const { drawing } = createAxisLine('horizontal', true);
drawing.setState({
mode: 'ready',
price: 50,
});
drawing.handlePointerDownForTest(createPointerEvent(20, 50, 1));
drawing.handlePointerMoveForTest(createPointerEvent(20, 90, 1));
expect(drawing.getState()).toMatchObject({
mode: 'dragging',
price: 90,
});
drawing.handlePointerUpForTest(createPointerEvent(20, 90, 1));
expect(drawing.getState()).toMatchObject({
mode: 'ready',
price: 90,
});
});
it('не должен реагировать на move с другим pointerId', () => {
const { drawing } = createAxisLine('vertical', true);
drawing.setState({
mode: 'ready',
time: 50,
});
drawing.handlePointerDownForTest(createPointerEvent(50, 20, 1));
drawing.handlePointerMoveForTest(createPointerEvent(90, 20, 2));
expect(drawing.getState()).toMatchObject({
mode: 'dragging',
time: 50,
});
});
it('не должен завершать drag другим pointerId', () => {
const { drawing } = createAxisLine('vertical', true);
drawing.setState({
mode: 'ready',
time: 50,
});
drawing.handlePointerDownForTest(createPointerEvent(50, 20, 1));
drawing.handlePointerUpForTest(createPointerEvent(50, 20, 2));
expect(drawing.getState()).toMatchObject({
mode: 'dragging',
});
});
it('должен снимать выделение при нажатии вне выбранной линии', () => {
const { drawing, interaction } = createAxisLine('vertical', true);
drawing.setState({
mode: 'ready',
time: 50,
});
drawing.handlePointerDownForTest(createPointerEvent(100, 20));
expect(interaction.deselect).toHaveBeenCalledTimes(1);
});
});
describe('double click', () => {
it('должен открывать настройки при двойном клике по линии', () => {
const { drawing, openSettings } = createAxisLine('vertical');
drawing.setState({
mode: 'ready',
time: 50,
});
const event = createMouseEvent(50, 20);
drawing.handleDoubleClickForTest(event);
expect(openSettings).toHaveBeenCalledTimes(1);
expect(event.preventDefault).toHaveBeenCalled();
expect(event.stopPropagation).toHaveBeenCalled();
});
it('должен открывать настройки при двойном клике по handle', () => {
const { drawing, openSettings } = createAxisLine('vertical', true);
drawing.setState({
mode: 'ready',
time: 50,
});
drawing.handleDoubleClickForTest(createMouseEvent(50, 150));
expect(openSettings).toHaveBeenCalledTimes(1);
});
it('не должен открывать настройки вне линии', () => {
const { drawing, openSettings } = createAxisLine('vertical');
drawing.setState({
mode: 'ready',
time: 50,
});
drawing.handleDoubleClickForTest(createMouseEvent(100, 20));
expect(openSettings).not.toHaveBeenCalled();
});
it('не должен открывать настройки для idle', () => {
const { drawing, openSettings } = createAxisLine('vertical');
drawing.handleDoubleClickForTest(createMouseEvent(50, 20));
expect(openSettings).not.toHaveBeenCalled();
});
});
describe('settings', () => {
it('должен обновлять настройки через SeriesDrawingBase', () => {
const { drawing } = createAxisLine('vertical');
drawing.updateSettings({
lineColor: '#ff0000',
text: 'Updated',
});
expect(drawing.getSettings()).toMatchObject({
lineColor: '#ff0000',
text: 'Updated',
fontSize: 14,
isBold: false,
});
});
it('должен уведомлять подписчика об изменении настроек', () => {
const { drawing } = createAxisLine('vertical');
const callback = jest.fn();
const subscription = drawing.subscribeSettings(callback);
expect(callback).toHaveBeenCalledWith(
expect.objectContaining({
lineColor: '#3366ff',
fontSize: 14,
}),
);
drawing.updateSettings({
lineColor: '#ff0000',
});
expect(callback).toHaveBeenLastCalledWith(
expect.objectContaining({
lineColor: '#ff0000',
}),
);
subscription.unsubscribe();
});
});
});
describe('AxisLinePaneRenderer', () => {
it('не должен рисовать при отсутствии render data', () => {
const { target, useBitmapCoordinateSpace } = createCanvasTarget();
const renderer = new AxisLinePaneRenderer({
getRenderData: () => null,
} as AxisLine);
renderer.draw(target);
expect(useBitmapCoordinateSpace).not.toHaveBeenCalled();
});
it('должен рисовать вертикальную линию', () => {
const { context, target } = createCanvasTarget();
const renderer = new AxisLinePaneRenderer({
getRenderData: () => ({
direction: 'vertical',
coordinate: 20,
lineColor: '#ff0000',
text: '',
fontSize: 14,
isBold: false,
isItalic: false,
textColor: '#ffffff',
}),
} as AxisLine);
renderer.draw(target);
expect(context.fillStyle).toBe('#ff0000');
expect(context.fillRect).toHaveBeenCalledWith(38, 0, 4, 200);
});
it('должен рисовать горизонтальную линию', () => {
const { context, target } = createCanvasTarget();
const renderer = new AxisLinePaneRenderer({
getRenderData: () => ({
direction: 'horizontal',
coordinate: 30,
lineColor: '#ff0000',
text: '',
fontSize: 14,
isBold: false,
isItalic: false,
textColor: '#ffffff',
}),
} as AxisLine);
renderer.draw(target);
expect(context.fillStyle).toBe('#ff0000');
expect(context.fillRect).toHaveBeenCalledWith(0, 58, 300, 4);
});
it('не должен рисовать текст при пустом значении', () => {
const { context, target } = createCanvasTarget();
const renderer = new AxisLinePaneRenderer({
getRenderData: () => ({
direction: 'vertical',
coordinate: 20,
lineColor: '#ff0000',
text: '',
fontSize: 14,
isBold: false,
isItalic: false,
textColor: '#ffffff',
}),
} as AxisLine);
renderer.draw(target);
expect(context.fillText).not.toHaveBeenCalled();
});
it('должен рисовать текст вертикальной линии с поворотом', () => {
const { context, target } = createCanvasTarget();
const renderer = new AxisLinePaneRenderer({
getRenderData: () => ({
direction: 'vertical',
coordinate: 20,
lineColor: '#ff0000',
text: 'Test',
fontSize: 14,
isBold: true,
isItalic: true,
textColor: '#ffffff',
}),
} as AxisLine);
renderer.draw(target);
expect(context.translate).toHaveBeenCalledWith(40, 100);
expect(context.rotate).toHaveBeenCalledWith(-Math.PI / 2);
expect(context.font).toContain('italic');
expect(context.font).toContain('700');
expect(context.fillText).toHaveBeenCalledWith('Test', 0, expect.any(Number));
});
it('должен рисовать текст горизонтальной линии без поворота', () => {
const { context, target } = createCanvasTarget();
const renderer = new AxisLinePaneRenderer({
getRenderData: () => ({
direction: 'horizontal',
coordinate: 30,
lineColor: '#ff0000',
text: 'Test',
fontSize: 14,
isBold: false,
isItalic: false,
textColor: '#ffffff',
}),
} as AxisLine);
renderer.draw(target);
expect(context.translate).toHaveBeenCalledWith(150, 60);
expect(context.rotate).toHaveBeenCalledWith(0);
expect(context.fillText).toHaveBeenCalledWith('Test', 0, expect.any(Number));
});
});
describe('AxisLinePaneView', () => {
it('должен возвращать renderer', () => {
const axisLine = {
getRenderData: jest.fn(),
} as unknown as AxisLine;
const view = new AxisLinePaneView(axisLine);
expect(view.renderer()).toBeInstanceOf(AxisLinePaneRenderer);
});
it('должен иметь top zOrder', () => {
const axisLine = {
getRenderData: jest.fn(),
} as unknown as AxisLine;
const view = new AxisLinePaneView(axisLine);
expect(view.zOrder()).toBe('top');
});
});
describe('AxisLine settings', () => {
it('должен создавать настройки по умолчанию', () => {
expect(createDefaultSettings()).toEqual({
lineColor: '#3366ff',
text: '',
fontSize: 14,
isBold: false,
isItalic: false,
textColor: '#3366ff',
});
});
it('должен создавать вкладки style и text', () => {
const settings = createDefaultSettings();
const tabs = getAxisLineSettingsTabs(settings);
expect(tabs.map((tab) => tab.key)).toEqual(['style', 'text']);
});
it('должен передавать текущие значения в style settings', () => {
const settings = {
...createDefaultSettings(),
lineColor: '#ff0000',
};
const tabs = getAxisLineSettingsTabs(settings);
const styleTab = tabs.find((tab) => tab.key === 'style');
expect(styleTab?.fields).toEqual([
expect.objectContaining({
key: 'lineColor',
label: 'Line',
type: 'color',
defaultValue: '#ff0000',
}),
]);
});
it('должен передавать текущие значения в text settings', () => {
const settings = {
...createDefaultSettings(),
text: 'Test',
fontSize: 18,
isBold: true,
isItalic: true,
textColor: '#00ff00',
};
const tabs = getAxisLineSettingsTabs(settings);
const textTab = tabs.find((tab) => tab.key === 'text');
expect(textTab?.fields).toEqual(
expect.arrayContaining([
expect.objectContaining({
key: 'fontSize',
defaultValue: 18,
min: 8,
max: 24,
}),
expect.objectContaining({
key: 'text',
defaultValue: 'Test',
}),
expect.objectContaining({
key: 'isBold',
defaultValue: true,
}),
expect.objectContaining({
key: 'isItalic',
defaultValue: true,
}),
expect.objectContaining({
key: 'textColor',
defaultValue: '#00ff00',
}),
]),
);
});
});