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


import {
  DrawingBase,
  type BaseDrawingParams,
} from '@src/core/Drawings/base';

import {
  createDrawingTestEnvironment,
  createMouseEvent,
  createPointerEvent,
} from '../../drawingTestUtils';

import type {
  Anchor,
  AxisLabel,
  AxisSegment,
  Bounds,
  Point,
  UpdatableView,
} from '@core/Drawings/types';
import type {
  IPrimitivePaneView,
  PrimitiveHoveredItem,
  SeriesAttachedParameter,
  SeriesOptionsMap,
  Time,
} from 'lightweight-charts';
import type { SettingsTab, SettingsValues } from '@src/types';

jest.mock('@src/theme', () => ({
  getThemeStore: () => ({
    colors: {
      chartPriceLineText: '#ffffff',
      axisMarkerLabelFill: '#112233',
      axisMarkerAreaFill: '#445566',
    },
  }),
}));

type TestMode = 'idle' | 'ready';

class TestDrawingBase extends DrawingBase<SettingsValues, 'main'> {
  protected mode: TestMode = 'ready';
  protected settings: SettingsValues = {
    lineWidth: 1,
  };

  private geometry: Bounds | null = null;
  private hoveredItem: PrimitiveHoveredItem | null = null;

  public getRenderData(): Bounds | null {
    return this.geometry;
  }

  public getState() {
    return {
      hidden: this.hidden,
      mode: this.mode,
      settings: this.settings,
    };
  }

  public setState(state: unknown): void {
    if (!state || typeof state !== 'object') {
      return;
    }

    const nextState = state as Partial<{
      hidden: boolean;
      mode: TestMode;
    }>;

    if (typeof nextState.hidden === 'boolean') {
      this.hidden = nextState.hidden;
    }

    if (nextState.mode) {
      this.mode = nextState.mode;
    }
  }

  public getSettingsTabs(): SettingsTab[] {
    return [];
  }

  public isCreationPending(): boolean {
    return this.mode === 'idle';
  }

  public setMode(mode: TestMode): void {
    this.mode = mode;
  }

  public setGeometry(geometry: Bounds | null): void {
    this.geometry = geometry;
  }

  public setHoveredItem(item: PrimitiveHoveredItem | null): void {
    this.hoveredItem = item;
  }

  public createAnchor(point: Point): Anchor | null {
    return this.createAnchorFromPoint(point);
  }

  public getAnchorPoint(anchor: Anchor | null): Point | null {
    return this.getPointFromAnchor(anchor);
  }

  public createLabel(
    coordinate: number | null,
    text: string,
    style?: Partial<Pick<AxisLabel, 'textColor' | 'backgroundColor'>>,
  ): AxisLabel | null {
    return this.createAxisLabel(coordinate, text, style);
  }

  public createSegment(
    from: number,
    to: number,
    color?: string,
  ): AxisSegment {
    return this.createAxisSegment(from, to, color);
  }

  public getBoundsSegments(axis: 'time' | 'price'): AxisSegment[] {
    return this.getBoundsAxisSegments(axis, () => this.geometry);
  }

  public getInteractiveLabel(
    kind: string,
    kinds: readonly string[],
    getLabel: (kind: string) => AxisLabel | null,
  ): AxisLabel | null {
    return this.getInteractiveAxisLabel(kind, kinds, getLabel);
  }

  public consume(event: MouseEvent): void {
    this.consumeEvent(event);
  }

  public selectOnDown(
    event: PointerEvent,
    isHit: () => boolean,
  ): boolean {
    return this.selectOnPointerDown(event, isHit);
  }

  public openSettings(
    event: MouseEvent,
    isHit: (point: Point) => boolean,
    callback: (() => void) | undefined,
    selectedOnly = false,
  ): void {
    this.openSettingsOnDoubleClick(
      event,
      isHit,
      callback,
      selectedOnly,
    );
  }

  public initialize(
    paneView: IPrimitivePaneView & UpdatableView,
    timeLabels: readonly string[],
    priceLabels: readonly string[],
  ): void {
    this.initializeViews(
      paneView,
      timeLabels,
      priceLabels,
    );
  }

  public triggerRender(): void {
    this.render();
  }

  protected getHoveredItem(): PrimitiveHoveredItem | null {
    return this.hoveredItem;
  }

  protected getGeometry(): Bounds | null {
    return this.geometry;
  }

  protected getTimeAxisSegments(): AxisSegment[] {
    return [];
  }

  protected getPriceAxisSegments(): AxisSegment[] {
    return [];
  }

  protected getTimeAxisLabel(): AxisLabel | null {
    return null;
  }

  protected getPriceAxisLabel(): AxisLabel | null {
    return null;
  }
}

