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


import { CustomAxisView } from '@core/Drawings/axis/CustomAxisView';

import type { AxisLabel } from '@core/Drawings/types';

class TestCustomAxisView extends CustomAxisView {
  protected getViewportSize(rect: DOMRect): number {
    return rect.width;
  }
}

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

function createLabel(
  overrides: Partial<AxisLabel> = {},
): AxisLabel {
  return {
    coordinate: 100,
    text: 'Label',
    textColor: '#ffffff',
    backgroundColor: '#000000',
    ...overrides,
  };
}

describe('CustomAxisView', () => {
  it('должен возвращать данные текущего label', () => {
    const getAxisLabel = jest.fn(
      () => createLabel(),
    );

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

    expect(view.coordinate()).toBe(100);
    expect(view.text()).toBe('Label');
    expect(view.textColor()).toBe('#ffffff');
    expect(view.backColor()).toBe('#000000');
    expect(getAxisLabel).toHaveBeenCalledWith('start');
  });

  it('должен возвращать значения по умолчанию при отсутствии label', () => {
    const view = new TestCustomAxisView({
      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('не должен показывать label с пустым текстом', () => {
    const view = new TestCustomAxisView({
      getAxisLabel: () =>
        createLabel({
          text: '',
        }),
      labelKind: 'start',
      viewport: createViewport(),
    });

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

  it('должен показывать label внутри viewport', () => {
    const view = new TestCustomAxisView({
      getAxisLabel: () =>
        createLabel({
          coordinate: 200,
        }),
      labelKind: 'start',
      viewport: createViewport(400),
    });

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

  it.each([
    -1,
    401,
  ])(
    'не должен показывать label вне viewport: %s',
    (coordinate) => {
      const view = new TestCustomAxisView({
        getAxisLabel: () =>
          createLabel({
            coordinate,
          }),
        labelKind: 'start',
        viewport: createViewport(400),
      });

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

  it.each([
    0,
    400,
  ])(
    'должен показывать label на границе viewport: %s',
    (coordinate) => {
      const view = new TestCustomAxisView({
        getAxisLabel: () =>
          createLabel({
            coordinate,
          }),
        labelKind: 'start',
        viewport: createViewport(400),
      });

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

  it.each([
    Number.NaN,
    Number.POSITIVE_INFINITY,
    Number.NEGATIVE_INFINITY,
  ])(
    'не должен показывать label с невалидной coordinate: %s',
    (coordinate) => {
      const view = new TestCustomAxisView({
        getAxisLabel: () =>
          createLabel({
            coordinate,
          }),
        labelKind: 'start',
        viewport: createViewport(),
      });

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

  it('tick не должен быть видимым', () => {
    const view = new TestCustomAxisView({
      getAxisLabel: () =>
        createLabel(),
      labelKind: 'start',
      viewport: createViewport(),
    });

    expect(
      view.tickVisible(),
    ).toBe(false);
  });

  it('update не должен изменять состояние view', () => {
    const getAxisLabel = jest.fn(
      () => createLabel(),
    );

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

    view.update();

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

















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('update не должен пересоздавать renderer', () => {
    const view = new CustomPriceAxisPaneView({
      getAxisSegments: () => [],
    });

    const renderer = view.renderer();

    view.update();

    expect(view.renderer()).toBe(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('должен учитывать verticalPixelRatio', () => {
    const { context, target } = createTarget(3);

    const view = new CustomPriceAxisPaneView({
      getAxisSegments: () => [
        {
          from: 10,
          to: 30,
          color: '#ff0000',
        },
      ],
    });

    view.renderer().draw(target);

    expect(context.fillRect).toHaveBeenCalledWith(
      0,
      30,
      300,
      60,
    );
  });

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

    expect(context.save).toHaveBeenCalledTimes(1);
    expect(context.restore).toHaveBeenCalledTimes(1);
  });

  it('должен получать актуальные segments при каждом draw', () => {
    const { context, target } = createTarget();

    const getAxisSegments = jest
      .fn()
      .mockReturnValueOnce([
        {
          from: 10,
          to: 20,
          color: '#ff0000',
        },
      ])
      .mockReturnValueOnce([
        {
          from: 30,
          to: 50,
          color: '#00ff00',
        },
      ]);

    const view = new CustomPriceAxisPaneView({
      getAxisSegments,
    });

    view.renderer().draw(target);
    view.renderer().draw(target);

    expect(getAxisSegments).toHaveBeenCalledTimes(2);

    expect(context.fillRect).toHaveBeenNthCalledWith(
      1,
      0,
      20,
      300,
      20,
    );

    expect(context.fillRect).toHaveBeenNthCalledWith(
      2,
      0,
      60,
      300,
      40,
    );
  });
});

describe('CustomPriceAxisView', () => {
  it('должен использовать height viewport для проверки видимости', () => {
    const view = new CustomPriceAxisView({
      getAxisLabel: () => ({
        coordinate: 250,
        text: '100.50',
        textColor: '#ffffff',
        backgroundColor: '#000000',
      }),
      labelKind: 'entry',
      viewport: createViewport(100, 300),
    });

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

  it('не должен использовать width viewport для проверки видимости', () => {
    const view = new CustomPriceAxisView({
      getAxisLabel: () => ({
        coordinate: 150,
        text: '100.50',
        textColor: '#ffffff',
        backgroundColor: '#000000',
      }),
      labelKind: 'entry',
      viewport: createViewport(400, 100),
    });

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

  it('должен считать нижнюю границу height видимой', () => {
    const view = new CustomPriceAxisView({
      getAxisLabel: () => ({
        coordinate: 300,
        text: '100.50',
        textColor: '#ffffff',
        backgroundColor: '#000000',
      }),
      labelKind: 'entry',
      viewport: createViewport(100, 300),
    });

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

  it('не должен показывать label ниже height viewport', () => {
    const view = new CustomPriceAxisView({
      getAxisLabel: () => ({
        coordinate: 301,
        text: '100.50',
        textColor: '#ffffff',
        backgroundColor: '#000000',
      }),
      labelKind: 'entry',
      viewport: createViewport(1000, 300),
    });

    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('update не должен пересоздавать renderer', () => {
    const view = new CustomTimeAxisPaneView({
      getAxisSegments: () => [],
    });

    const renderer = view.renderer();

    view.update();

    expect(view.renderer()).toBe(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('должен учитывать horizontalPixelRatio', () => {
    const { context, target } = createTarget(3);

    const view = new CustomTimeAxisPaneView({
      getAxisSegments: () => [
        {
          from: 10,
          to: 30,
          color: '#ff0000',
        },
      ],
    });

    view.renderer().draw(target);

    expect(context.fillRect).toHaveBeenCalledWith(
      30,
      0,
      60,
      200,
    );
  });

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

    expect(context.save).toHaveBeenCalledTimes(1);
    expect(context.restore).toHaveBeenCalledTimes(1);
  });

  it('должен получать актуальные segments при каждом draw', () => {
    const { context, target } = createTarget();

    const getAxisSegments = jest
      .fn()
      .mockReturnValueOnce([
        {
          from: 10,
          to: 20,
          color: '#ff0000',
        },
      ])
      .mockReturnValueOnce([
        {
          from: 30,
          to: 50,
          color: '#00ff00',
        },
      ]);

    const view = new CustomTimeAxisPaneView({
      getAxisSegments,
    });

    view.renderer().draw(target);
    view.renderer().draw(target);

    expect(getAxisSegments).toHaveBeenCalledTimes(2);

    expect(context.fillRect).toHaveBeenNthCalledWith(
      1,
      20,
      0,
      20,
      200,
    );

    expect(context.fillRect).toHaveBeenNthCalledWith(
      2,
      60,
      0,
      40,
      200,
    );
  });
});

describe('CustomTimeAxisView', () => {
  it('должен использовать width viewport для проверки видимости', () => {
    const view = new CustomTimeAxisView({
      getAxisLabel: () => ({
        coordinate: 250,
        text: '09:30',
        textColor: '#ffffff',
        backgroundColor: '#000000',
      }),
      labelKind: 'start',
      viewport: createViewport(300, 100),
    });

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

  it('не должен использовать height viewport для проверки видимости', () => {
    const view = new CustomTimeAxisView({
      getAxisLabel: () => ({
        coordinate: 150,
        text: '09:30',
        textColor: '#ffffff',
        backgroundColor: '#000000',
      }),
      labelKind: 'start',
      viewport: createViewport(100, 400),
    });

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

  it('должен считать правую границу width видимой', () => {
    const view = new CustomTimeAxisView({
      getAxisLabel: () => ({
        coordinate: 300,
        text: '09:30',
        textColor: '#ffffff',
        backgroundColor: '#000000',
      }),
      labelKind: 'start',
      viewport: createViewport(300, 100),
    });

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

  it('не должен показывать label правее width viewport', () => {
    const view = new CustomTimeAxisView({
      getAxisLabel: () => ({
        coordinate: 301,
        text: '09:30',
        textColor: '#ffffff',
        backgroundColor: '#000000',
      }),
      labelKind: 'start',
      viewport: createViewport(300, 1000),
    });

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