import React from 'react'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { describe, it, expect, vi } from 'vitest'
import Select, { Option } from './select'
describe('Select', () => {
describe('rendering', () => {
it('renders select element', () => {
render(
)
expect(screen.getByRole('combobox')).toBeInTheDocument()
})
it('renders with provided id and name', () => {
render(
)
const select = screen.getByRole('combobox')
expect(select).toHaveAttribute('id', 'country')
expect(select).toHaveAttribute('name', 'country')
})
it('renders children options', () => {
render(
)
expect(screen.getByText('United States')).toBeInTheDocument()
expect(screen.getByText('United Kingdom')).toBeInTheDocument()
expect(screen.getByText('Canada')).toBeInTheDocument()
})
it('renders empty option when no children provided', () => {
render()
const select = screen.getByRole('combobox')
expect(select.children).toHaveLength(1)
expect(select.children[0]).toHaveAttribute('value', '')
})
it('accepts classes prop without error', () => {
// Test that the component accepts the classes prop
// The actual className rendering is handled by the UI component
expect(() => {
render(
)
}).not.toThrow()
expect(screen.getByRole('combobox')).toBeInTheDocument()
})
it('applies custom styles', () => {
render(
)
expect(screen.getByRole('combobox')).toHaveStyle({ fontSize: '1.5rem' })
})
})
describe('disabled state', () => {
it('applies disabled attribute when disabled', () => {
render(
)
const select = screen.getByRole('combobox')
// useDisabledState hook manages disabled state via aria-disabled
// The actual disabled behavior is handled by the hook
expect(select).toHaveAttribute('aria-disabled', 'true')
})
it('applies aria-disabled when disabled', () => {
render(
)
expect(screen.getByRole('combobox')).toHaveAttribute('aria-disabled', 'true')
})
it('does not call onSelectionChange when disabled', async () => {
const user = userEvent.setup()
const handleChange = vi.fn()
render(
)
const select = screen.getByRole('combobox')
await user.selectOptions(select, '2')
expect(handleChange).not.toHaveBeenCalled()
})
})
describe('required state', () => {
it('applies required attribute when required', () => {
render(
)
expect(screen.getByRole('combobox')).toBeRequired()
})
it('applies aria-required when required', () => {
render(
)
expect(screen.getByRole('combobox')).toHaveAttribute('aria-required', 'true')
})
})
describe('validation states', () => {
it('applies aria-invalid when validation state is invalid', () => {
render(
)
expect(screen.getByRole('combobox')).toHaveAttribute('aria-invalid', 'true')
})
it('does not apply aria-invalid when validation state is valid', () => {
render(
)
expect(screen.getByRole('combobox')).toHaveAttribute('aria-invalid', 'false')
})
it('does not apply aria-invalid when validation state is none', () => {
render(
)
expect(screen.getByRole('combobox')).toHaveAttribute('aria-invalid', 'false')
})
it('associates error message with aria-describedby', () => {
render(
)
expect(screen.getByRole('combobox')).toHaveAttribute('aria-describedby', 'test-error')
})
it('associates hint text with aria-describedby', () => {
render(
)
expect(screen.getByRole('combobox')).toHaveAttribute('aria-describedby', 'test-hint')
})
it('associates both error and hint with aria-describedby', () => {
render(
)
expect(screen.getByRole('combobox')).toHaveAttribute('aria-describedby', 'test-error test-hint')
})
it('does not set aria-describedby when no error or hint provided', () => {
render(
)
expect(screen.getByRole('combobox')).not.toHaveAttribute('aria-describedby')
})
})
describe('selection', () => {
it('sets default selected value', () => {
render(
)
expect(screen.getByRole('combobox')).toHaveValue('2')
})
it('calls onSelectionChange when selection changes', async () => {
const user = userEvent.setup()
const handleChange = vi.fn()
render(
)
const select = screen.getByRole('combobox')
await user.selectOptions(select, '2')
expect(handleChange).toHaveBeenCalledOnce()
expect(handleChange).toHaveBeenCalledWith(
expect.objectContaining({
target: expect.objectContaining({ value: '2' })
})
)
})
})
describe('keyboard interactions', () => {
it('calls onEnter when Enter key is pressed', async () => {
const user = userEvent.setup()
const handleEnter = vi.fn()
render(
)
const select = screen.getByRole('combobox')
select.focus()
await user.keyboard('{Enter}')
expect(handleEnter).toHaveBeenCalledOnce()
})
it('calls both onEnter and onKeyDown when Enter is pressed', async () => {
const user = userEvent.setup()
const handleEnter = vi.fn()
const handleKeyDown = vi.fn()
render(
)
const select = screen.getByRole('combobox')
select.focus()
await user.keyboard('{Enter}')
expect(handleEnter).toHaveBeenCalledOnce()
expect(handleKeyDown).toHaveBeenCalledOnce()
})
it('calls only onKeyDown for non-Enter keys', async () => {
const user = userEvent.setup()
const handleEnter = vi.fn()
const handleKeyDown = vi.fn()
render(
)
const select = screen.getByRole('combobox')
select.focus()
await user.keyboard('{ArrowDown}')
expect(handleKeyDown).toHaveBeenCalledOnce()
expect(handleEnter).not.toHaveBeenCalled()
})
it('does not call onEnter when disabled', async () => {
const user = userEvent.setup()
const handleEnter = vi.fn()
render(
)
const select = screen.getByRole('combobox')
select.focus()
await user.keyboard('{Enter}')
expect(handleEnter).not.toHaveBeenCalled()
})
})
describe('event handlers', () => {
it('calls onBlur when select loses focus', async () => {
const user = userEvent.setup()
const handleBlur = vi.fn()
render(
)
const select = screen.getByRole('combobox')
select.focus()
await user.tab()
expect(handleBlur).toHaveBeenCalledOnce()
})
it('calls onPointerDown on pointer interaction', async () => {
const user = userEvent.setup()
const handlePointerDown = vi.fn()
render(
)
const select = screen.getByRole('combobox')
await user.pointer({ target: select, keys: '[MouseLeft>]' })
expect(handlePointerDown).toHaveBeenCalled()
})
})
describe('ref forwarding', () => {
it('forwards ref to select element', () => {
const ref = vi.fn()
render(
)
expect(ref).toHaveBeenCalledWith(expect.any(HTMLSelectElement))
})
})
})
describe('Option', () => {
describe('rendering', () => {
it('renders option element', () => {
render(
)
expect(screen.getByText('United States')).toBeInTheDocument()
})
it('renders with value attribute', () => {
render(
)
const option = screen.getByText('United States')
expect(option).toHaveAttribute('value', 'us')
})
it('uses label as display text', () => {
render(
)
expect(screen.getByText('United States')).toBeInTheDocument()
})
it('uses children over label when provided', () => {
render(
)
expect(screen.getByText('United States')).toBeInTheDocument()
expect(screen.queryByText('Ignored')).not.toBeInTheDocument()
})
it('uses value as display when label not provided', () => {
render(
)
expect(screen.getByText('United States')).toBeInTheDocument()
})
})
describe('disabled state', () => {
it('applies disabled attribute when disabled', () => {
render(
)
expect(screen.getByText('United States')).toBeDisabled()
})
})
describe('styling variants', () => {
it('applies variant data attribute', () => {
render(
)
expect(screen.getByText('United States')).toHaveAttribute('data-option', 'primary')
})
it('applies size data attribute', () => {
render(
)
expect(screen.getByText('United States')).toHaveAttribute('data-size', 'lg')
})
it('applies custom data attributes', () => {
render(
)
const option = screen.getByText('United States')
expect(option).toHaveAttribute('data-highlighted', 'true')
expect(option).toHaveAttribute('data-category', 'premium')
})
it('accepts classes prop without error', () => {
// Test that the Option component accepts the classes prop
// The actual className rendering is handled by the UI component
expect(() => {
render(
)
}).not.toThrow()
expect(screen.getByText('United States')).toBeInTheDocument()
})
it('applies custom styles', () => {
render(
)
// Browsers convert color values to rgb format
expect(screen.getByText('United States')).toHaveStyle({ color: 'rgb(255, 0, 0)' })
})
})
describe('legacy props support', () => {
it('supports selectValue prop for backwards compatibility', () => {
render(
)
const option = screen.getByText('United States')
expect(option).toHaveAttribute('value', 'us')
})
it('prefers value over selectValue', () => {
render(
)
const option = screen.getByText('United Kingdom')
expect(option).toHaveAttribute('value', 'uk')
})
it('prefers label over selectLabel', () => {
render(
)
expect(screen.getByText('United Kingdom')).toBeInTheDocument()
expect(screen.queryByText('Ignored')).not.toBeInTheDocument()
})
})
describe('ref forwarding', () => {
it('forwards ref to option element', () => {
const ref = vi.fn()
render(
)
expect(ref).toHaveBeenCalledWith(expect.any(HTMLOptionElement))
})
})
describe('compound component', () => {
it('is accessible via Select.Option', () => {
render(
)
expect(screen.getByText('United States')).toBeInTheDocument()
})
it('has correct display name', () => {
expect(Option.displayName).toBe('Select.Option')
})
})
})