Загрузка данных
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);
});
});