Загрузка данных
import { 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 = {
handlePercent: jest.fn(),
handleNewScale: jest.fn(),
handleNewPanel: jest.fn(),
};
const mockContract = {
issKey: 'RU000A0JQ0Y0',
};
beforeEach(() => {
jest.clearAllMocks();
});
it('should render correctly with no selected instruments', () => {
// Arrange & Act
render(
<MoexChartActions
selectedInstrumentRows={[]}
customActionsFooterHandlers={mockHandlers}
/>,
);
// Assert
expect(screen.getByText('%')).toBeInTheDocument();
expect(screen.getByText('Новая шкала')).toBeInTheDocument();
expect(screen.getByText('Новая панель')).toBeInTheDocument();
});
it('should render correctly with selected instruments', () => {
// Arrange
const selectedInstruments = [mockContract];
// Act
render(
<MoexChartActions
selectedInstrumentRows={selectedInstruments as Contract[]}
customActionsFooterHandlers={mockHandlers}
/>,
);
// Assert
expect(screen.getByText('%')).toBeInTheDocument();
expect(screen.getByText('Новая шкала')).toBeInTheDocument();
expect(screen.getByText('Новая панель')).toBeInTheDocument();
// All buttons should be enabled
const percentButton = screen.getByText('%');
const newScaleButton = screen.getByText('Новая шкала');
const newPanelButton = screen.getByText('Новая панель');
expect(percentButton).not.toBeDisabled();
expect(newScaleButton).not.toBeDisabled();
expect(newPanelButton).not.toBeDisabled();
});
it('should call handlePercent when percent button is clicked and instrument is selected', () => {
// Arrange
const selectedInstruments = [mockContract];
render(
<MoexChartActions
selectedInstrumentRows={selectedInstruments as Contract[]}
customActionsFooterHandlers={mockHandlers}
/>,
);
const percentButton = screen.getByText('%');
expect(percentButton).not.toBeDisabled();
// Act
percentButton.click();
// Assert
expect(mockHandlers.handlePercent).toHaveBeenCalledWith(mockContract);
});
it('should call handleNewScale when new scale button is clicked and instrument is selected', () => {
// Arrange
const selectedInstruments = [mockContract];
render(
<MoexChartActions
selectedInstrumentRows={selectedInstruments as Contract[]}
customActionsFooterHandlers={mockHandlers}
/>,
);
const newScaleButton = screen.getByText('Новая шкала');
expect(newScaleButton).not.toBeDisabled();
// Act
newScaleButton.click();
// Assert
expect(mockHandlers.handleNewScale).toHaveBeenCalledWith(mockContract);
});
it('should call handleNewPanel when new panel button is clicked and instrument is selected', () => {
// Arrange
const selectedInstruments = [mockContract];
render(
<MoexChartActions
selectedInstrumentRows={selectedInstruments as Contract[]}
customActionsFooterHandlers={mockHandlers}
/>,
);
const newPanelButton = screen.getByText('Новая панель');
expect(newPanelButton).not.toBeDisabled();
// Act
newPanelButton.click();
// Assert
expect(mockHandlers.handleNewPanel).toHaveBeenCalledWith(mockContract);
});
it('should handle multiple selected instruments (only first is used)', () => {
// Arrange
const multipleContracts = [
{
issKey: 'RU000A0JQ0Y0',
},
{
issKey: 'RU000A0JQ0Y1',
},
];
render(
<MoexChartActions
selectedInstrumentRows={multipleContracts as Contract[]}
customActionsFooterHandlers={mockHandlers}
/>,
);
const percentButton = screen.getByText('%');
const newScaleButton = screen.getByText('Новая шкала');
const newPanelButton = screen.getByText('Новая панель');
expect(percentButton).not.toBeDisabled();
expect(newScaleButton).not.toBeDisabled();
expect(newPanelButton).not.toBeDisabled();
// Act
percentButton.click();
newScaleButton.click();
newPanelButton.click();
// Assert
// Verify that handlers are called with the first selected instrument
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: {
Percentage: 'PCT',
NewScale: 'SCALE',
NewPane: 'PANE',
},
}));
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
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',
{
symbolId: 'MXSE:TQBR:SBER',
symbol: 'SBER',
symbolName: 'Сбербанк',
},
mode,
);
});
it('should delegate missing symbol metadata fallback to moex chart', () => {
// Arrange
renderComponent();
const instrument = {
issKey: 'MXSE:TQBR:SBER',
displayName: '',
symbol: '',
} as Contract;
// Act
getInstrumentSearchProps().customActionsFooterHandlers.handlePercent(instrument);
// Assert
expect(mockSetSymbolMode).toHaveBeenCalledWith(
'Line',
{
symbolId: 'MXSE:TQBR:SBER',
symbol: '',
symbolName: '',
},
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();
});
});