import { describe, it, expect, vi } from 'vitest' import { fireEvent, render, screen } from '@testing-library/react' import { RangeUI } from './range-ui' describe('', () => { it('renders a two-thumb slider per item', () => { render() const sliders = screen.getAllByRole('slider') expect(sliders).toHaveLength(2) expect(sliders[0]?.getAttribute('aria-valuenow')).toBe('20') expect(sliders[1]?.getAttribute('aria-valuenow')).toBe('80') }) it('defaults the value to [min, max] when omitted', () => { render() const sliders = screen.getAllByRole('slider') expect(sliders[0]?.getAttribute('aria-valuenow')).toBe('10') expect(sliders[1]?.getAttribute('aria-valuenow')).toBe('90') }) it('renders editable min/max text inputs below the slider', () => { render() const min = screen.getByLabelText('Minimum value') const max = screen.getByLabelText('Maximum value') expect(min).toBeTruthy() expect(max).toBeTruthy() // The displayed value passes through the formatter (default toString). expect(min.value).toBe('20') expect(max.value).toBe('80') }) it('runs the formatter through the text inputs', () => { render( `$${n}`} />, ) const min = screen.getByLabelText('Minimum value') const max = screen.getByLabelText('Maximum value') expect(min.value).toBe('$100') expect(max.value).toBe('$900') }) it('renders a caption below the inputs when `note` is provided', () => { render( , ) expect(screen.getByText('Filter window')).toBeTruthy() }) it('renders multiple sliders stacked', () => { render( , ) expect(screen.getByText('A')).toBeTruthy() expect(screen.getByText('B')).toBeTruthy() expect(screen.getByText('C')).toBeTruthy() // Three two-thumb sliders → 6 thumbs total. expect(screen.getAllByRole('slider')).toHaveLength(6) }) it('fires onChange with the item index and a clamped [low, high] tuple', () => { const onChange = vi.fn() render( , ) // Drag the second item's lower thumb to 42. const sliders = screen.getAllByRole('slider') fireEvent.change(sliders[2]!, { target: { value: '42' } }) expect(onChange).toHaveBeenCalled() const call = onChange.mock.calls[0] as [number, readonly [number, number]] expect(call[0]).toBe(1) expect(Array.isArray(call[1])).toBe(true) expect(call[1]).toHaveLength(2) }) it('respects per-item disabled on both slider and inputs', () => { render( , ) const sliders = screen.getAllByRole('slider') expect(sliders[0]?.hasAttribute('disabled')).toBe(true) expect(sliders[1]?.hasAttribute('disabled')).toBe(true) expect( screen.getByLabelText('Minimum value').hasAttribute('disabled'), ).toBe(true) expect( screen.getByLabelText('Maximum value').hasAttribute('disabled'), ).toBe(true) }) it('commits a typed value on Enter (fires both onChange and onChangeCommitted), clamped to [min, max]', () => { // A text-input edit is both a value change and a final commit, so it // notifies `onChange` AND `onChangeCommitted` — consumers that only wired // `onChange` (the pre-`onChangeCommitted` API) still update. const onChange = vi.fn() const onChangeCommitted = vi.fn() render( , ) const min = screen.getByLabelText('Minimum value') // Type 150 — should clamp to max (100), then preserve ordering (low <= high). fireEvent.change(min, { target: { value: '150' } }) fireEvent.keyDown(min, { key: 'Enter' }) expect(onChangeCommitted).toHaveBeenCalled() expect(onChange).toHaveBeenCalled() const last = onChangeCommitted.mock.calls.at(-1) as [ number, readonly [number, number], ] expect(last[0]).toBe(0) expect(last[1][0]).toBeLessThanOrEqual(last[1][1]) expect(last[1][1]).toBeLessThanOrEqual(100) }) })