function createDrawing(
  options: Parameters<typeof createDrawingTestEnvironment>[0] = {},
) {
  const env = createDrawingTestEnvironment(options);
  const drawing = new TestDrawingBase(env);

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

function attachDrawing(
  drawing: TestDrawingBase,
  env: ReturnType<typeof createDrawingTestEnvironment>,
  requestUpdate = jest.fn(),
): jest.Mock {
  drawing.attached({
    chart: env.chart,
    series: env.series,
    requestUpdate,
  } as unknown as SeriesAttachedParameter<Time, keyof SeriesOptionsMap>);

  requestUpdate.mockClear();

  return requestUpdate;
}

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

  describe('anchors', () => {
    it('должен создавать anchor из point', () => {
      const { drawing } = createDrawing();

      expect(
        drawing.createAnchor({
          x: 120,
          y: 80,
        }),
      ).toEqual({
        time: 120,
        price: 80,
      });
    });

    it('должен возвращать null, если anchor создать невозможно', () => {
      const { drawing, timeScale } = createDrawing();

      timeScale.coordinateToTime.mockReturnValue(null);

      expect(
        drawing.createAnchor({
          x: 120,
          y: 80,
        }),
      ).toBeNull();
    });

    it('должен преобразовывать anchor в point без округления', () => {
      const { drawing } = createDrawing();

      expect(
        drawing.getAnchorPoint({
          time: 120.25 as Time,
          price: 80.75,
        }),
      ).toEqual({
        x: 120.25,
        y: 80.75,
      });
    });

    it('должен возвращать null для пустого anchor', () => {
      const { drawing } = createDrawing();

      expect(
        drawing.getAnchorPoint(null),
      ).toBeNull();
    });
  });

  describe('axis helpers', () => {
    it('должен создавать axis label с theme цветами', () => {
      const { drawing } = createDrawing();

      expect(
        drawing.createLabel(
          100,
          '100',
        ),
      ).toEqual({
        coordinate: 100,
        text: '100',
        textColor: '#ffffff',
        backgroundColor: '#112233',
      });
    });

    it('должен поддерживать пользовательские цвета axis label', () => {
      const { drawing } = createDrawing();

      expect(
        drawing.createLabel(
          100,
          '100',
          {
            textColor: '#111111',
            backgroundColor: '#222222',
          },
        ),
      ).toEqual({
        coordinate: 100,
        text: '100',
        textColor: '#111111',
        backgroundColor: '#222222',
      });
    });

    it('должен возвращать null для label без coordinate', () => {
      const { drawing } = createDrawing();

      expect(
        drawing.createLabel(
          null,
          '100',
        ),
      ).toBeNull();
    });

    it('должен возвращать null для label без текста', () => {
      const { drawing } = createDrawing();

      expect(
        drawing.createLabel(
          100,
          '',
        ),
      ).toBeNull();
    });

    it('должен создавать axis segment', () => {
      const { drawing } = createDrawing();

      expect(
        drawing.createSegment(
          10,
          20,
        ),
      ).toEqual({
        from: 10,
        to: 20,
        color: '#445566',
      });
    });

    it('должен строить time segment по bounds только для interactive axis', () => {
      const {
        drawing,
        setSelected,
      } = createDrawing();

      drawing.setGeometry({
        left: 10,
        right: 120,
        top: 30,
        bottom: 80,
      });

      expect(
        drawing.getBoundsSegments('time'),
      ).toEqual([]);

      setSelected(true);

      expect(
        drawing.getBoundsSegments('time'),
      ).toEqual([
        {
          from: 10,
          to: 120,
          color: '#445566',
        },
      ]);
    });

    it('должен строить price segment по bounds', () => {
      const {
        drawing,
        setSelected,
      } = createDrawing();

      drawing.setGeometry({
        left: 10,
        right: 120,
        top: 30,
        bottom: 80,
      });

      setSelected(true);

      expect(
        drawing.getBoundsSegments('price'),
      ).toEqual([
        {
          from: 30,
          to: 80,
          color: '#445566',
        },
      ]);
    });

    it('должен показывать interactive axis во время создания', () => {
      const { drawing } = createDrawing();

      drawing.setGeometry({
        left: 10,
        right: 120,
        top: 30,
        bottom: 80,
      });

      drawing.setMode('idle');

      expect(
        drawing.getBoundsSegments('time'),
      ).toHaveLength(1);
    });

    it('должен возвращать interactive label только для разрешённого kind', () => {
      const {
        drawing,
        setSelected,
      } = createDrawing();

      setSelected(true);

      const getLabel = jest.fn(
        (kind: string): AxisLabel => ({
          coordinate: 100,
          text: kind,
          textColor: '#ffffff',
          backgroundColor: '#112233',
        }),
      );

      expect(
        drawing.getInteractiveLabel(
          'start',
          ['start', 'end'],
          getLabel,
        ),
      ).toEqual({
        coordinate: 100,
        text: 'start',
        textColor: '#ffffff',
        backgroundColor: '#112233',
      });

      expect(
        drawing.getInteractiveLabel(
          'unknown',
          ['start', 'end'],
          getLabel,
        ),
      ).toBeNull();
    });
  });

  describe('views', () => {
    it('должен регистрировать pane и axis views через initializeViews', () => {
      const { drawing } = createDrawing();

      const paneView = {
        update: jest.fn(),
        renderer: jest.fn(),
        zOrder: jest.fn(),
      } as unknown as IPrimitivePaneView & UpdatableView;

      drawing.initialize(
        paneView,
        ['start', 'end'],
        ['price'],
      );

      expect(
        drawing.paneViews(),
      ).toEqual([
        paneView,
      ]);

      expect(
        drawing.timeAxisPaneViews(),
      ).toHaveLength(1);

      expect(
        drawing.priceAxisPaneViews(),
      ).toHaveLength(1);

      expect(
        drawing.timeAxisViews(),
      ).toHaveLength(2);

      expect(
        drawing.priceAxisViews(),
      ).toHaveLength(1);
    });

    it('должен обновлять зарегистрированный pane view', () => {
      const { drawing } = createDrawing();

      const paneView = {
        update: jest.fn(),
        renderer: jest.fn(),
        zOrder: jest.fn(),
      } as unknown as IPrimitivePaneView & UpdatableView;

      drawing.initialize(
        paneView,
        [],
        [],
      );

      drawing.updateAllViews();

      expect(
        paneView.update,
      ).toHaveBeenCalledTimes(1);
    });
  });

  describe('settings', () => {
    it('должен сразу отдавать текущие settings при подписке', () => {
      const { drawing } = createDrawing();
      const callback = jest.fn();

      drawing.subscribeSettings(
        callback,
      );

      expect(
        callback,
      ).toHaveBeenCalledWith({
        lineWidth: 1,
      });
    });

    it('должен уведомлять подписчика после updateSettings', () => {
      const { drawing } = createDrawing();
      const callback = jest.fn();

      drawing.subscribeSettings(
        callback,
      );

      callback.mockClear();

      drawing.updateSettings({
        lineWidth: 3,
      });

      expect(
        callback,
      ).toHaveBeenCalledWith({
        lineWidth: 3,
      });
    });
  });

  describe('render lifecycle', () => {
    it('должен вызывать updateAllViews и requestUpdate после render', () => {
      const env = createDrawingTestEnvironment();
      const drawing = new TestDrawingBase(env);
      const updateAllViews = jest.spyOn(
        drawing,
        'updateAllViews',
      );

      const requestUpdate = attachDrawing(
        drawing,
        env,
      );

      updateAllViews.mockClear();

      drawing.hide();

      expect(
        drawing.getState().hidden,
      ).toBe(true);

      expect(
        updateAllViews,
      ).toHaveBeenCalledTimes(1);

      expect(
        requestUpdate,
      ).toHaveBeenCalledTimes(1);
    });

    it('должен возвращать drawing после show', () => {
      const { drawing } = createDrawing();

      drawing.hide();
      drawing.show();

      expect(
        drawing.getState().hidden,
      ).toBe(false);
    });

    it('должен сразу завершать waitTillReady для ready drawing', async () => {
      const { drawing } = createDrawing();

      await expect(
        drawing.waitTillReady(),
      ).resolves.toBeUndefined();
    });

    it('должен считать idle drawing ожидающим создания', () => {
      const { drawing } = createDrawing();

      drawing.setMode('idle');

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

      expect(
        drawing.shouldShowInObjectTree(),
      ).toBe(false);
    });
  });

  describe('interaction helpers', () => {
    it('должен consume event', () => {
      const {
        drawing,
        container,
      } = createDrawing();

      const event = createMouseEvent(
        container,
        10,
        20,
      );

      drawing.consume(event);

      expect(
        event.preventDefault,
      ).toHaveBeenCalledTimes(1);

      expect(
        event.stopPropagation,
      ).toHaveBeenCalledTimes(1);
    });

    it('должен выбирать drawing при pointerDown по нему', () => {
      const {
        drawing,
        container,
        interaction,
      } = createDrawing();

      const event = createPointerEvent(
        container,
        10,
        20,
      );

      expect(
        drawing.selectOnDown(
          event,
          () => true,
        ),
      ).toBe(true);

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

      expect(
        event.preventDefault,
      ).toHaveBeenCalledTimes(1);

      expect(
        event.stopPropagation,
      ).toHaveBeenCalledTimes(1);
    });

    it('не должен повторно обрабатывать selection для уже выбранного drawing', () => {
      const {
        drawing,
        container,
        setSelected,
      } = createDrawing();

      setSelected(true);

      const event = createPointerEvent(
        container,
        10,
        20,
      );

      expect(
        drawing.selectOnDown(
          event,
          () => true,
        ),
      ).toBe(false);
    });

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

      const openSettings = jest.fn();
      const event = createMouseEvent(
        container,
        30,
        40,
        2,
      );

      drawing.openSettings(
        event,
        () => true,
        openSettings,
      );

      expect(
        openSettings,
      ).toHaveBeenCalledTimes(1);

      expect(
        event.preventDefault,
      ).toHaveBeenCalledTimes(1);

      expect(
        event.stopPropagation,
      ).toHaveBeenCalledTimes(1);
    });

    it('не должен открывать selectedOnly settings для невыбранного drawing', () => {
      const {
        drawing,
        container,
      } = createDrawing();

      const openSettings = jest.fn();
      const event = createMouseEvent(
        container,
        30,
        40,
        2,
      );

      drawing.openSettings(
        event,
        () => true,
        openSettings,
        true,
      );

      expect(
        openSettings,
      ).not.toHaveBeenCalled();
    });

    it('должен заменять cursor на pointer для locked drawing', () => {
      const {
        drawing,
        setLocked,
      } = createDrawing();

      drawing.setHoveredItem({
        cursorStyle: 'move',
        externalId: 'test',
        zOrder: 'top',
      });

      setLocked(true);

      expect(
        drawing.hitTest(
          10,
          20,
        ),
      ).toEqual({
        cursorStyle: 'pointer',
        externalId: 'test',
        zOrder: 'top',
      });
    });
  });

  describe('rebind', () => {
    it('должен отвязывать drawing от старой series и привязывать к новой', () => {
      const firstEnv = createDrawingTestEnvironment();
      const secondEnv = createDrawingTestEnvironment();
      const drawing = new TestDrawingBase(firstEnv);

      drawing.rebind(
        secondEnv.series,
      );

      expect(
        firstEnv.series.detachPrimitive,
      ).toHaveBeenCalledWith(
        drawing,
      );

      expect(
        secondEnv.series.attachPrimitive,
      ).toHaveBeenCalledWith(
        drawing,
      );
    });
  });
});

















