import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { describe, it, expect, vi } from 'vitest'
import { axe } from 'vitest-axe'
import { Slider } from './slider'
describe('Slider', () => {
it('renders a single thumb when no value is provided', () => {
render()
const thumbs = screen.getAllByRole('slider')
expect(thumbs).toHaveLength(1)
expect(thumbs[0]).toHaveAttribute('aria-label', 'Volume')
})
it('reflects defaultValue for a single thumb', () => {
render()
expect(screen.getByRole('slider')).toHaveAttribute('aria-valuenow', '42')
})
it('renders one thumb per value in a range slider', () => {
render( (i === 0 ? 'Min' : 'Max')} />)
const thumbs = screen.getAllByRole('slider')
expect(thumbs).toHaveLength(2)
expect(thumbs[0]).toHaveAttribute('aria-valuenow', '20')
expect(thumbs[1]).toHaveAttribute('aria-valuenow', '80')
expect(thumbs[0]).toHaveAttribute('aria-label', 'Min')
expect(thumbs[1]).toHaveAttribute('aria-label', 'Max')
})
it('forwards min, max, step to the thumb input', () => {
render()
const thumb = screen.getByRole('slider')
expect(thumb).toHaveAttribute('min', '0')
expect(thumb).toHaveAttribute('max', '20')
expect(thumb).toHaveAttribute('step', '2')
})
it('fires onValueChange with the new value on ArrowRight', async () => {
const user = userEvent.setup()
const onValueChange = vi.fn()
render()
const thumb = screen.getByRole('slider')
thumb.focus()
await user.keyboard('{ArrowRight}')
expect(onValueChange).toHaveBeenCalled()
expect(onValueChange.mock.calls[0]![0]).toBe(11)
})
it('does not fire onValueChange when disabled', async () => {
const user = userEvent.setup()
const onValueChange = vi.fn()
render()
const thumb = screen.getByRole('slider')
thumb.focus()
await user.keyboard('{ArrowRight}')
expect(onValueChange).not.toHaveBeenCalled()
})
it('respects controlled value', async () => {
const user = userEvent.setup()
const onValueChange = vi.fn()
render()
const thumb = screen.getByRole('slider')
thumb.focus()
await user.keyboard('{ArrowRight}')
expect(onValueChange).toHaveBeenCalled()
// Parent never updates - value stays at 30.
expect(thumb).toHaveAttribute('aria-valuenow', '30')
})
it('sets data-orientation on the root for vertical sliders', () => {
const { container } = render()
const root = container.querySelector('[data-slot="slider"]')
expect(root).toHaveAttribute('data-orientation', 'vertical')
})
it('does not render a tooltip when tooltipVisibility="never"', () => {
const { container } = render()
expect(container.querySelector('[data-slot="slider-tooltip"]')).toBeNull()
})
it('renders a tooltip per thumb when tooltipVisibility="always"', () => {
const { container } = render(
(i === 0 ? 'Min' : 'Max')} />,
)
const tooltips = container.querySelectorAll('[data-slot="slider-tooltip"]')
expect(tooltips).toHaveLength(2)
expect(tooltips[0]?.textContent).toBe('10')
expect(tooltips[1]?.textContent).toBe('90')
})
it('shows the tooltip on hover when tooltipVisibility="auto" (default)', async () => {
const user = userEvent.setup()
const { container } = render()
expect(container.querySelector('[data-slot="slider-tooltip"]')).toBeNull()
const thumbWrapper = container.querySelector('[data-slot="slider-thumb"]') as HTMLElement
expect(thumbWrapper).not.toBeNull()
await user.hover(thumbWrapper)
expect(container.querySelector('[data-slot="slider-tooltip"]')).not.toBeNull()
})
it('forwards className to the root', () => {
const { container } = render()
expect(container.querySelector('[data-slot="slider"]')?.className).toContain('custom-slider')
})
it('has no accessibility violations (single thumb)', async () => {
const { container } = render()
expect(await axe(container)).toHaveNoViolations()
})
it('has no accessibility violations (range)', async () => {
const { container } = render( (i === 0 ? 'Min' : 'Max')} />)
expect(await axe(container)).toHaveNoViolations()
})
})