import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { describe, expect, it, vi } from 'vitest'; import { RangeSlider } from './range-slider'; describe('RangeSlider', () => { it('renders one thumb per value', () => { render(); expect(screen.getAllByRole('slider')).toHaveLength(2); }); it('renders a single thumb by default', () => { render(); expect(screen.getAllByRole('slider')).toHaveLength(1); }); it('exposes the value range to assistive tech', () => { render(); const thumb = screen.getByRole('slider'); expect(thumb).toHaveAttribute('aria-valuenow', '40'); expect(thumb).toHaveAttribute('aria-valuemin', '10'); expect(thumb).toHaveAttribute('aria-valuemax', '90'); }); describe('accessible names', () => { it('uses thumbLabels in order', () => { render(); expect(screen.getByRole('slider', { name: 'Minimum' })).toBeInTheDocument(); expect(screen.getByRole('slider', { name: 'Maximum' })).toBeInTheDocument(); }); it('gives a lone thumb the slider’s own label', () => { render(); expect(screen.getByRole('slider', { name: 'Volume' })).toBeInTheDocument(); }); it('suffixes the position when several thumbs share one label', () => { render(); expect(screen.getByRole('slider', { name: 'Price 1' })).toBeInTheDocument(); expect(screen.getByRole('slider', { name: 'Price 2' })).toBeInTheDocument(); }); }); describe('keyboard', () => { it('steps the focused thumb with the arrow keys', async () => { const user = userEvent.setup(); const onValueChange = vi.fn(); render( ); await user.tab(); await user.keyboard('{ArrowRight}'); expect(onValueChange).toHaveBeenLastCalledWith([41]); await user.keyboard('{ArrowLeft}{ArrowLeft}'); expect(onValueChange).toHaveBeenLastCalledWith([39]); }); it('honours the step size', async () => { const user = userEvent.setup(); const onValueChange = vi.fn(); render( ); await user.tab(); await user.keyboard('{ArrowRight}'); expect(onValueChange).toHaveBeenLastCalledWith([75]); }); it('jumps to the bounds with Home and End', async () => { const user = userEvent.setup(); const onValueChange = vi.fn(); render( ); await user.tab(); await user.keyboard('{Home}'); expect(onValueChange).toHaveBeenLastCalledWith([0]); await user.keyboard('{End}'); expect(onValueChange).toHaveBeenLastCalledWith([100]); }); it('moves only the focused thumb of a range', async () => { const user = userEvent.setup(); const onValueChange = vi.fn(); render( ); /* Second tab lands on the upper thumb; the lower one must not shift. */ await user.tab(); await user.tab(); await user.keyboard('{ArrowRight}'); expect(onValueChange).toHaveBeenLastCalledWith([20, 61]); }); }); it('stays put when disabled', async () => { const user = userEvent.setup(); const onValueChange = vi.fn(); render( ); await user.tab(); await user.keyboard('{ArrowRight}'); expect(onValueChange).not.toHaveBeenCalled(); }); it('holds a controlled value until the caller updates it', async () => { const user = userEvent.setup(); const onValueChange = vi.fn(); render(); await user.tab(); await user.keyboard('{ArrowRight}'); expect(onValueChange).toHaveBeenCalledWith([41]); expect(screen.getByRole('slider')).toHaveAttribute('aria-valuenow', '40'); }); describe('marks', () => { it('renders the label, falling back to the value', () => { render( ); expect(screen.getByText('Low')).toBeInTheDocument(); expect(screen.getByText('100')).toBeInTheDocument(); }); it('positions each mark by its share of the scale', () => { render( ); expect(screen.getByText('Quarter')).toHaveStyle({ left: '25%' }); }); it('keeps marks out of the accessibility tree', () => { const { container } = render( ); expect(container.querySelector('[data-slot="range-slider-marks"]')).toHaveAttribute( 'aria-hidden' ); }); }); });