import {
  TwoPointDrawingBase,
  type TwoPointAnchorKind,
  type TwoPointGeometry,
} from '@src/core/Drawings/base';

import { createDrawingTestEnvironment } from '../../drawingTestUtils';

import type {
  Anchor,
  AxisLabel,
  AxisSegment,
  Point,
} from '@core/Drawings/types';
import type {
  PrimitiveHoveredItem,
  Time,
} from 'lightweight-charts';
import type {
  SettingsTab,
  SettingsValues,
} from '@src/types';

class TestTwoPointDrawingBase extends TwoPointDrawingBase<SettingsValues> {
  protected mode: 'idle' | 'ready' = 'ready';
  protected settings: SettingsValues = {};

  public getRenderData(): TwoPointGeometry | null {
    return this.getTwoPointGeometry();
  }

  public getState() {
    return {
      startAnchor: this.startAnchor,
      endAnchor: this.endAnchor,
    };
  }

  public setState(): void {}

  public getSettingsTabs(): SettingsTab[] {
    return [];
  }

  public isCreationPending(): boolean {
    return this.mode === 'idle';
  }

  public setTestAnchors(
    startAnchor: Anchor | null,
    endAnchor: Anchor | null,
  ): void {
    this.setAnchors(
      startAnchor,
      endAnchor,
    );
  }

  public getTestAnchor(
    kind: TwoPointAnchorKind,
  ): Anchor | null {
    return this.getAnchor(kind);
  }

  public setTestAnchor(
    kind: TwoPointAnchorKind,
    anchor: Anchor | null,
  ): void {
    this.setAnchor(
      kind,
      anchor,
    );
  }

  public createTestAnchor(
    point: Point,
  ): Anchor | null {
    return this.createAnchor(point);
  }

  public setTestAnchorFromPoint(
    kind: TwoPointAnchorKind,
    point: Point,
  ): boolean {
    return this.setAnchorFromPoint(
      kind,
      point,
    );
  }

  public setTestAnchorsFromPoints(
    startPoint: Point,
    endPoint: Point,
  ): boolean {
    return this.setAnchorsFromPoints(
      startPoint,
      endPoint,
    );
  }

  public getTestTimeCoordinate(
    kind: TwoPointAnchorKind,
  ): number | null {
    return this.getAnchorTimeCoordinate(
      kind,
    );
  }

  public getTestPriceCoordinate(
    kind: TwoPointAnchorKind,
  ): number | null {
    return this.getAnchorPriceCoordinate(
      kind,
    );
  }

