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'); }); });