Загрузка данных
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('@components/InstrumentSearch', () => ({
InstrumentSearch: jest.fn(() => null),
}));
interface CompareActions {
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
onClose={jest.fn()}
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', () => {
// Arrange
mockIsNewScaleDisabled.mockReturnValue(true);
// Act
renderComponent();
const instrumentSearchProps = getInstrumentSearchProps();
// Assert
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({
handlePercent: expect.any(Function),
handleNewScale: expect.any(Function),
handleNewPanel: expect.any(Function),
});
});
it('should subscribe to new scale disabled state', () => {
// Arrange & Act
renderComponent();
// Assert
expect(mockIsNewScaleDisabled).toHaveBeenCalledTimes(1);
expect(mockIsNewScaleDisabledObservable).toHaveBeenCalledTimes(1);
expect(mockSubscribe).toHaveBeenCalledTimes(1);
});
it('should update new scale disabled state from manager observable', () => {
// Arrange
renderComponent();
// Act
act(() => {
newScaleDisabledListener?.(true);
});
// Assert
expect(getInstrumentSearchProps().isNewScaleDisabled).toBe(true);
});
it('should unsubscribe from manager observable on unmount', () => {
// Arrange
const { unmount } = renderComponent();
// Act
unmount();
// Assert
expect(mockUnsubscribe).toHaveBeenCalledTimes(1);
});
it('should not subscribe when modal is closed', () => {
// Arrange & Act
renderComponent(false);
// Assert
expect(mockIsNewScaleDisabled).not.toHaveBeenCalled();
expect(mockIsNewScaleDisabledObservable).not.toHaveBeenCalled();
expect(getInstrumentSearchProps().isNewScaleDisabled).toBe(false);
});
it('should not subscribe when compare manager is unavailable', () => {
// Arrange
compareManagerRef.current = null;
// Act
renderComponent();
// Assert
expect(mockIsNewScaleDisabled).not.toHaveBeenCalled();
expect(mockIsNewScaleDisabledObservable).not.toHaveBeenCalled();
expect(getInstrumentSearchProps().isNewScaleDisabled).toBe(false);
});
it.each([
['handlePercent', CompareMode.Percentage],
['handleNewScale', CompareMode.NewScale],
['handleNewPanel', CompareMode.NewPane],
] as const)('should add compare instrument using %s action', (handlerName, mode) => {
// Arrange
renderComponent();
const instrument = {
issKey: 'MXSE:TQBR:SBER',
displayName: 'Сбербанк',
symbol: 'SBER',
} as Contract;
const handlers = getInstrumentSearchProps().customActionsFooterHandlers;
// Act
handlers[handlerName](instrument);
// Assert
expect(mockSetSymbolMode).toHaveBeenCalledTimes(1);
expect(mockSetSymbolMode).toHaveBeenCalledWith(
'Line',
{
symbol: 'MXSE:TQBR:SBER',
symbolName: 'Сбербанк',
symbolTicker: 'SBER',
},
mode,
);
});
it('should use request symbol when display name and ticker are empty', () => {
// Arrange
renderComponent();
const instrument = {
issKey: 'MXSE:TQBR:SBER',
displayName: '',
symbol: '',
} as Contract;
// Act
getInstrumentSearchProps().customActionsFooterHandlers.handlePercent(instrument);
// Assert
expect(mockSetSymbolMode).toHaveBeenCalledWith(
'Line',
{
symbol: 'MXSE:TQBR:SBER',
symbolName: 'MXSE:TQBR:SBER',
symbolTicker: 'MXSE:TQBR:SBER',
},
CompareMode.Percentage,
);
});
it('should not add compare instrument without issKey', () => {
// Arrange
renderComponent();
const instrument = {
issKey: '',
displayName: 'Сбербанк',
symbol: 'SBER',
} as Contract;
// Act
getInstrumentSearchProps().customActionsFooterHandlers.handlePercent(instrument);
// Assert
expect(mockSetSymbolMode).not.toHaveBeenCalled();
});
});