  public moveByPixels(
    startAnchor: Anchor | null,
    endAnchor: Anchor | null,
    dragStartPoint: Point | null,
    point: Point,
  ): boolean {
    return this.moveAnchorsByPixels(
      startAnchor,
      endAnchor,
      dragStartPoint,
      point,
    );
  }

  public moveWithinContainer(
    startAnchor: Anchor | null,
    endAnchor: Anchor | null,
    dragStartPoint: Point | null,
    point: Point,
    bounds: {
      left: number;
      right: number;
      top: number;
      bottom: number;
    },
  ): boolean {
    return this.moveAnchorsWithinContainer(
      startAnchor,
      endAnchor,
      dragStartPoint,
      point,
      bounds,
    );
  }

  public getTestGeometry(): TwoPointGeometry | null {
    return this.getTwoPointGeometry();
  }

  public restoreTestAnchor(
    kind: TwoPointAnchorKind,
    state: Partial<{
      startTime: Time | null;
      endTime: Time | null;
      startPrice: number | null;
      endPrice: number | null;
    }>,
  ): void {
    this.restoreAnchor(
      kind,
      state,
    );
  }

  public getTestVolumeInRange(): number {
    return this.getVolumeInRange();
  }

  public findTestIndexByTime(
    time: Time,
  ): number {
    return this.findIndexByTime(
      time,
    );
  }

  protected getHoveredItem(): PrimitiveHoveredItem | null {
    return null;
  }

  protected getGeometry(): TwoPointGeometry | null {
    return this.getTwoPointGeometry();
  }

  protected getTimeAxisSegments(): AxisSegment[] {
    return [];
  }

  protected getPriceAxisSegments(): AxisSegment[] {
    return [];
  }

  protected getTimeAxisLabel(): AxisLabel | null {
    return null;
  }

  protected getPriceAxisLabel(): AxisLabel | null {
    return null;
  }
}

function createDrawing(
  options: Parameters<typeof createDrawingTestEnvironment>[0] = {},
) {
  const env = createDrawingTestEnvironment(options);
  const drawing = new TestTwoPointDrawingBase(env);

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

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

  describe('anchors', () => {
    it('должен хранить start и end anchors', () => {
      const { drawing } = createDrawing();

      const startAnchor = {
        time: 10 as Time,
        price: 100,
      };

      const endAnchor = {
        time: 20 as Time,
        price: 200,
      };

      drawing.setTestAnchors(
        startAnchor,
        endAnchor,
      );

      expect(
        drawing.getTestAnchor('start'),
      ).toEqual(startAnchor);

      expect(
        drawing.getTestAnchor('end'),
      ).toEqual(endAnchor);
    });

    it('должен создавать anchor из screen point', () => {
      const { drawing } = createDrawing();

      expect(
        drawing.createTestAnchor({
          x: 10,
          y: 20,
        }),
      ).toEqual({
        time: 10,
        price: 20,
      });
    });

    it('должен изменять один anchor из point', () => {
      const { drawing } = createDrawing();

      expect(
        drawing.setTestAnchorFromPoint(
          'start',
          {
            x: 15,
            y: 25,
          },
        ),
      ).toBe(true);

      expect(
        drawing.getTestAnchor('start'),
      ).toEqual({
        time: 15,
        price: 25,
      });
    });

    it('не должен частично менять anchors, если одна из точек не преобразовалась', () => {
      const {
        drawing,
        timeScale,
      } = createDrawing();

      drawing.setTestAnchors(
        {
          time: 1 as Time,
          price: 10,
        },
        {
          time: 2 as Time,
          price: 20,
        },
      );

      timeScale.coordinateToTime
        .mockReturnValueOnce(
          100 as Time,
        )
        .mockReturnValueOnce(
          null,
        );

      expect(
        drawing.setTestAnchorsFromPoints(
          {
            x: 100,
            y: 110,
          },
          {
            x: 200,
            y: 210,
          },
        ),
      ).toBe(false);

      expect(
        drawing.getState(),
      ).toEqual({
        startAnchor: {
          time: 1,
          price: 10,
        },
        endAnchor: {
          time: 2,
          price: 20,
        },
      });
    });
  });

  describe('coordinates and geometry', () => {
    it('должен сохранять дробные coordinates anchors', () => {
      const { drawing } = createDrawing();

      drawing.setTestAnchors(
        {
          time: 10.25 as Time,
          price: 30.75,
        },
        {
          time: 20.5 as Time,
          price: 40.125,
        },
      );

      expect(
        drawing.getTestTimeCoordinate(
          'start',
        ),
      ).toBe(10.25);

      expect(
        drawing.getTestPriceCoordinate(
          'start',
        ),
      ).toBe(30.75);
    });

    it('должен строить нормализованную geometry из anchors', () => {
      const { drawing } = createDrawing();

      drawing.setTestAnchors(
        {
          time: 80 as Time,
          price: 90,
        },
        {
          time: 20 as Time,
          price: 30,
        },
      );

      expect(
        drawing.getTestGeometry(),
      ).toEqual({
        startPoint: {
          x: 80,
          y: 90,
        },
        endPoint: {
          x: 20,
          y: 30,
        },
        left: 20,
        right: 80,
        top: 30,
        bottom: 90,
      });
    });

    it('должен возвращать null geometry без двух anchors', () => {
      const { drawing } = createDrawing();

      drawing.setTestAnchor(
        'start',
        {
          time: 10 as Time,
          price: 20,
        },
      );

      expect(
        drawing.getTestGeometry(),
      ).toBeNull();
    });
  });

  describe('moving', () => {
    it('должен смещать оба anchors по времени и цене', () => {
      const { drawing } = createDrawing();

      const startAnchor = {
        time: 10 as Time,
        price: 100,
      };

      const endAnchor = {
        time: 20 as Time,
        price: 200,
      };

      expect(
        drawing.moveByPixels(
          startAnchor,
          endAnchor,
          {
            x: 50,
            y: 50,
          },
          {
            x: 70,
            y: 80,
          },
        ),
      ).toBe(true);

      expect(
        drawing.getState(),
      ).toEqual({
        startAnchor: {
          time: 30,
          price: 130,
        },
        endAnchor: {
          time: 40,
          price: 230,
        },
      });
    });

    it('должен ограничивать смещение границами container', () => {
      const { drawing } = createDrawing({
        width: 200,
        height: 200,
      });

      expect(
        drawing.moveWithinContainer(
          {
            time: 50 as Time,
            price: 40,
          },
          {
            time: 150 as Time,
            price: 140,
          },
          {
            x: 50,
            y: 50,
          },
          {
            x: 200,
            y: 200,
          },
          {
            left: 50,
            right: 150,
            top: 40,
            bottom: 140,
          },
        ),
      ).toBe(true);

      expect(
        drawing.getState(),
      ).toEqual({
        startAnchor: {
          time: 100,
          price: 100,
        },
        endAnchor: {
          time: 200,
          price: 200,
        },
      });
    });

    it('не должен двигать anchors без dragStartPoint', () => {
      const { drawing } = createDrawing();

      expect(
        drawing.moveByPixels(
          {
            time: 10 as Time,
            price: 100,
          },
          {
            time: 20 as Time,
            price: 200,
          },
          null,
          {
            x: 30,
            y: 40,
          },
        ),
      ).toBe(false);
    });
  });

  describe('state helpers', () => {
    it('должен частично восстанавливать anchor из legacy state', () => {
      const { drawing } = createDrawing();

      drawing.setTestAnchor(
        'start',
        {
          time: 10 as Time,
          price: 100,
        },
      );

      drawing.restoreTestAnchor(
        'start',
        {
          startPrice: 150,
        },
      );

      expect(
        drawing.getTestAnchor('start'),
      ).toEqual({
        time: 10,
        price: 150,
      });
    });

    it('должен очищать anchor, если state содержит null time', () => {
      const { drawing } = createDrawing();

      drawing.setTestAnchor(
        'start',
        {
          time: 10 as Time,
          price: 100,
        },
      );

      drawing.restoreTestAnchor(
        'start',
        {
          startTime: null,
        },
      );

      expect(
        drawing.getTestAnchor('start'),
      ).toBeNull();
    });
  });

  describe('series helpers', () => {
    it('должен находить index по времени', () => {
      const { drawing } = createDrawing({
        seriesData: [
          {
            time: 10,
          },
          {
            time: 20,
          },
          {
            time: 30,
          },
        ],
      });

      expect(
        drawing.findTestIndexByTime(
          20 as Time,
        ),
      ).toBe(1);
    });

    it('должен суммировать volume из обычного и customValues поля', () => {
      const { drawing } = createDrawing({
        seriesData: [
          {
            time: 10,
            volume: 5,
          },
          {
            time: 20,
            customValues: {
              volume: 7,
            },
          },
          {
            time: 30,
            volume: 3,
          },
        ],
      });

      drawing.setTestAnchors(
        {
          time: 10 as Time,
          price: 100,
        },
        {
          time: 30 as Time,
          price: 200,
        },
      );

      expect(
        drawing.getTestVolumeInRange(),
      ).toBe(15);
    });

    it('должен возвращать 0 volume без anchors', () => {
      const { drawing } = createDrawing({
        seriesData: [
          {
            time: 10,
            volume: 5,
          },
        ],
      });

      expect(
        drawing.getTestVolumeInRange(),
      ).toBe(0);
    });
  });
});

















