import { describe, expect, test, beforeEach, afterEach, vi } from 'vitest' import { render, cleanup, act, waitFor } from '@testing-library/react' import { useState, useEffect, useRef } from 'react' import SlimSelect, { Option } from '../index' import SlimSelectReact, { SlimSelectRef } from './react' describe('SlimSelect React Component', () => { let consoleInfoSpy: any let consoleWarnSpy: any beforeEach(() => { // Mock console methods consoleInfoSpy = vi.spyOn(console, 'info').mockImplementation(() => {}) consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) }) afterEach(() => { cleanup() // Restore console methods consoleInfoSpy.mockRestore() consoleWarnSpy.mockRestore() }) describe('Basic Rendering', () => { test('renders a select element', () => { const { container } = render() const select = container.querySelector('select') expect(select).toBeTruthy() }) test('renders with single select by default', () => { const { container } = render() const select = container.querySelector('select') expect(select?.hasAttribute('multiple')).toBe(false) }) test('renders with multiple attribute when multiple prop is true', () => { const { container } = render() const select = container.querySelector('select') expect(select?.hasAttribute('multiple')).toBe(true) }) }) describe('Value and onChange binding', () => { test('accepts initial value for single select', async () => { const TestComponent = () => { const [value, setValue] = useState('2') const ref = useRef(null) return ( setValue(val as string)} data={[ { value: '1', text: 'Option 1' }, { value: '2', text: 'Option 2' }, { value: '3', text: 'Option 3' } ]} /> ) } const { container } = render() await waitFor( () => { const select = container.querySelector('select') const slimInstance = (select as any)?._slimSelect as SlimSelect if (slimInstance) { const selected = slimInstance.getSelected() expect(selected[0]).toBe('2') } }, { timeout: 500 } ) }) test('accepts initial value for multiple select', async () => { const TestComponent = () => { const [value, setValue] = useState(['2', '3']) return ( setValue(val as string[])} multiple data={[ { value: '1', text: 'Option 1' }, { value: '2', text: 'Option 2' }, { value: '3', text: 'Option 3' } ]} /> ) } const { container } = render() await waitFor( () => { const select = container.querySelector('select') const slimInstance = (select as any)?._slimSelect as SlimSelect if (slimInstance) { const selected = slimInstance.getSelected() expect(selected).toEqual(['2', '3']) } }, { timeout: 500 } ) }) test('calls onChange when selection changes', async () => { const onChangeMock = vi.fn() const TestComponent = () => { return ( ) } const { container } = render() await waitFor( async () => { const select = container.querySelector('select') const slimInstance = (select as any)?._slimSelect as SlimSelect if (slimInstance) { await act(async () => { slimInstance.setSelected('2') }) await waitFor( () => { expect(onChangeMock).toHaveBeenCalledWith('2') }, { timeout: 500 } ) } }, { timeout: 500 } ) }) test('updates selection when value prop changes', async () => { const TestComponent = ({ initialValue }: { initialValue: string }) => { const [value, setValue] = useState(initialValue) useEffect(() => { // Simulate parent updating value const timer = setTimeout(() => { act(() => { setValue('2') }) }, 100) return () => clearTimeout(timer) }, []) return ( setValue(val as string)} data={[ { value: '1', text: 'Option 1' }, { value: '2', text: 'Option 2' } ]} /> ) } const { container } = render() await waitFor( () => { const select = container.querySelector('select') const slimInstance = (select as any)?._slimSelect as SlimSelect if (slimInstance) { const selected = slimInstance.getSelected() expect(selected[0]).toBe('2') } }, { timeout: 500 } ) }) }) describe('Data Prop', () => { test('initializes with data prop', async () => { const testData = [ { value: '1', text: 'Option 1' }, { value: '2', text: 'Option 2' }, { value: '3', text: 'Option 3' } ] const { container } = render() await new Promise((resolve) => setTimeout(resolve, 100)) const select = container.querySelector('select') const slimInstance = (select as any)?._slimSelect as SlimSelect if (slimInstance) { const data = slimInstance.getData() expect(data).toHaveLength(3) } }) test('updates data when data prop changes', async () => { const TestComponent = () => { const [data, setData] = useState([ { value: '1', text: 'Option 1' }, { value: '2', text: 'Option 2' } ]) useEffect(() => { const timer = setTimeout(() => { act(() => { setData([ { value: '3', text: 'Option 3' }, { value: '4', text: 'Option 4' }, { value: '5', text: 'Option 5' } ]) }) }, 100) return () => clearTimeout(timer) }, []) return } const { container } = render() await waitFor( () => { const select = container.querySelector('select') const slimInstance = (select as any)?._slimSelect as SlimSelect if (slimInstance) { const data = slimInstance.getData() expect(data).toHaveLength(3) } }, { timeout: 500 } ) }) }) describe('Settings Prop', () => { test('applies settings prop', async () => { const { container } = render( ) await new Promise((resolve) => setTimeout(resolve, 100)) const select = container.querySelector('select') const slimInstance = (select as any)?._slimSelect as SlimSelect if (slimInstance) { expect(slimInstance.settings.showSearch).toBe(false) expect(slimInstance.settings.placeholderText).toBe('Custom Placeholder') } }) }) describe('Events Prop', () => { test('calls afterChange event from events prop', async () => { const afterChangeMock = vi.fn() const TestComponent = () => { return ( ) } const { container } = render() await new Promise((resolve) => setTimeout(resolve, 100)) const select = container.querySelector('select') const slimInstance = (select as any)?._slimSelect as SlimSelect if (slimInstance) { slimInstance.setSelected('2') await new Promise((resolve) => setTimeout(resolve, 100)) expect(afterChangeMock).toHaveBeenCalled() } }) test('calls both custom afterChange and onChange', async () => { const afterChangeMock = vi.fn() const onChangeMock = vi.fn() const TestComponent = () => { return ( ) } const { container } = render() await new Promise((resolve) => setTimeout(resolve, 100)) const select = container.querySelector('select') const slimInstance = (select as any)?._slimSelect as SlimSelect if (slimInstance) { slimInstance.setSelected('2') await new Promise((resolve) => setTimeout(resolve, 100)) expect(afterChangeMock).toHaveBeenCalled() expect(onChangeMock).toHaveBeenCalledWith('2') } }) }) describe('Empty and Invalid Values', () => { test('handles empty string as value for single select', async () => { const TestComponent = () => { const [value, setValue] = useState('') return ( setValue(val as string)} data={[ { value: '1', text: 'Option 1' }, { value: '2', text: 'Option 2' } ]} /> ) } const { container } = render() await new Promise((resolve) => setTimeout(resolve, 100)) const select = container.querySelector('select') const slimInstance = (select as any)?._slimSelect as SlimSelect if (slimInstance) { // Verify placeholder exists in data const data = slimInstance.getData() const hasPlaceholder = (data as Option[]).some((opt: any) => opt.placeholder) expect(hasPlaceholder).toBe(true) // CRITICAL: Verify what's actually selected - should be empty string (placeholder) const selectedValues = slimInstance.getSelected() expect(selectedValues).toEqual(['']) // Verify no valid option is selected const hasValidOptionSelected = selectedValues.some((val) => val !== '' && ['1', '2'].includes(val)) expect(hasValidOptionSelected).toBe(false) } }) test('shows placeholder when value does not exist in options for single select', async () => { const TestComponent = () => { const [value, setValue] = useState('banana') return ( setValue(val as string)} data={[ { value: '1', text: 'Option 1' }, { value: '2', text: 'Option 2' }, { value: '3', text: 'Option 3' } ]} /> ) } const { container } = render() await waitFor( () => { const select = container.querySelector('select') const slimInstance = (select as any)?._slimSelect as SlimSelect if (slimInstance) { // Verify placeholder exists in data (with empty value, not 'banana') const data = slimInstance.getData() const hasPlaceholder = (data as Option[]).some((opt: any) => opt.placeholder) expect(hasPlaceholder).toBe(true) // CRITICAL: Check what's actually displayed in the SlimSelect UI // SlimSelect renders a custom UI - check the .ss-single or .ss-placeholder element const slimMain = container.querySelector('.ss-main') as HTMLElement expect(slimMain).toBeDefined() // Check what's in the .ss-values container const ssValues = slimMain?.querySelector('.ss-values') as HTMLElement expect(ssValues).toBeDefined() // If placeholder is selected, it should show placeholder (no .ss-single element) // If a real option is selected, it will show .ss-single with the option text const singleValue = ssValues?.querySelector('.ss-single') as HTMLElement const placeholderEl = ssValues?.querySelector('.ss-placeholder') as HTMLElement // CRITICAL: If placeholder is correctly selected, we should NOT see .ss-single with "Option 1" // Instead, we should either see placeholder OR empty (if placeholder text is empty) if (singleValue) { const displayedText = singleValue.textContent?.trim() || '' // This should NOT be showing a real option expect(displayedText).not.toBe('Option 1') expect(displayedText).not.toBe('Option 2') expect(displayedText).not.toBe('Option 3') // If it's showing something, it might be the placeholder (empty text is OK) // But we should verify the placeholder option is actually selected in the store } // Check the store to see what's actually selected const selectedOptions = (slimInstance as any).store.getSelectedOptions() const hasPlaceholderSelected = selectedOptions.some((opt: any) => opt.placeholder && opt.selected) const hasValidOptionSelected = selectedOptions.some( (opt: any) => opt.selected && !opt.placeholder && ['1', '2', '3'].includes(opt.value) ) // The placeholder should be selected, not a valid option expect(hasPlaceholderSelected).toBe(true) expect(hasValidOptionSelected).toBe(false) } }, { timeout: 500 } ) }) test('handles empty array as value for multiple select', async () => { const TestComponent = () => { const [value, setValue] = useState([] as string[]) return ( setValue(val as string[])} multiple data={[ { value: '1', text: 'Option 1' }, { value: '2', text: 'Option 2' } ]} /> ) } const { container } = render() await new Promise((resolve) => setTimeout(resolve, 100)) // Should not throw errors expect(container.querySelector('select')).toBeTruthy() }) test('updates onChange for multiple select when invalid values are provided', async () => { const onChangeMock = vi.fn() const TestComponent = () => { return ( ) } const { container } = render() await new Promise((resolve) => setTimeout(resolve, 100)) const select = container.querySelector('select') const slimInstance = (select as any)?._slimSelect as SlimSelect if (slimInstance) { // When user manually selects valid options, onChange should update slimInstance.setSelected(['1', '2']) await new Promise((resolve) => setTimeout(resolve, 100)) // Verify onChange has been called with the valid values expect(onChangeMock).toHaveBeenCalledWith(['1', '2']) } }) }) })