Загрузка данных
diff --git a/package-lock.json b/package-lock.json
index ad067c16b..ae9d41638 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -39,7 +39,7 @@
"markdown-it-link-attributes": "^4.0.1",
"markdown-it-underline": "^1.0.1",
"meow": "^8.1.2",
- "moex-chart": "^0.1.12",
+ "moex-chart": "^0.1.13-dev.1",
"rc-virtual-list": "^3.14.5",
"react": "^18.2.0",
"react-chartjs-2": "^5.2.0",
@@ -28809,9 +28809,9 @@
}
},
"node_modules/moex-chart": {
- "version": "0.1.12",
- "resolved": "https://nexus-dev.tech.moex.com/repository/trade-radar-npm-private-group/moex-chart/-/moex-chart-0.1.12.tgz",
- "integrity": "sha1-0uaftIpE4fqxiyRu7ZNEseyXAcM=",
+ "version": "0.1.13-dev.1",
+ "resolved": "https://nexus-dev.tech.moex.com/repository/trade-radar-npm-private-group/moex-chart/-/moex-chart-0.1.13-dev.1.tgz",
+ "integrity": "sha1-9NYDPHV+t3agqhu2FTkHFRthMfM=",
"dependencies": {
"@dnd-kit/core": "^6.1.0",
"@dnd-kit/modifiers": "^7.0.0",
@@ -60585,9 +60585,9 @@
"dev": true
},
"moex-chart": {
- "version": "0.1.12",
- "resolved": "https://nexus-dev.tech.moex.com/repository/trade-radar-npm-private-group/moex-chart/-/moex-chart-0.1.12.tgz",
- "integrity": "sha1-0uaftIpE4fqxiyRu7ZNEseyXAcM=",
+ "version": "0.1.13-dev.1",
+ "resolved": "https://nexus-dev.tech.moex.com/repository/trade-radar-npm-private-group/moex-chart/-/moex-chart-0.1.13-dev.1.tgz",
+ "integrity": "sha1-9NYDPHV+t3agqhu2FTkHFRthMfM=",
"requires": {
"@dnd-kit/core": "^6.1.0",
"@dnd-kit/modifiers": "^7.0.0",
diff --git a/package.json b/package.json
index df913f9be..73d90934c 100644
--- a/package.json
+++ b/package.json
@@ -146,7 +146,7 @@
"markdown-it-link-attributes": "^4.0.1",
"markdown-it-underline": "^1.0.1",
"meow": "^8.1.2",
- "moex-chart": "^0.1.12",
+ "moex-chart": "^0.1.13-dev.1",
"rc-virtual-list": "^3.14.5",
"react": "^18.2.0",
"react-chartjs-2": "^5.2.0",
diff --git a/src/components/InstrumentSearch/components/Actions/MoexChartActions.tsx b/src/components/InstrumentSearch/components/Actions/MoexChartActions.tsx
index 4fad0da7a..63170eaae 100644
--- a/src/components/InstrumentSearch/components/Actions/MoexChartActions.tsx
+++ b/src/components/InstrumentSearch/components/Actions/MoexChartActions.tsx
@@ -6,12 +6,12 @@ import { Button } from '@uikit/Button';
import styles from './styles.module.scss';
type TMoexChartActionsProps = {
- selectedInstrumentRows: [] | Contract[];
+ selectedInstrumentRows: Contract[];
isNewScaleDisabled?: boolean;
customActionsFooterHandlers: {
- handlePercent: (symbol: string) => void;
- handleNewScale: (symbol: string) => void;
- handleNewPanel: (symbol: string) => void;
+ handlePercent: (instrument: Contract) => void;
+ handleNewScale: (instrument: Contract) => void;
+ handleNewPanel: (instrument: Contract) => void;
};
};
@@ -22,16 +22,16 @@ export const MoexChartActions = ({
}: TMoexChartActionsProps) => {
const { handlePercent, handleNewScale, handleNewPanel } = customActionsFooterHandlers;
- const selectedSymbol = selectedInstrumentRows[0]?.issKey;
- const isButtonDisabled = !selectedSymbol;
+ const selectedInstrument = selectedInstrumentRows[0];
+ const isButtonDisabled = !selectedInstrument?.issKey;
return (
<div className={styles.actionWrapper}>
<Button
text="%"
onClick={() => {
- if (selectedSymbol) {
- handlePercent(selectedSymbol);
+ if (selectedInstrument?.issKey) {
+ handlePercent(selectedInstrument);
}
}}
disabled={isButtonDisabled}
@@ -40,8 +40,8 @@ export const MoexChartActions = ({
<Button
text="Новая шкала"
onClick={() => {
- if (selectedSymbol) {
- handleNewScale(selectedSymbol);
+ if (selectedInstrument?.issKey) {
+ handleNewScale(selectedInstrument);
}
}}
disabled={isButtonDisabled || isNewScaleDisabled}
@@ -50,8 +50,8 @@ export const MoexChartActions = ({
<Button
text="Новая панель"
onClick={() => {
- if (selectedSymbol) {
- handleNewPanel(selectedSymbol);
+ if (selectedInstrument?.issKey) {
+ handleNewPanel(selectedInstrument);
}
}}
disabled={isButtonDisabled}
diff --git a/src/components/InstrumentSearch/types/hooks.ts b/src/components/InstrumentSearch/types/hooks.ts
index a623c5b80..d82fa0da3 100644
--- a/src/components/InstrumentSearch/types/hooks.ts
+++ b/src/components/InstrumentSearch/types/hooks.ts
@@ -74,9 +74,9 @@ export type InstrumentSearchType = {
* TODO: убрать, когда разработаем свою модалку для moex_chart
*/
customActionsFooterHandlers?: {
- handlePercent: (symbol: string) => void;
- handleNewScale: (symbol: string) => void;
- handleNewPanel: (symbol: string) => void;
+ handlePercent: (instrument: Contract) => void;
+ handleNewScale: (instrument: Contract) => void;
+ handleNewPanel: (instrument: Contract) => void;
};
isNewScaleDisabled?: boolean;
};
diff --git a/src/widgets/Chart/__tests__/CompareModal.test.tsx b/src/widgets/Chart/__tests__/CompareModal.test.tsx
new file mode 100644
index 000000000..183efc828
--- /dev/null
+++ b/src/widgets/Chart/__tests__/CompareModal.test.tsx
@@ -0,0 +1,256 @@
+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
+ 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();
+ });
+});
diff --git a/src/widgets/Chart/__tests__/MoexChart.test.tsx b/src/widgets/Chart/__tests__/MoexChart.test.tsx
new file mode 100644
index 000000000..839d59c72
--- /dev/null
+++ b/src/widgets/Chart/__tests__/MoexChart.test.tsx
@@ -0,0 +1,230 @@
+import { act, render } from '@testing-library/react';
+
+import React from 'react';
+
+import { CompareModal } from '../components/MoexChart/components/CompareModal';
+import { SymbolSearchModal } from '../components/MoexChart/components/SymbolSearchModal';
+import { useMoexChart } from '../components/MoexChart/hooks';
+import MoexChartComponent from '../components/MoexChart/MoexChart';
+
+import type { Contract } from '@modules/contracts';
+import type { __CompareManager__ } from 'moex-chart';
+import type { MutableRefObject } from 'react';
+
+jest.mock('../components/MoexChart/hooks', () => ({
+ useMoexChart: jest.fn(),
+}));
+
+jest.mock('../components/MoexChart/components/CompareModal', () => ({
+ CompareModal: jest.fn(() => null),
+}));
+
+jest.mock('../components/MoexChart/components/SymbolSearchModal', () => ({
+ SymbolSearchModal: jest.fn(() => null),
+}));
+
+interface CompareModalMockProps {
+ widgetId: number;
+ isOpen: boolean;
+ setOpen: (isOpen: boolean) => void;
+ onClose: () => void;
+ compareManager: MutableRefObject<__CompareManager__ | null>;
+}
+
+interface SymbolSearchModalMockProps {
+ widgetId: number;
+ isOpen: boolean;
+ setOpen: (isOpen: boolean) => void;
+ onSymbolChange: (instrument: Contract) => void;
+}
+
+describe('MoexChart', () => {
+ const mockUseMoexChart = useMoexChart as jest.Mock;
+ const mockCompareModal = CompareModal as jest.Mock;
+ const mockSymbolSearchModal = SymbolSearchModal as jest.Mock;
+
+ const mockSetIsCompareOpen = jest.fn();
+ const mockSetIsSymbolSearchOpen = jest.fn();
+ const mockAddInstrumentFromModal = jest.fn();
+
+ let containerRef: MutableRefObject<HTMLDivElement | null>;
+ let compareManagerRef: MutableRefObject<__CompareManager__ | null>;
+
+ const renderComponent = () =>
+ render(
+ <MoexChartComponent
+ symbol="MXSE:TQBR:SBER"
+ symbolName="Сбербанк"
+ symbolTicker="SBER"
+ widgetId={42}
+ addInstrumentFromModal={mockAddInstrumentFromModal}
+ />,
+ );
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+
+ containerRef = {
+ current: null,
+ };
+
+ compareManagerRef = {
+ current: null,
+ };
+
+ mockUseMoexChart.mockReturnValue({
+ containerRef,
+ isCompareOpen: false,
+ isSymbolSearchOpen: false,
+ compareManagerRef,
+ setIsCompareOpen: mockSetIsCompareOpen,
+ setIsSymbolSearchOpen: mockSetIsSymbolSearchOpen,
+ saveSnapshot: jest.fn(),
+ applySnapshot: jest.fn(),
+ hasSavedSnapshot: false,
+ });
+ });
+
+ it('should initialize chart hook with symbol, name and ticker', () => {
+ // Arrange & Act
+ renderComponent();
+
+ // Assert
+ expect(mockUseMoexChart).toHaveBeenCalledTimes(1);
+ expect(mockUseMoexChart).toHaveBeenCalledWith({
+ symbol: 'MXSE:TQBR:SBER',
+ symbolName: 'Сбербанк',
+ symbolTicker: 'SBER',
+ indicativeData: undefined,
+ });
+ });
+
+ it('should attach chart container ref', () => {
+ // Arrange & Act
+ renderComponent();
+
+ // Assert
+ expect(containerRef.current).toBeInstanceOf(HTMLDivElement);
+ });
+
+ it('should not render modals when they are closed', () => {
+ // Arrange & Act
+ renderComponent();
+
+ // Assert
+ expect(mockCompareModal).not.toHaveBeenCalled();
+ expect(mockSymbolSearchModal).not.toHaveBeenCalled();
+ });
+
+ it('should render compare modal with chart manager', () => {
+ // Arrange
+ mockUseMoexChart.mockReturnValue({
+ containerRef,
+ isCompareOpen: true,
+ isSymbolSearchOpen: false,
+ compareManagerRef,
+ setIsCompareOpen: mockSetIsCompareOpen,
+ setIsSymbolSearchOpen: mockSetIsSymbolSearchOpen,
+ saveSnapshot: jest.fn(),
+ applySnapshot: jest.fn(),
+ hasSavedSnapshot: false,
+ });
+
+ // Act
+ renderComponent();
+
+ const compareModalProps = mockCompareModal.mock.calls[0]?.[0] as CompareModalMockProps;
+
+ // Assert
+ expect(compareModalProps.widgetId).toBe(42);
+ expect(compareModalProps.isOpen).toBe(true);
+ expect(compareModalProps.setOpen).toBe(mockSetIsCompareOpen);
+ expect(compareModalProps.compareManager).toBe(compareManagerRef);
+ });
+
+ it('should close compare modal through onClose callback', () => {
+ // Arrange
+ mockUseMoexChart.mockReturnValue({
+ containerRef,
+ isCompareOpen: true,
+ isSymbolSearchOpen: false,
+ compareManagerRef,
+ setIsCompareOpen: mockSetIsCompareOpen,
+ setIsSymbolSearchOpen: mockSetIsSymbolSearchOpen,
+ saveSnapshot: jest.fn(),
+ applySnapshot: jest.fn(),
+ hasSavedSnapshot: false,
+ });
+
+ renderComponent();
+
+ const compareModalProps = mockCompareModal.mock.calls[0]?.[0] as CompareModalMockProps;
+
+ // Act
+ act(() => {
+ compareModalProps.onClose();
+ });
+
+ // Assert
+ expect(mockSetIsCompareOpen).toHaveBeenCalledWith(false);
+ });
+
+ it('should render symbol search modal', () => {
+ // Arrange
+ mockUseMoexChart.mockReturnValue({
+ containerRef,
+ isCompareOpen: false,
+ isSymbolSearchOpen: true,
+ compareManagerRef,
+ setIsCompareOpen: mockSetIsCompareOpen,
+ setIsSymbolSearchOpen: mockSetIsSymbolSearchOpen,
+ saveSnapshot: jest.fn(),
+ applySnapshot: jest.fn(),
+ hasSavedSnapshot: false,
+ });
+
+ // Act
+ renderComponent();
+
+ const symbolSearchModalProps = mockSymbolSearchModal.mock.calls[0]?.[0] as SymbolSearchModalMockProps;
+
+ // Assert
+ expect(symbolSearchModalProps.widgetId).toBe(42);
+ expect(symbolSearchModalProps.isOpen).toBe(true);
+ expect(symbolSearchModalProps.setOpen).toBe(mockSetIsSymbolSearchOpen);
+ });
+
+ it('should pass selected instrument from symbol search modal', () => {
+ // Arrange
+ mockUseMoexChart.mockReturnValue({
+ containerRef,
+ isCompareOpen: false,
+ isSymbolSearchOpen: true,
+ compareManagerRef,
+ setIsCompareOpen: mockSetIsCompareOpen,
+ setIsSymbolSearchOpen: mockSetIsSymbolSearchOpen,
+ saveSnapshot: jest.fn(),
+ applySnapshot: jest.fn(),
+ hasSavedSnapshot: false,
+ });
+
+ renderComponent();
+
+ const symbolSearchModalProps = mockSymbolSearchModal.mock.calls[0]?.[0] as SymbolSearchModalMockProps;
+
+ const instrument = {
+ issKey: 'MXSE:TQBR:GAZP',
+ displayName: 'Газпром',
+ symbol: 'GAZP',
+ } as Contract;
+
+ // Act
+ act(() => {
+ symbolSearchModalProps.onSymbolChange(instrument);
+ });
+
+ // Assert
+ expect(mockAddInstrumentFromModal).toHaveBeenCalledTimes(1);
+ expect(mockAddInstrumentFromModal).toHaveBeenCalledWith([instrument]);
+ });
+});
diff --git a/src/widgets/Chart/__tests__/SymbolSearchModal.test.tsx b/src/widgets/Chart/__tests__/SymbolSearchModal.test.tsx
index b61b7fc18..543d392cb 100644
--- a/src/widgets/Chart/__tests__/SymbolSearchModal.test.tsx
+++ b/src/widgets/Chart/__tests__/SymbolSearchModal.test.tsx
@@ -1,128 +1,275 @@
-import { render } from '@testing-library/react';
-import React from 'react';
+import { renderHook } from '@testing-library/react';
-import { InstrumentSearch } from '@components/InstrumentSearch';
+import { useAppSelect } from '@hooks/useAppSelector';
+import { useContracts } from '@modules/contracts';
+import { filterByUniqIssKey } from '@utils/filterByUniqIssKey';
+import { DEFAULT_SYMBOL } from '@widgets/Chart/const';
-import { SymbolSearchModal } from '../components/MoexChart/components/SymbolSearchModal';
+import { useChartPublicContext } from '../hooks/useChartPublicContext';
import type { Contract } from '@modules/contracts';
-jest.mock('@components/InstrumentSearch', () => ({
- InstrumentSearch: jest.fn(() => null),
+// Mock the dependencies
+jest.mock('@hooks/useAppSelector');
+jest.mock('@modules/contracts');
+jest.mock('@utils/filterByUniqIssKey');
+
+jest.mock('@api/index', () => ({
+ updateMenuLocked: jest.fn(),
+ widgetsController: {
+ delete: jest.fn(),
+ },
+ widgetPropertiesController: {
+ update: jest.fn(),
+ },
+ workspaceController: {
+ update: jest.fn(),
+ },
}));
-interface InstrumentSearchMockProps {
- widgetId: number;
- variant: string;
- isOpen: boolean;
- setOpen: (isOpen: boolean) => void;
- addInstruments: (instruments: Contract[]) => void;
-}
-
-describe('SymbolSearchModal', () => {
- const mockInstrumentSearch = InstrumentSearch as jest.Mock;
-
- const mockSetOpen = jest.fn();
- const mockOnSymbolChange = jest.fn();
-
- const renderComponent = (): InstrumentSearchMockProps => {
- render(
- <SymbolSearchModal
- widgetId={42}
- isOpen
- setOpen={mockSetOpen}
- onSymbolChange={mockOnSymbolChange}
- />,
- );
+jest.mock('@api/controllers/workspace', () => ({
+ workspaceController: {
+ update: jest.fn(),
+ },
+}));
+
+describe('useChartPublicContext', () => {
+ const mockUseAppSelect = useAppSelect as jest.Mock;
+ const mockUseContracts = useContracts as jest.Mock;
+ const mockFilterByUniqIssKey = filterByUniqIssKey as jest.Mock;
+
+ const mockContracts = [
+ {
+ issKey: DEFAULT_SYMBOL,
+ displayName: 'Инструмент по умолчанию',
+ symbol: 'DEFAULT',
+ // ... other contract properties
+ },
+ {
+ issKey: 'MOEX:TEST1',
+ displayName: 'Test Instrument 1',
+ symbol: 'TEST1',
+ // ... other contract properties
+ },
+ {
+ issKey: 'MOEX:TEST2',
+ displayName: 'Test Instrument 2',
+ symbol: 'TEST2',
+ // ... other contract properties
+ },
+ {
+ issKey: 'MOEX:TEST3',
+ displayName: 'Test Instrument 3',
+ symbol: 'TEST3',
+ // ... other contract properties
+ },
+ ] as Contract[];
- return mockInstrumentSearch.mock.calls[0]?.[0] as InstrumentSearchMockProps;
+ const mockWidget = {
+ id: 1,
+ name: 'Test Widget',
+ type: 'graphic',
+ master: 123,
+ externalProperties: [
+ {
+ key: 'instrument',
+ value: 'MOEX:TEST1',
+ },
+ ],
+ // ... other widget properties
+ };
+
+ const mockPublicContext = {
+ instrument: 'MOEX:TEST1',
};
beforeEach(() => {
jest.clearAllMocks();
+
+ // Mock the useAppSelect to return our test data
+ mockUseAppSelect.mockImplementation((selector) => {
+ if (selector.toString().includes('publicContext')) {
+ return mockPublicContext;
+ }
+
+ if (selector.toString().includes('widgets')) {
+ return mockWidget;
+ }
+
+ return {};
+ });
+
+ // Mock useContracts to return our test contracts
+ mockUseContracts.mockReturnValue({
+ contracts: mockContracts,
+ });
+
+ // Mock filterByUniqIssKey to return the same contracts
+ mockFilterByUniqIssKey.mockImplementation((contracts: Contract[]) => contracts);
});
- it('should pass modal properties to instrument search', () => {
- // Arrange & Act
- const instrumentSearchProps = renderComponent();
+ it('should return the correct issKey when a matching instrument is found', () => {
+ // Arrange
+ const mockSetCurrInstrument = jest.fn();
+ const mockGetMasterInstrumentFromPublicContext = jest.fn().mockReturnValue('MOEX:TEST1');
+
+ // Act
+ const { result } = renderHook(() =>
+ useChartPublicContext({
+ widgetId: 1,
+ setCurrInstrument: mockSetCurrInstrument,
+ getMasterInstrumentFromPublicContext: mockGetMasterInstrumentFromPublicContext,
+ }),
+ );
// Assert
- expect(instrumentSearchProps.widgetId).toBe(42);
- expect(instrumentSearchProps.variant).toBe('single');
- expect(instrumentSearchProps.isOpen).toBe(true);
- expect(instrumentSearchProps.setOpen).toBe(mockSetOpen);
- expect(instrumentSearchProps.addInstruments).toEqual(expect.any(Function));
+ expect(result.current.issKey).toBe('MOEX:TEST1');
});
- it('should change symbol after instrument selection', () => {
+ it('should pass instrument id, display name and ticker when a matching instrument is found', () => {
// Arrange
- const instrumentSearchProps = renderComponent();
+ const mockSetCurrInstrument = jest.fn();
+ const mockGetMasterInstrumentFromPublicContext = jest.fn().mockReturnValue('MOEX:TEST2');
+
+ // Act
+ renderHook(() =>
+ useChartPublicContext({
+ widgetId: 1,
+ setCurrInstrument: mockSetCurrInstrument,
+ getMasterInstrumentFromPublicContext: mockGetMasterInstrumentFromPublicContext,
+ }),
+ );
- const instrument = {
- issKey: 'MOEX:SBER',
- } as Contract;
+ // Assert
+ expect(mockSetCurrInstrument).toHaveBeenCalledTimes(1);
+ expect(mockSetCurrInstrument).toHaveBeenCalledWith('MOEX:TEST2', 'Test Instrument 2', 'TEST2');
+ });
+
+ it('should return undefined when no matching instrument is found', () => {
+ // Arrange
+ const mockSetCurrInstrument = jest.fn();
+ const mockGetMasterInstrumentFromPublicContext = jest.fn().mockReturnValue('MOEX:NONEXISTENT');
// Act
- instrumentSearchProps.addInstruments([instrument]);
+ const { result } = renderHook(() =>
+ useChartPublicContext({
+ widgetId: 1,
+ setCurrInstrument: mockSetCurrInstrument,
+ getMasterInstrumentFromPublicContext: mockGetMasterInstrumentFromPublicContext,
+ }),
+ );
// Assert
- expect(mockOnSymbolChange).toHaveBeenCalledTimes(1);
- expect(mockOnSymbolChange).toHaveBeenCalledWith('MOEX:SBER');
+ expect(result.current.issKey).toBeUndefined();
+ expect(mockSetCurrInstrument).not.toHaveBeenCalled();
});
- it('should close modal after instrument selection', () => {
+ it('should return undefined when getMasterInstrumentFromPublicContext returns null', () => {
// Arrange
- const instrumentSearchProps = renderComponent();
+ const mockSetCurrInstrument = jest.fn();
+ const mockGetMasterInstrumentFromPublicContext = jest.fn().mockReturnValue(null);
- const instrument = {
- issKey: 'MOEX:SBER',
- } as Contract;
+ // Act
+ const { result } = renderHook(() =>
+ useChartPublicContext({
+ widgetId: 1,
+ setCurrInstrument: mockSetCurrInstrument,
+ getMasterInstrumentFromPublicContext: mockGetMasterInstrumentFromPublicContext,
+ }),
+ );
+
+ // Assert
+ expect(result.current.issKey).toBeUndefined();
+ });
+
+ it('should return undefined when getMasterInstrumentFromPublicContext returns undefined', () => {
+ // Arrange
+ const mockSetCurrInstrument = jest.fn();
+ const mockGetMasterInstrumentFromPublicContext = jest.fn().mockReturnValue(undefined);
// Act
- instrumentSearchProps.addInstruments([instrument]);
+ const { result } = renderHook(() =>
+ useChartPublicContext({
+ widgetId: 1,
+ setCurrInstrument: mockSetCurrInstrument,
+ getMasterInstrumentFromPublicContext: mockGetMasterInstrumentFromPublicContext,
+ }),
+ );
// Assert
- expect(mockSetOpen).toHaveBeenCalledTimes(1);
- expect(mockSetOpen).toHaveBeenCalledWith(false);
+ expect(result.current.issKey).toBeUndefined();
});
- it('should not change symbol when instruments list is empty', () => {
+ it('should set default instrument with display name and ticker when context value is empty', () => {
// Arrange
- const instrumentSearchProps = renderComponent();
+ const mockSetCurrInstrument = jest.fn();
+ const mockGetMasterInstrumentFromPublicContext = jest.fn().mockReturnValue(null);
// Act
- instrumentSearchProps.addInstruments([]);
+ renderHook(() =>
+ useChartPublicContext({
+ widgetId: 1,
+ setCurrInstrument: mockSetCurrInstrument,
+ getMasterInstrumentFromPublicContext: mockGetMasterInstrumentFromPublicContext,
+ }),
+ );
// Assert
- expect(mockOnSymbolChange).not.toHaveBeenCalled();
- expect(mockSetOpen).not.toHaveBeenCalled();
+ expect(mockSetCurrInstrument).toHaveBeenCalledTimes(1);
+ expect(mockSetCurrInstrument).toHaveBeenCalledWith(DEFAULT_SYMBOL, 'Инструмент по умолчанию', 'DEFAULT');
});
- it('should not change symbol when selected instrument has no issKey', () => {
+ it('should resolve instrument when widget is not found', () => {
// Arrange
- const instrumentSearchProps = renderComponent();
+ const mockSetCurrInstrument = jest.fn();
+ const mockGetMasterInstrumentFromPublicContext = jest.fn().mockReturnValue('MOEX:TEST1');
+
+ mockUseAppSelect.mockImplementation((selector) => {
+ if (selector.toString().includes('publicContext')) {
+ return mockPublicContext;
+ }
+
+ if (selector.toString().includes('widgets')) {
+ return null;
+ }
+
+ return {};
+ });
// Act
- instrumentSearchProps.addInstruments([{} as Contract]);
+ const { result } = renderHook(() =>
+ useChartPublicContext({
+ widgetId: 999,
+ setCurrInstrument: mockSetCurrInstrument,
+ getMasterInstrumentFromPublicContext: mockGetMasterInstrumentFromPublicContext,
+ }),
+ );
// Assert
- expect(mockOnSymbolChange).not.toHaveBeenCalled();
- expect(mockSetOpen).not.toHaveBeenCalled();
+ expect(result.current.issKey).toBe('MOEX:TEST1');
+ expect(mockSetCurrInstrument).toHaveBeenCalledWith('MOEX:TEST1', 'Test Instrument 1', 'TEST1');
});
- it('should not change symbol when selected instrument has empty issKey', () => {
+ it('should not set instrument when contracts array is empty', () => {
// Arrange
- const instrumentSearchProps = renderComponent();
+ const mockSetCurrInstrument = jest.fn();
+ const mockGetMasterInstrumentFromPublicContext = jest.fn().mockReturnValue('MOEX:TEST1');
- const instrument = {
- issKey: '',
- } as Contract;
+ mockUseContracts.mockReturnValue({
+ contracts: [],
+ });
// Act
- instrumentSearchProps.addInstruments([instrument]);
+ const { result } = renderHook(() =>
+ useChartPublicContext({
+ widgetId: 1,
+ setCurrInstrument: mockSetCurrInstrument,
+ getMasterInstrumentFromPublicContext: mockGetMasterInstrumentFromPublicContext,
+ }),
+ );
// Assert
- expect(mockOnSymbolChange).not.toHaveBeenCalled();
- expect(mockSetOpen).not.toHaveBeenCalled();
+ expect(result.current.issKey).toBeUndefined();
+ expect(mockSetCurrInstrument).not.toHaveBeenCalled();
});
});
diff --git a/src/widgets/Chart/__tests__/dataSourceProvide.test.ts b/src/widgets/Chart/__tests__/dataSourceProvide.test.ts
index 68529685a..18dc7d236 100644
--- a/src/widgets/Chart/__tests__/dataSourceProvide.test.ts
+++ b/src/widgets/Chart/__tests__/dataSourceProvide.test.ts
@@ -247,12 +247,12 @@ describe('DataSourceProvider', () => {
// Assert
expect(mockRequestRealtimeBars).toHaveBeenCalledWith({
- currencyPair: 'MOEX.SBER',
+ currencyPair: 'moex.sber',
interval: '1',
- ticker: 'MOEX:SBER',
+ ticker: 'moex:sber',
indicativeData: undefined,
});
- expect(mockUpdate).toHaveBeenCalledWith('MOEX:SBER', localMockedBar);
+ expect(mockUpdate).toHaveBeenCalledWith('moex:sber', localMockedBar);
});
it('should request realtime data with indicative data', async () => {
diff --git a/src/widgets/Chart/__tests__/useChartComponentFacade.test.tsx b/src/widgets/Chart/__tests__/useChartComponentFacade.test.tsx
new file mode 100644
index 000000000..01f477bdf
--- /dev/null
+++ b/src/widgets/Chart/__tests__/useChartComponentFacade.test.tsx
@@ -0,0 +1,370 @@
+import { act, renderHook } from '@testing-library/react';
+
+import { useDispatch } from 'react-redux';
+
+import { communicator } from '@core/comm';
+import { useAppSelect } from '@hooks/useAppSelector';
+import { CORPACTIONS_OPEN_EXESTED_WIDGET_EVENT, HIGHLIGHT_WIDGET_EVENT } from '@modules/widgets/shared';
+import { addContentPropsToWidget, unbindWidgets } from '@store/slices/widgets';
+import { getState } from '@store/store';
+import { useWidgetsBind } from '@utils/hooks/useWidgetsBind';
+
+import useChartComponentFacade from '../hooks/useChartComponentFacade';
+import { useChartPublicContext } from '../hooks/useChartPublicContext';
+
+import type { WidgetProperties } from '../properties/types';
+import type { ChartContainerProps } from '../types';
+
+jest.mock('@store/store', () => ({
+ getState: jest.fn(),
+}));
+jest.mock('@store/slices/widgets', () => ({
+ addContentPropsToWidget: jest.fn(),
+ unbindWidgets: jest.fn(),
+}));
+jest.mock('react-redux', () => ({
+ useDispatch: jest.fn(),
+}));
+jest.mock('@core/comm', () => ({
+ communicator: {
+ listen: jest.fn(),
+ },
+}));
+jest.mock('@hooks/useAppSelector', () => ({
+ useAppSelect: jest.fn(),
+}));
+jest.mock('@utils/hooks/useWidgetsBind', () => ({
+ useWidgetsBind: jest.fn(),
+}));
+jest.mock('../hooks/useChartPublicContext', () => ({
+ useChartPublicContext: jest.fn(),
+}));
+
+interface PublicContextMockParams {
+ setCurrInstrument: (instrumentId: string, instrumentName?: string, instrumentTicker?: string) => void;
+}
+
+describe('useChartComponentFacade', () => {
+ const mockUseDispatch = useDispatch as jest.Mock;
+ const mockUseAppSelect = useAppSelect as jest.Mock;
+ const mockAddContentPropsToWidget = addContentPropsToWidget as unknown as jest.Mock;
+ const mockUnbindWidgets = unbindWidgets as unknown as jest.Mock;
+ const mockGetState = getState as jest.Mock;
+ const mockUseWidgetsBind = useWidgetsBind as jest.Mock;
+ const mockUseChartPublicContext = useChartPublicContext as jest.Mock;
+ const mockCommunicatorListen = communicator.listen as jest.Mock;
+
+ const mockDispatch = jest.fn();
+ const mockTriggerRelatedWidgetsToUpdate = jest.fn();
+ const mockGetMasterInstrumentFromPublicContext = jest.fn();
+
+ const mockUnsubscribeCorpActions = jest.fn();
+ const mockUnsubscribeHighlighter = jest.fn();
+
+ const widgetProperties = {
+ chartState: {
+ savedInstrument: 'MOEX:SBER',
+ savedInstrumentName: 'Сбербанк',
+ savedInstrumentTicker: 'SBER',
+ interval: '1m',
+ },
+ indicativeData: {
+ id: 1,
+ key: 'INDICATIVE',
+ secId: 'INAV',
+ instrumentName: 'Индикатив',
+ settlement: 'Расчётный',
+ firmName: 'Тестовая фирма',
+ },
+ } as WidgetProperties;
+
+ const renderFacade = () =>
+ renderHook(() =>
+ useChartComponentFacade({
+ widgetId: 42,
+ } as ChartContainerProps),
+ );
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+
+ mockUseDispatch.mockReturnValue(mockDispatch);
+ mockUseAppSelect.mockReturnValue(widgetProperties);
+
+ mockGetState.mockReturnValue({
+ widgets: {
+ widgets: [
+ {
+ id: 42,
+ widgetContentProps: widgetProperties,
+ },
+ ],
+ },
+ });
+
+ mockUseWidgetsBind.mockReturnValue({
+ triggerRelatedWidgetsToUpdate: mockTriggerRelatedWidgetsToUpdate,
+ getMasterInstrumentFromPublicContext: mockGetMasterInstrumentFromPublicContext,
+ });
+
+ mockUseChartPublicContext.mockReturnValue({
+ issKey: undefined,
+ });
+
+ mockAddContentPropsToWidget.mockImplementation((payload) => ({
+ type: 'widgets/addContentPropsToWidget',
+ payload,
+ }));
+
+ mockUnbindWidgets.mockImplementation((payload) => ({
+ type: 'widgets/unbindWidgets',
+ payload,
+ }));
+
+ mockCommunicatorListen.mockImplementation(({ messageType }, listener) => {
+ if (messageType === CORPACTIONS_OPEN_EXESTED_WIDGET_EVENT) {
+ return mockUnsubscribeCorpActions;
+ }
+
+ if (messageType === HIGHLIGHT_WIDGET_EVENT) {
+ return mockUnsubscribeHighlighter;
+ }
+
+ return jest.fn();
+ });
+ });
+
+ it('should initialize symbol, display name and ticker from widget properties', () => {
+ // Arrange & Act
+ const { result } = renderFacade();
+
+ // Assert
+ expect(result.current.currentInstrument).toBe('MOEX:SBER');
+ expect(result.current.currentInstrumentName).toBe('Сбербанк');
+ expect(result.current.currentInstrumentTicker).toBe('SBER');
+ });
+
+ it('should update and save instrument selected from modal', () => {
+ // Arrange
+ const { result } = renderFacade();
+
+ jest.clearAllMocks();
+
+ // Act
+ act(() => {
+ result.current.addInstrumentFromModal([
+ {
+ issKey: 'MOEX:GAZP',
+ displayName: 'Газпром',
+ symbol: 'GAZP',
+ },
+ ]);
+ });
+
+ // Assert
+ expect(result.current.currentInstrument).toBe('MOEX:GAZP');
+ expect(result.current.currentInstrumentName).toBe('Газпром');
+ expect(result.current.currentInstrumentTicker).toBe('GAZP');
+
+ expect(mockUnbindWidgets).toHaveBeenCalledWith({
+ widgetId: 42,
+ });
+
+ expect(mockAddContentPropsToWidget).toHaveBeenCalledWith({
+ id: 42,
+ withoutSend: true,
+ widgetContentProps: {
+ chartState: {
+ ...widgetProperties.chartState,
+ savedInstrument: 'MOEX:GAZP',
+ savedInstrumentName: 'Газпром',
+ savedInstrumentTicker: 'GAZP',
+ },
+ indicativeData: undefined,
+ },
+ });
+
+ expect(mockTriggerRelatedWidgetsToUpdate).toHaveBeenCalledWith('MOEX:GAZP');
+ });
+
+ it('should update name and ticker without unbinding when symbol is unchanged', () => {
+ // Arrange
+ const { result } = renderFacade();
+
+ jest.clearAllMocks();
+
+ // Act
+ act(() => {
+ result.current.addInstrumentFromModal([
+ {
+ issKey: 'MOEX:SBER',
+ displayName: 'Сбербанк ПАО',
+ symbol: 'SBERP',
+ },
+ ]);
+ });
+
+ // Assert
+ expect(result.current.currentInstrument).toBe('MOEX:SBER');
+ expect(result.current.currentInstrumentName).toBe('Сбербанк ПАО');
+ expect(result.current.currentInstrumentTicker).toBe('SBERP');
+
+ expect(mockUnbindWidgets).not.toHaveBeenCalled();
+ expect(mockTriggerRelatedWidgetsToUpdate).not.toHaveBeenCalled();
+
+ expect(mockAddContentPropsToWidget).toHaveBeenCalledWith({
+ id: 42,
+ withoutSend: true,
+ widgetContentProps: {
+ chartState: {
+ ...widgetProperties.chartState,
+ savedInstrument: 'MOEX:SBER',
+ savedInstrumentName: 'Сбербанк ПАО',
+ savedInstrumentTicker: 'SBERP',
+ },
+ indicativeData: widgetProperties.indicativeData,
+ },
+ });
+ });
+
+ it('should send dropped instrument update immediately', () => {
+ // Arrange
+ const { result } = renderFacade();
+
+ jest.clearAllMocks();
+
+ // Act
+ act(() => {
+ result.current.onDropInstruments(
+ {
+ issKey: 'MOEX:LKOH',
+ displayName: 'Лукойл',
+ symbol: 'LKOH',
+ },
+ true,
+ );
+ });
+
+ // Assert
+ expect(mockAddContentPropsToWidget).toHaveBeenCalledWith(
+ expect.objectContaining({
+ id: 42,
+ withoutSend: false,
+ widgetContentProps: expect.objectContaining({
+ chartState: expect.objectContaining({
+ savedInstrument: 'MOEX:LKOH',
+ savedInstrumentName: 'Лукойл',
+ savedInstrumentTicker: 'LKOH',
+ }),
+ }),
+ }),
+ );
+ });
+
+ it('should update instrument from public context without unbinding widget', () => {
+ // Arrange
+ renderFacade();
+
+ const publicContextParams = mockUseChartPublicContext.mock.calls[0]?.[0] as PublicContextMockParams;
+
+ jest.clearAllMocks();
+
+ // Act
+ act(() => {
+ publicContextParams.setCurrInstrument('MOEX:ROSN', 'Роснефть', 'ROSN');
+ });
+
+ // Assert
+ expect(mockUnbindWidgets).not.toHaveBeenCalled();
+
+ expect(mockAddContentPropsToWidget).toHaveBeenCalledWith(
+ expect.objectContaining({
+ withoutSend: false,
+ widgetContentProps: expect.objectContaining({
+ chartState: expect.objectContaining({
+ savedInstrument: 'MOEX:ROSN',
+ savedInstrumentName: 'Роснефть',
+ savedInstrumentTicker: 'ROSN',
+ }),
+ }),
+ }),
+ );
+ });
+
+ it('should use instrument id as name and ticker when metadata is unavailable', () => {
+ // Arrange
+ renderFacade();
+
+ const corpActionsListener = mockCommunicatorListen.mock.calls.find(
+ ([options]) => options.messageType === CORPACTIONS_OPEN_EXESTED_WIDGET_EVENT,
+ )?.[1] as (message: Record<number, string>) => void;
+
+ // Act
+ act(() => {
+ corpActionsListener({
+ 42: 'MOEX:UNKNOWN',
+ });
+ });
+
+ // Assert
+ expect(mockAddContentPropsToWidget).toHaveBeenCalledWith(
+ expect.objectContaining({
+ widgetContentProps: expect.objectContaining({
+ chartState: expect.objectContaining({
+ savedInstrument: 'MOEX:UNKNOWN',
+ savedInstrumentName: 'MOEX:UNKNOWN',
+ savedInstrumentTicker: 'MOEX:UNKNOWN',
+ }),
+ }),
+ }),
+ );
+ });
+
+ it('should not update instrument when modal selection is empty', () => {
+ // Arrange
+ const { result } = renderFacade();
+
+ jest.clearAllMocks();
+
+ // Act
+ act(() => {
+ result.current.addInstrumentFromModal([]);
+ });
+
+ // Assert
+ expect(mockAddContentPropsToWidget).not.toHaveBeenCalled();
+ expect(mockUnbindWidgets).not.toHaveBeenCalled();
+ expect(mockTriggerRelatedWidgetsToUpdate).not.toHaveBeenCalled();
+ });
+
+ it('should update widget highlight state', () => {
+ // Arrange
+ const { result } = renderFacade();
+
+ const highlighterListener = mockCommunicatorListen.mock.calls.find(
+ ([options]) => options.messageType === HIGHLIGHT_WIDGET_EVENT,
+ )?.[1] as (message: Record<number, boolean>) => void;
+
+ // Act
+ act(() => {
+ highlighterListener({
+ 42: true,
+ });
+ });
+
+ // Assert
+ expect(result.current.isOver).toBe(true);
+ });
+
+ it('should unsubscribe communicator listeners on unmount', () => {
+ // Arrange
+ const { unmount } = renderFacade();
+
+ // Act
+ unmount();
+
+ // Assert
+ expect(mockUnsubscribeCorpActions).toHaveBeenCalledTimes(1);
+ expect(mockUnsubscribeHighlighter).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/src/widgets/Chart/__tests__/useChartPublicContext.test.tsx b/src/widgets/Chart/__tests__/useChartPublicContext.test.tsx
index 666aa3073..bf2e80ff0 100644
--- a/src/widgets/Chart/__tests__/useChartPublicContext.test.tsx
+++ b/src/widgets/Chart/__tests__/useChartPublicContext.test.tsx
@@ -7,6 +7,8 @@ import { DEFAULT_SYMBOL } from '@widgets/Chart/const';
import { useChartPublicContext } from '../hooks/useChartPublicContext';
+import type { Contract } from '@modules/contracts';
+
// Mock the dependencies
jest.mock('@hooks/useAppSelector');
jest.mock('@modules/contracts');
@@ -24,6 +26,7 @@ jest.mock('@api/index', () => ({
update: jest.fn(),
},
}));
+
jest.mock('@api/controllers/workspace', () => ({
workspaceController: {
update: jest.fn(),
@@ -36,32 +39,43 @@ describe('useChartPublicContext', () => {
const mockFilterByUniqIssKey = filterByUniqIssKey as jest.Mock;
const mockContracts = [
+ {
+ issKey: DEFAULT_SYMBOL,
+ displayName: 'Инструмент по умолчанию',
+ symbol: 'DEFAULT',
+ // ... other contract properties
+ },
{
issKey: 'MOEX:TEST1',
- instrName: 'Test Instrument 1',
+ displayName: 'Test Instrument 1',
symbol: 'TEST1',
// ... other contract properties
},
{
issKey: 'MOEX:TEST2',
- instrName: 'Test Instrument 2',
+ displayName: 'Test Instrument 2',
symbol: 'TEST2',
// ... other contract properties
},
{
issKey: 'MOEX:TEST3',
- instrName: 'Test Instrument 3',
+ displayName: 'Test Instrument 3',
symbol: 'TEST3',
// ... other contract properties
},
- ];
+ ] as Contract[];
const mockWidget = {
id: 1,
name: 'Test Widget',
type: 'graphic',
master: 123,
- externalProperties: [{ key: 'instrument', value: 'MOEX:TEST1' }],
+ externalProperties: [
+ {
+ key: 'instrument',
+ value: 'MOEX:TEST1',
+ },
+ ],
// ... other widget properties
};
@@ -77,9 +91,11 @@ describe('useChartPublicContext', () => {
if (selector.toString().includes('publicContext')) {
return mockPublicContext;
}
+
if (selector.toString().includes('widgets')) {
return mockWidget;
}
+
return {};
});
@@ -89,7 +105,7 @@ describe('useChartPublicContext', () => {
});
// Mock filterByUniqIssKey to return the same contracts
- mockFilterByUniqIssKey.mockImplementation((contracts, keys) => contracts);
+ mockFilterByUniqIssKey.mockImplementation((contracts: Contract[]) => contracts);
});
it('should return the correct issKey when a matching instrument is found', () => {
@@ -110,13 +126,13 @@ describe('useChartPublicContext', () => {
expect(result.current.issKey).toBe('MOEX:TEST1');
});
- it('should return undefined when no matching instrument is found', () => {
+ it('should pass instrument id and display name when a matching instrument is found', () => {
// Arrange
const mockSetCurrInstrument = jest.fn();
- const mockGetMasterInstrumentFromPublicContext = jest.fn().mockReturnValue('MOEX:NONEXISTENT');
+ const mockGetMasterInstrumentFromPublicContext = jest.fn().mockReturnValue('MOEX:TEST2');
// Act
- const { result } = renderHook(() =>
+ renderHook(() =>
useChartPublicContext({
widgetId: 1,
setCurrInstrument: mockSetCurrInstrument,
@@ -125,13 +141,14 @@ describe('useChartPublicContext', () => {
);
// Assert
- expect(result.current.issKey).toBeUndefined();
+ expect(mockSetCurrInstrument).toHaveBeenCalledTimes(1);
+ expect(mockSetCurrInstrument).toHaveBeenCalledWith('MOEX:TEST2', 'Test Instrument 2', 'TEST2');
});
- it('should return undefined when getMasterInstrumentFromPublicContext returns null', () => {
+ it('should return undefined when no matching instrument is found', () => {
// Arrange
const mockSetCurrInstrument = jest.fn();
- const mockGetMasterInstrumentFromPublicContext = jest.fn().mockReturnValue(null);
+ const mockGetMasterInstrumentFromPublicContext = jest.fn().mockReturnValue('MOEX:NONEXISTENT');
// Act
const { result } = renderHook(() =>
@@ -144,12 +161,13 @@ describe('useChartPublicContext', () => {
// Assert
expect(result.current.issKey).toBeUndefined();
+ expect(mockSetCurrInstrument).not.toHaveBeenCalled();
});
- it('should return undefined when getMasterInstrumentFromPublicContext returns undefined', () => {
+ it('should return undefined when getMasterInstrumentFromPublicContext returns null', () => {
// Arrange
const mockSetCurrInstrument = jest.fn();
- const mockGetMasterInstrumentFromPublicContext = jest.fn().mockReturnValue(undefined);
+ const mockGetMasterInstrumentFromPublicContext = jest.fn().mockReturnValue(null);
// Act
const { result } = renderHook(() =>
@@ -164,13 +182,13 @@ describe('useChartPublicContext', () => {
expect(result.current.issKey).toBeUndefined();
});
- it('should set current instrument to DEFAULT_SYMBOL when no field value and widget has master', () => {
+ it('should return undefined when getMasterInstrumentFromPublicContext returns undefined', () => {
// Arrange
const mockSetCurrInstrument = jest.fn();
- const mockGetMasterInstrumentFromPublicContext = jest.fn().mockReturnValue(null);
+ const mockGetMasterInstrumentFromPublicContext = jest.fn().mockReturnValue(undefined);
// Act
- renderHook(() =>
+ const { result } = renderHook(() =>
useChartPublicContext({
widgetId: 1,
setCurrInstrument: mockSetCurrInstrument,
@@ -179,13 +197,13 @@ describe('useChartPublicContext', () => {
);
// Assert
- expect(mockSetCurrInstrument).toHaveBeenCalledWith(DEFAULT_SYMBOL);
+ expect(result.current.issKey).toBeUndefined();
});
- it('should set current instrument to the found issKey when a matching instrument exists', () => {
+ it('should set current instrument to DEFAULT_SYMBOL with display name when no field value and widget has master', () => {
// Arrange
const mockSetCurrInstrument = jest.fn();
- const mockGetMasterInstrumentFromPublicContext = jest.fn().mockReturnValue('MOEX:TEST2');
+ const mockGetMasterInstrumentFromPublicContext = jest.fn().mockReturnValue(null);
// Act
renderHook(() =>
@@ -197,7 +215,8 @@ describe('useChartPublicContext', () => {
);
// Assert
- expect(mockSetCurrInstrument).toHaveBeenCalledWith('MOEX:TEST2');
+ expect(mockSetCurrInstrument).toHaveBeenCalledTimes(1);
+ expect(mockSetCurrInstrument).toHaveBeenCalledWith(DEFAULT_SYMBOL, 'Инструмент по умолчанию', 'DEFAULT');
});
it('should handle case when widget is not found in widgets array', () => {
@@ -205,14 +224,16 @@ describe('useChartPublicContext', () => {
const mockSetCurrInstrument = jest.fn();
const mockGetMasterInstrumentFromPublicContext = jest.fn().mockReturnValue('MOEX:TEST1');
- // Mock useAppSelect to return null for widget (widget not found)
+ // Mock useAppSelect to return null for widget
mockUseAppSelect.mockImplementation((selector) => {
if (selector.toString().includes('publicContext')) {
return mockPublicContext;
}
+
if (selector.toString().includes('widgets')) {
return null; // Widget not found
}
+
return {};
});
@@ -227,6 +248,7 @@ describe('useChartPublicContext', () => {
// Assert
expect(result.current.issKey).toBe('MOEX:TEST1');
+ expect(mockSetCurrInstrument).toHaveBeenCalledWith('MOEX:TEST1', 'Test Instrument 1', 'TEST1');
});
it('should handle case when contracts array is empty', () => {
@@ -250,5 +272,6 @@ describe('useChartPublicContext', () => {
// Assert
expect(result.current.issKey).toBeUndefined();
+ expect(mockSetCurrInstrument).not.toHaveBeenCalled();
});
});
diff --git a/src/widgets/Chart/__tests__/useMoexchart.test.tsx b/src/widgets/Chart/__tests__/useMoexchart.test.tsx
index 2396e0910..9a4670b33 100644
--- a/src/widgets/Chart/__tests__/useMoexchart.test.tsx
+++ b/src/widgets/Chart/__tests__/useMoexchart.test.tsx
@@ -4,11 +4,11 @@ import { MoexChart, Timeframes } from 'moex-chart';
import React from 'react';
import { useChangeProperties, useSelectProperties } from '@modules/widgetProperties';
-import { ChartIndicativeData } from '@widgets/Chart/types';
import { useMoexChart } from '../components/MoexChart/hooks';
-// Mock the dependencies
+import type { ChartIndicativeData } from '@widgets/Chart/types';
+
jest.mock('@modules/widgetProperties');
jest.mock('../components/MoexChart/dataSourceProvide', () => ({
@@ -59,13 +59,38 @@ const mockedDataSourceProvide = jest.requireMock('../components/MoexChart/dataSo
type TimeframeValue = (typeof Timeframes)[keyof typeof Timeframes];
+interface MockCompareSeries {
+ name: string;
+ seriesOptions?: {
+ priceScaleId?: string;
+ color?: string;
+ };
+}
+
+interface MockIndicatorConfig {
+ symbol?: string;
+ symbolTicker?: string;
+ label?: string;
+ newPane?: boolean;
+ series: MockCompareSeries[];
+}
+
+interface MockIndicatorSnapshot {
+ id?: string;
+ indicatorType?: string;
+ dataSource?: unknown;
+ config?: MockIndicatorConfig;
+}
+
interface MockPaneSnapshot {
- indicators: unknown[];
+ indicators: MockIndicatorSnapshot[];
}
interface MockChartSnapshot {
charts: {
symbol: string;
+ symbolName?: string;
+ symbolTicker?: string;
timeframe: TimeframeValue;
chartSeriesType: string;
panes: MockPaneSnapshot[];
@@ -88,7 +113,8 @@ interface MockMoexChartConfig {
}
interface MockMoexChartState {
- tf?: TimeframeValue;
+ timeframe: TimeframeValue;
+ initialInterval?: string;
savedData?: string;
}
@@ -102,6 +128,9 @@ type TimeframeChangeCallback = (timeframe: TimeframeValue) => void;
interface TestComponentProps {
symbol?: string;
+ symbolName?: string;
+ symbolTicker?: string;
+ indicativeData?: ChartIndicativeData;
}
describe('useMoexChart', () => {
@@ -117,6 +146,7 @@ describe('useMoexChart', () => {
const mockSetSnapshot = jest.fn();
const mockSetSymbol = jest.fn();
const mockGetCompareManager = jest.fn();
+ const mockSetSettings = jest.fn();
const mockDataSource = jest.fn();
const mockStartRealtime = jest.fn();
@@ -131,6 +161,8 @@ describe('useMoexChart', () => {
charts: [
{
symbol: 'OLD:SYMBOL',
+ symbolName: 'Old instrument',
+ symbolTicker: 'OLD',
timeframe: Timeframes['5m'],
chartSeriesType: 'Candlestick',
panes: [
@@ -146,10 +178,17 @@ describe('useMoexChart', () => {
let lastMoexChartConfig: MockMoexChartConfig | null = null;
let mockMoexChartState: MockMoexChartState | undefined;
- const TestComponent = ({ symbol = 'MOEX:SBER' }: TestComponentProps): React.ReactElement => {
+ const TestComponent = ({
+ symbol = 'MOEX:SBER',
+ symbolName = 'Сбербанк',
+ symbolTicker = 'SBER',
+ indicativeData,
+ }: TestComponentProps): React.ReactElement => {
hookResult = useMoexChart({
symbol,
- indicativeData: undefined,
+ symbolName,
+ symbolTicker,
+ indicativeData,
});
return <div ref={hookResult.containerRef} />;
@@ -157,12 +196,13 @@ describe('useMoexChart', () => {
beforeEach(() => {
jest.clearAllMocks();
+ jest.useFakeTimers();
hookResult = null;
lastMoexChartConfig = null;
mockMoexChartState = {
- tf: Timeframes['1m'],
+ timeframe: Timeframes['1m'],
savedData: undefined,
};
@@ -177,8 +217,8 @@ describe('useMoexChart', () => {
});
mockGetDataSource.mockImplementation(
- (_indicativeData?: ChartIndicativeData, callback?: (timeframe: Timeframes) => void) =>
- (timeframe: Timeframes) => {
+ (_indicativeData?: ChartIndicativeData, callback?: (timeframe: TimeframeValue) => void) =>
+ (timeframe: TimeframeValue) => {
callback?.(timeframe);
return mockDataSource();
@@ -203,46 +243,66 @@ describe('useMoexChart', () => {
getSnapshot: mockGetSnapshot,
setSnapshot: mockSetSnapshot,
setSymbol: mockSetSymbol,
+ setSettings: mockSetSettings,
getCompareManager: mockGetCompareManager,
};
});
});
- it('should create moex chart with current symbol and timeframe', () => {
+ afterEach(() => {
+ jest.clearAllTimers();
+ jest.useRealTimers();
+ });
+
+ it('should create moex chart with current symbol, name, ticker and timeframe', () => {
// Arrange & Act
- render(<TestComponent symbol="MOEX:SBER" />);
+ render(
+ <TestComponent
+ symbol="MOEX:SBER"
+ symbolName="Сбербанк"
+ symbolTicker="SBER"
+ />,
+ );
// Assert
expect(mockMoexChart).toHaveBeenCalledTimes(1);
expect(mockDataSourceProvider).toHaveBeenCalledTimes(1);
-
expect(lastMoexChartConfig?.container).toBeInstanceOf(HTMLDivElement);
expect(lastMoexChartConfig?.snapshot.charts[0]?.symbol).toBe('MOEX:SBER');
+ expect(lastMoexChartConfig?.snapshot.charts[0]?.symbolName).toBe('Сбербанк');
+ expect(lastMoexChartConfig?.snapshot.charts[0]?.symbolTicker).toBe('SBER');
expect(lastMoexChartConfig?.snapshot.charts[0]?.timeframe).toBe(Timeframes['1m']);
-
expect(hookResult?.compareManagerRef.current).toBe(mockCompareManager);
+ expect(hookResult?.hasSavedSnapshot).toBe(false);
});
- it('should create chart with saved snapshot when saved data exists', () => {
+ it('should create chart with saved snapshot and current instrument data', () => {
// Arrange
mockMoexChartState = {
- tf: Timeframes['5m'],
+ timeframe: Timeframes['5m'],
savedData: JSON.stringify(mockSnapshot),
};
// Act
- render(<TestComponent symbol="MOEX:GAZP" />);
+ render(
+ <TestComponent
+ symbol="MOEX:GAZP"
+ symbolName="Газпром"
+ symbolTicker="GAZP"
+ />,
+ );
// Assert
expect(lastMoexChartConfig?.snapshot.charts[0]?.symbol).toBe('MOEX:GAZP');
+ expect(lastMoexChartConfig?.snapshot.charts[0]?.symbolName).toBe('Газпром');
+ expect(lastMoexChartConfig?.snapshot.charts[0]?.symbolTicker).toBe('GAZP');
expect(lastMoexChartConfig?.snapshot.charts[0]?.timeframe).toBe(Timeframes['5m']);
-
expect(hookResult?.hasSavedSnapshot).toBe(true);
});
it('should save chart snapshot to widget properties', () => {
// Arrange
- render(<TestComponent symbol="MOEX:SBER" />);
+ render(<TestComponent />);
// Act
act(() => {
@@ -257,23 +317,131 @@ describe('useMoexChart', () => {
const mockState: MockPropertiesState = {
moexChartState: {
- tf: Timeframes['1m'],
+ timeframe: Timeframes['1m'],
},
};
updateCallback(mockState);
expect(mockState.moexChartState).toEqual({
- tf: Timeframes['1m'],
+ timeframe: Timeframes['1m'],
savedData: JSON.stringify(mockSnapshot),
});
});
+ it('should serialize compare symbol, display name and ticker in snapshot', () => {
+ // Arrange
+ const snapshotWithIndicators: MockChartSnapshot = {
+ charts: [
+ {
+ symbol: 'MOEX:SBER',
+ symbolName: 'Сбербанк',
+ symbolTicker: 'SBER',
+ timeframe: Timeframes['1m'],
+ chartSeriesType: 'Candlestick',
+ panes: [
+ {
+ indicators: [
+ {
+ id: 'compare-gazp',
+ indicatorType: undefined,
+ dataSource: {
+ subscription: {},
+ },
+ config: {
+ symbol: 'MOEX:GAZP',
+ symbolTicker: 'GAZP',
+ label: 'Газпром',
+ newPane: false,
+ series: [
+ {
+ name: 'Line',
+ seriesOptions: {
+ priceScaleId: 'left',
+ color: '#FFFFFF',
+ },
+ },
+ ],
+ },
+ },
+ {
+ id: 'rsi',
+ indicatorType: 'RSI',
+ dataSource: {
+ subscription: {},
+ },
+ config: {
+ label: 'RSI',
+ newPane: true,
+ series: [
+ {
+ name: 'Line',
+ seriesOptions: {
+ priceScaleId: 'right',
+ },
+ },
+ ],
+ },
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ };
+
+ mockGetSnapshot.mockReturnValue(snapshotWithIndicators);
+
+ render(<TestComponent />);
+
+ // Act
+ act(() => {
+ hookResult?.saveSnapshot();
+ });
+
+ const updateCallback = mockUpdateProperties.mock.calls[0]?.[0] as UpdatePropertiesCallback;
+
+ const mockState: MockPropertiesState = {
+ moexChartState: {
+ timeframe: Timeframes['1m'],
+ },
+ };
+
+ updateCallback(mockState);
+
+ const savedSnapshot = JSON.parse(mockState.moexChartState?.savedData ?? '{}') as MockChartSnapshot;
+ const [compareIndicator, regularIndicator] = savedSnapshot.charts[0]?.panes[0]?.indicators ?? [];
+
+ // Assert
+ expect(compareIndicator).toEqual({
+ id: 'compare-gazp',
+ config: {
+ symbol: 'MOEX:GAZP',
+ symbolTicker: 'GAZP',
+ label: 'Газпром',
+ newPane: false,
+ series: [
+ {
+ name: 'Line',
+ seriesOptions: {
+ priceScaleId: 'left',
+ },
+ },
+ ],
+ },
+ });
+
+ expect(regularIndicator).toEqual({
+ id: 'rsi',
+ indicatorType: 'RSI',
+ });
+ });
+
it('should not save snapshot when chart does not return snapshot', () => {
// Arrange
mockGetSnapshot.mockReturnValue(undefined);
- render(<TestComponent symbol="MOEX:SBER" />);
+ render(<TestComponent />);
// Act
act(() => {
@@ -285,14 +453,20 @@ describe('useMoexChart', () => {
expect(mockUpdateProperties).not.toHaveBeenCalled();
});
- it('should apply saved snapshot with current symbol and timeframe', () => {
+ it('should apply saved snapshot with current instrument data and timeframe', () => {
// Arrange
mockMoexChartState = {
- tf: Timeframes['5m'],
+ timeframe: Timeframes['5m'],
savedData: JSON.stringify(mockSnapshot),
};
- render(<TestComponent symbol="MOEX:SBER" />);
+ render(
+ <TestComponent
+ symbol="MOEX:SBER"
+ symbolName="Сбербанк"
+ symbolTicker="SBER"
+ />,
+ );
// Act
act(() => {
@@ -306,6 +480,8 @@ describe('useMoexChart', () => {
{
...mockSnapshot.charts[0],
symbol: 'MOEX:SBER',
+ symbolName: 'Сбербанк',
+ symbolTicker: 'SBER',
timeframe: Timeframes['5m'],
},
],
@@ -316,7 +492,7 @@ describe('useMoexChart', () => {
it('should update compare modal state', () => {
// Arrange
- render(<TestComponent symbol="MOEX:SBER" />);
+ render(<TestComponent />);
// Act
act(() => {
@@ -329,7 +505,7 @@ describe('useMoexChart', () => {
it('should open compare modal from chart preset callback', () => {
// Arrange
- render(<TestComponent symbol="MOEX:SBER" />);
+ render(<TestComponent />);
// Act
act(() => {
@@ -342,7 +518,7 @@ describe('useMoexChart', () => {
it('should update symbol search modal state', () => {
// Arrange
- render(<TestComponent symbol="MOEX:SBER" />);
+ render(<TestComponent />);
// Act
act(() => {
@@ -355,7 +531,7 @@ describe('useMoexChart', () => {
it('should open symbol search modal from chart preset callback', () => {
// Arrange
- render(<TestComponent symbol="MOEX:SBER" />);
+ render(<TestComponent />);
// Act
act(() => {
@@ -366,82 +542,157 @@ describe('useMoexChart', () => {
expect(hookResult?.isSymbolSearchOpen).toBe(true);
});
- it('should normalize and change main symbol', () => {
+ it('should update chart when external instrument changes', () => {
// Arrange
- render(<TestComponent symbol="MOEX:SBER" />);
+ const { rerender } = render(<TestComponent />);
// Act
- act(() => {
- hookResult?.setMainSymbol(' MOEX:GAZP ');
- });
+ rerender(
+ <TestComponent
+ symbol="MOEX:GAZP"
+ symbolName="Газпром"
+ symbolTicker="GAZP"
+ />,
+ );
// Assert
expect(mockSetSymbol).toHaveBeenCalledTimes(1);
- expect(mockSetSymbol).toHaveBeenCalledWith('MOEX:GAZP');
+ expect(mockSetSymbol).toHaveBeenCalledWith('MOEX:GAZP', 'Газпром', 'GAZP');
});
- it('should not change main symbol when value is empty', () => {
+ it('should update chart when only external symbol name changes', () => {
// Arrange
- render(<TestComponent symbol="MOEX:SBER" />);
+ const { rerender } = render(<TestComponent />);
// Act
- act(() => {
- hookResult?.setMainSymbol('');
- hookResult?.setMainSymbol(' ');
- });
+ rerender(
+ <TestComponent
+ symbol="MOEX:SBER"
+ symbolName="Сбербанк ПАО"
+ symbolTicker="SBER"
+ />,
+ );
// Assert
- expect(mockSetSymbol).not.toHaveBeenCalled();
+ expect(mockSetSymbol).toHaveBeenCalledTimes(1);
+ expect(mockSetSymbol).toHaveBeenCalledWith('MOEX:SBER', 'Сбербанк ПАО', 'SBER');
});
- it('should not change main symbol when it is already selected', () => {
+ it('should update chart when only external symbol ticker changes', () => {
// Arrange
- render(<TestComponent symbol="MOEX:SBER" />);
+ const { rerender } = render(<TestComponent />);
// Act
- act(() => {
- hookResult?.setMainSymbol('MOEX:SBER');
- });
+ rerender(
+ <TestComponent
+ symbol="MOEX:SBER"
+ symbolName="Сбербанк"
+ symbolTicker="SBERP"
+ />,
+ );
// Assert
- expect(mockSetSymbol).not.toHaveBeenCalled();
+ expect(mockSetSymbol).toHaveBeenCalledTimes(1);
+ expect(mockSetSymbol).toHaveBeenCalledWith('MOEX:SBER', 'Сбербанк', 'SBERP');
});
- it('should update chart when external symbol changes', () => {
+ it('should use symbol as display name when symbol name is empty', () => {
// Arrange
- const { rerender } = render(<TestComponent symbol="MOEX:SBER" />);
+ const { rerender } = render(<TestComponent />);
// Act
- rerender(<TestComponent symbol="MOEX:GAZP" />);
+ rerender(
+ <TestComponent
+ symbol="MOEX:SBER"
+ symbolName=""
+ symbolTicker="SBER"
+ />,
+ );
// Assert
expect(mockSetSymbol).toHaveBeenCalledTimes(1);
- expect(mockSetSymbol).toHaveBeenCalledWith('MOEX:GAZP');
+ expect(mockSetSymbol).toHaveBeenCalledWith('MOEX:SBER', 'MOEX:SBER', 'SBER');
+ });
+
+ it('should use symbol as ticker when symbol ticker is empty', () => {
+ // Arrange
+ const { rerender } = render(<TestComponent />);
+
+ // Act
+ rerender(
+ <TestComponent
+ symbol="MOEX:SBER"
+ symbolName="Сбербанк"
+ symbolTicker=""
+ />,
+ );
+
+ // Assert
+ expect(mockSetSymbol).toHaveBeenCalledTimes(1);
+ expect(mockSetSymbol).toHaveBeenCalledWith('MOEX:SBER', 'Сбербанк', 'MOEX:SBER');
+ });
+
+ it('should not update chart when symbol, name and ticker are unchanged', () => {
+ // Arrange
+ const { rerender } = render(<TestComponent />);
+
+ // Act
+ rerender(<TestComponent />);
+
+ // Assert
+ expect(mockSetSymbol).not.toHaveBeenCalled();
+ });
+
+ it('should not update chart when external symbol is empty', () => {
+ // Arrange
+ const { rerender } = render(<TestComponent />);
+
+ // Act
+ rerender(
+ <TestComponent
+ symbol=""
+ symbolName="Пустой инструмент"
+ symbolTicker="EMPTY"
+ />,
+ );
+
+ // Assert
+ expect(mockSetSymbol).not.toHaveBeenCalled();
});
- it('should not recreate chart when external symbol changes', () => {
+ it('should not recreate chart when external instrument changes', () => {
// Arrange
- const { rerender } = render(<TestComponent symbol="MOEX:SBER" />);
+ const { rerender } = render(<TestComponent />);
// Act
- rerender(<TestComponent symbol="MOEX:GAZP" />);
+ rerender(
+ <TestComponent
+ symbol="MOEX:GAZP"
+ symbolName="Газпром"
+ symbolTicker="GAZP"
+ />,
+ );
// Assert
expect(mockMoexChart).toHaveBeenCalledTimes(1);
});
- it('should apply saved snapshot with symbol selected from search modal', () => {
+ it('should apply saved snapshot with externally selected instrument', () => {
// Arrange
mockMoexChartState = {
- tf: Timeframes['5m'],
+ timeframe: Timeframes['5m'],
savedData: JSON.stringify(mockSnapshot),
};
- render(<TestComponent symbol="MOEX:SBER" />);
+ const { rerender } = render(<TestComponent />);
- act(() => {
- hookResult?.setMainSymbol('MOEX:GAZP');
- });
+ rerender(
+ <TestComponent
+ symbol="MOEX:GAZP"
+ symbolName="Газпром"
+ symbolTicker="GAZP"
+ />,
+ );
// Act
act(() => {
@@ -455,15 +706,50 @@ describe('useMoexChart', () => {
{
...mockSnapshot.charts[0],
symbol: 'MOEX:GAZP',
+ symbolName: 'Газпром',
+ symbolTicker: 'GAZP',
timeframe: Timeframes['5m'],
},
],
});
});
+ it('should initialize indicative instrument with id, name and ticker', () => {
+ // Arrange
+ const indicativeData: ChartIndicativeData = {
+ id: 1,
+ title: 'Indicative instrument',
+ secId: 'INAV',
+ instrumentName: 'Индикатив',
+ settlement: 'Расчётный',
+ firmName: 'Тестовая фирма',
+ key: '2xOFZ:INAV',
+ };
+
+ // Act
+ render(
+ <TestComponent
+ symbol="2xOFZ:INAV"
+ symbolName="Индикатив Расчётный"
+ symbolTicker="INAV"
+ indicativeData={indicativeData}
+ />,
+ );
+
+ // Assert
+ expect(mockGetDataSource).toHaveBeenCalledWith(indicativeData, expect.any(Function));
+ expect(lastMoexChartConfig?.snapshot.charts[0]).toEqual(
+ expect.objectContaining({
+ symbol: '2xOFZ:INAV',
+ symbolName: 'Индикатив Расчётный',
+ symbolTicker: 'INAV',
+ }),
+ );
+ });
+
it('should pass realtime params to data source provider', () => {
// Arrange
- render(<TestComponent symbol="MOEX:SBER" />);
+ render(<TestComponent />);
const getSymbols = jest.fn(() => ['MOEX:SBER']);
const getTimeframe = jest.fn(() => Timeframes['1m']);
@@ -478,13 +764,12 @@ describe('useMoexChart', () => {
getTimeframe,
update,
});
-
expect(unsubscribe).toBe(mockRealtimeUnsubscribe);
});
it('should update timeframe from data source callback', () => {
// Arrange
- render(<TestComponent symbol="MOEX:SBER" />);
+ render(<TestComponent />);
const timeframeChangeCallback = mockGetDataSource.mock.calls[0]?.[1] as TimeframeChangeCallback;
@@ -500,18 +785,18 @@ describe('useMoexChart', () => {
const mockState: MockPropertiesState = {
moexChartState: {
- tf: Timeframes['1m'],
+ timeframe: Timeframes['1m'],
},
};
updateCallback(mockState);
- expect(mockState.moexChartState?.tf).toBe(Timeframes['5m']);
+ expect(mockState.moexChartState?.timeframe).toBe(Timeframes['5m']);
});
it('should not update timeframe when it is the same as current timeframe', () => {
// Arrange
- render(<TestComponent symbol="MOEX:SBER" />);
+ render(<TestComponent />);
const timeframeChangeCallback = mockGetDataSource.mock.calls[0]?.[1] as TimeframeChangeCallback;
@@ -526,7 +811,7 @@ describe('useMoexChart', () => {
it('should destroy chart on unmount', () => {
// Arrange
- const { unmount } = render(<TestComponent symbol="MOEX:SBER" />);
+ const { unmount } = render(<TestComponent />);
// Act
unmount();
diff --git a/src/widgets/Chart/components/MoexChart/MoexChart.tsx b/src/widgets/Chart/components/MoexChart/MoexChart.tsx
index 86e7bb981..03ca8faf6 100644
--- a/src/widgets/Chart/components/MoexChart/MoexChart.tsx
+++ b/src/widgets/Chart/components/MoexChart/MoexChart.tsx
@@ -3,67 +3,71 @@ import React from 'react';
import { Contract } from '@modules/contracts';
import { SymbolSearchModal } from '@widgets/Chart/components/MoexChart/components/SymbolSearchModal';
-import { ChartIndicativeData } from '../../types';
+import { ChartIndicativeData, SelectedInstrument } from '../../types';
import { CompareModal } from './components/CompareModal';
import { useMoexChart } from './hooks';
import 'moex-chart/dist/styles.css';
-type TRProps = {
- fullName: string;
+interface TRProps {
+ symbol: string;
+ symbolName: string;
+ symbolTicker: string;
indicativeData?: ChartIndicativeData | undefined;
widgetId: number;
- addInstrumentFromModal: (instruments: Pick<Contract, 'issKey'>[]) => void;
-};
+ addInstrumentFromModal: (instruments: SelectedInstrument[]) => void;
+}
-export default React.memo(({ fullName, indicativeData, widgetId, addInstrumentFromModal }: TRProps) => {
- const {
- containerRef,
- isCompareOpen,
- isSymbolSearchOpen,
- compareManagerRef,
- setIsCompareOpen,
- setIsSymbolSearchOpen,
- setMainSymbol,
- } = useMoexChart({
- indicativeData,
- symbol: fullName,
- });
+export default React.memo(
+ ({ symbol, symbolName, symbolTicker, indicativeData, widgetId, addInstrumentFromModal }: TRProps) => {
+ const {
+ containerRef,
+ isCompareOpen,
+ isSymbolSearchOpen,
+ compareManagerRef,
+ setIsCompareOpen,
+ setIsSymbolSearchOpen,
+ } = useMoexChart({
+ indicativeData,
+ symbol,
+ symbolName,
+ symbolTicker,
+ });
- const handleSymbolChange = (symbol: string): void => {
- addInstrumentFromModal([{ issKey: symbol }]);
- setMainSymbol(symbol);
- };
+ const handleSymbolChange = (instrument: Contract): void => {
+ addInstrumentFromModal([instrument]);
+ };
- return (
- <div
- style={{
- flex: '1 1 0',
- minHeight: 0,
- minWidth: 0,
- }}
- >
- <div ref={containerRef} />
+ return (
+ <div
+ style={{
+ flex: '1 1 0',
+ minHeight: 0,
+ minWidth: 0,
+ }}
+ >
+ <div ref={containerRef} />
- {isCompareOpen && (
- <CompareModal
- onClose={() => setIsCompareOpen(false)}
- compareManager={compareManagerRef}
- widgetId={widgetId}
- isOpen={isCompareOpen}
- setOpen={setIsCompareOpen}
- />
- )}
+ {isCompareOpen && (
+ <CompareModal
+ onClose={() => setIsCompareOpen(false)}
+ compareManager={compareManagerRef}
+ widgetId={widgetId}
+ isOpen={isCompareOpen}
+ setOpen={setIsCompareOpen}
+ />
+ )}
- {isSymbolSearchOpen && (
- <SymbolSearchModal
- widgetId={widgetId}
- isOpen={isSymbolSearchOpen}
- setOpen={setIsSymbolSearchOpen}
- onSymbolChange={handleSymbolChange}
- />
- )}
- </div>
- );
-});
+ {isSymbolSearchOpen && (
+ <SymbolSearchModal
+ widgetId={widgetId}
+ isOpen={isSymbolSearchOpen}
+ setOpen={setIsSymbolSearchOpen}
+ onSymbolChange={handleSymbolChange}
+ />
+ )}
+ </div>
+ );
+ },
+);
diff --git a/src/widgets/Chart/components/MoexChart/components/CompareModal.tsx b/src/widgets/Chart/components/MoexChart/components/CompareModal.tsx
index 092f6df1a..8c6d67725 100644
--- a/src/widgets/Chart/components/MoexChart/components/CompareModal.tsx
+++ b/src/widgets/Chart/components/MoexChart/components/CompareModal.tsx
@@ -2,6 +2,7 @@ import { __CompareManager__, CompareMode } from 'moex-chart';
import React, { MutableRefObject, useEffect, useState } from 'react';
import { InstrumentSearch } from '@components/InstrumentSearch';
+import { Contract } from '@modules/contracts';
export const CompareModal = ({
compareManager,
@@ -33,16 +34,34 @@ export const CompareModal = ({
return () => subscription.unsubscribe();
}, [compareManager, isOpen]);
- const handlePercent = (symbol: string) => {
- compareManager?.current?.setSymbolMode('Line', symbol, CompareMode.Percentage);
+ const setCompareMode = (instrument: Contract, mode: CompareMode): void => {
+ const symbol = instrument.issKey;
+
+ if (!symbol) {
+ return;
+ }
+
+ compareManager.current?.setSymbolMode(
+ 'Line',
+ {
+ symbol,
+ symbolName: instrument.displayName || symbol,
+ symbolTicker: instrument.symbol || symbol,
+ },
+ mode,
+ );
+ };
+
+ const handlePercent = (instrument: Contract): void => {
+ setCompareMode(instrument, CompareMode.Percentage);
};
- const handleNewScale = (symbol: string) => {
- compareManager?.current?.setSymbolMode('Line', symbol, CompareMode.NewScale);
+ const handleNewScale = (instrument: Contract): void => {
+ setCompareMode(instrument, CompareMode.NewScale);
};
- const handleNewPanel = (symbol: string) => {
- compareManager?.current?.setSymbolMode('Line', symbol, CompareMode.NewPane);
+ const handleNewPanel = (instrument: Contract): void => {
+ setCompareMode(instrument, CompareMode.NewPane);
};
return (
diff --git a/src/widgets/Chart/components/MoexChart/components/SymbolSearchModal.tsx b/src/widgets/Chart/components/MoexChart/components/SymbolSearchModal.tsx
index efb08af16..10347dfc3 100644
--- a/src/widgets/Chart/components/MoexChart/components/SymbolSearchModal.tsx
+++ b/src/widgets/Chart/components/MoexChart/components/SymbolSearchModal.tsx
@@ -8,18 +8,18 @@ interface SymbolSearchModalProps {
widgetId: number;
isOpen: boolean;
setOpen: (isOpen: boolean) => void;
- onSymbolChange: (symbol: string) => void;
+ onSymbolChange: (instrument: Contract) => void;
}
export const SymbolSearchModal = ({ widgetId, isOpen, setOpen, onSymbolChange }: SymbolSearchModalProps) => {
const handleAddInstruments = (instruments: Contract[]) => {
- const symbol = instruments[0]?.issKey;
+ const selectedInstrument = instruments[0];
- if (!symbol) {
+ if (!selectedInstrument?.issKey) {
return;
}
- onSymbolChange(symbol);
+ onSymbolChange(selectedInstrument);
setOpen(false);
};
diff --git a/src/widgets/Chart/components/MoexChart/constants.ts b/src/widgets/Chart/components/MoexChart/constants.ts
index 288036c50..737666528 100644
--- a/src/widgets/Chart/components/MoexChart/constants.ts
+++ b/src/widgets/Chart/components/MoexChart/constants.ts
@@ -3,7 +3,7 @@ import { DateFormat, IndicatorsIds, Locale, Timeframes } from 'moex-chart';
import type { ChartCollectionPreset, IMoexChart, MoexChartSnapshot } from 'moex-chart';
type ChartCollectionPresetConfig = Omit<ChartCollectionPreset, 'getDataSource' | 'startRealtime'>;
-type ChartSnapshotItemConfig = Omit<MoexChartSnapshot['charts'][number], 'symbol'>;
+type ChartSnapshotItemConfig = Omit<MoexChartSnapshot['charts'][number], 'symbol' | 'symbolName' | 'symbolTicker'>;
type MoexChartSnapshotConfig = Omit<MoexChartSnapshot, 'charts'> & {
charts: ChartSnapshotItemConfig[];
};
diff --git a/src/widgets/Chart/components/MoexChart/hooks/useMoexchart.ts b/src/widgets/Chart/components/MoexChart/hooks/useMoexchart.ts
index 58c504900..895ce171a 100644
--- a/src/widgets/Chart/components/MoexChart/hooks/useMoexchart.ts
+++ b/src/widgets/Chart/components/MoexChart/hooks/useMoexchart.ts
@@ -3,10 +3,9 @@ import { MoexChart, Timeframes } from 'moex-chart';
import { useEffect, useRef, useState } from 'react';
import { useChangeProperties, useSelectProperties } from '@modules/widgetProperties';
+import { WidgetProperties } from '@widgets/Chart/properties/types';
import { ChartIndicativeData } from '@widgets/Chart/types';
-import { WidgetProperties } from '../../../properties/types';
-
import { MOEX_CHART_CONFIG } from '../constants';
import { DataSourceProvider } from '../dataSourceProvide';
@@ -14,10 +13,12 @@ import type { __CompareManager__, IMoexChart } from 'moex-chart';
interface TUseMoexChartProps {
symbol: string;
+ symbolName: string;
+ symbolTicker: string;
indicativeData?: ChartIndicativeData;
}
-export const useMoexChart = ({ symbol, indicativeData }: TUseMoexChartProps) => {
+export const useMoexChart = ({ symbol, symbolName, symbolTicker, indicativeData }: TUseMoexChartProps) => {
const moexChartState = useSelectProperties((wProps: Partial<WidgetProperties>) => wProps.moexChartState);
const { updateProperties } = useChangeProperties<WidgetProperties>();
@@ -29,30 +30,35 @@ export const useMoexChart = ({ symbol, indicativeData }: TUseMoexChartProps) =>
const chartRef = useRef<MoexChart | null>(null);
const compareManagerRef = useRef<null | __CompareManager__>(null);
const currentSymbolRef = useRef(symbol);
+ const currentSymbolNameRef = useRef(symbolName || symbol);
+ const currentSymbolTickerRef = useRef(symbolTicker);
const timeframeRef = useRef<Timeframes | undefined>(moexChartState?.timeframe);
const savedDataRef = useRef<string | undefined>(moexChartState?.savedData);
const updateTimeframeRef = useRef<((tf: Timeframes) => void) | null>(null);
useEffect(() => {
- if (!symbol || currentSymbolRef.current === symbol) {
+ if (!symbol) {
return;
}
- currentSymbolRef.current = symbol;
- chartRef.current?.setSymbol(symbol);
- }, [symbol]);
+ const nextSymbolName = symbolName || symbol;
+ const nextSymbolTicker = symbolTicker || symbol;
- const setMainSymbol = (nextSymbol: string) => {
- const normalizedSymbol = nextSymbol.trim();
+ const symbolChanged = currentSymbolRef.current !== symbol;
+ const symbolNameChanged = currentSymbolNameRef.current !== nextSymbolName;
+ const symbolTickerChanged = currentSymbolTickerRef.current !== nextSymbolTicker;
- if (!normalizedSymbol || currentSymbolRef.current === normalizedSymbol) {
+ if (!symbolChanged && !symbolNameChanged && !symbolTickerChanged) {
return;
}
- currentSymbolRef.current = normalizedSymbol;
- chartRef.current?.setSymbol(normalizedSymbol);
- };
+ currentSymbolRef.current = symbol;
+ currentSymbolNameRef.current = nextSymbolName;
+ currentSymbolTickerRef.current = nextSymbolTicker;
+
+ chartRef.current?.setSymbol(symbol, nextSymbolName, nextSymbolTicker);
+ }, [symbol, symbolName, symbolTicker]);
useEffect(() => {
timeframeRef.current = moexChartState?.timeframe;
@@ -96,6 +102,8 @@ export const useMoexChart = ({ symbol, indicativeData }: TUseMoexChartProps) =>
config:
indicator.indicatorType === undefined && indicator.config?.label && compareSeries
? {
+ symbol: indicator.config.symbol,
+ symbolTicker: indicator.config.symbolTicker,
label: indicator.config.label,
newPane: indicator.config.newPane,
series: [
@@ -138,6 +146,8 @@ export const useMoexChart = ({ symbol, indicativeData }: TUseMoexChartProps) =>
charts: savedSnapshot.charts.map((chartSnapshot) => ({
...chartSnapshot,
symbol: currentSymbolRef.current,
+ symbolName: currentSymbolNameRef.current,
+ symbolTicker: currentSymbolTickerRef.current,
timeframe,
})),
});
@@ -166,6 +176,8 @@ export const useMoexChart = ({ symbol, indicativeData }: TUseMoexChartProps) =>
charts: savedSnapshot.charts.map((chartSnapshot) => ({
...chartSnapshot,
symbol: currentSymbolRef.current,
+ symbolName: currentSymbolNameRef.current,
+ symbolTicker: currentSymbolTickerRef.current,
timeframe,
})),
},
@@ -208,7 +220,6 @@ export const useMoexChart = ({ symbol, indicativeData }: TUseMoexChartProps) =>
compareManagerRef,
setIsCompareOpen,
setIsSymbolSearchOpen,
- setMainSymbol,
saveSnapshot,
applySnapshot,
hasSavedSnapshot: Boolean(moexChartState?.savedData),
diff --git a/src/widgets/Chart/hooks/useChartComponentFacade.tsx b/src/widgets/Chart/hooks/useChartComponentFacade.tsx
index ece1436b8..254d48f30 100644
--- a/src/widgets/Chart/hooks/useChartComponentFacade.tsx
+++ b/src/widgets/Chart/hooks/useChartComponentFacade.tsx
@@ -13,9 +13,8 @@ import { DEFAULT_SYMBOL } from '../const';
import { useChartPublicContext } from './useChartPublicContext';
import type { WidgetProperties } from '../properties/types';
-import type { ChartContainerProps } from '../types';
+import type { ChartContainerProps, SelectedInstrument } from '../types';
-import type { Contract } from '@modules/contracts';
import type { Dispatch, SetStateAction } from 'react';
import type { Widget } from 'types/Widgets';
@@ -23,10 +22,12 @@ interface UseChartComponentFacadeReturn {
dropDownOpen: boolean;
setDropdownOpen: Dispatch<SetStateAction<boolean>>;
currentInstrument: string;
+ currentInstrumentName: string;
+ currentInstrumentTicker: string;
isWidgetHeaderContextMenuOpen: boolean;
setIsWidgetHeaderContextMenuOpen: Dispatch<SetStateAction<boolean>>;
- onDropInstruments: (val: string, withUpdate?: boolean) => void;
- addInstrumentFromModal: (instruments: Pick<Contract, 'issKey'>[]) => void;
+ onDropInstruments: (instrument: SelectedInstrument, withUpdate?: boolean) => void;
+ addInstrumentFromModal: (instruments: SelectedInstrument[]) => void;
isOver: boolean;
}
@@ -49,11 +50,18 @@ export default function useChartComponentFacade(props: ChartContainerProps): Use
) as WidgetProperties | undefined;
const initialInstrument = widgetProperties?.chartState?.savedInstrument ?? DEFAULT_SYMBOL;
+ const initialInstrumentName = widgetProperties?.chartState?.savedInstrumentName ?? initialInstrument;
+ const initialInstrumentTicker = widgetProperties?.chartState?.savedInstrumentTicker ?? initialInstrument;
// TODO временно реф из-за непредсказуемого изменения если используется useState,
// нужен рефакторинг и вернуть обратно useState
const currentInstrumentRef = useRef(initialInstrument);
+ const currentInstrumentNameRef = useRef(initialInstrumentName);
+ const currentInstrumentTickerRef = useRef(initialInstrumentTicker);
+
const [currentInstrument, setCurrentInstrument] = useState(initialInstrument);
+ const [currentInstrumentName, setCurrentInstrumentName] = useState(initialInstrumentName);
+ const [currentInstrumentTicker, setCurrentInstrumentTicker] = useState(initialInstrumentTicker);
const { triggerRelatedWidgetsToUpdate, getMasterInstrumentFromPublicContext } = useWidgetsBind({
widgetId,
@@ -95,60 +103,91 @@ export default function useChartComponentFacade(props: ChartContainerProps): Use
);
const onInstrumentChange = useCallback(
- (newVal: string, withUpdate?: boolean, unbind = true): void => {
+ (newVal: string, newName?: string, newTicker?: string, withUpdate?: boolean, unbind = true): void => {
if (!newVal) {
return;
}
setIsWidgetHeaderContextMenuOpen(false);
- if (currentInstrumentRef.current === newVal) {
+ const symbolChanged = currentInstrumentRef.current !== newVal;
+
+ const normalizedName = newName?.trim();
+ const normalizedTicker = newTicker?.trim();
+
+ const nextInstrumentName = normalizedName || (symbolChanged ? newVal : currentInstrumentNameRef.current);
+ const nextInstrumentTicker = normalizedTicker || (symbolChanged ? newVal : currentInstrumentTickerRef.current);
+
+ const nameChanged = currentInstrumentNameRef.current !== nextInstrumentName;
+ const tickerChanged = currentInstrumentTickerRef.current !== nextInstrumentTicker;
+
+ if (!symbolChanged && !nameChanged && !tickerChanged) {
return;
}
- currentInstrumentRef.current = newVal;
- setCurrentInstrument(newVal);
+ if (symbolChanged) {
+ currentInstrumentRef.current = newVal;
+ setCurrentInstrument(newVal);
+
+ if (unbind) {
+ dispatch(unbindWidgets({ widgetId }));
+ }
+ }
+
+ if (nameChanged) {
+ currentInstrumentNameRef.current = nextInstrumentName;
+ setCurrentInstrumentName(nextInstrumentName);
+ }
- if (unbind) {
- dispatch(unbindWidgets({ widgetId }));
+ if (tickerChanged) {
+ currentInstrumentTickerRef.current = nextInstrumentTicker;
+ setCurrentInstrumentTicker(nextInstrumentTicker);
}
// при смене инструмента очищаем индикативные данные виджета график
// т.к. логика для графика индикатива построена на наличии в widgetContentProps данных indicativeData
saveContentProps({
savedInstrument: newVal,
+ savedInstrumentName: nextInstrumentName,
+ savedInstrumentTicker: nextInstrumentTicker,
withUpdate,
- cleanIndicativeData: true,
+ cleanIndicativeData: symbolChanged,
});
- triggerRelatedWidgetsToUpdateRef.current(newVal);
+ if (symbolChanged) {
+ triggerRelatedWidgetsToUpdateRef.current(newVal);
+ }
},
[dispatch, saveContentProps, widgetId],
);
const addInstrumentFromModal = useCallback(
- (instruments: Pick<Contract, 'issKey'>[]) => {
- const issKey = instruments[0]?.issKey;
+ (instruments: SelectedInstrument[]): void => {
+ const instrument = instruments[0];
- if (!issKey) {
+ if (!instrument?.issKey) {
return;
}
- onInstrumentChange(issKey);
+ onInstrumentChange(instrument.issKey, instrument.displayName, instrument.symbol);
},
[onInstrumentChange],
);
const onInstrumentChangeFromBind = useCallback(
- (instrumentId: string) => {
- onInstrumentChange(instrumentId, true, false);
+ (instrumentId: string, instrumentName?: string, instrumentTicker?: string): void => {
+ onInstrumentChange(instrumentId, instrumentName, instrumentTicker, true, false);
},
[onInstrumentChange],
);
const onDropInstruments = useCallback(
- (val: string, withUpdate?: boolean): void => {
- onInstrumentChange(val, withUpdate);
+ (instrument: SelectedInstrument, withUpdate?: boolean): void => {
+ if (!instrument.issKey) {
+ return;
+ }
+
+ onInstrumentChange(instrument.issKey, instrument.displayName, instrument.symbol, withUpdate);
},
[onInstrumentChange],
);
@@ -172,7 +211,7 @@ export default function useChartComponentFacade(props: ChartContainerProps): Use
const issKey = (message as Record<number, string>)[widgetId];
if (issKey) {
- addInstrumentFromModal([{ issKey }]);
+ onInstrumentChange(issKey);
}
},
);
@@ -194,12 +233,14 @@ export default function useChartComponentFacade(props: ChartContainerProps): Use
unsubscribe();
unsubscribeHighlighter();
};
- }, [addInstrumentFromModal, widgetId]);
+ }, [onInstrumentChange, widgetId]);
return {
dropDownOpen,
setDropdownOpen,
currentInstrument,
+ currentInstrumentName,
+ currentInstrumentTicker,
isWidgetHeaderContextMenuOpen,
setIsWidgetHeaderContextMenuOpen,
onDropInstruments,
diff --git a/src/widgets/Chart/hooks/useChartPublicContext.ts b/src/widgets/Chart/hooks/useChartPublicContext.ts
index 0edfadcae..e8999c74d 100644
--- a/src/widgets/Chart/hooks/useChartPublicContext.ts
+++ b/src/widgets/Chart/hooks/useChartPublicContext.ts
@@ -8,7 +8,7 @@ import { DEFAULT_SYMBOL } from '@widgets/Chart/const';
import { Widget } from 'types/Widgets';
interface UseChartPublicContextArg {
- setCurrInstrument: (instrumentId: string) => void;
+ setCurrInstrument: (instrumentId: string, instrumentName?: string, instrumentTicker?: string) => void;
widgetId: Widget['id'];
getMasterInstrumentFromPublicContext: () => string | number | null | undefined;
}
@@ -39,12 +39,13 @@ export function useChartPublicContext({
useEffect(() => {
const fieldValue = getMasterInstrumentFromPublicContext();
if (!fieldValue && widget?.master) {
- setCurrInstrument(DEFAULT_SYMBOL);
+ const defaultInstrument = instruments.find((item) => item.issKey === DEFAULT_SYMBOL);
+ setCurrInstrument(DEFAULT_SYMBOL, defaultInstrument?.displayName, defaultInstrument?.symbol);
return;
}
- const instrKey = instruments.find((item) => item.issKey === fieldValue)?.issKey;
- if (instrKey) {
- setCurrInstrument(instrKey);
+ const instrument = instruments.find((item) => item.issKey === fieldValue);
+ if (instrument?.issKey) {
+ setCurrInstrument(instrument.issKey, instrument.displayName, instrument.symbol);
}
// eslint-disable-next-line react-hooks/exhaustive-deps -- Посмотреть этот момент.
}, [publicContext, instruments, widget?.externalProperties, setCurrInstrument]);
diff --git a/src/widgets/Chart/index.tsx b/src/widgets/Chart/index.tsx
index e38b1552a..f6175b50a 100644
--- a/src/widgets/Chart/index.tsx
+++ b/src/widgets/Chart/index.tsx
@@ -22,6 +22,8 @@ export const Chart: FC<ChartContainerProps> = function (props): JSX.Element {
dropDownOpen,
setDropdownOpen,
currentInstrument,
+ currentInstrumentName,
+ currentInstrumentTicker,
setIsWidgetHeaderContextMenuOpen,
isWidgetHeaderContextMenuOpen,
onDropInstruments,
@@ -49,7 +51,8 @@ export const Chart: FC<ChartContainerProps> = function (props): JSX.Element {
const instrumentName = indicativeData
? `${indicativeData.instrumentName} ${indicativeData.settlement} - ${indicativeData.firmName}`
- : contractsInstrumentName;
+ : currentInstrumentName || contractsInstrumentName || currentInstrument;
+ const instrumentTicker = indicativeData ? indicativeData.secId : currentInstrumentTicker || currentInstrument;
return (
<DNDWrapper
@@ -78,7 +81,9 @@ export const Chart: FC<ChartContainerProps> = function (props): JSX.Element {
}}
>
<MoexChart
- fullName={currentInstrument}
+ symbol={currentInstrument}
+ symbolName={instrumentName}
+ symbolTicker={instrumentTicker}
indicativeData={indicativeData}
widgetId={props.widgetId}
addInstrumentFromModal={addInstrumentFromModal}
diff --git a/src/widgets/Chart/properties/types.ts b/src/widgets/Chart/properties/types.ts
index afdbdf473..3a5cc442d 100644
--- a/src/widgets/Chart/properties/types.ts
+++ b/src/widgets/Chart/properties/types.ts
@@ -2,11 +2,15 @@ import { Contract } from '@modules/contracts/types';
import { ChartIndicativeData } from '../types';
+import type { WidgetProperties as BaseWidgetProperties } from '@modules/widgetProperties/types';
+
import type { Timeframes } from 'moex-chart';
-export type WidgetProperties = {
+export interface WidgetProperties extends BaseWidgetProperties {
chartState: {
savedInstrument: Contract['issKey'] | null;
+ savedInstrumentName?: string;
+ savedInstrumentTicker?: string;
interval: string;
savedData?: string;
};
@@ -15,4 +19,4 @@ export type WidgetProperties = {
timeframe: Timeframes;
savedData?: string;
};
-};
+}
diff --git a/src/widgets/Chart/types.ts b/src/widgets/Chart/types.ts
index bbc0155d5..519b47289 100644
--- a/src/widgets/Chart/types.ts
+++ b/src/widgets/Chart/types.ts
@@ -1,12 +1,22 @@
-import type { WidgetContentBasicProps } from 'types/Widgets';
+import { Contract } from '@modules/contracts';
+
import type { WidgetProperties } from './properties/types';
-export type ChartIndicativeData = {
+import type { WidgetProperties as BaseWidgetProperties } from '@modules/widgetProperties/types';
+import type { WidgetContentBasicProps } from 'types/Widgets';
+
+export interface ChartIndicativeData extends BaseWidgetProperties {
secId: string;
instrumentName: string;
settlement: string;
firmName: string;
key: string;
-};
+}
+
+export interface SelectedInstrument {
+ issKey: Contract['issKey'];
+ displayName: Contract['displayName'];
+ symbol: Contract['symbol'];
+}
export type ChartContainerProps = WidgetContentBasicProps<WidgetProperties>;