import {
  LinearDrawingBase,
  type TwoPointGeometry,
} from '@src/core/Drawings/base';

import {
  createDrawingTestEnvironment,
  createPointerEvent,
} from '../../drawingTestUtils';

import type {
  Anchor,
  AxisLabel,
  Point,
} from '@core/Drawings/types';
import type {
  PrimitiveHoveredItem,
} from 'lightweight-charts';
import type {
  SettingsTab,
  SettingsValues,
} from '@src/types';

type HandleId = 'start' | 'end';

type TestMode =
  | 'idle'
  | 'drawing'
  | 'ready'
  | 'dragging-start'
  | 'dragging-end'
  | 'dragging-body';

interface TestState {
  mode: TestMode;
  startAnchor: Anchor | null;
  endAnchor: Anchor | null;
}

class TestLinearDrawingBase extends LinearDrawingBase<
  SettingsValues,
  HandleId,
  TestState
> {
  protected mode: TestMode = 'ready';
  protected settings: SettingsValues = {};

  public readonly finishDrawingMock = jest.fn();
  public readonly readyPointerDownMock = jest.fn();

  private strokeColor: string | undefined;

  public getRenderData(): TwoPointGeometry | null {
    return this.getTwoPointGeometry();
  }

  public getState(): TestState {
    return {
      mode: this.mode,
      startAnchor: this.startAnchor,
      endAnchor: this.endAnchor,
    };
  }

  public setState(): void {}

  public getSettingsTabs(): SettingsTab[] {
    return [];
  }

  public isCreationPending(): boolean {
    return (
      this.mode === 'idle' ||
      this.mode === 'drawing'
    );
  }

  public setMode(mode: TestMode): void {
    this.mode = mode;
  }

  public setTestAnchors(
    startAnchor: Anchor | null,
    endAnchor: Anchor | null,
  ): void {
    this.setAnchors(
      startAnchor,
      endAnchor,
    );
  }

  public setStrokeColor(
    color: string | undefined,
  ): void {
    this.strokeColor = color;
  }

  public getHandles() {
    return this.getDrawingHandles();
  }

  public getTimeSegments() {
    return this.getTimeAxisSegments();
  }

  public getPriceSegments() {
    return this.getPriceAxisSegments();
  }

  public isNearLine(
    point: Point,
    tolerance: number,
  ): boolean {
    return this.isPointNearLine(
      point,
      tolerance,
    );
  }

  public start(point: Point): void {
    this.startDrawing(point);
  }

  public update(point: Point): void {
    this.updateDrawing(point);
  }

  public startDrag(
    mode: `dragging-${HandleId | 'body'}`,
    point: Point,
    pointerId: number,
  ): void {
    this.startDragging(
      mode,
      point,
      pointerId,
    );
  }

  public finishDrag(): void {
    this.finishDragging();
  }

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

  public finishLinear(
    geometry: TwoPointGeometry | null,
    minimumSize: number,
    removeSelf: () => void,
  ): void {
    this.finishLinearDrawing(
      geometry,
      minimumSize,
      removeSelf,
    );
  }

  public getDragSnapshot(): TestState | null {
    return this.dragStateSnapshot;
  }

  public getDragPointerId(): number | null {
    return this.dragPointerId;
  }

  public getDragStartPoint(): Point | null {
    return this.dragStartPoint;
  }

  protected getStartHandleId(): HandleId {
    return 'start';
  }

  protected getEndHandleId(): HandleId {
    return 'end';
  }

  protected getLineHandleStrokeColor(): string | undefined {
    return this.strokeColor;
  }

  protected finishDrawing(): void {
    this.finishDrawingMock();
  }

  protected handleReadyPointerDown(
    event: PointerEvent,
    point: Point,
  ): void {
    this.readyPointerDownMock(
      event,
      point,
    );
  }

  protected getHoveredItem(): PrimitiveHoveredItem | null {
    return null;
  }

  protected getGeometry(): TwoPointGeometry | null {
    return this.getTwoPointGeometry();
  }

  protected getTimeAxisLabel(): AxisLabel | null {
    return null;
  }

  protected getPriceAxisLabel(): AxisLabel | null {
    return null;
  }
}

