Загрузка данных
import { fireEvent, render, screen } from '@testing-library/react';
import React from 'react';
import { Contract } from '@modules/contracts';
import '@testing-library/jest-dom';
import { MoexChartActions } from '../MoexChartActions';
describe('MoexChartActions', () => {
const mockHandlers = {
handleAbsolute: jest.fn(),
handlePercent: jest.fn(),
handleNewScale: jest.fn(),
handleNewPanel: jest.fn(),
};
const mockContract = {
issKey: 'RU000A0JQ0Y0',
} as Contract;
const renderComponent = (
selectedInstrumentRows: Contract[] = [],
isNewScaleDisabled = false,
) =>
render(
<MoexChartActions
selectedInstrumentRows={selectedInstrumentRows}
customActionsFooterHandlers={mockHandlers}
isNewScaleDisabled={isNewScaleDisabled}
/>,
);
beforeEach(() => {
jest.clearAllMocks();
});
it('should render all action buttons', () => {
renderComponent();
expect(screen.getByText('Абсолютная шкала')).toBeInTheDocument();
expect(screen.getByText('%')).toBeInTheDocument();
expect(screen.getByText('Новая шкала')).toBeInTheDocument();
expect(screen.getByText('Новая панель')).toBeInTheDocument();
});
it('should disable all action buttons when instrument is not selected', () => {
renderComponent();
expect(screen.getByText('Абсолютная шкала')).toBeDisabled();
expect(screen.getByText('%')).toBeDisabled();
expect(screen.getByText('Новая шкала')).toBeDisabled();
expect(screen.getByText('Новая панель')).toBeDisabled();
});
it('should enable all action buttons when instrument is selected', () => {
renderComponent([mockContract]);
expect(screen.getByText('Абсолютная шкала')).not.toBeDisabled();
expect(screen.getByText('%')).not.toBeDisabled();
expect(screen.getByText('Новая шкала')).not.toBeDisabled();
expect(screen.getByText('Новая панель')).not.toBeDisabled();
});
it('should disable only new scale button when new scale is unavailable', () => {
renderComponent([mockContract], true);
expect(screen.getByText('Абсолютная шкала')).not.toBeDisabled();
expect(screen.getByText('%')).not.toBeDisabled();
expect(screen.getByText('Новая шкала')).toBeDisabled();
expect(screen.getByText('Новая панель')).not.toBeDisabled();
});
it.each([
['Абсолютная шкала', 'handleAbsolute'],
['%', 'handlePercent'],
['Новая шкала', 'handleNewScale'],
['Новая панель', 'handleNewPanel'],
] as const)('should call %s handler when button is clicked', (buttonText, handlerName) => {
renderComponent([mockContract]);
fireEvent.click(screen.getByText(buttonText));
expect(mockHandlers[handlerName]).toHaveBeenCalledTimes(1);
expect(mockHandlers[handlerName]).toHaveBeenCalledWith(mockContract);
});
it('should use first instrument when multiple instruments are selected', () => {
const multipleContracts = [
{
issKey: 'RU000A0JQ0Y0',
},
{
issKey: 'RU000A0JQ0Y1',
},
] as Contract[];
renderComponent(multipleContracts);
fireEvent.click(screen.getByText('Абсолютная шкала'));
fireEvent.click(screen.getByText('%'));
fireEvent.click(screen.getByText('Новая шкала'));
fireEvent.click(screen.getByText('Новая панель'));
expect(mockHandlers.handleAbsolute).toHaveBeenCalledWith(multipleContracts[0]);
expect(mockHandlers.handlePercent).toHaveBeenCalledWith(multipleContracts[0]);
expect(mockHandlers.handleNewScale).toHaveBeenCalledWith(multipleContracts[0]);
expect(mockHandlers.handleNewPanel).toHaveBeenCalledWith(multipleContracts[0]);
});
});
import { act, render } from '@testing-library/react';
import { CompareMode } from 'moex-chart';
import React from 'react';
import { InstrumentSearch } from '@components/InstrumentSearch';
import { CompareModal } from '../components/MoexChart/components/CompareModal';
import type { Contract } from '@modules/contracts';
import type { __CompareManager__ } from 'moex-chart';
import type { MutableRefObject } from 'react';
jest.mock('moex-chart', () => ({
__esModule: true,
CompareMode: {
Absolute: 'ABS',
Percentage: 'PCT',
NewScale: 'SCALE',
NewPane: 'PANE',
},
}));
jest.mock('@components/InstrumentSearch', () => ({
InstrumentSearch: jest.fn(() => null),
}));
interface CompareActions {
handleAbsolute: (instrument: Contract) => void;
handlePercent: (instrument: Contract) => void;
handleNewScale: (instrument: Contract) => void;
handleNewPanel: (instrument: Contract) => void;
}
interface InstrumentSearchMockProps {
widgetId: number;
variant: string;
isOpen: boolean;
setOpen: (isOpen: boolean) => void;
isNewScaleDisabled: boolean;
customActionsFooterHandlers: CompareActions;
}
describe('CompareModal', () => {
const mockInstrumentSearch = InstrumentSearch as jest.Mock;
const mockSetOpen = jest.fn();
const mockSetSymbolMode = jest.fn();
const mockIsNewScaleDisabled = jest.fn();
const mockIsNewScaleDisabledObservable = jest.fn();
const mockSubscribe = jest.fn();
const mockUnsubscribe = jest.fn();
let newScaleDisabledListener: ((disabled: boolean) => void) | null;
let compareManager: __CompareManager__;
let compareManagerRef: MutableRefObject<__CompareManager__ | null>;
const getInstrumentSearchProps = (): InstrumentSearchMockProps => {
const lastCall = mockInstrumentSearch.mock.calls[mockInstrumentSearch.mock.calls.length - 1];
return lastCall?.[0] as InstrumentSearchMockProps;
};
const renderComponent = (isOpen = true) =>
render(
<CompareModal
widgetId={42}
compareManager={compareManagerRef}
isOpen={isOpen}
setOpen={mockSetOpen}
/>,
);
beforeEach(() => {
jest.clearAllMocks();
newScaleDisabledListener = null;
mockIsNewScaleDisabled.mockReturnValue(false);
mockSetSymbolMode.mockResolvedValue(undefined);
mockSubscribe.mockImplementation((listener: (disabled: boolean) => void) => {
newScaleDisabledListener = listener;
return {
unsubscribe: mockUnsubscribe,
};
});
mockIsNewScaleDisabledObservable.mockReturnValue({
subscribe: mockSubscribe,
});
compareManager = {
setSymbolMode: mockSetSymbolMode,
isNewScaleDisabled: mockIsNewScaleDisabled,
isNewScaleDisabledObservable: mockIsNewScaleDisabledObservable,
} as unknown as __CompareManager__;
compareManagerRef = {
current: compareManager,
};
});
it('should pass modal properties and current scale state to instrument search', () => {
mockIsNewScaleDisabled.mockReturnValue(true);
renderComponent();
const instrumentSearchProps = getInstrumentSearchProps();
expect(instrumentSearchProps.widgetId).toBe(42);
expect(instrumentSearchProps.variant).toBe('single');
expect(instrumentSearchProps.isOpen).toBe(true);
expect(instrumentSearchProps.setOpen).toBe(mockSetOpen);
expect(instrumentSearchProps.isNewScaleDisabled).toBe(true);
expect(instrumentSearchProps.customActionsFooterHandlers).toEqual({
handleAbsolute: expect.any(Function),
handlePercent: expect.any(Function),
handleNewScale: expect.any(Function),
handleNewPanel: expect.any(Function),
});
});
it('should subscribe to new scale disabled state', () => {
renderComponent();
expect(mockIsNewScaleDisabled).toHaveBeenCalledTimes(1);
expect(mockIsNewScaleDisabledObservable).toHaveBeenCalledTimes(1);
expect(mockSubscribe).toHaveBeenCalledTimes(1);
});
it('should update new scale disabled state from manager observable', () => {
renderComponent();
act(() => {
newScaleDisabledListener?.(true);
});
expect(getInstrumentSearchProps().isNewScaleDisabled).toBe(true);
});
it('should unsubscribe from manager observable on unmount', () => {
const { unmount } = renderComponent();
unmount();
expect(mockUnsubscribe).toHaveBeenCalledTimes(1);
});
it('should not subscribe when modal is closed', () => {
renderComponent(false);
expect(mockIsNewScaleDisabled).not.toHaveBeenCalled();
expect(mockIsNewScaleDisabledObservable).not.toHaveBeenCalled();
expect(getInstrumentSearchProps().isNewScaleDisabled).toBe(false);
});
it('should not subscribe when compare manager is unavailable', () => {
compareManagerRef.current = null;
renderComponent();
expect(mockIsNewScaleDisabled).not.toHaveBeenCalled();
expect(mockIsNewScaleDisabledObservable).not.toHaveBeenCalled();
expect(getInstrumentSearchProps().isNewScaleDisabled).toBe(false);
});
it.each([
['handleAbsolute', CompareMode.Absolute],
['handlePercent', CompareMode.Percentage],
['handleNewScale', CompareMode.NewScale],
['handleNewPanel', CompareMode.NewPane],
] as const)('should add compare instrument using %s action', (handlerName, mode) => {
renderComponent();
const instrument = {
issKey: 'MXSE:TQBR:SBER',
displayName: 'Сбербанк',
symbol: 'SBER',
} as Contract;
const handlers = getInstrumentSearchProps().customActionsFooterHandlers;
handlers[handlerName](instrument);
expect(mockSetSymbolMode).toHaveBeenCalledTimes(1);
expect(mockSetSymbolMode).toHaveBeenCalledWith(
'Line',
{
symbolId: 'MXSE:TQBR:SBER',
symbol: 'SBER',
symbolName: 'Сбербанк',
},
mode,
);
});
it('should delegate missing symbol metadata fallback to moex chart', () => {
renderComponent();
const instrument = {
issKey: 'MXSE:TQBR:SBER',
displayName: '',
symbol: '',
} as Contract;
getInstrumentSearchProps().customActionsFooterHandlers.handlePercent(instrument);
expect(mockSetSymbolMode).toHaveBeenCalledWith(
'Line',
{
symbolId: 'MXSE:TQBR:SBER',
symbol: '',
symbolName: '',
},
CompareMode.Percentage,
);
});
it('should not add compare instrument without issKey', () => {
renderComponent();
const instrument = {
issKey: '',
displayName: 'Сбербанк',
symbol: 'SBER',
} as Contract;
getInstrumentSearchProps().customActionsFooterHandlers.handleAbsolute(instrument);
expect(mockSetSymbolMode).not.toHaveBeenCalled();
});
});