Загрузка данных


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,
  };
}

function createViewport(width = 400, height = 300): HTMLElement {
  const viewport = document.createElement('div');

  jest.spyOn(viewport, 'getBoundingClientRect').mockReturnValue({
    width,
    height,
    x: 0,
    y: 0,
    top: 0,
    left: 0,
    right: width,
    bottom: height,
    toJSON: () => ({}),
  });

  return viewport;
}

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 внутри viewport', () => {
    const viewport = createViewport(50, 300);
    const getAxisLabel = jest.fn(() => ({
      coordinate: 125,
      text: '100.50',
      textColor: '#ffffff',
      backgroundColor: '#000000',
    }));

    const view = new CustomPriceAxisView({
      getAxisLabel,
      labelKind: 'entry',
      viewport,
    });

    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',
      viewport: createViewport(),
    });

    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',
      viewport: createViewport(),
    });

    expect(view.visible()).toBe(false);
  });

  it.each([-1, 301])('не должен быть видимым вне viewport по вертикали: %s', (coordinate) => {
    const view = new CustomPriceAxisView({
      getAxisLabel: () => ({
        coordinate,
        text: '100.50',
        textColor: '#ffffff',
        backgroundColor: '#000000',
      }),
      labelKind: 'entry',
      viewport: createViewport(400, 300),
    });

    expect(view.visible()).toBe(false);
  });

  it.each([0, 300])('должен быть видимым на границе viewport по вертикали: %s', (coordinate) => {
    const view = new CustomPriceAxisView({
      getAxisLabel: () => ({
        coordinate,
        text: '100.50',
        textColor: '#ffffff',
        backgroundColor: '#000000',
      }),
      labelKind: 'entry',
      viewport: createViewport(400, 300),
    });

    expect(view.visible()).toBe(true);
  });

  it('не должен быть видимым при некорректной координате', () => {
    const view = new CustomPriceAxisView({
      getAxisLabel: () => ({
        coordinate: Number.NaN,
        text: '100.50',
        textColor: '#ffffff',
        backgroundColor: '#000000',
      }),
      labelKind: 'entry',
      viewport: createViewport(),
    });

    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,
  };
}

function createViewport(width = 400, height = 300): HTMLElement {
  const viewport = document.createElement('div');

  jest.spyOn(viewport, 'getBoundingClientRect').mockReturnValue({
    width,
    height,
    x: 0,
    y: 0,
    top: 0,
    left: 0,
    right: width,
    bottom: height,
    toJSON: () => ({}),
  });

  return viewport;
}

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 внутри viewport', () => {
    const viewport = createViewport(300, 50);
    const getAxisLabel = jest.fn(() => ({
      coordinate: 150,
      text: '09:30',
      textColor: '#ffffff',
      backgroundColor: '#000000',
    }));

    const view = new CustomTimeAxisView({
      getAxisLabel,
      labelKind: 'start',
      viewport,
    });

    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',
      viewport: createViewport(),
    });

    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',
      viewport: createViewport(),
    });

    expect(view.visible()).toBe(false);
  });

  it.each([-1, 401])('не должен быть видимым вне viewport по горизонтали: %s', (coordinate) => {
    const view = new CustomTimeAxisView({
      getAxisLabel: () => ({
        coordinate,
        text: '09:30',
        textColor: '#ffffff',
        backgroundColor: '#000000',
      }),
      labelKind: 'start',
      viewport: createViewport(400, 300),
    });

    expect(view.visible()).toBe(false);
  });

  it.each([0, 400])('должен быть видимым на границе viewport по горизонтали: %s', (coordinate) => {
    const view = new CustomTimeAxisView({
      getAxisLabel: () => ({
        coordinate,
        text: '09:30',
        textColor: '#ffffff',
        backgroundColor: '#000000',
      }),
      labelKind: 'start',
      viewport: createViewport(400, 300),
    });

    expect(view.visible()).toBe(true);
  });

  it('не должен быть видимым при некорректной координате', () => {
    const view = new CustomTimeAxisView({
      getAxisLabel: () => ({
        coordinate: Number.NaN,
        text: '09:30',
        textColor: '#ffffff',
        backgroundColor: '#000000',
      }),
      labelKind: 'start',
      viewport: createViewport(),
    });

    expect(view.visible()).toBe(false);
  });
});

















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 {
  createCanvasTarget,
  createDrawingTestEnvironment,
  createMouseEvent,
  createPointerEvent,
} from '../drawingTestUtils';