function createDrawing(
  selected = false,
) {
  const env = createDrawingTestEnvironment({
    selected,
  });

  const drawing = new TestLinearDrawingBase(
    env,
  );

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

function setAnchors(
  drawing: TestLinearDrawingBase,
): void {
  drawing.setTestAnchors(
    {
      time: 10,
      price: 20,
    },
    {
      time: 110,
      price: 120,
    },
  );
}

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

  describe('handles', () => {
    it('должен создавать start и end handles', () => {
      const { drawing } =
        createDrawing();

      setAnchors(drawing);

      expect(
        drawing.getHandles(),
      ).toEqual([
        {
          id: 'start',
          x: 10,
          y: 20,
          shape: 'circle',
        },
        {
          id: 'end',
          x: 110,
          y: 120,
          shape: 'circle',
        },
      ]);
    });

    it('должен применять strokeColor к handles', () => {
      const { drawing } =
        createDrawing();

      setAnchors(drawing);

      drawing.setStrokeColor(
        '#123456',
      );

      expect(
        drawing.getHandles(),
      ).toEqual([
        {
          id: 'start',
          x: 10,
          y: 20,
          shape: 'circle',
          strokeColor: '#123456',
        },
        {
          id: 'end',
          x: 110,
          y: 120,
          shape: 'circle',
          strokeColor: '#123456',
        },
      ]);
    });
  });

  describe('axis segments', () => {
    it('не должен возвращать axis segments для ready unselected drawing', () => {
      const { drawing } =
        createDrawing();

      setAnchors(drawing);

      expect(
        drawing.getTimeSegments(),
      ).toEqual([]);

      expect(
        drawing.getPriceSegments(),
      ).toEqual([]);
    });

    it('должен возвращать axis segments для selected drawing', () => {
      const {
        drawing,
        setSelected,
      } = createDrawing();

      setAnchors(drawing);
      setSelected(true);

      expect(
        drawing.getTimeSegments(),
      ).toHaveLength(1);

      expect(
        drawing.getTimeSegments()[0],
      ).toMatchObject({
        from: 10,
        to: 110,
      });

      expect(
        drawing.getPriceSegments()[0],
      ).toMatchObject({
        from: 20,
        to: 120,
      });
    });

    it('должен возвращать axis segments во время создания', () => {
      const { drawing } =
        createDrawing();

      setAnchors(drawing);
      drawing.setMode('drawing');

      expect(
        drawing.getTimeSegments(),
      ).toHaveLength(1);
    });
  });

  describe('hit testing', () => {
    it('должен определять point рядом с line', () => {
      const { drawing } =
        createDrawing();

      drawing.setTestAnchors(
        {
          time: 0,
          price: 0,
        },
        {
          time: 100,
          price: 0,
        },
      );

      expect(
        drawing.isNearLine(
          {
            x: 50,
            y: 4,
          },
          5,
        ),
      ).toBe(true);

      expect(
        drawing.isNearLine(
          {
            x: 50,
            y: 10,
          },
          5,
        ),
      ).toBe(false);
    });
  });

  describe('drawing lifecycle', () => {
    it('должен начинать drawing с одинаковых anchors', () => {
      const { drawing } =
        createDrawing();

      drawing.setMode('idle');

      drawing.start({
        x: 20,
        y: 30,
      });

      expect(
        drawing.getState(),
      ).toEqual({
        mode: 'drawing',
        startAnchor: {
          time: 20,
          price: 30,
        },
        endAnchor: {
          time: 20,
          price: 30,
        },
      });
    });

    it('должен обновлять end anchor во время drawing', () => {
      const { drawing } =
        createDrawing();

      drawing.setMode('idle');

      drawing.start({
        x: 20,
        y: 30,
      });

      drawing.update({
        x: 80,
        y: 90,
      });

      expect(
        drawing.getState().endAnchor,
      ).toEqual({
        time: 80,
        price: 90,
      });
    });

    it('должен вызывать finishDrawing вторым pointerDown', () => {
      const {
        drawing,
        container,
      } = createDrawing();

      drawing.setMode('idle');

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

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

      expect(
        drawing.finishDrawingMock,
      ).toHaveBeenCalledTimes(1);
    });

    it('должен передавать ready pointerDown concrete drawing', () => {
      const {
        drawing,
        container,
      } = createDrawing();

      drawing.setMode('ready');

      const event =
        createPointerEvent(
          container,
          40,
          50,
        );

      drawing.pointerDown(event);

      expect(
        drawing.readyPointerDownMock,
      ).toHaveBeenCalledWith(
        event,
        {
          x: 40,
          y: 50,
        },
      );
    });
  });

  describe('dragging', () => {
    it('должен сохранять state snapshot при начале drag', () => {
      const {
        drawing,
        chart,
      } = createDrawing();

      setAnchors(drawing);

      drawing.startDrag(
        'dragging-body',
        {
          x: 50,
          y: 60,
        },
        7,
      );

      expect(
        drawing.getDragPointerId(),
      ).toBe(7);

      expect(
        drawing.getDragStartPoint(),
      ).toEqual({
        x: 50,
        y: 60,
      });

      expect(
        drawing.getDragSnapshot(),
      ).toEqual({
        mode: 'dragging-body',
        startAnchor: {
          time: 10,
          price: 20,
        },
        endAnchor: {
          time: 110,
          price: 120,
        },
      });

      expect(
        chart.applyOptions,
      ).toHaveBeenCalledWith({
        crosshair: {
          mode: expect.any(Number),
        },
      });
    });

    it('должен очищать drag state после finishDragging', () => {
      const {
        drawing,
      } = createDrawing();

      setAnchors(drawing);

      drawing.startDrag(
        'dragging-body',
        {
          x: 50,
          y: 60,
        },
        7,
      );

      drawing.finishDrag();

      expect(
        drawing.getState().mode,
      ).toBe('ready');

      expect(
        drawing.getDragPointerId(),
      ).toBeNull();

      expect(
        drawing.getDragStartPoint(),
      ).toBeNull();

      expect(
        drawing.getDragSnapshot(),
      ).toBeNull();
    });
  });

  describe('finishLinearDrawing', () => {
    it('должен удалять слишком короткий drawing', () => {
      const { drawing } =
        createDrawing();

      const removeSelf =
        jest.fn();

      drawing.finishLinear(
        {
          startPoint: {
            x: 10,
            y: 10,
          },
          endPoint: {
            x: 12,
            y: 12,
          },
          left: 10,
          right: 12,
          top: 10,
          bottom: 12,
        },
        4,
        removeSelf,
      );

      expect(
        removeSelf,
      ).toHaveBeenCalledTimes(1);
    });

    it('должен переводить корректный drawing в ready', () => {
      const { drawing } =
        createDrawing();

      drawing.setMode('drawing');

      const removeSelf =
        jest.fn();

      drawing.finishLinear(
        {
          startPoint: {
            x: 10,
            y: 10,
          },
          endPoint: {
            x: 30,
            y: 10,
          },
          left: 10,
          right: 30,
          top: 10,
          bottom: 10,
        },
        4,
        removeSelf,
      );

      expect(
        removeSelf,
      ).not.toHaveBeenCalled();

      expect(
        drawing.getState().mode,
      ).toBe('ready');
    });
  });
});

















