import { describe, test, expect, beforeEach, vi } from 'vitest'
import { render, screen, fireEvent } from '@testing-library/react'
import { RangeUI } from './range-ui'
import { useWidgetStore } from '../stores/widget-store'
import type { RangeWidgetState } from './types'
describe('RangeUI', () => {
beforeEach(() => {
useWidgetStore.getState().clearWidgets()
})
test('returns null when widget does not exist', () => {
const { container } = render()
expect(container.firstChild).toBeNull()
})
test('renders with single range item', () => {
useWidgetStore.getState().setWidget('test-range', {
type: 'range',
data: [{ min: 0, max: 100, value: [20, 80] }],
})
render()
const inputs = screen.getAllByRole('textbox')
expect(inputs).toHaveLength(2)
expect((inputs[0] as HTMLInputElement).value).toBe('20')
expect((inputs[1] as HTMLInputElement).value).toBe('80')
})
test('renders with multiple range items', () => {
useWidgetStore.getState().setWidget('test-range', {
type: 'range',
data: [
{ min: 0, max: 100, value: [20, 80] },
{ min: 0, max: 50, value: [10, 40] },
],
})
render()
const inputs = screen.getAllByRole('textbox')
expect(inputs).toHaveLength(4)
expect((inputs[0] as HTMLInputElement).value).toBe('20')
expect((inputs[1] as HTMLInputElement).value).toBe('80')
expect((inputs[2] as HTMLInputElement).value).toBe('10')
expect((inputs[3] as HTMLInputElement).value).toBe('40')
})
test('uses formatter when provided', () => {
useWidgetStore.getState().setWidget('test-range', {
type: 'range',
data: [{ min: 0, max: 100, value: [20, 80] }],
formatter: (val: number) => `$${val}`,
})
render()
const inputs = screen.getAllByRole('textbox')
expect((inputs[0] as HTMLInputElement).value).toBe('$20')
expect((inputs[1] as HTMLInputElement).value).toBe('$80')
})
test('handles onChange callback', () => {
const onChange = vi.fn()
useWidgetStore.getState().setWidget('test-range', {
type: 'range',
data: [{ min: 0, max: 100, value: [20, 80] }],
onChange,
})
render()
const minInput = screen.getAllByRole('textbox')[0]
fireEvent.change(minInput!, { target: { value: '30' } })
fireEvent.blur(minInput!)
expect(onChange).toHaveBeenCalledWith([30, 80], 0)
})
test('renders disabled state', () => {
useWidgetStore.getState().setWidget('test-range', {
type: 'range',
data: [{ min: 0, max: 100, disabled: true }],
})
render()
const inputs = screen.getAllByRole('textbox')
expect((inputs[0] as HTMLInputElement).disabled).toBe(true)
expect((inputs[1] as HTMLInputElement).disabled).toBe(true)
})
test('handles empty data array', () => {
useWidgetStore.getState().setWidget('test-range', {
type: 'range',
data: [],
})
const { container } = render()
expect(container.firstChild).toBeNull()
})
test('handles undefined data', () => {
useWidgetStore.getState().setWidget('test-range', {
type: 'range',
})
const { container } = render()
expect(container.firstChild).toBeNull()
})
test('uses default min/max when value is not provided', () => {
useWidgetStore.getState().setWidget('test-range', {
type: 'range',
data: [{ min: 10, max: 90 }],
})
render()
const inputs = screen.getAllByRole('textbox')
expect((inputs[0] as HTMLInputElement).value).toBe('10')
expect((inputs[1] as HTMLInputElement).value).toBe('90')
})
test('validates min input on blur', () => {
const onChange = vi.fn()
useWidgetStore.getState().setWidget('test-range', {
type: 'range',
data: [{ min: 0, max: 100, value: [20, 80] }],
onChange,
})
render()
const minInput = screen.getAllByRole('textbox')[0]
// Try to set min higher than max
fireEvent.change(minInput!, { target: { value: '90' } })
fireEvent.blur(minInput!)
// Should clamp to max value
expect(onChange).toHaveBeenCalledWith([80, 80], 0)
})
test('validates max input on blur', () => {
const onChange = vi.fn()
useWidgetStore.getState().setWidget('test-range', {
type: 'range',
data: [{ min: 0, max: 100, value: [20, 80] }],
onChange,
})
render()
const maxInput = screen.getAllByRole('textbox')[1]
// Try to set max lower than min
fireEvent.change(maxInput!, { target: { value: '10' } })
fireEvent.blur(maxInput!)
// Should clamp to min value
expect(onChange).toHaveBeenCalledWith([20, 20], 0)
})
test('updates when widget data changes', () => {
useWidgetStore.getState().setWidget('test-range', {
type: 'range',
data: [{ min: 0, max: 100, value: [20, 80] }],
})
const { rerender } = render()
let inputs = screen.getAllByRole('textbox')
expect((inputs[0] as HTMLInputElement).value).toBe('20')
expect((inputs[1] as HTMLInputElement).value).toBe('80')
useWidgetStore.getState().setWidget('test-range', {
data: [{ min: 0, max: 100, value: [30, 70] }],
})
rerender()
inputs = screen.getAllByRole('textbox')
expect((inputs[0] as HTMLInputElement).value).toBe('30')
expect((inputs[1] as HTMLInputElement).value).toBe('70')
})
test('handles widget removal gracefully', () => {
useWidgetStore.getState().setWidget('test-range', {
type: 'range',
data: [{ min: 0, max: 100 }],
})
const { container, rerender } = render()
expect(container.firstChild).toBeTruthy()
useWidgetStore.getState().removeWidget('test-range')
rerender()
expect(container.firstChild).toBeNull()
})
})