import type { DrawingHandle } from '@core/Drawings/handles';
import type { AxisLabel } from '@core/Drawings/types';
import type { PrimitiveHoveredItem, Time } 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 handles(): readonly DrawingHandle<'main'>[] {
    return this.getDrawingHandles();
  }

  public hovered(x: number, y: number): PrimitiveHoveredItem | null {
    return this.getHoveredItem(x, y);
  }

  public timeLabel(kind: string): AxisLabel | null {
    return this.getTimeAxisLabel(kind);
  }

  public priceLabel(kind: string): AxisLabel | null {
    return this.getPriceAxisLabel(kind);
  }

  public pointerDown(event: PointerEvent): void {
    this.handlePointerDown(event);
  }

  public pointerMove(event: PointerEvent): void {
    this.handlePointerMove(event);
  }

  public pointerUp(event: PointerEvent): void {
    this.handlePointerUp(event);
  }

  public doubleClick(event: MouseEvent): void {
    this.handleDoubleClick(event);
  }
}

function createAxisLine(direction: 'vertical' | 'horizontal', selected = false) {
  const env = createDrawingTestEnvironment({ selected });
  const openSettings = jest.fn();

  const drawing = new TestAxisLine({
    ...env,
    direction,
    openSettings,
  });

  return {
    ...env,
    drawing,
    openSettings,
  };
}

