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


import { BehaviorSubject, Subscription } from 'rxjs';

import { Chart } from '@core/Chart';
import { CHART_PANE_LEGEND_CONTAINER } from '@src/constants';
import * as utils from '@src/utils';

interface ChartInternals {
  eventManager: {
    symbolId: () => BehaviorSubject<string>;
    getInterval: () => BehaviorSubject<null>;
  };
  compareManager: {
    itemsObs: () => BehaviorSubject<{ symbolId: string }[]>;
  };
  dataSource: {
    setSymbols: jest.Mock;
    loadTill: jest.Mock;
  };
  lwcChart: {
    timeScale: () => {
      getVisibleRange: () => null;
    };
    takeScreenshot: jest.Mock;
    chartElement: jest.Mock;
  };
  subscriptions: Subscription;
  activeSymbolIds: string[];
  currentInterval: null;
  setupDataSourceSubs: () => void;
  copyImage: () => Promise<void>;
}

describe('Chart', () => {
  let clipboardWrite: jest.Mock;

  function createChartInternals(): ChartInternals {
    return Object.assign(Object.create(Chart.prototype), {
      eventManager: {
        symbolId: () => new BehaviorSubject(''),
        getInterval: () => new BehaviorSubject(null),
      },
      compareManager: {
        itemsObs: () =>
          new BehaviorSubject([
            { symbolId: 'SBER' },
            { symbolId: 'SBER' },
          ]),
      },
      dataSource: {
        setSymbols: jest.fn(),
        loadTill: jest.fn(() => Promise.resolve()),
      },
      lwcChart: {
        timeScale: () => ({
          getVisibleRange: () => null,
        }),
        takeScreenshot: jest.fn(),
        chartElement: jest.fn(),
      },
      subscriptions: new Subscription(),
      activeSymbolIds: [],
      currentInterval: null,
    }) as ChartInternals;
  }

  beforeEach(() => {
    clipboardWrite = jest.fn().mockResolvedValue(undefined);

    Object.defineProperty(navigator, 'clipboard', {
      configurable: true,
      value: { write: clipboardWrite },
    });

    Object.defineProperty(globalThis, 'ClipboardItem', {
      configurable: true,
      value: class {
        constructor(public data: Record<string, Blob>) {}
      },
    });
  });

  it('должен исключать пустой основной symbolId и удалять дубликаты', () => {
    const chart = createChartInternals();

    chart.setupDataSourceSubs();

    expect(chart.dataSource.setSymbols).toHaveBeenLastCalledWith(['SBER']);

    chart.subscriptions.unsubscribe();
  });

  describe('copyImage', () => {
    it('должен копировать изображение графика вместе с видимыми легендами', async () => {
      const chart = createChartInternals();
      const canvas = document.createElement('canvas');
      const chartElement = document.createElement('div');
      const visibleLegend = document.createElement('div');
      const hiddenLegend = document.createElement('div');
      const displayNoneLegend = document.createElement('div');
      const blob = new Blob(['chart'], { type: 'image/png' });

      visibleLegend.classList.add(CHART_PANE_LEGEND_CONTAINER);
      hiddenLegend.classList.add(CHART_PANE_LEGEND_CONTAINER);
      displayNoneLegend.classList.add(CHART_PANE_LEGEND_CONTAINER);

      hiddenLegend.hidden = true;
      displayNoneLegend.style.display = 'none';

      chartElement.append(visibleLegend, hiddenLegend, displayNoneLegend);

      chart.lwcChart.takeScreenshot.mockReturnValue(canvas);
      chart.lwcChart.chartElement.mockReturnValue(chartElement);

      const drawTextLayer = jest.spyOn(utils, 'drawTextLayer').mockImplementation();
      const canvasToBlob = jest.spyOn(utils, 'canvasToBlob').mockResolvedValue(blob);

      await chart.copyImage();

      expect(chart.lwcChart.takeScreenshot).toHaveBeenCalledWith(true, false);
      expect(chart.lwcChart.chartElement).toHaveBeenCalledTimes(1);
      expect(drawTextLayer).toHaveBeenCalledTimes(1);
      expect(drawTextLayer).toHaveBeenCalledWith(canvas, visibleLegend, chartElement);
      expect(canvasToBlob).toHaveBeenCalledWith(canvas);
      expect(clipboardWrite).toHaveBeenCalledTimes(1);

      const items = clipboardWrite.mock.calls[0][0] as Array<{ data: Record<string, Blob> }>;

      expect(items[0].data).toEqual({
        'image/png': blob,
      });
    });

    it('не должен дорисовывать скрытые легенды', async () => {
      const chart = createChartInternals();
      const canvas = document.createElement('canvas');
      const chartElement = document.createElement('div');
      const hiddenLegend = document.createElement('div');

      hiddenLegend.classList.add(CHART_PANE_LEGEND_CONTAINER);
      hiddenLegend.hidden = true;
      chartElement.append(hiddenLegend);

      chart.lwcChart.takeScreenshot.mockReturnValue(canvas);
      chart.lwcChart.chartElement.mockReturnValue(chartElement);

      const drawTextLayer = jest.spyOn(utils, 'drawTextLayer').mockImplementation();

      jest.spyOn(utils, 'canvasToBlob').mockResolvedValue(new Blob(['chart'], { type: 'image/png' }));

      await chart.copyImage();

      expect(drawTextLayer).not.toHaveBeenCalled();
      expect(clipboardWrite).toHaveBeenCalledTimes(1);
    });

    it('не должен записывать данные в clipboard, если Blob не удалось создать', async () => {
      const chart = createChartInternals();
      const canvas = document.createElement('canvas');
      const chartElement = document.createElement('div');

      chart.lwcChart.takeScreenshot.mockReturnValue(canvas);
      chart.lwcChart.chartElement.mockReturnValue(chartElement);

      jest.spyOn(utils, 'canvasToBlob').mockRejectedValue(new Error('Failed to create chart image'));

      await expect(chart.copyImage()).rejects.toThrow('Failed to create chart image');

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

















import { canvasToBlob, drawTextLayer } from '@src/utils';

describe('chart image utils', () => {
  describe('drawTextLayer', () => {
    it('не должен рисовать текст без canvas context', () => {
      const canvas = document.createElement('canvas');
      const container = document.createElement('div');
      const root = document.createElement('div');
      const createTreeWalker = jest.spyOn(document, 'createTreeWalker');

      jest.spyOn(canvas, 'getContext').mockReturnValue(null);

      drawTextLayer(canvas, container, root);

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

    it('должен рисовать текст DOM элемента на canvas', () => {
      const canvas = document.createElement('canvas');
      const container = document.createElement('div');
      const textElement = document.createElement('span');
      const root = document.createElement('div');

      canvas.width = 200;
      canvas.height = 100;
      textElement.textContent = 'APPL 124.51';
      container.append(textElement);

      const context = {
        save: jest.fn(),
        scale: jest.fn(),
        restore: jest.fn(),
        fillText: jest.fn(),
        font: '',
        fillStyle: '',
        textBaseline: '',
      };

      jest.spyOn(canvas, 'getContext').mockReturnValue(context as unknown as CanvasRenderingContext2D);

      jest.spyOn(root, 'getBoundingClientRect').mockReturnValue({
        left: 10,
        top: 20,
        width: 100,
        height: 50,
        right: 110,
        bottom: 70,
        x: 10,
        y: 20,
        toJSON: () => ({}),
      });

      const selectNodeContents = jest.fn();

      jest.spyOn(document, 'createRange').mockReturnValue({
        selectNodeContents,
        getBoundingClientRect: () => ({
          left: 30,
          top: 40,
          width: 50,
          height: 12,
          right: 80,
          bottom: 52,
          x: 30,
          y: 40,
          toJSON: () => ({}),
        }),
      } as unknown as Range);

      jest.spyOn(window, 'getComputedStyle').mockReturnValue({
        fontStyle: 'italic',
        fontWeight: '700',
        fontSize: '12px',
        fontFamily: 'Inter',
        color: 'rgb(1, 2, 3)',
      } as CSSStyleDeclaration);

      drawTextLayer(canvas, container, root);

      expect(context.scale).toHaveBeenCalledWith(2, 2);
      expect(selectNodeContents).toHaveBeenCalledTimes(1);
      expect(context.font).toBe('italic 700 12px Inter');
      expect(context.fillStyle).toBe('rgb(1, 2, 3)');
      expect(context.textBaseline).toBe('top');
      expect(context.fillText).toHaveBeenCalledWith('APPL 124.51', 20, 20);
      expect(context.restore).toHaveBeenCalledTimes(1);
    });
  });

  describe('canvasToBlob', () => {
    it('должен возвращать PNG Blob', async () => {
      const canvas = document.createElement('canvas');
      const blob = new Blob(['image'], { type: 'image/png' });
      const toBlob = jest.fn((callback: BlobCallback) => callback(blob));

      Object.defineProperty(canvas, 'toBlob', {
        configurable: true,
        value: toBlob,
      });

      await expect(canvasToBlob(canvas)).resolves.toBe(blob);
      expect(toBlob).toHaveBeenCalledWith(expect.any(Function), 'image/png');
    });

    it('должен выбрасывать ошибку, если Blob не создан', async () => {
      const canvas = document.createElement('canvas');

      Object.defineProperty(canvas, 'toBlob', {
        configurable: true,
        value: (callback: BlobCallback) => callback(null),
      });

      await expect(canvasToBlob(canvas)).rejects.toThrow('Failed to create chart image');
    });
  });
});

















import { fireEvent, render, screen } from '@testing-library/react';

import { SnapshotMenu } from '@src/components/Menu/SnapshotMenu';

jest.mock('@src/components/Icon', () => ({
  CopyIcon: () => <span data-testid="copy-icon" />,
}));

jest.mock('@src/translations', () => ({
  t: (key: string) => key,
}));

describe('SnapshotMenu', () => {
  it('должен отображать действие копирования изображения', () => {
    render(<SnapshotMenu onCopyImage={jest.fn()} />);

    expect(screen.getByText('Copy image')).toBeInTheDocument();
    expect(screen.getByTestId('copy-icon')).toBeInTheDocument();
  });

  it('должен вызывать onCopyImage при выборе действия', () => {
    const onCopyImage = jest.fn().mockResolvedValue(undefined);

    render(<SnapshotMenu onCopyImage={onCopyImage} />);

    fireEvent.click(screen.getByText('Copy image'));

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

















import { render, screen } from '@testing-library/react';
import type { ReactNode } from 'react';

import { Tooltip } from '@src/components/Tooltip';
import { CHART_DRAWING_TOOLTIP } from '@src/constants';

jest.mock('exchange-elements/v2', () => ({
  Tooltip: ({
    children,
    direction,
    location,
    showMessageOnFocus,
    tooltipClassName,
  }: {
    children: ReactNode;
    direction?: boolean;
    location?: string;
    showMessageOnFocus?: boolean;
    tooltipClassName?: string;
  }) => (
    <div
      data-testid="exchange-tooltip"
      data-direction={String(direction)}
      data-location={location}
      data-focus={String(showMessageOnFocus)}
      className={tooltipClassName}
    >
      {children}
    </div>
  ),
}));

describe('Tooltip', () => {
  it('должен устанавливать direction=false по умолчанию и передавать остальные props', () => {
    render(
      <Tooltip label="Tooltip" location="top" showMessageOnFocus>
        <span>content</span>
      </Tooltip>,
    );

    const tooltip = screen.getByTestId('exchange-tooltip');

    expect(tooltip).toHaveAttribute('data-direction', 'false');
    expect(tooltip).toHaveAttribute('data-location', 'top');
    expect(tooltip).toHaveAttribute('data-focus', 'true');
    expect(tooltip).toHaveClass(CHART_DRAWING_TOOLTIP);
    expect(screen.getByText('content')).toBeInTheDocument();
  });

  it('должен позволять переопределить direction и добавлять переданный класс', () => {
    render(
      <Tooltip label="Tooltip" direction tooltipClassName="custom-tooltip">
        <span>content</span>
      </Tooltip>,
    );

    const tooltip = screen.getByTestId('exchange-tooltip');

    expect(tooltip).toHaveAttribute('data-direction', 'true');
    expect(tooltip).toHaveClass(CHART_DRAWING_TOOLTIP);
    expect(tooltip).toHaveClass('custom-tooltip');
  });
});

















import { fireEvent, render, screen } from '@testing-library/react';
import type { ReactNode } from 'react';
import { BehaviorSubject } from 'rxjs';

import { Header } from '@src/components/Header';

jest.mock('exchange-elements/v2', () => ({
  Button: ({ onClick, label, disabled }: { onClick?: () => void; label?: ReactNode; disabled?: boolean }) => (
    <button
      type="button"
      onClick={onClick}
      disabled={disabled}
    >
      {label}
    </button>
  ),
  Divider: () => <hr />,
}));

jest.mock('@src/components/IndicatorsSelect', () => ({
  IndicatorsSelect: () => <div />,
}));

jest.mock('@src/components/Tooltip', () => ({
  Tooltip: ({ children, label }: { children: ReactNode; label?: string }) => (
    <div data-tooltip={label}>{children}</div>
  ),
}));

jest.mock('@src/components/Dropdown', () => ({
  Dropdown: ({ children, selectedValue }: { children: ReactNode; selectedValue?: ReactNode }) => (
    <div>
      <div>{selectedValue}</div>
      {children}
    </div>
  ),
}));

jest.mock('@src/translations', () => ({
  t: (key: string) => key,
}));

describe('Header', () => {
  it('должен отображать меню снимка и вызывать copyImage', () => {
    const copyImage = jest.fn().mockResolvedValue(undefined);
    const fullscreen = {
      isFullscreen: false,
      onChange: jest.fn(() => jest.fn()),
      toggle: jest.fn(),
    };

    render(
      <Header
        timeframes={[]}
        selectedTimeframeObs={new BehaviorSubject(null) as never}
        setTimeframe={jest.fn()}
        seriesTypes={[]}
        setSelectedSeries={jest.fn()}
        selectedSeriesObs={new BehaviorSubject('Candlestick') as never}
        showSettingsModal={undefined}
        addIndicatorToChart={jest.fn()}
        toggleToolbarVisible={jest.fn(() => false)}
        showCompareButton={false}
        showSymbolSearchButton={false}
        undoRedo={undefined}
        showMenuButton={false}
        showFullscreenButton={false}
        fullscreen={fullscreen as never}
        copyImage={copyImage}
        isMXT={false}
      />,
    );

    expect(document.querySelector('[data-tooltip="Snapshot"]')).not.toBeNull();

    fireEvent.click(screen.getByText('Copy image'));

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

















import {
  CHART_DRAWING_TOOLBAR_CONTAINER,
  CHART_MODAL_CONTAINER_CLASSNAME,
  CHART_PANE_EMPTY_STATE_CONTAINER,
  CHART_PANE_LEGEND_CONTAINER,
  CHART_PANE_OVERLAY_CONTAINER,
  CHART_ROOT_CLASSNAME,
} from '@src/constants';

import { ContainerManager } from '../ContainerManager';

jest.mock('@src/constants', () => ({
  CHART_DRAWING_TOOLBAR_CONTAINER: 'chart-drawing-toolbar-container',
  CHART_MODAL_CONTAINER_CLASSNAME: 'chart-modal-container',
  CHART_PANE_EMPTY_STATE_CONTAINER: 'chart-pane-empty-state-container',
  CHART_PANE_LEGEND_CONTAINER: 'chart-pane-legend-container',
  CHART_PANE_OVERLAY_CONTAINER: 'chart-pane-overlay-container',
  CHART_ROOT_CLASSNAME: 'chart-root',
  CHART_SCROLLABLE_CLASSNAME: 'chart-scrollable',
}));

describe('ContainerManager', () => {
  let parentContainer: HTMLDivElement;

  beforeEach(() => {
    document.querySelectorAll('link[href*="fonts.googleapis.com"]').forEach((link) => link.remove());

    parentContainer = document.createElement('div');
    document.body.appendChild(parentContainer);
  });

  afterEach(() => {
    parentContainer.remove();
  });

  describe('createContainers', () => {
    it('должен создавать основные контейнеры графика с корректной структурой', () => {
      const {
        headerContainer,
        footerContainer,
        chartContainer,
        chartAreaContainer,
        toolBarContainer,
        modalContainer,
        controlBarContainer,
        drawingToolbarContainer,
      } = ContainerManager.createContainers({
        parentContainer,
        showBottomPanel: true,
        showMenuButton: true,
      });

      expect(parentContainer.classList.contains(CHART_ROOT_CLASSNAME)).toBe(true);
      expect(parentContainer.children[0]).toBe(headerContainer);
      expect(parentContainer.children[1]).toBe(chartContainer);
      expect(parentContainer.children[2]).toBe(footerContainer);

      expect(chartContainer.contains(toolBarContainer)).toBe(true);
      expect(chartContainer.contains(chartAreaContainer)).toBe(true);
      expect(chartContainer.contains(modalContainer)).toBe(true);

      expect(chartAreaContainer.contains(controlBarContainer)).toBe(true);
      expect(chartAreaContainer.contains(drawingToolbarContainer)).toBe(true);

      expect(drawingToolbarContainer.classList.contains(CHART_DRAWING_TOOLBAR_CONTAINER)).toBe(true);
      expect(modalContainer.classList.contains(CHART_MODAL_CONTAINER_CLASSNAME)).toBe(true);
      expect(modalContainer.hidden).toBe(true);
    });

    it('должен создавать сетку с нижней панелью', () => {
      ContainerManager.createContainers({
        parentContainer,
        showBottomPanel: true,
        showMenuButton: true,
      });

      expect(parentContainer.style.gridTemplateRows).toBe('36px minmax(0, 1fr) 32px');
    });

    it('должен создавать сетку без нижней панели', () => {
      const { footerContainer } = ContainerManager.createContainers({
        parentContainer,
        showBottomPanel: false,
        showMenuButton: true,
      });

      expect(parentContainer.style.gridTemplateRows).toBe('36px minmax(0, 1fr)');
      expect(parentContainer.contains(footerContainer)).toBe(false);
    });

    it('должен отображать панель инструментов при включённой кнопке меню', () => {
      const { chartContainer, toolBarContainer } = ContainerManager.createContainers({
        parentContainer,
        showMenuButton: true,
      });

      expect(chartContainer.contains(toolBarContainer)).toBe(true);
      expect(chartContainer.style.gridTemplateColumns).toBe('42px minmax(0, 1fr)');
    });

    it('должен скрывать панель инструментов при выключенной кнопке меню', () => {
      const { chartContainer, toolBarContainer } = ContainerManager.createContainers({
        parentContainer,
        showMenuButton: false,
      });

      expect(chartContainer.contains(toolBarContainer)).toBe(false);
      expect(chartContainer.style.gridTemplateColumns).toBe('minmax(0, 1fr)');
    });

    it('должен переключать видимость панели инструментов', () => {
      const { chartContainer, toolBarContainer, toggleToolbar } = ContainerManager.createContainers({
        parentContainer,
        showMenuButton: true,
      });

      expect(toggleToolbar()).toBe(false);
      expect(chartContainer.contains(toolBarContainer)).toBe(false);
      expect(chartContainer.style.gridTemplateColumns).toBe('minmax(0, 1fr)');

      expect(toggleToolbar()).toBe(true);
      expect(chartContainer.contains(toolBarContainer)).toBe(true);
      expect(chartContainer.style.gridTemplateColumns).toBe('42px minmax(0, 1fr)');
    });

    it('должен удалять старое содержимое перед созданием контейнеров', () => {
      const oldElement = document.createElement('div');

      parentContainer.appendChild(oldElement);

      ContainerManager.createContainers({
        parentContainer,
        showMenuButton: true,
      });

      expect(parentContainer.contains(oldElement)).toBe(false);
    });

    it('должен добавлять шрифт только один раз', () => {
      ContainerManager.createContainers({
        parentContainer,
        showMenuButton: true,
      });

      ContainerManager.createContainers({
        parentContainer,
        showMenuButton: true,
      });

      expect(document.querySelectorAll('link[href*="fonts.googleapis.com"]')).toHaveLength(1);
    });
  });

  describe('createPaneContainers', () => {
    it('должен создавать контейнеры pane с корректными настройками', () => {
      const { legendContainer, paneOverlayContainer, paneEmptyStateContainer } =
        ContainerManager.createPaneContainers();

      expect(legendContainer.classList.contains(CHART_PANE_LEGEND_CONTAINER)).toBe(true);
      expect(legendContainer.style.position).toBe('absolute');
      expect(legendContainer.style.pointerEvents).toBe('none');

      expect(paneOverlayContainer.classList.contains(CHART_PANE_OVERLAY_CONTAINER)).toBe(true);
      expect(paneOverlayContainer.style.position).toBe('absolute');
      expect(paneOverlayContainer.style.inset).toBe('0');
      expect(paneOverlayContainer.style.pointerEvents).toBe('none');

      expect(paneEmptyStateContainer.classList.contains(CHART_PANE_EMPTY_STATE_CONTAINER)).toBe(true);
      expect(paneEmptyStateContainer.hidden).toBe(true);
    });

    it('должен создавать отдельные контейнеры для каждого pane', () => {
      const first = ContainerManager.createPaneContainers();
      const second = ContainerManager.createPaneContainers();

      expect(first.legendContainer).not.toBe(second.legendContainer);
      expect(first.paneOverlayContainer).not.toBe(second.paneOverlayContainer);
      expect(first.paneEmptyStateContainer).not.toBe(second.paneEmptyStateContainer);
    });
  });

  describe('clearContainers', () => {
    it('должен очищать содержимое контейнера', () => {
      parentContainer.innerHTML = '<div>content</div>';

      ContainerManager.clearContainers(parentContainer);

      expect(parentContainer.innerHTML).toBe('');
    });
  });
});

















import type { ReactElement } from 'react';

import { IMoexChart, MoexChart } from '@core/MoexChart';

describe('MoexChart', () => {
  it('должен очищать основной инструмент через EventManager', () => {
    const clearSymbol = jest.fn();
    const chart = Object.create(MoexChart.prototype) as MoexChart;

    (chart as unknown as { eventManager: { clearSymbol: () => void } }).eventManager = {
      clearSymbol,
    };

    chart.clearSymbol();

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

  it('должен устанавливать основной инструмент через EventManager', () => {
    const setSymbol = jest.fn();
    const chart = Object.create(MoexChart.prototype) as MoexChart;

    (chart as unknown as { eventManager: { setSymbol: typeof setSymbol } }).eventManager = {
      setSymbol,
    };

    chart.setSymbol({
      symbolId: 'SBER',
      symbol: 'SBER',
      symbolName: 'Sberbank',
    });

    expect(setSymbol).toHaveBeenCalledWith({
      symbolId: 'SBER',
      symbol: 'SBER',
      symbolName: 'Sberbank',
    });
  });

  it('должен передавать copyImage графика в Header', () => {
    const copyImage = jest.fn().mockResolvedValue(undefined);
    const headerRenderer = { renderComponent: jest.fn() };
    const drawingToolbarRenderer = { renderComponent: jest.fn() };

    const drawingsCollectionManager = {
      selectedDrawing: jest.fn(),
      updateSelectedDrawingSettings: jest.fn(),
      toggleSelectedDrawingLock: jest.fn(),
      openSelectedDrawingSettings: jest.fn(),
      deleteSelectedDrawing: jest.fn(),
    };

    const chart = Object.create(MoexChart.prototype) as MoexChart;

    const internals = chart as unknown as {
      chart: {
        copyImage: () => Promise<void>;
        getDrawingsCollectionManager: () => typeof drawingsCollectionManager;
      };
      drawingToolbarRenderer: typeof drawingToolbarRenderer;
      headerRenderer: typeof headerRenderer;
      toolbarRenderer: undefined;
      controlBarRenderer: undefined;
      footerRenderer: undefined;
      eventManager: {
        getTimeframeObs: jest.Mock;
        setTimeframe: jest.Mock;
        getSelectedSeries: jest.Mock;
        setSeriesSelected: jest.Mock;
      };
      modalRenderer: object;
      fullscreen: object;
      renderAttachments: (config: IMoexChart, toggleToolbar: () => boolean) => void;
    };

    internals.chart = {
      copyImage,
      getDrawingsCollectionManager: () => drawingsCollectionManager,
    };

    internals.drawingToolbarRenderer = drawingToolbarRenderer;
    internals.headerRenderer = headerRenderer;
    internals.toolbarRenderer = undefined;
    internals.controlBarRenderer = undefined;
    internals.footerRenderer = undefined;
    internals.modalRenderer = {};
    internals.fullscreen = {};

    internals.eventManager = {
      getTimeframeObs: jest.fn(),
      setTimeframe: jest.fn(),
      getSelectedSeries: jest.fn(),
      setSeriesSelected: jest.fn(),
    };

    const config = {
      chartCollectionPreset: {
        supportedTimeframes: [],
        supportedChartSeriesTypes: [],
        showSettingsButton: false,
        showMenuButton: false,
        showFullscreenButton: false,
        undoRedoEnabled: false,
        showCompareButton: false,
        openCompareModal: undefined,
        openSymbolSearchModal: undefined,
        theme: 'tr',
      },
    } as unknown as IMoexChart;

    internals.renderAttachments(config, jest.fn(() => false));

    const header = headerRenderer.renderComponent.mock.calls[0][0] as ReactElement<{
      copyImage: () => Promise<void>;
    }>;

    expect(header.props.copyImage).toBe(copyImage);
  });
});