Files
frontend-svelte/src/shared/ui/Slider/Slider.svelte.test.ts
T
Ilia Mashkov ae2d0e3c2f
Workflow / build (pull_request) Successful in 1m33s
Workflow / e2e (pull_request) Successful in 1m21s
Workflow / publish (pull_request) Has been skipped
fix(slider): focus thumb on pointerdown for keyboard parity
2026-06-02 11:14:10 +03:00

170 lines
7.3 KiB
TypeScript

import {
fireEvent,
render,
screen,
} from '@testing-library/svelte';
import Slider from './Slider.svelte';
describe('Slider', () => {
describe('Rendering', () => {
it('renders a slider element', () => {
render(Slider);
expect(screen.getByRole('slider')).toBeInTheDocument();
});
it('displays formatted value', () => {
render(Slider, { value: 50 });
expect(screen.getByText('50')).toBeInTheDocument();
});
it('applies a custom formatter', () => {
const { container } = render(Slider, { value: 25, format: (v: number) => `${v}%` });
expect(container.textContent).toContain('25%');
});
});
describe('Props', () => {
it('respects min and max attributes', () => {
render(Slider, { min: 10, max: 90, value: 50 });
const slider = screen.getByRole('slider');
expect(slider).toHaveAttribute('aria-valuemin', '10');
expect(slider).toHaveAttribute('aria-valuemax', '90');
});
it('reflects value as aria-valuenow', () => {
render(Slider, { value: 42 });
expect(screen.getByRole('slider')).toHaveAttribute('aria-valuenow', '42');
});
it('is disabled when disabled=true', () => {
render(Slider, { disabled: true });
expect(screen.getByRole('slider')).toHaveAttribute('aria-disabled', 'true');
});
it('is not disabled by default', () => {
render(Slider, { value: 0 });
expect(screen.getByRole('slider')).not.toHaveAttribute('aria-disabled', 'true');
});
});
describe('Orientations', () => {
it('renders horizontal by default', () => {
const { container } = render(Slider);
expect(screen.getByRole('slider')).toHaveAttribute('aria-orientation', 'horizontal');
expect(container.querySelector('.cursor-col-resize')).toBeInTheDocument();
});
it('renders vertical when orientation="vertical"', () => {
const { container } = render(Slider, { orientation: 'vertical' });
expect(screen.getByRole('slider')).toHaveAttribute('aria-orientation', 'vertical');
expect(container.querySelector('.cursor-row-resize')).toBeInTheDocument();
});
});
});
describe('Keyboard', () => {
it('increments by step on ArrowRight / ArrowUp', async () => {
const onValueChange = vi.fn();
render(Slider, { value: 50, step: 5, onValueChange });
const thumb = screen.getByRole('slider');
await fireEvent.keyDown(thumb, { key: 'ArrowRight' });
expect(thumb).toHaveAttribute('aria-valuenow', '55');
expect(onValueChange).toHaveBeenCalledWith(55);
});
it('decrements by step on ArrowLeft / ArrowDown', async () => {
render(Slider, { value: 50, step: 5 });
const thumb = screen.getByRole('slider');
await fireEvent.keyDown(thumb, { key: 'ArrowDown' });
expect(thumb).toHaveAttribute('aria-valuenow', '45');
});
it('jumps to min on Home and max on End', async () => {
render(Slider, { value: 50, min: 10, max: 90 });
const thumb = screen.getByRole('slider');
await fireEvent.keyDown(thumb, { key: 'Home' });
expect(thumb).toHaveAttribute('aria-valuenow', '10');
await fireEvent.keyDown(thumb, { key: 'End' });
expect(thumb).toHaveAttribute('aria-valuenow', '90');
});
it('moves by step*10 on PageUp / PageDown', async () => {
render(Slider, { value: 50, step: 2 });
const thumb = screen.getByRole('slider');
await fireEvent.keyDown(thumb, { key: 'PageUp' });
expect(thumb).toHaveAttribute('aria-valuenow', '70');
await fireEvent.keyDown(thumb, { key: 'PageDown' });
expect(thumb).toHaveAttribute('aria-valuenow', '50');
});
it('clamps at the bounds', async () => {
render(Slider, { value: 98, max: 100, step: 5 });
const thumb = screen.getByRole('slider');
await fireEvent.keyDown(thumb, { key: 'End' });
expect(thumb).toHaveAttribute('aria-valuenow', '100');
});
it('does nothing when disabled', async () => {
const onValueChange = vi.fn();
render(Slider, { value: 50, disabled: true, onValueChange });
const thumb = screen.getByRole('slider');
await fireEvent.keyDown(thumb, { key: 'ArrowRight' });
expect(thumb).toHaveAttribute('aria-valuenow', '50');
expect(onValueChange).not.toHaveBeenCalled();
});
});
describe('Pointer', () => {
/**
* Force a deterministic track rect since jsdom has no layout.
*/
function mockTrackRect(container: HTMLElement) {
const track = container.querySelector('[role="presentation"]') as HTMLElement;
track.getBoundingClientRect = () =>
({ left: 0, right: 200, top: 0, bottom: 20, width: 200, height: 20 }) as DOMRect;
return track;
}
it('seeks to the clicked position (click-to-seek)', async () => {
const onValueChange = vi.fn();
const { container } = render(Slider, { value: 0, min: 0, max: 100, onValueChange });
const track = mockTrackRect(container);
await fireEvent.pointerDown(track, { clientX: 100, clientY: 10, pointerId: 1 });
expect(screen.getByRole('slider')).toHaveAttribute('aria-valuenow', '50');
expect(onValueChange).toHaveBeenCalledWith(50);
});
it('updates while dragging after pointerdown', async () => {
const { container } = render(Slider, { value: 0, min: 0, max: 100 });
const track = mockTrackRect(container);
await fireEvent.pointerDown(track, { clientX: 50, clientY: 10, pointerId: 1 });
await fireEvent.pointerMove(track, { clientX: 150, clientY: 10, pointerId: 1 });
expect(screen.getByRole('slider')).toHaveAttribute('aria-valuenow', '75');
});
it('ignores pointer when disabled', async () => {
const { container } = render(Slider, { value: 0, disabled: true });
const track = mockTrackRect(container);
await fireEvent.pointerDown(track, { clientX: 100, clientY: 10, pointerId: 1 });
expect(screen.getByRole('slider')).toHaveAttribute('aria-valuenow', '0');
});
it('focuses the thumb on pointerdown so arrow keys work immediately', async () => {
const { container } = render(Slider, { value: 0, min: 0, max: 100 });
const track = container.querySelector('[role="presentation"]') as HTMLElement;
track.getBoundingClientRect = () =>
({ left: 0, right: 200, top: 0, bottom: 20, width: 200, height: 20 }) as DOMRect;
await fireEvent.pointerDown(track, { clientX: 100, clientY: 10, pointerId: 1 });
expect(screen.getByRole('slider')).toBe(document.activeElement);
});
it('maps a vertical drag with the inverted axis (bottom→min, top→max)', async () => {
const { container } = render(Slider, { value: 0, min: 0, max: 100, orientation: 'vertical' });
const track = container.querySelector('[role="presentation"]') as HTMLElement;
track.getBoundingClientRect = () =>
({ left: 0, right: 20, top: 0, bottom: 200, width: 20, height: 200 }) as DOMRect;
await fireEvent.pointerDown(track, { clientX: 10, clientY: 50, pointerId: 1 });
expect(screen.getByRole('slider')).toHaveAttribute('aria-valuenow', '75');
});
});