Загрузка данных
'import/no-extraneous-dependencies': [
'error',
{
devDependencies: [
'**/*.{test,spec}.{js,jsx,ts,tsx}',
'**/*.config.{js,ts}',
],
},
],
import { fireEvent, render, screen } from '@testing-library/react';
import { BehaviorSubject } from 'rxjs';
import { LegendComponent } from '@components/Legend';
import { LegendModel } from '@core/Legend';
import type { ReactNode } from 'react';
jest.mock('exchange-elements/v2', () => ({
Button: ({ onClick, label }: { onClick?: () => void; label: ReactNode }) => (
<button
type="button"
onClick={onClick}
>
{label}
</button>
),
}));
jest.mock('@components/Icon', () => ({
GearIcon: () => <span>settings</span>,
TrashIcon: () => <span>remove</span>,
}));
jest.mock('@src/utils', () => ({
formatDisplayText: (value: unknown) => (value === null || value === undefined ? '' : String(value)),
useObservable: (observable: { getValue: () => unknown }) => observable.getValue(),
}));
function createMainSeriesModel(): LegendModel {
return [
{
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')).toBeTruthy();
expect(screen.getByText('100')).toBeTruthy();
expect(screen.getByText('High')).toBeTruthy();
expect(screen.getByText('110')).toBeTruthy();
expect(screen.getByText('Percentage')).toBeTruthy();
expect(screen.getByText('10%')).toBeTruthy();
expect(screen.queryByText('Absolute')).toBeNull();
});
it('должен скрывать относительное изменение при выключенном showPercentageChange', () => {
const viewModel = new BehaviorSubject(createMainSeriesModel());
render(
<LegendComponent
viewModel={viewModel}
ohlcConfig={{
show: true,
showPercentageChange: false,
}}
/>,
);
expect(screen.getByText('Open')).toBeTruthy();
expect(screen.queryByText('Percentage')).toBeNull();
expect(screen.queryByText('10%')).toBeNull();
});
it('должен отображать абсолютное изменение при включенном showAbsoluteChange', () => {
const viewModel = new BehaviorSubject(createMainSeriesModel());
render(
<LegendComponent
viewModel={viewModel}
ohlcConfig={{
show: true,
showAbsoluteChange: true,
}}
/>,
);
expect(screen.getByText('Absolute')).toBeTruthy();
expect(screen.getByText('10')).toBeTruthy();
});
it('должен отображать изменения независимо от OHLC', () => {
const viewModel = new BehaviorSubject(createMainSeriesModel());
render(
<LegendComponent
viewModel={viewModel}
ohlcConfig={{
show: false,
showAbsoluteChange: true,
showPercentageChange: true,
}}
/>,
);
expect(screen.queryByText('Open')).toBeNull();
expect(screen.queryByText('High')).toBeNull();
expect(screen.getByText('Absolute')).toBeTruthy();
expect(screen.getByText('10')).toBeTruthy();
expect(screen.getByText('Percentage')).toBeTruthy();
expect(screen.getByText('10%')).toBeTruthy();
});
it('должен скрывать оба изменения при выключенных флагах', () => {
const viewModel = new BehaviorSubject(createMainSeriesModel());
render(
<LegendComponent
viewModel={viewModel}
ohlcConfig={{
show: true,
showAbsoluteChange: false,
showPercentageChange: false,
}}
/>,
);
expect(screen.queryByText('Absolute')).toBeNull();
expect(screen.queryByText('Percentage')).toBeNull();
});
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')).toBeTruthy();
expect(screen.getByText('55')).toBeTruthy();
});
it('должен вызывать обработчики настроек и удаления', () => {
const settings = jest.fn();
const remove = jest.fn();
const model: LegendModel = [
{
id: 'ema',
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 { ChartTooltip } from '@components/ChartTooltip';
import { TooltipVM } from '@core/Tooltip';
import { Defaults } from '@src/types/defaults';
import type { Timeframes, TooltipConfig } from '@src/types';
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) => (value === null || value === undefined ? '' : String(value)),
}));
const formatObs = new BehaviorSubject({
dateFormat: Defaults.dateFormat,
timeFormat: Defaults.timeFormat,
});
const timeframeObs = new BehaviorSubject('1m' as Timeframes);
function createTooltipConfig(): TooltipConfig {
return {
high: {
visible: false,
label: 'High',
},
low: {
visible: false,
label: 'Low',
},
absoluteChange: {
visible: false,
label: 'Абсолютное изменение',
},
percentageChange: {
visible: false,
label: 'Относительное изменение',
},
};
}
function createViewModel(): BehaviorSubject<TooltipVM> {
return new BehaviorSubject({
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);
}
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('Абсолютное изменение')).toBeTruthy();
expect(screen.getByText('10')).toBeTruthy();
expect(screen.queryByText('Относительное изменение')).toBeNull();
expect(screen.queryByText('10%')).toBeNull();
});
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('Относительное изменение')).toBeTruthy();
expect(screen.getByText('10%')).toBeTruthy();
expect(screen.queryByText('Абсолютное изменение')).toBeNull();
expect(screen.queryByText('10')).toBeNull();
});
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')).toBeTruthy();
expect(screen.getByText('Percentage')).toBeTruthy();
expect(screen.getByText('10')).toBeTruthy();
expect(screen.getByText('10%')).toBeTruthy();
});
it('должен использовать подписи из конфигурации', () => {
const tooltipConfig = createTooltipConfig();
tooltipConfig.absoluteChange = {
visible: true,
label: 'Custom absolute',
};
tooltipConfig.percentageChange = {
visible: true,
label: 'Custom percentage',
};
render(
<ChartTooltip
formatObs={formatObs}
timeframeObs={timeframeObs}
tooltipConfig={tooltipConfig}
viewModel={createViewModel()}
ohlcConfig={{ show: true }}
/>,
);
expect(screen.getByText('Custom absolute')).toBeTruthy();
expect(screen.getByText('Custom percentage')).toBeTruthy();
});
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')).toBeNull();
});
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')).toBeTruthy();
expect(screen.getByText('55')).toBeTruthy();
});
});
import { MouseEventParams, Time } from 'lightweight-charts';
import { BehaviorSubject } from 'rxjs';
import { EventManager } from '@core/EventManager';
import { Indicator } from '@core/Indicator';
import { Legend, LegendModel, LegendParams } from '@core/Legend';
import { SeriesStrategies } from '@src/modules/series-strategies/SeriesFactory';
import type { OHLCConfig } from '@src/types';
describe('Legend', () => {
let crosshairMoveHandler: ((param: MouseEventParams) => void) | null;
let indicators: BehaviorSubject<Map<string, Indicator>>;
let mainSeriesSubject: BehaviorSubject<SeriesStrategies | null>;
let mainSeries: SeriesStrategies;
let eventManager: EventManager;
let getLegendData: jest.Mock;
let subscribeDataChanged: jest.Mock;
let unsubscribeDataChanged: jest.Mock;
let legend: Legend | null;
const config: OHLCConfig = {
show: true,
showAbsoluteChange: false,
showPercentageChange: true,
};
beforeEach(() => {
crosshairMoveHandler = null;
legend = null;
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();
mainSeries = {
getLegendData,
subscribeDataChanged,
unsubscribeDataChanged,
} as unknown as SeriesStrategies;
eventManager = {
symbolName: jest.fn().mockReturnValue(new BehaviorSubject('AAPL')),
} as unknown as EventManager;
indicators = new BehaviorSubject<Map<string, Indicator>>(new Map());
mainSeriesSubject = new BehaviorSubject<SeriesStrategies | null>(mainSeries);
});
afterEach(() => {
legend?.destroy();
});
function createLegend(): Legend {
const subscribeChartEvent = jest.fn((event, handler) => {
if (event === 'crosshairMove') {
crosshairMoveHandler = handler as (param: MouseEventParams) => void;
}
return () => {};
}) as unknown as LegendParams['subscribeChartEvent'];
legend = new Legend({
config,
eventManager,
indicators,
subscribeChartEvent,
mainSeries: mainSeriesSubject,
paneId: 1,
paneIndex: () => 0,
openIndicatorSettings: jest.fn(),
});
return legend;
}
function createCrosshairParam(paneIndex = 0): MouseEventParams {
return {
point: {
x: 10,
y: 20,
},
time: 100 as Time,
paneIndex,
seriesData: new Map([['main-series', {}]]),
} as unknown as MouseEventParams;
}
function getMainSeriesValues(model: LegendModel) {
return model[0].values as unknown as Map<
string,
{
value: unknown;
color: string;
name?: string;
}
>;
}
it('должен добавлять абсолютное и относительное изменение в модель основной серии', () => {
const currentLegend = createLegend();
let model: LegendModel = [];
currentLegend.getLegendViewModel().subscribe((value) => {
model = value;
});
const values = getMainSeriesValues(model);
expect(values.get('absoluteChange')?.value).toBe(10);
expect(values.get('percentageChange')?.value).toBe('10%');
});
it('должен обновлять абсолютное и относительное изменение данными под курсором', () => {
getLegendData.mockImplementation((param?: MouseEventParams) => {
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 currentLegend = createLegend();
crosshairMoveHandler?.(createCrosshairParam());
let model: LegendModel = [];
currentLegend.getLegendViewModel().subscribe((value) => {
model = value;
});
const values = getMainSeriesValues(model);
expect(values.get('absoluteChange')?.value).toBe(20);
expect(values.get('percentageChange')?.value).toBe('20%');
});
it('должен отображать тултип при наведении на текущий pane', () => {
const currentLegend = createLegend();
let visible = false;
let position = null;
currentLegend.subscribeCursorVisability((value) => {
visible = value;
});
currentLegend.subscribeCursorPosition((value) => {
position = value;
});
crosshairMoveHandler?.(createCrosshairParam());
expect(visible).toBe(true);
expect(position).toEqual({
x: 10,
y: 20,
});
});
it('должен скрывать тултип при наведении на другой pane', () => {
const currentLegend = createLegend();
let visible = true;
currentLegend.subscribeCursorVisability((value) => {
visible = value;
});
crosshairMoveHandler?.(createCrosshairParam(1));
expect(visible).toBe(false);
});
it('должен подписываться и отписываться от изменений основной серии', () => {
const currentLegend = createLegend();
expect(subscribeDataChanged).toHaveBeenCalledTimes(1);
currentLegend.destroy();
legend = null;
expect(unsubscribeDataChanged).toHaveBeenCalledTimes(1);
});
it('должен возвращать конфигурацию легенды', () => {
const currentLegend = createLegend();
expect(currentLegend.getConfig()).toBe(config);
});
});