Загрузка данных
import { act, fireEvent, render, screen } from '@testing-library/react';
import { PriceScaleMode } from 'lightweight-charts';
import { BehaviorSubject, Subject } from 'rxjs';
import { SeasonalityControls } from '@src/components/SeasonalityControls';
import { createInitialSeasonalityState } from '@src/constants';
import { SeasonalityViewType } from '@src/types';
import type { SeasonalityState } from '@src/types';
import type { ReactNode } from 'react';
interface RangeFieldTestProps {
value: [number, number];
min: number;
max: number;
marks: number[];
disabled: boolean;
onRangeChange: (value: [number, number]) => void;
}
jest.mock('@src/components/Button', () => ({
Button: ({
onClick,
label,
disabled,
pressed,
}: {
onClick?: () => void;
label?: ReactNode;
disabled?: boolean;
pressed?: boolean;
}) => (
<button
type="button"
onClick={onClick}
disabled={disabled}
data-pressed={pressed}
>
{label}
</button>
),
}));
jest.mock('@src/components/Icon', () => ({
LineIcon: () => <span data-testid="line-icon" />,
TableIcon: () => <span data-testid="table-icon" />,
}));
jest.mock('@src/components/Dropdown', () => ({
Dropdown: ({ children, selectedValue }: { children: ReactNode; selectedValue: ReactNode }) => (
<div>
<div data-testid="selected-scale">{selectedValue}</div>
{children}
</div>
),
}));
jest.mock('@src/components/Menu/SeasonalityScaleMenu', () => ({
SeasonalityScaleMenu: ({ onClick }: { onClick: (scale: number) => void }) => (
<>
<button
type="button"
onClick={() => onClick(0)}
>
Set price
</button>
<button
type="button"
onClick={() => onClick(2)}
>
Set percent
</button>
</>
),
}));
jest.mock('@src/components/FormFields', () => ({
RangeField: ({ value, min, max, marks, disabled, onRangeChange }: RangeFieldTestProps) => (
<div
data-testid="year-range"
data-marks={marks.join(',')}
>
<input
aria-label="From year"
type="number"
value={value[0]}
min={min}
max={max}
disabled={disabled}
onChange={(event) => onRangeChange([Number(event.target.value), value[1]])}
/>
<input
aria-label="To year"
type="number"
value={value[1]}
min={min}
max={max}
disabled={disabled}
onChange={(event) => onRangeChange([value[0], Number(event.target.value)])}
/>
</div>
),
}));
jest.mock('@src/translations', () => ({
t: (key: string) => key,
}));
function createTestContext(overrides: Partial<SeasonalityState> = {}) {
const stateObs = new BehaviorSubject<SeasonalityState>({
...createInitialSeasonalityState(),
status: 'ready',
minYear: 2020,
maxYear: 2025,
fromYear: 2021,
toYear: 2025,
...overrides,
});
const setDisplayMode = jest.fn();
const setScale = jest.fn();
const setRange = jest.fn();
return { stateObs, setDisplayMode, setScale, setRange };
}
describe('SeasonalityControls', () => {
it('не должен отображаться до получения состояния и при idle', () => {
const stateObs = new Subject<SeasonalityState>();
const { container } = render(
<SeasonalityControls
stateObs={stateObs}
setDisplayMode={jest.fn()}
setScale={jest.fn()}
setRange={jest.fn()}
/>,
);
expect(container.children).toHaveLength(0);
act(() => stateObs.next(createInitialSeasonalityState()));
expect(container.children).toHaveLength(0);
});
it('должен отображать выбранный диапазон и границы доступных лет', () => {
const props = createTestContext();
render(<SeasonalityControls {...props} />);
const from = screen.getByLabelText('From year') as HTMLInputElement;
const to = screen.getByLabelText('To year') as HTMLInputElement;
expect(from.value).toBe('2021');
expect(to.value).toBe('2025');
expect(from.min).toBe('2020');
expect(from.max).toBe('2025');
expect(screen.getByTestId('selected-scale').textContent).toBe('Price');
});
it('должен переключать вид и обновлять выбранную кнопку из состояния', () => {
const props = createTestContext();
render(<SeasonalityControls {...props} />);
const chartButton = screen.getByTestId('line-icon').closest('button') as HTMLButtonElement;
const tableButton = screen.getByTestId('table-icon').closest('button') as HTMLButtonElement;
expect(chartButton.getAttribute('data-pressed')).toBe('true');
expect(tableButton.getAttribute('data-pressed')).toBe('false');
fireEvent.click(tableButton);
expect(props.setDisplayMode).toHaveBeenNthCalledWith(1, SeasonalityViewType.Table);
expect(chartButton.getAttribute('data-pressed')).toBe('true');
act(() =>
props.stateObs.next({
...props.stateObs.value,
displayMode: SeasonalityViewType.Table,
}),
);
expect(chartButton.getAttribute('data-pressed')).toBe('false');
expect(tableButton.getAttribute('data-pressed')).toBe('true');
fireEvent.click(chartButton);
expect(props.setDisplayMode).toHaveBeenNthCalledWith(2, SeasonalityViewType.Chart);
act(() =>
props.stateObs.next({
...props.stateObs.value,
displayMode: SeasonalityViewType.Chart,
}),
);
expect(chartButton.getAttribute('data-pressed')).toBe('true');
expect(tableButton.getAttribute('data-pressed')).toBe('false');
});
it.each(['loading', 'error', 'empty'] as const)('должен позволять переключить вид при статусе %s', (status) => {
const props = createTestContext({ status });
render(<SeasonalityControls {...props} />);
fireEvent.click(screen.getByTestId('table-icon'));
expect(props.setDisplayMode).toHaveBeenCalledWith(SeasonalityViewType.Table);
});
it('должен передавать изменение обеих границ диапазона', () => {
const props = createTestContext();
render(<SeasonalityControls {...props} />);
fireEvent.change(screen.getByLabelText('From year'), {
target: { value: '2022' },
});
fireEvent.change(screen.getByLabelText('To year'), {
target: { value: '2024' },
});
expect(props.setRange).toHaveBeenNthCalledWith(1, {
fromYear: 2022,
toYear: 2025,
});
expect(props.setRange).toHaveBeenNthCalledWith(2, {
fromYear: 2021,
toYear: 2024,
});
});
it('должен переключать шкалу и обновлять подпись из состояния', () => {
const props = createTestContext();
render(<SeasonalityControls {...props} />);
fireEvent.click(screen.getByText('Set percent'));
fireEvent.click(screen.getByText('Set price'));
expect(props.setScale).toHaveBeenNthCalledWith(1, PriceScaleMode.Percentage);
expect(props.setScale).toHaveBeenNthCalledWith(2, PriceScaleMode.Normal);
act(() =>
props.stateObs.next({
...props.stateObs.value,
scale: PriceScaleMode.Percentage,
}),
);
expect(screen.getByTestId('selected-scale').textContent).toBe('Percent');
});
it.each(['loading', 'error'] as const)('должен блокировать диапазон при статусе %s', (status) => {
const props = createTestContext({ status });
render(<SeasonalityControls {...props} />);
expect((screen.getByLabelText('From year') as HTMLInputElement).disabled).toBe(true);
expect((screen.getByLabelText('To year') as HTMLInputElement).disabled).toBe(true);
act(() =>
props.stateObs.next({
...props.stateObs.value,
status: 'ready',
}),
);
expect((screen.getByLabelText('From year') as HTMLInputElement).disabled).toBe(false);
});
it('должен позволять изменить пустой диапазон и скрываться после очистки', () => {
const props = createTestContext({ status: 'empty' });
const { container } = render(<SeasonalityControls {...props} />);
fireEvent.change(screen.getByLabelText('From year'), {
target: { value: '2020' },
});
expect(props.setRange).toHaveBeenCalledWith({
fromYear: 2020,
toYear: 2025,
});
act(() =>
props.stateObs.next({
...props.stateObs.value,
status: 'idle',
}),
);
expect(container.children).toHaveLength(0);
});
it('должен корректно отображать единственный доступный год', () => {
const props = createTestContext({
minYear: 2025,
maxYear: 2025,
fromYear: 2025,
toYear: 2025,
});
render(<SeasonalityControls {...props} />);
expect(screen.getByTestId('year-range').getAttribute('data-marks')).toBe('2025');
expect((screen.getByLabelText('From year') as HTMLInputElement).value).toBe('2025');
expect((screen.getByLabelText('To year') as HTMLInputElement).value).toBe('2025');
});
});