Загрузка данных
import { createElement, type ReactElement, type ReactNode } from 'react';
import { CHART_DRAWING_TOOLTIP } from '@src/constants';
import { Tooltip } from '../Tooltip';
jest.mock('@src/constants', () => ({
CHART_DRAWING_TOOLTIP: 'chart-drawing-tooltip',
}));
interface TooltipElementProps {
direction?: boolean;
location?: string;
showMessageOnFocus?: boolean;
tooltipClassName?: string;
children?: ReactNode;
}
describe('Tooltip', () => {
it('должен использовать direction=false по умолчанию и передавать остальные props', () => {
const children = createElement('span', null, 'content');
const element = Tooltip({
label: 'Tooltip label',
location: 'top',
showMessageOnFocus: true,
children,
} as Parameters<typeof Tooltip>[0]) as ReactElement<TooltipElementProps>;
expect(element.props.direction).toBe(false);
expect(element.props.location).toBe('top');
expect(element.props.showMessageOnFocus).toBe(true);
expect(element.props.children).toBe(children);
expect(element.props.tooltipClassName).toContain(CHART_DRAWING_TOOLTIP);
});
it('должен позволять переопределить direction и объединять tooltipClassName', () => {
const element = Tooltip({
label: 'Tooltip label',
direction: true,
tooltipClassName: 'custom-tooltip',
children: createElement('span'),
} as Parameters<typeof Tooltip>[0]) as ReactElement<TooltipElementProps>;
expect(element.props.direction).toBe(true);
expect(element.props.tooltipClassName).toContain(CHART_DRAWING_TOOLTIP);
expect(element.props.tooltipClassName).toContain('custom-tooltip');
});
});
import type { ReactElement } from 'react';
import { MenuList, type MenuOption } from '@src/components/Menu/MenuList';
import { SnapshotMenu } from '@src/components/Menu/SnapshotMenu';
import { t } from '@src/translations';
type SnapshotAction = 'copy-image';
interface SnapshotMenuElementProps {
mode: 'single';
value?: SnapshotAction;
options: MenuOption<SnapshotAction>[];
onClick: (action: SnapshotAction) => void;
}
describe('SnapshotMenu', () => {
it('должен отображать действие копирования изображения', () => {
const onCopyImage = jest.fn(() => Promise.resolve());
const element = SnapshotMenu({
onCopyImage,
}) as ReactElement<SnapshotMenuElementProps>;
expect(element.type).toBe(MenuList);
expect(element.props.mode).toBe('single');
expect(element.props.value).toBeUndefined();
expect(element.props.options).toHaveLength(1);
expect(element.props.options[0]).toEqual(
expect.objectContaining({
value: 'copy-image',
label: t('Copy image'),
}),
);
expect(element.props.options[0].icon).toBeDefined();
});
it('должен вызывать onCopyImage при выборе действия копирования', () => {
const onCopyImage = jest.fn(() => Promise.resolve());
const element = SnapshotMenu({
onCopyImage,
}) as ReactElement<SnapshotMenuElementProps>;
element.props.onClick('copy-image');
expect(onCopyImage).toHaveBeenCalledTimes(1);
});
});
import { act } from 'react';
import { createRoot, type Root } from 'react-dom/client';
import { BehaviorSubject } from 'rxjs';
import { Header } from '@src/components/Header';
jest.mock('exchange-elements/v2', () => {
const React = jest.requireActual('react') as typeof import('react');
return {
Button: ({
onClick,
label,
disabled,
className,
}: {
onClick?: () => void;
label?: import('react').ReactNode;
disabled?: boolean;
className?: string;
}) =>
React.createElement(
'button',
{
type: 'button',
onClick,
disabled,
className,
},
label,
),
Divider: () =>
React.createElement('div', {
'data-testid': 'divider',
}),
};
});
jest.mock('@src/components/Tooltip', () => {
const React = jest.requireActual('react') as typeof import('react');
return {
Tooltip: ({
label,
children,
}: {
label?: string;
children?: import('react').ReactNode;
}) =>
React.createElement(
'div',
{
'data-tooltip': label,
},
children,
),
};
});
jest.mock('@src/components/IndicatorsSelect', () => {
const React = jest.requireActual('react') as typeof import('react');
return {
IndicatorsSelect: () =>
React.createElement('div', {
'data-testid': 'indicators-select',
}),
};
});
jest.mock('@src/components/Dropdown', () => {
const React = jest.requireActual('react') as typeof import('react');
return {
Dropdown: ({
selectedValue,
children,
}: {
selectedValue?: import('react').ReactNode;
children?: import('react').ReactNode;
}) =>
React.createElement(
'div',
{
'data-testid': 'dropdown',
},
React.createElement(
'div',
{
'data-testid': 'dropdown-trigger',
},
selectedValue,
),
children,
),
};
});
jest.mock('@src/components/Menu', () => {
const React = jest.requireActual('react') as typeof import('react');
return {
TimeframesMenu: () => React.createElement('div'),
SeriesMenu: () => React.createElement('div'),
SnapshotMenu: ({
onCopyImage,
}: {
onCopyImage: () => Promise<void>;
}) =>
React.createElement(
'button',
{
type: 'button',
'data-testid': 'copy-image-action',
onClick: () => {
void onCopyImage();
},
},
'copy image',
),
};
});
jest.mock('@src/components/Icon', () => {
const React = jest.requireActual('react') as typeof import('react');
const createIcon = (testId: string) => () =>
React.createElement('span', {
'data-testid': testId,
});
return {
BarIcon: createIcon('bar-icon'),
BurgerMenuIcon: createIcon('burger-icon'),
CameraIcon: createIcon('camera-icon'),
CandleStickIcon: createIcon('candlestick-icon'),
FullscreenIcon: createIcon('fullscreen-icon'),
LineIcon: createIcon('line-icon'),
PlusCircleIcon: createIcon('plus-icon'),
RedoIcon: createIcon('redo-icon'),
SearchIcon: createIcon('search-icon'),
UndoIcon: createIcon('undo-icon'),
};
});
jest.mock('@src/translations', () => ({
t: (key: string) => key,
}));
jest.mock('@src/utils', () => ({
useObservable: (
observable: { getValue?: () => unknown } | undefined,
defaultValue?: unknown,
) => observable?.getValue?.() ?? defaultValue,
}));
describe('Header', () => {
let container: HTMLDivElement;
let root: Root;
beforeEach(() => {
(
globalThis as {
IS_REACT_ACT_ENVIRONMENT?: boolean;
}
).IS_REACT_ACT_ENVIRONMENT = true;
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
});
afterEach(() => {
act(() => {
root.unmount();
});
container.remove();
});
it('должен отображать меню снимка и передавать действие копирования изображения', () => {
const copyImage = jest.fn(() => Promise.resolve());
const fullscreen = {
isFullscreen: false,
onChange: jest.fn(() => jest.fn()),
toggle: jest.fn(),
};
act(() => {
root.render(
<Header
timeframes={[]}
selectedTimeframeObs={
new BehaviorSubject('1m') 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(
container.querySelector(
'[data-testid="camera-icon"]',
),
).not.toBeNull();
expect(
container.querySelector(
'[data-tooltip="Snapshot"]',
),
).not.toBeNull();
const copyButton =
container.querySelector<HTMLButtonElement>(
'[data-testid="copy-image-action"]',
);
expect(copyButton).not.toBeNull();
act(() => {
copyButton?.click();
});
expect(copyImage).toHaveBeenCalledTimes(1);
});
});
import {
canvasToBlob,
drawTextLayer,
} from '@src/utils';
describe('chart image utils', () => {
afterEach(() => {
jest.restoreAllMocks();
});
describe('drawTextLayer', () => {
it('не должен рисовать текст, если canvas context недоступен', () => {
const canvas = document.createElement('canvas');
const container = document.createElement('div');
const relativeTo = document.createElement('div');
Object.defineProperty(canvas, 'getContext', {
configurable: true,
value: jest.fn(() => null),
});
const createTreeWalkerSpy = jest.spyOn(
document,
'createTreeWalker',
);
drawTextLayer(
canvas,
container,
relativeTo,
);
expect(
createTreeWalkerSpy,
).not.toHaveBeenCalled();
});
it('должен переносить текст и его вычисленные стили на canvas', () => {
const canvas = document.createElement('canvas');
canvas.width = 200;
canvas.height = 100;
const container = document.createElement('div');
const textElement = document.createElement('span');
const emptyElement = document.createElement('span');
const relativeTo = document.createElement('div');
textElement.textContent = 'APPL 124.51';
textElement.style.fontStyle = 'italic';
textElement.style.fontWeight = '700';
textElement.style.fontSize = '12px';
textElement.style.fontFamily = 'Inter';
textElement.style.color = 'rgb(1, 2, 3)';
emptyElement.textContent = ' ';
container.append(
textElement,
emptyElement,
);
const context = {
save: jest.fn(),
scale: jest.fn(),
restore: jest.fn(),
fillText: jest.fn(),
font: '',
fillStyle: '',
textBaseline: '',
};
Object.defineProperty(canvas, 'getContext', {
configurable: true,
value: jest.fn(() => context),
});
jest
.spyOn(
relativeTo,
'getBoundingClientRect',
)
.mockReturnValue({
left: 10,
top: 20,
right: 110,
bottom: 70,
width: 100,
height: 50,
x: 10,
y: 20,
toJSON: () => ({}),
});
const selectNodeContents = jest.fn();
jest
.spyOn(document, 'createRange')
.mockReturnValue({
selectNodeContents,
getBoundingClientRect: () => ({
left: 30,
top: 40,
right: 80,
bottom: 52,
width: 50,
height: 12,
x: 30,
y: 40,
toJSON: () => ({}),
}),
} as unknown as Range);
drawTextLayer(
canvas,
container,
relativeTo,
);
expect(
context.save,
).toHaveBeenCalledTimes(1);
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,
type?: string,
) => {
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('должен отклонять Promise, если canvas не создал Blob', async () => {
const canvas =
document.createElement('canvas');
const toBlob = jest.fn(
(callback: BlobCallback) => {
callback(null);
},
);
Object.defineProperty(
canvas,
'toBlob',
{
configurable: true,
value: toBlob,
},
);
await expect(
canvasToBlob(canvas),
).rejects.toThrow(
'Failed to create chart image',
);
});
});
});
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,
});
const fontLinks =
document.querySelectorAll(
'link[href*="fonts.googleapis.com"]',
);
expect(fontLinks).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 {
MoexChart,
type IMoexChart,
} 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(
() => Promise.resolve(),
);
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.eventManager = {
getTimeframeObs: jest.fn(),
setTimeframe: jest.fn(),
getSelectedSeries: jest.fn(),
setSeriesSelected: jest.fn(),
};
internals.modalRenderer = {};
internals.fullscreen = {};
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 headerElement =
headerRenderer
.renderComponent
.mock
.calls[0][0] as ReactElement<{
copyImage:
() => Promise<void>;
}>;
expect(
headerElement.props.copyImage,
).toBe(copyImage);
});
});
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 canvasToBlobMock =
canvasToBlob as jest.MockedFunction<typeof canvasToBlob>;
const drawTextLayerMock =
drawTextLayer as jest.MockedFunction<typeof drawTextLayer>;
let clipboardDescriptor: PropertyDescriptor | undefined;
let clipboardItemDescriptor: PropertyDescriptor | undefined;
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();
clipboardDescriptor = Object.getOwnPropertyDescriptor(
navigator,
'clipboard',
);
clipboardItemDescriptor = Object.getOwnPropertyDescriptor(
globalThis,
'ClipboardItem',
);
});
afterEach(() => {
if (clipboardDescriptor) {
Object.defineProperty(
navigator,
'clipboard',
clipboardDescriptor,
);
} else {
Reflect.deleteProperty(
navigator,
'clipboard',
);
}
if (clipboardItemDescriptor) {
Object.defineProperty(
globalThis,
'ClipboardItem',
clipboardItemDescriptor,
);
} else {
Reflect.deleteProperty(
globalThis,
'ClipboardItem',
);
}
});
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');
visibleLegend.classList.add(
CHART_PANE_LEGEND_CONTAINER,
);
hiddenLegend.classList.add(
CHART_PANE_LEGEND_CONTAINER,
);
hiddenLegend.hidden = true;
displayNoneLegend.classList.add(
CHART_PANE_LEGEND_CONTAINER,
);
displayNoneLegend.style.display =
'none';
chartElement.append(
visibleLegend,
hiddenLegend,
displayNoneLegend,
);
chart.lwcChart.takeScreenshot.mockReturnValue(
canvas,
);
chart.lwcChart.chartElement.mockReturnValue(
chartElement,
);
const blob = new Blob(
['chart'],
{
type: 'image/png',
},
);
canvasToBlobMock.mockResolvedValue(
blob,
);
const clipboardWrite =
jest.fn(() =>
Promise.resolve(),
);
class ClipboardItemMock {
public readonly data: Record<
string,
Blob
>;
constructor(
data: Record<string, Blob>,
) {
this.data = data;
}
}
Object.defineProperty(
navigator,
'clipboard',
{
configurable: true,
value: {
write: clipboardWrite,
},
},
);
Object.defineProperty(
globalThis,
'ClipboardItem',
{
configurable: true,
value: ClipboardItemMock,
},
);
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(
drawTextLayerMock,
).not.toHaveBeenCalledWith(
canvas,
hiddenLegend,
chartElement,
);
expect(
drawTextLayerMock,
).not.toHaveBeenCalledWith(
canvas,
displayNoneLegend,
chartElement,
);
expect(
canvasToBlobMock,
).toHaveBeenCalledWith(
canvas,
);
expect(
clipboardWrite,
).toHaveBeenCalledTimes(1);
const clipboardItems =
clipboardWrite.mock.calls[0][0] as unknown as ClipboardItemMock[];
expect(
clipboardItems,
).toHaveLength(1);
expect(
clipboardItems[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.appendChild(
hiddenLegend,
);
chart.lwcChart.takeScreenshot.mockReturnValue(
canvas,
);
chart.lwcChart.chartElement.mockReturnValue(
chartElement,
);
canvasToBlobMock.mockResolvedValue(
new Blob(['chart'], {
type: 'image/png',
}),
);
const clipboardWrite =
jest.fn(() =>
Promise.resolve(),
);
Object.defineProperty(
navigator,
'clipboard',
{
configurable: true,
value: {
write: clipboardWrite,
},
},
);
Object.defineProperty(
globalThis,
'ClipboardItem',
{
configurable: true,
value: class {
constructor(
public readonly data: Record<
string,
Blob
>,
) {}
},
},
);
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,
);
const error = new Error(
'Failed to create chart image',
);
canvasToBlobMock.mockRejectedValue(
error,
);
const clipboardWrite =
jest.fn(() =>
Promise.resolve(),
);
Object.defineProperty(
navigator,
'clipboard',
{
configurable: true,
value: {
write: clipboardWrite,
},
},
);
Object.defineProperty(
globalThis,
'ClipboardItem',
{
configurable: true,
value: class {},
},
);
await expect(
chart.copyImage(),
).rejects.toThrow(
'Failed to create chart image',
);
expect(
clipboardWrite,
).not.toHaveBeenCalled();
});
});
});