import {
  AreaDrawingBase,
} from '@src/core/Drawings/base';

import {
  createDrawingTestEnvironment,
  createPointerEvent,
} from '../../drawingTestUtils';

import type {
  Anchor,
  AxisLabel,
  Bounds,
  Point,
} from '@core/Drawings/types';
import type {
  PrimitiveHoveredItem,
} from 'lightweight-charts';
import type {
  SettingsTab,
  SettingsValues,
} from '@src/types';

type HandleId =
  | 'start'
  | 'end';

type TestMode =
  | 'idle'
  | 'drawing'
  | 'ready'
  | 'dragging';

class TestAreaDrawingBase extends AreaDrawingBase<
  SettingsValues,
  HandleId
> {
  protected mode: TestMode = 'idle';
  protected settings: SettingsValues = {};

  protected dragPointerId: number | null = null;
  protected activeDragTarget: HandleId | 'body' | null = null;

  public readonly finishDrawingMock = jest.fn();
  public readonly startDraggingMock = jest.fn();
  public readonly moveWholeMock = jest.fn();
  public readonly resizeMock = jest.fn();

  private geometry: Bounds | null = null;
  private containsPointValue = false;
  private dragTarget: HandleId | 'body' | null = null;

  public getRenderData(): Bounds | null {
    return this.geometry;
  }

  public getState() {
    return {
      mode: this.mode,
      startAnchor: this.startAnchor,
      endAnchor: this.endAnchor,
    };
  }

  public setState(): void {}

  public getSettingsTabs(): SettingsTab[] {
    return [];
  }

  public isCreationPending(): boolean {
    return (
      this.mode === 'idle' ||
      this.mode === 'drawing'
    );
  }

  public setMode(mode: TestMode): void {
    this.mode = mode;
  }

  public setGeometry(
    geometry: Bounds | null,
  ): void {
    this.geometry = geometry;
  }

  public setContainsPoint(
    value: boolean,
  ): void {
    this.containsPointValue = value;
  }

  public setTestDragTarget(
    target: HandleId | 'body' | null,
  ): void {
    this.dragTarget = target;
  }

  public setDragState(
    target: HandleId | 'body' | null,
    pointerId: number | null,
  ): void {
    this.activeDragTarget = target;
    this.dragPointerId = pointerId;
  }

  public start(point: Point): void {
    this.startDrawing(point);
  }

  public update(point: Point): void {
    this.updateDrawing(point);
  }

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

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

  public getTestAnchors(): {
    startAnchor: Anchor | null;
    endAnchor: Anchor | null;
  } {
    return {
      startAnchor: this.startAnchor,
      endAnchor: this.endAnchor,
    };
  }

  public getTimeSegments() {
    return this.getTimeAxisSegments();
  }

  public getPriceSegments() {
    return this.getPriceAxisSegments();
  }

  public getBoundsLabel(
    kind: string,
    kinds: readonly ('left' | 'right' | 'top' | 'bottom')[],
  ): AxisLabel | null {
    return this.getBoundsAxisLabel(
      kind,
      kinds,
      (labelKind) =>
        String(labelKind),
    );
  }

  protected finishDrawing(): void {
    this.finishDrawingMock();
  }

  protected containsPoint(): boolean {
    return this.containsPointValue;
  }

  protected getDragTarget(): HandleId | 'body' | null {
    return this.dragTarget;
  }

  protected startDragging(
    point: Point,
    pointerId: number,
    target: HandleId | 'body',
  ): void {
    this.startDraggingMock(
      point,
      pointerId,
      target,
    );

    this.activeDragTarget =
      target;

    this.dragPointerId =
      pointerId;

    this.mode =
      'dragging';
  }

  protected moveWhole(
    point: Point,
  ): void {
    this.moveWholeMock(
      point,
    );
  }

  protected resize(
    point: Point,
  ): void {
    this.resizeMock(
      point,
    );
  }

  protected getGeometry(): Bounds | null {
    return this.geometry;
  }

  protected getHoveredItem(): PrimitiveHoveredItem | null {
    return null;
  }

  protected getTimeAxisLabel(): AxisLabel | null {
    return null;
  }

  protected getPriceAxisLabel(): AxisLabel | null {
    return null;
  }
}

