Загрузка данных
import { fireEvent, render, screen } from '@testing-library/react';
import { BehaviorSubject } from 'rxjs';
import { LegendModel } from '@core/Legend';
import type { OHLCConfig } from '@src/types';
import { LegendComponent } from './index';
jest.mock('exchange-elements/v2', () => ({
Button: ({ onClick, label }: { onClick?: () => void; label: React.ReactNode }) => (
<button onClick={onClick}>{label}</button>
),
}));
jest.mock('@components/Icon', () => ({
GearIcon: () => <span>settings</span>,
TrashIcon: () => <span>remove</span>,
}));
jest.mock('@src/utils', () => ({
formatDisplayText: (value: unknown) => String(value),
useObservable: (observable: { getValue: () => unknown }) => observable.getValue(),
}));
const createMainSeriesModel = (): LegendModel => [
{
id: 'main-series',
name: 'AAPL',
isIndicator: false,
values: {
open: { value: 100, color: 'white', name: 'Open' },
high: { value: 110, color: 'white', name: 'High' },
absoluteChange: { value: 10, color: 'green', name: 'Absolute' },
percentageChange: { value: '10%', color: 'green', name: 'Percentage' },
},
},
];
describe('LegendComponent', () => {
it('должен отображать OHLC и относительное изменение по умолчанию', () => {
const viewModel = new BehaviorSubject(createMainSeriesModel());
render(<LegendComponent viewModel={viewModel} ohlcConfig={{ show: true }} />);
expect(screen.getByText('Open')).toBeInTheDocument();
expect(screen.getByText('100')).toBeInTheDocument();
expect(screen.getByText('High')).toBeInTheDocument();
expect(screen.getByText('110')).toBeInTheDocument();
expect(screen.getByText('Percentage')).toBeInTheDocument();
expect(screen.getByText('10%')).toBeInTheDocument();
expect(screen.queryByText('Absolute')).not.toBeInTheDocument();
});
it('должен скрывать относительное изменение при выключенном showPercentageChange', () => {
const viewModel = new BehaviorSubject(createMainSeriesModel());
render(
<LegendComponent
viewModel={viewModel}
ohlcConfig={{ show: true, showPercentageChange: false }}
/>,
);
expect(screen.getByText('Open')).toBeInTheDocument();
expect(screen.queryByText('Percentage')).not.toBeInTheDocument();
expect(screen.queryByText('10%')).not.toBeInTheDocument();
});
it('должен отображать абсолютное изменение при включенном showAbsoluteChange', () => {
const viewModel = new BehaviorSubject(createMainSeriesModel());
render(
<LegendComponent
viewModel={viewModel}
ohlcConfig={{ show: true, showAbsoluteChange: true }}
/>,
);
expect(screen.getByText('Absolute')).toBeInTheDocument();
expect(screen.getByText('10')).toBeInTheDocument();
});
it('должен отображать изменения независимо от отображения OHLC', () => {
const viewModel = new BehaviorSubject(createMainSeriesModel());
const config: OHLCConfig = {
show: false,
showAbsoluteChange: true,
showPercentageChange: true,
};
render(<LegendComponent viewModel={viewModel} ohlcConfig={config} />);
expect(screen.queryByText('Open')).not.toBeInTheDocument();
expect(screen.queryByText('High')).not.toBeInTheDocument();
expect(screen.getByText('Absolute')).toBeInTheDocument();
expect(screen.getByText('10')).toBeInTheDocument();
expect(screen.getByText('Percentage')).toBeInTheDocument();
expect(screen.getByText('10%')).toBeInTheDocument();
});
it('должен скрывать оба изменения при выключенных флагах', () => {
const viewModel = new BehaviorSubject(createMainSeriesModel());
render(
<LegendComponent
viewModel={viewModel}
ohlcConfig={{
show: true,
showAbsoluteChange: false,
showPercentageChange: false,
}}
/>,
);
expect(screen.queryByText('Absolute')).not.toBeInTheDocument();
expect(screen.queryByText('Percentage')).not.toBeInTheDocument();
});
it('должен отображать значения индикаторов независимо от OHLC конфигурации', () => {
const model: LegendModel = [
{
id: 'rsi',
name: 'RSI',
isIndicator: true,
values: {
value: { value: 55, color: 'white' },
},
},
];
render(
<LegendComponent
viewModel={new BehaviorSubject(model)}
ohlcConfig={{
show: false,
showAbsoluteChange: false,
showPercentageChange: false,
}}
/>,
);
expect(screen.getByText('RSI')).toBeInTheDocument();
expect(screen.getByText('55')).toBeInTheDocument();
});
it('должен вызывать обработчики настроек и удаления', () => {
const settings = jest.fn();
const remove = jest.fn();
const model: LegendModel = [
{
id: 'indicator',
name: 'EMA',
isIndicator: true,
values: {
value: { value: 100, color: 'white' },
},
settings,
remove,
},
];
render(<LegendComponent viewModel={new BehaviorSubject(model)} ohlcConfig={{ show: true }} />);
fireEvent.click(screen.getByText('settings'));
fireEvent.click(screen.getByText('remove'));
expect(settings).toHaveBeenCalledTimes(1);
expect(remove).toHaveBeenCalledTimes(1);
});
});
import { render, screen } from '@testing-library/react';
import { BehaviorSubject } from 'rxjs';
import type { TooltipVM } from '@core/Tooltip';
import type { TimeFormat, Timeframes, TooltipConfig } from '@src/types';
import type { DateFormat } from '@src/utils/formatter';
import { ChartTooltip } from './index';
jest.mock('exchange-elements/v2', () => ({
Divider: () => <hr data-testid="divider" />,
}));
jest.mock('@src/theme', () => ({
getThemeStore: () => ({
colors: {
chartTooltipBackground: '#000000',
},
}),
}));
jest.mock('@src/utils', () => ({
calcTooltipPosition: () => ({ left: 10, top: 20 }),
shouldShowTime: () => true,
useObservable: (observable: { getValue: () => unknown }) => observable.getValue(),
}));
jest.mock('@src/utils/formatter', () => ({
formatDate: () => '01.01.2026',
formatDisplayText: (value: unknown) => String(value),
}));
const formatObs = new BehaviorSubject({
dateFormat: 'DD.MM.YYYY' as DateFormat,
timeFormat: 'HH:mm:ss' as TimeFormat,
});
const timeframeObs = new BehaviorSubject('1m' as Timeframes);
const createViewModel = () =>
new BehaviorSubject<TooltipVM>({
visible: true,
position: { x: 100, y: 100 },
viewport: { width: 500, height: 300 },
vm: [
{
id: 'main-series',
name: 'AAPL',
isIndicator: false,
values: {
high: { value: 110, color: 'white', name: 'High' },
low: { value: 90, color: 'white', name: 'Low' },
absoluteChange: { value: 10, color: 'green', name: 'Absolute' },
percentageChange: { value: '10%', color: 'green', name: 'Percentage' },
},
},
],
} as TooltipVM);
const createTooltipConfig = (): TooltipConfig => ({
high: { visible: false, label: 'High' },
low: { visible: false, label: 'Low' },
absoluteChange: {
visible: false,
label: 'Абсолютное изменение',
},
percentageChange: {
visible: false,
label: 'Относительное изменение',
},
});
describe('ChartTooltip', () => {
it('не должен отображаться при выключенной видимости', () => {
const viewModel = createViewModel();
viewModel.next({
...viewModel.getValue(),
visible: false,
});
const { container } = render(
<ChartTooltip
formatObs={formatObs}
timeframeObs={timeframeObs}
tooltipConfig={createTooltipConfig()}
viewModel={viewModel}
ohlcConfig={{ show: true }}
/>,
);
expect(container.firstChild).toBeNull();
});
it('должен отдельно отображать абсолютное изменение', () => {
const tooltipConfig = createTooltipConfig();
tooltipConfig.absoluteChange = {
visible: true,
label: 'Абсолютное изменение',
};
render(
<ChartTooltip
formatObs={formatObs}
timeframeObs={timeframeObs}
tooltipConfig={tooltipConfig}
viewModel={createViewModel()}
ohlcConfig={{ show: true }}
/>,
);
expect(screen.getByText('Абсолютное изменение')).toBeInTheDocument();
expect(screen.getByText('10')).toBeInTheDocument();
expect(screen.queryByText('Относительное изменение')).not.toBeInTheDocument();
expect(screen.queryByText('10%')).not.toBeInTheDocument();
});
it('должен отдельно отображать относительное изменение', () => {
const tooltipConfig = createTooltipConfig();
tooltipConfig.percentageChange = {
visible: true,
label: 'Относительное изменение',
};
render(
<ChartTooltip
formatObs={formatObs}
timeframeObs={timeframeObs}
tooltipConfig={tooltipConfig}
viewModel={createViewModel()}
ohlcConfig={{ show: true }}
/>,
);
expect(screen.getByText('Относительное изменение')).toBeInTheDocument();
expect(screen.getByText('10%')).toBeInTheDocument();
expect(screen.queryByText('Абсолютное изменение')).not.toBeInTheDocument();
});
it('должен одновременно отображать абсолютное и относительное изменение', () => {
const tooltipConfig = createTooltipConfig();
tooltipConfig.absoluteChange = {
visible: true,
label: 'Absolute',
};
tooltipConfig.percentageChange = {
visible: true,
label: 'Percentage',
};
render(
<ChartTooltip
formatObs={formatObs}
timeframeObs={timeframeObs}
tooltipConfig={tooltipConfig}
viewModel={createViewModel()}
ohlcConfig={{ show: true }}
/>,
);
expect(screen.getByText('Absolute')).toBeInTheDocument();
expect(screen.getByText('Percentage')).toBeInTheDocument();
expect(screen.getByText('10')).toBeInTheDocument();
expect(screen.getByText('10%')).toBeInTheDocument();
});
it('должен использовать подписи из конфигурации', () => {
const tooltipConfig = createTooltipConfig();
tooltipConfig.absoluteChange = {
visible: true,
label: 'Custom absolute label',
};
tooltipConfig.percentageChange = {
visible: true,
label: 'Custom percentage label',
};
render(
<ChartTooltip
formatObs={formatObs}
timeframeObs={timeframeObs}
tooltipConfig={tooltipConfig}
viewModel={createViewModel()}
ohlcConfig={{ show: true }}
/>,
);
expect(screen.getByText('Custom absolute label')).toBeInTheDocument();
expect(screen.getByText('Custom percentage label')).toBeInTheDocument();
});
it('не должен отображать изменение без label', () => {
const tooltipConfig = createTooltipConfig();
tooltipConfig.absoluteChange = {
visible: true,
};
render(
<ChartTooltip
formatObs={formatObs}
timeframeObs={timeframeObs}
tooltipConfig={tooltipConfig}
viewModel={createViewModel()}
ohlcConfig={{ show: true }}
/>,
);
expect(screen.queryByText('10')).not.toBeInTheDocument();
});
it('должен отображать данные индикатора', () => {
const viewModel = createViewModel();
viewModel.next({
...viewModel.getValue(),
vm: [
{
id: 'rsi',
name: 'RSI',
isIndicator: true,
values: {
value: {
value: 55,
color: 'white',
name: 'RSI',
},
},
},
],
} as TooltipVM);
render(
<ChartTooltip
formatObs={formatObs}
timeframeObs={timeframeObs}
tooltipConfig={createTooltipConfig()}
viewModel={viewModel}
ohlcConfig={{ show: true }}
/>,
);
expect(screen.getByText('RSI')).toBeInTheDocument();
expect(screen.getByText('55')).toBeInTheDocument();
});
});
import { BehaviorSubject } from 'rxjs';
import { Legend, LegendModel } from '@core/Legend';
import type { EventManager } from '@core/EventManager';
import type { SeriesStrategies } from '@src/modules/series-strategies/SeriesFactory';
import type { OHLCConfig } from '@src/types';
describe('Legend', () => {
let crosshairMoveHandler: ((param: any) => void) | undefined;
let mainSeries: jest.Mocked<SeriesStrategies>;
let eventManager: EventManager;
let indicators: BehaviorSubject<Map<string, never>>;
let mainSeriesSubject: BehaviorSubject<SeriesStrategies | null>;
const config: OHLCConfig = {
show: true,
showAbsoluteChange: false,
showPercentageChange: true,
};
beforeEach(() => {
crosshairMoveHandler = undefined;
mainSeries = {
getLegendData: jest.fn().mockReturnValue({
open: { value: 100, color: 'white', name: 'Open' },
absoluteChange: { value: 10, color: 'green', name: 'Absolute' },
percentageChange: { value: '10%', color: 'green', name: 'Percentage' },
}),
subscribeDataChanged: jest.fn(),
unsubscribeDataChanged: jest.fn(),
} as unknown as jest.Mocked<SeriesStrategies>;
eventManager = {
symbolName: jest.fn().mockReturnValue(new BehaviorSubject('AAPL')),
} as unknown as EventManager;
indicators = new BehaviorSubject(new Map());
mainSeriesSubject = new BehaviorSubject<SeriesStrategies | null>(mainSeries);
});
const createLegend = () =>
new Legend({
config,
eventManager,
indicators,
mainSeries: mainSeriesSubject,
paneId: 1,
paneIndex: () => 0,
subscribeChartEvent: jest.fn((event, handler) => {
if (event === 'crosshairMove') {
crosshairMoveHandler = handler;
}
return () => {};
}),
openIndicatorSettings: jest.fn(),
});
it('должен добавлять абсолютное и относительное изменение в модель основной серии', () => {
const legend = createLegend();
let model: LegendModel = [];
legend.getLegendViewModel().subscribe((value) => {
model = value;
});
const values = model[0].values as Map<string, { value: unknown }>;
expect(values.get('absoluteChange')?.value).toBe(10);
expect(values.get('percentageChange')?.value).toBe('10%');
legend.destroy();
});
it('должен обновлять абсолютное и относительное изменение данными под курсором', () => {
mainSeries.getLegendData.mockImplementation((param?: unknown) => {
if (param) {
return {
absoluteChange: { value: 20, color: 'green', name: 'Absolute' },
percentageChange: { value: '20%', color: 'green', name: 'Percentage' },
};
}
return {
absoluteChange: { value: 10, color: 'green', name: 'Absolute' },
percentageChange: { value: '10%', color: 'green', name: 'Percentage' },
};
});
const legend = createLegend();
crosshairMoveHandler?.({
point: { x: 10, y: 20 },
time: 100,
paneIndex: 0,
seriesData: new Map([['main', {}]]),
});
let model: LegendModel = [];
legend.getLegendViewModel().subscribe((value) => {
model = value;
});
const values = model[0].values as Map<string, { value: unknown }>;
expect(values.get('absoluteChange')?.value).toBe(20);
expect(values.get('percentageChange')?.value).toBe('20%');
legend.destroy();
});
it('должен отображать тултип при наведении на текущий pane', () => {
const legend = createLegend();
let visible = false;
let position = null;
legend.subscribeCursorVisability((value) => {
visible = value;
});
legend.subscribeCursorPosition((value) => {
position = value;
});
crosshairMoveHandler?.({
point: { x: 10, y: 20 },
time: 100,
paneIndex: 0,
seriesData: new Map([['main', {}]]),
});
expect(visible).toBe(true);
expect(position).toEqual({ x: 10, y: 20 });
legend.destroy();
});
it('должен скрывать тултип при наведении на другой pane', () => {
const legend = createLegend();
let visible = true;
legend.subscribeCursorVisability((value) => {
visible = value;
});
crosshairMoveHandler?.({
point: { x: 10, y: 20 },
time: 100,
paneIndex: 1,
seriesData: new Map([['main', {}]]),
});
expect(visible).toBe(false);
legend.destroy();
});
it('должен возвращать переданную конфигурацию', () => {
const legend = createLegend();
expect(legend.getConfig()).toBe(config);
legend.destroy();
});
it('должен подписываться и отписываться от изменений основной серии', () => {
const legend = createLegend();
expect(mainSeries.subscribeDataChanged).toHaveBeenCalledTimes(1);
legend.destroy();
expect(mainSeries.unsubscribeDataChanged).toHaveBeenCalledTimes(1);
});
});