describe('AxisLine', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  it('должен начинаться в idle', () => {
    const { drawing } = createAxisLine('vertical');

    expect(drawing.isCreationPending()).toBe(true);

    expect(drawing.getState()).toMatchObject({
      mode: 'idle',
      time: null,
      price: null,
    });
  });

  it('должен создавать vertical line', () => {
    const { drawing, container, timeScale } = createAxisLine('vertical');

    drawing.pointerDown(createPointerEvent(container, 80, 120));

    expect(timeScale.coordinateToTime).toHaveBeenCalledWith(80);

    expect(drawing.getState()).toMatchObject({
      mode: 'ready',
      time: 80,
      price: null,
    });
  });

  it('должен создавать horizontal line', () => {
    const { drawing, container, coordinateToPrice } = createAxisLine('horizontal');

    drawing.pointerDown(createPointerEvent(container, 80, 120));

    expect(coordinateToPrice).toHaveBeenCalledWith(120);

    expect(drawing.getState()).toMatchObject({
      mode: 'ready',
      time: null,
      price: 120,
    });
  });

  it('не должен начинать drawing правой кнопкой', () => {
    const { drawing, container } = createAxisLine('vertical');

    drawing.pointerDown(createPointerEvent(container, 80, 120, 1, 2));

    expect(drawing.getState()).toMatchObject({
      mode: 'idle',
      time: null,
    });
  });

  it('должен восстанавливать state', () => {
    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('не должен возвращать render data для hidden line', () => {
    const { drawing } = createAxisLine('horizontal');

    drawing.setState({
      hidden: true,
      mode: 'ready',
      price: 100,
    });

    expect(drawing.getRenderData()).toBeNull();
  });

  it('должен возвращать render data vertical line', () => {
    const { drawing } = createAxisLine('vertical');

    drawing.setState({
      mode: 'ready',
      time: 100 as Time,
    });

    expect(drawing.getRenderData()).toEqual({
      direction: 'vertical',
      coordinate: 100,
      lineColor: '#3366ff',
      text: '',
      fontSize: 14,
      isBold: false,
      isItalic: false,
      textColor: '#3366ff',
    });
  });

  it('должен возвращать render data horizontal line', () => {
    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',
    });
  });

  it('должен размещать vertical handle по центру высоты', () => {
    const { drawing } = createAxisLine('vertical');

    drawing.setState({
      mode: 'ready',
      time: 80 as Time,
    });

    expect(drawing.handles()).toEqual([
      {
        id: 'main',
        x: 80,
        y: 150,
        shape: 'rounded',
      },
    ]);
  });

  it('должен размещать horizontal handle по центру ширины', () => {
    const { drawing } = createAxisLine('horizontal');

    drawing.setState({
      mode: 'ready',
      price: 120,
    });

    expect(drawing.handles()).toEqual([
      {
        id: 'main',
        x: 200,
        y: 120,
        shape: 'rounded',
      },
    ]);
  });

  it('должен возвращать time axis view только для vertical line', () => {
    const { drawing } = createAxisLine('vertical');

    expect(drawing.timeAxisViews()).toHaveLength(1);
    expect(drawing.priceAxisViews()).toHaveLength(0);
  });

  it('должен возвращать price axis view только для horizontal line', () => {
    const { drawing } = createAxisLine('horizontal');

    expect(drawing.timeAxisViews()).toHaveLength(0);
    expect(drawing.priceAxisViews()).toHaveLength(1);
  });

  it('должен возвращать time label для selected vertical line', () => {
    const { drawing } = createAxisLine('vertical', true);

    drawing.setState({
      mode: 'ready',
      time: 80 as Time,
    });

    expect(drawing.timeLabel('main')).toEqual({
      coordinate: 80,
      text: 'formatted-date',
      textColor: '#ffffff',
      backgroundColor: '#112233',
    });
  });

  it('должен возвращать price label для selected horizontal line', () => {
    const { drawing } = createAxisLine('horizontal', true);

    drawing.setState({
      mode: 'ready',
      price: 120,
    });

    expect(drawing.priceLabel('main')).toEqual({
      coordinate: 120,
      text: '120',
      textColor: '#ffffff',
      backgroundColor: '#112233',
    });
  });

  it('должен сохранять time label для unselected vertical line', () => {
    const { drawing } = createAxisLine('vertical');

    drawing.setState({
      mode: 'ready',
      time: 80 as Time,
    });

    expect(drawing.timeLabel('main')).toEqual({
      coordinate: 80,
      text: 'formatted-date',
      textColor: '#ffffff',
      backgroundColor: '#112233',
    });
  });

  it('должен сохранять price label для unselected horizontal line', () => {
    const { drawing } = createAxisLine('horizontal');

    drawing.setState({
      mode: 'ready',
      price: 120,
    });

    expect(drawing.priceLabel('main')).toEqual({
      coordinate: 120,
      text: '120',
      textColor: '#ffffff',
      backgroundColor: '#112233',
    });
  });

  it('не должен возвращать time label для hidden vertical line', () => {
    const { drawing } = createAxisLine('vertical');

    drawing.setState({
      hidden: true,
      mode: 'ready',
      time: 80 as Time,
    });

    expect(drawing.timeLabel('main')).toBeNull();
  });

  it('не должен возвращать price label для hidden horizontal line', () => {
    const { drawing } = createAxisLine('horizontal');

    drawing.setState({
      hidden: true,
      mode: 'ready',
      price: 120,
    });

    expect(drawing.priceLabel('main')).toBeNull();
  });

  it('не должен возвращать time label до завершения создания vertical line', () => {
    const { drawing } = createAxisLine('vertical');

    drawing.setState({
      mode: 'idle',
      time: 80 as Time,
    });

    expect(drawing.timeLabel('main')).toBeNull();
  });

  it('не должен возвращать price label до завершения создания horizontal line', () => {
    const { drawing } = createAxisLine('horizontal');

    drawing.setState({
      mode: 'idle',
      price: 120,
    });

    expect(drawing.priceLabel('main')).toBeNull();
  });

  it('не должен возвращать time label для horizontal line', () => {
    const { drawing } = createAxisLine('horizontal');

    drawing.setState({
      mode: 'ready',
      price: 120,
    });

    expect(drawing.timeLabel('main')).toBeNull();
  });

  it('не должен возвращать price label для vertical line', () => {
    const { drawing } = createAxisLine('vertical');

    drawing.setState({
      mode: 'ready',
      time: 80 as Time,
    });

    expect(drawing.priceLabel('main')).toBeNull();
  });

  it('не должен возвращать label для неизвестного kind', () => {
    const { drawing } = createAxisLine('vertical');

    drawing.setState({
      mode: 'ready',
      time: 80 as Time,
    });

    expect(drawing.timeLabel('unknown')).toBeNull();
    expect(drawing.priceLabel('unknown')).toBeNull();
  });

  it('должен определять hover vertical line', () => {
    const { drawing } = createAxisLine('vertical');

    drawing.setState({
      mode: 'ready',
      time: 50 as Time,
    });

    expect(drawing.hovered(54, 20)).toEqual({
      cursorStyle: 'ew-resize',
      externalId: 'axis-line',
      zOrder: 'top',
    });
  });

  it('должен определять hover horizontal line', () => {
    const { drawing } = createAxisLine('horizontal');

    drawing.setState({
      mode: 'ready',
      price: 50,
    });

    expect(drawing.hovered(20, 54)).toEqual({
      cursorStyle: 'ns-resize',
      externalId: 'axis-line',
      zOrder: 'top',
    });
  });

  it('не должен определять hover вдали от line', () => {
    const { drawing } = createAxisLine('vertical');

    drawing.setState({
      mode: 'ready',
      time: 50 as Time,
    });

    expect(drawing.hovered(70, 20)).toBeNull();
  });

  it('должен выбирать unselected line', () => {
    const { drawing, interaction, container } = createAxisLine('vertical');

    drawing.setState({
      mode: 'ready',
      time: 50 as Time,
    });

    drawing.pointerDown(createPointerEvent(container, 50, 20));

    expect(interaction.select).toHaveBeenCalledTimes(1);
  });

  it('должен перемещать vertical line', () => {
    const { drawing, container } = createAxisLine('vertical', true);

    drawing.setState({
      mode: 'ready',
      time: 50 as Time,
    });

    drawing.pointerDown(createPointerEvent(container, 50, 20, 1));
    drawing.pointerMove(createPointerEvent(container, 90, 20, 1));

    expect(drawing.getState()).toMatchObject({
      mode: 'dragging',
      time: 90,
    });

    drawing.pointerUp(createPointerEvent(container, 90, 20, 1));

    expect(drawing.getState()).toMatchObject({
      mode: 'ready',
      time: 90,
    });
  });

  it('должен перемещать horizontal line', () => {
    const { drawing, container } = createAxisLine('horizontal', true);

    drawing.setState({
      mode: 'ready',
      price: 50,
    });

    drawing.pointerDown(createPointerEvent(container, 20, 50, 1));
    drawing.pointerMove(createPointerEvent(container, 20, 90, 1));

    expect(drawing.getState()).toMatchObject({
      mode: 'dragging',
      price: 90,
    });
  });

  it('не должен реагировать на другой pointerId', () => {
    const { drawing, container } = createAxisLine('vertical', true);

    drawing.setState({
      mode: 'ready',
      time: 50 as Time,
    });

    drawing.pointerDown(createPointerEvent(container, 50, 20, 1));
    drawing.pointerMove(createPointerEvent(container, 90, 20, 2));

    expect(drawing.getState()).toMatchObject({
      time: 50,
    });
  });

  it('должен открывать settings по double click', () => {
    const { drawing, container, openSettings } = createAxisLine('vertical');

    drawing.setState({
      mode: 'ready',
      time: 50 as Time,
    });

    drawing.doubleClick(createMouseEvent(container, 50, 20));

    expect(openSettings).toHaveBeenCalledTimes(1);
  });

  it('должен обновлять settings через Base', () => {
    const { drawing } = createAxisLine('vertical');

    drawing.updateSettings({
      lineColor: '#ff0000',
      text: 'Updated',
    });

    expect(drawing.getSettings()).toMatchObject({
      lineColor: '#ff0000',
      text: 'Updated',
      fontSize: 14,
    });
  });
});

