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


import { BehaviorSubject, Subscription } from 'rxjs';

import { Chart } from '@core/Chart';
import { CHART_PANE_LEGEND_CONTAINER } from '@src/constants';
import { canvasToBlob, drawTextLayer } from '@src/utils';

jest.mock('@src/utils', () => ({
  ...jest.requireActual('@src/utils'),
  canvasToBlob: jest.fn(),
  drawTextLayer: jest.fn(),
}));

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', () => {
  const drawTextLayerMock = drawTextLayer as jest.MockedFunction<typeof drawTextLayer>;
  const canvasToBlobMock = canvasToBlob as jest.MockedFunction<typeof canvasToBlob>;

  const originalClipboard = navigator.clipboard;
  const originalClipboardItem = globalThis.ClipboardItem;

  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(() => {
    jest.clearAllMocks();
    drawTextLayerMock.mockReset();
    canvasToBlobMock.mockReset();

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

  afterAll(() => {
    Object.defineProperty(navigator, 'clipboard', {
      configurable: true,
      value: originalClipboard,
    });

    Object.defineProperty(globalThis, 'ClipboardItem', {
      configurable: true,
      value: originalClipboardItem,
    });
  });

  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);
      canvasToBlobMock.mockResolvedValue(blob);

      await chart.copyImage();

      expect(chart.lwcChart.takeScreenshot).toHaveBeenCalledWith(true, false);
      expect(chart.lwcChart.chartElement).toHaveBeenCalledTimes(1);

      expect(drawTextLayerMock).toHaveBeenCalledTimes(1);
      expect(drawTextLayerMock).toHaveBeenCalledWith(canvas, visibleLegend, chartElement);

      expect(canvasToBlobMock).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);
      canvasToBlobMock.mockResolvedValue(new Blob(['chart'], { type: 'image/png' }));

      await chart.copyImage();

      expect(drawTextLayerMock).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);

      canvasToBlobMock.mockRejectedValue(new Error('Failed to create chart image'));

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

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

















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/components/Menu', () => ({
  TimeframesMenu: () => <div />,
  SeriesMenu: () => <div />,
  SnapshotMenu: ({ onCopyImage }: { onCopyImage: () => Promise<void> }) => (
    <button
      type="button"
      onClick={() => void onCopyImage()}
    >
      Copy image
    </button>
  ),
}));

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