function createDrawing(
  selected = false,
) {
  const env =
    createDrawingTestEnvironment({
      selected,
    });

  const drawing =
    new TestAreaDrawingBase(
      env,
    );

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

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

  describe('drawing lifecycle', () => {
    it('должен начинать drawing с одинаковых anchors', () => {
      const { drawing } =
        createDrawing();

      drawing.start({
        x: 20,
        y: 30,
      });

      expect(
        drawing.getState(),
      ).toEqual({
        mode: 'drawing',
        startAnchor: {
          time: 20,
          price: 30,
        },
        endAnchor: {
          time: 20,
          price: 30,
        },
      });
    });

    it('должен обновлять end anchor с clamp по container', () => {
      const { drawing } =
        createDrawing();

      drawing.start({
        x: 20,
        y: 30,
      });

      drawing.update({
        x: 500,
        y: -50,
      });

      expect(
        drawing.getTestAnchors()
          .endAnchor,
      ).toEqual({
        time: 400,
        price: 0,
      });
    });

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

      const event =
        createPointerEvent(
          container,
          20,
          30,
        );

      drawing.pointerDown(
        event,
      );

      expect(
        drawing.getState().mode,
      ).toBe('drawing');

      expect(
        event.preventDefault,
      ).toHaveBeenCalledTimes(1);

      expect(
        event.stopPropagation,
      ).toHaveBeenCalledTimes(1);
    });

    it('должен завершать drawing через второй pointerDown', () => {
      const {
        drawing,
        container,
      } = createDrawing();

      drawing.start({
        x: 20,
        y: 30,
      });

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

      expect(
        drawing.finishDrawingMock,
      ).toHaveBeenCalledTimes(1);

      expect(
        drawing.getTestAnchors()
          .endAnchor,
      ).toEqual({
        time: 100,
        price: 120,
      });
    });
  });

  describe('selection and dragging', () => {
    it('должен только выбрать ready drawing при первом pointerDown', () => {
      const {
        drawing,
        container,
        interaction,
      } = createDrawing();

      drawing.setMode('ready');
      drawing.setContainsPoint(true);

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

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

      expect(
        drawing.startDraggingMock,
      ).not.toHaveBeenCalled();
    });

    it('должен deselect selected drawing при pointerDown вне drag target', () => {
      const {
        drawing,
        container,
        interaction,
      } = createDrawing(true);

      drawing.setMode('ready');
      drawing.setTestDragTarget(null);

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

      expect(
        interaction.deselect,
      ).toHaveBeenCalledTimes(1);
    });

    it('должен начинать drag выбранного target', () => {
      const {
        drawing,
        container,
      } = createDrawing(true);

      drawing.setMode('ready');
      drawing.setTestDragTarget(
        'start',
      );

      const event =
        createPointerEvent(
          container,
          50,
          60,
          7,
        );

      drawing.pointerDown(event);

      expect(
        drawing.startDraggingMock,
      ).toHaveBeenCalledWith(
        {
          x: 50,
          y: 60,
        },
        7,
        'start',
      );

      expect(
        event.preventDefault,
      ).toHaveBeenCalledTimes(1);

      expect(
        event.stopPropagation,
      ).toHaveBeenCalledTimes(1);
    });

    it('должен двигать body во время body drag', () => {
      const {
        drawing,
        container,
      } = createDrawing(true);

      drawing.setMode('dragging');
      drawing.setDragState(
        'body',
        5,
      );

      const event =
        createPointerEvent(
          container,
          80,
          90,
          5,
        );

      drawing.pointerMove(event);

      expect(
        drawing.moveWholeMock,
      ).toHaveBeenCalledWith({
        x: 80,
        y: 90,
      });

      expect(
        drawing.resizeMock,
      ).not.toHaveBeenCalled();

      expect(
        event.preventDefault,
      ).toHaveBeenCalledTimes(1);
    });

    it('должен resize handle во время handle drag', () => {
      const {
        drawing,
        container,
      } = createDrawing(true);

      drawing.setMode('dragging');
      drawing.setDragState(
        'end',
        5,
      );

      drawing.pointerMove(
        createPointerEvent(
          container,
          80,
          90,
          5,
        ),
      );

      expect(
        drawing.resizeMock,
      ).toHaveBeenCalledWith({
        x: 80,
        y: 90,
      });

      expect(
        drawing.moveWholeMock,
      ).not.toHaveBeenCalled();
    });

    it('не должен обрабатывать drag другого pointerId', () => {
      const {
        drawing,
        container,
      } = createDrawing(true);

      drawing.setMode('dragging');
      drawing.setDragState(
        'body',
        5,
      );

      drawing.pointerMove(
        createPointerEvent(
          container,
          80,
          90,
          6,
        ),
      );

      expect(
        drawing.moveWholeMock,
      ).not.toHaveBeenCalled();
    });
  });

  describe('axis', () => {
    it('должен строить time и price segments по geometry', () => {
      const {
        drawing,
        setSelected,
      } = createDrawing();

      drawing.setGeometry({
        left: 10,
        right: 100,
        top: 20,
        bottom: 80,
      });

      setSelected(true);

      expect(
        drawing.getTimeSegments()[0],
      ).toMatchObject({
        from: 10,
        to: 100,
      });

      expect(
        drawing.getPriceSegments()[0],
      ).toMatchObject({
        from: 20,
        to: 80,
      });
    });

    it('должен создавать label из соответствующего geometry bound', () => {
      const {
        drawing,
        setSelected,
      } = createDrawing();

      drawing.setGeometry({
        left: 10,
        right: 100,
        top: 20,
        bottom: 80,
      });

      setSelected(true);

      expect(
        drawing.getBoundsLabel(
          'left',
          ['left', 'right'],
        ),
      ).toMatchObject({
        coordinate: 10,
        text: 'left',
      });

      expect(
        drawing.getBoundsLabel(
          'bottom',
          ['top', 'bottom'],
        ),
      ).toMatchObject({
        coordinate: 80,
        text: 'bottom',
      });
    });

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

      drawing.setGeometry({
        left: 10,
        right: 100,
        top: 20,
        bottom: 80,
      });

      setSelected(true);

      expect(
        drawing.getBoundsLabel(
          'unknown',
          ['left', 'right'],
        ),
      ).toBeNull();
    });
  });
});