describe('AxisLinePaneRenderer', () => {
  it('не должен рисовать без render data', () => {
    const { target, useBitmapCoordinateSpace } = createCanvasTarget();

    const renderer = new AxisLinePaneRenderer({
      getRenderData: () => null,
    } as unknown as AxisLine);

    renderer.draw(target);

    expect(useBitmapCoordinateSpace).not.toHaveBeenCalled();
  });

  it('должен рисовать vertical line', () => {
    const { context, target } = createCanvasTarget({
      bitmapWidth: 300,
      bitmapHeight: 200,
    });

    const renderer = new AxisLinePaneRenderer({
      getRenderData: () => ({
        direction: 'vertical',
        coordinate: 20,
        lineColor: '#ff0000',
        text: '',
        fontSize: 14,
        isBold: false,
        isItalic: false,
        textColor: '#ffffff',
      }),
    } as unknown as AxisLine);

    renderer.draw(target);

    expect(context.fillRect).toHaveBeenCalledWith(38, 0, 4, 200);
  });

  it('должен рисовать horizontal line', () => {
    const { context, target } = createCanvasTarget({
      bitmapWidth: 300,
      bitmapHeight: 200,
    });

    const renderer = new AxisLinePaneRenderer({
      getRenderData: () => ({
        direction: 'horizontal',
        coordinate: 30,
        lineColor: '#ff0000',
        text: '',
        fontSize: 14,
        isBold: false,
        isItalic: false,
        textColor: '#ffffff',
      }),
    } as unknown as AxisLine);

    renderer.draw(target);

    expect(context.fillRect).toHaveBeenCalledWith(0, 58, 300, 4);
  });

  it('должен рисовать vertical text с поворотом', () => {
    const { context, target } = createCanvasTarget({
      bitmapWidth: 300,
      bitmapHeight: 200,
    });

    const renderer = new AxisLinePaneRenderer({
      getRenderData: () => ({
        direction: 'vertical',
        coordinate: 20,
        lineColor: '#ff0000',
        text: 'Test',
        fontSize: 14,
        isBold: true,
        isItalic: true,
        textColor: '#ffffff',
      }),
    } as unknown 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));
  });
});

describe('AxisLinePaneView', () => {
  it('должен возвращать renderer и top zOrder', () => {
    const view = new AxisLinePaneView({
      getRenderData: jest.fn(),
    } as unknown as AxisLine);

    expect(view.renderer()).toBeInstanceOf(AxisLinePaneRenderer);
    expect(view.zOrder()).toBe('top');
  });
});

describe('AxisLine settings', () => {
  it('должен создавать defaults', () => {
    expect(createDefaultSettings()).toEqual({
      lineColor: '#3366ff',
      text: '',
      fontSize: 14,
      isBold: false,
      isItalic: false,
      textColor: '#3366ff',
    });
  });

  it('должен создавать style и text tabs', () => {
    expect(getAxisLineSettingsTabs(createDefaultSettings()).map(({ key }) => key)).toEqual(['style', 'text']);
  });
});