import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; import { afterEach, describe, expect, it, vi } from 'vitest'; import MdComboBox from '../MdComboBox'; const mockOptions = [ { value: 'opt1', text: 'Option 1' }, { value: 'opt2', text: 'Option 2' }, { value: 'opt3', text: 'Option 3' }, ]; describe('MdComboBox', () => { describe('rendering', () => { it('renders combobox', () => { const { container } = render( {}} />); expect(container.querySelector('.md-combobox')).toBeInTheDocument(); }); it('renders input with combobox role', () => { render( {}} />); expect(screen.getByRole('combobox')).toBeInTheDocument(); }); it('renders label', () => { render( {}} />); expect(screen.getByText('Select option')).toBeInTheDocument(); }); it('renders placeholder when no value selected', () => { render( {}} />); expect(screen.getByPlaceholderText('Choose one')).toBeInTheDocument(); }); it('shows search icon by default', () => { const { container } = render( {}} />); expect(container.querySelector('.md-combobox__input--before')).toBeInTheDocument(); }); it('hides prefix icon when hidePrefixIcon is true', () => { const { container } = render( {}} hidePrefixIcon />, ); expect(container.querySelector('.md-combobox__input--before')).not.toBeInTheDocument(); }); it('does not render label section without label or helpText', () => { const { container } = render( {}} />); expect(container.querySelector('.md-combobox__label-wrapper')).not.toBeInTheDocument(); }); }); describe('modes', () => { it('applies medium mode by default', () => { const { container } = render( {}} />); expect(container.querySelector('.md-combobox--medium')).toBeInTheDocument(); }); it('applies large mode', () => { const { container } = render( {}} />, ); expect(container.querySelector('.md-combobox--large')).toBeInTheDocument(); }); it('applies small mode', () => { const { container } = render( {}} />, ); expect(container.querySelector('.md-combobox--small')).toBeInTheDocument(); }); }); describe('states', () => { it('disables input when disabled', () => { render( {}} disabled />); expect(screen.getByRole('combobox')).toBeDisabled(); }); it('applies disabled wrapper class', () => { const { container } = render( {}} disabled />); expect(container.querySelector('.md-combobox__input-wrapper--disabled')).toBeInTheDocument(); }); it('shows error state', () => { const { container } = render( {}} error errorText="Error" />, ); expect(container.querySelector('.md-combobox--has-error')).toBeInTheDocument(); }); it('displays error text', () => { render( {}} error errorText="Invalid selection" />, ); expect(screen.getByText('Invalid selection')).toBeInTheDocument(); }); it('shows loading spinner when isSearching', () => { const { container } = render( {}} isSearching />); expect(container.querySelector('.md-loading-spinner')).toBeInTheDocument(); }); it('shows has-value class when value is selected', () => { const { container } = render( {}} />); expect(container.querySelector('.md-combobox--has-value')).toBeInTheDocument(); }); }); describe('help text', () => { it('renders help button when helpText provided', () => { const { container } = render( {}} helpText="Help" />, ); expect(container.querySelector('.md-helpbutton')).toBeInTheDocument(); }); it('shows help text when help button is clicked', async () => { const user = userEvent.setup(); const { container } = render( {}} helpText="Select help" />, ); const helpButton = container.querySelector('.md-helpbutton') as HTMLElement; await user.click(helpButton); expect(screen.getByText('Select help')).toBeInTheDocument(); }); it('uses custom helpTextFor label', () => { const { container } = render( {}} helpText="Help" labels={{ helpTextFor: 'Help for' }} />, ); const helpButton = container.querySelector('.md-helpbutton') as HTMLElement; expect(helpButton).toHaveAttribute('aria-label', 'Help for Options'); }); }); describe('accessibility', () => { it('includes support text id in aria-describedby', () => { render( {}} supportText="Helpful context" />); const input = screen.getByRole('combobox'); const supportText = screen.getByText('Helpful context'); expect(input).toHaveAttribute('aria-describedby', supportText.getAttribute('id')); }); it('includes help, support, and error ids in aria-describedby', () => { render( {}} helpText="Help text" supportText="Support text" error errorText="Error text" />, ); const input = screen.getByRole('combobox'); const describedBy = input.getAttribute('aria-describedby'); expect(describedBy).toContain('md-combobox_help-text_'); expect(describedBy).toContain('md-combobox_support_'); expect(describedBy).toContain('md-combobox_error_'); }); }); describe('reset functionality', () => { it('shows reset button when allowReset is true and value is selected', () => { const { container } = render( {}} allowReset />, ); expect(container.querySelector('.md-combobox__reset')).toBeInTheDocument(); }); it('hides reset button when no value is selected', () => { const { container } = render( {}} allowReset />); expect(container.querySelector('.md-combobox__reset')).not.toBeInTheDocument(); }); it('calls onSelectOption with empty when reset is clicked', async () => { const user = userEvent.setup(); const onSelect = vi.fn(); const { container } = render( , ); const resetButton = container.querySelector('.md-combobox__reset') as HTMLElement; await user.click(resetButton); expect(onSelect).toHaveBeenCalledWith(''); }); }); describe('multi-select', () => { it('renders count indicator for multiple selections', () => { const { container } = render( {}} />, ); const afterDiv = container.querySelector('.md-combobox__input--after'); expect(afterDiv).toHaveTextContent('+2'); }); it('shows has-value class with array value', () => { const { container } = render( {}} />); expect(container.querySelector('.md-combobox--has-value')).toBeInTheDocument(); }); }); describe('dropdown', () => { it('opens dropdown on input focus and shows options', async () => { const user = userEvent.setup(); const { baseElement } = render( {}} />); const input = screen.getByRole('combobox'); await user.click(input); await waitFor(() => { expect(baseElement.querySelector('.md-combobox__popover')).toBeInTheDocument(); }); }); it('shows no results text when no options match', async () => { const user = userEvent.setup(); const { baseElement } = render( {}} noResultsText="No matches" />, ); const input = screen.getByRole('combobox'); await user.click(input); await user.type(input, 'xyz'); await waitFor(() => { expect(baseElement).toHaveTextContent('No matches'); }); }); it('applies custom dropdownHeight', async () => { const user = userEvent.setup(); const { baseElement } = render( {}} dropdownHeight={200} />, ); const input = screen.getByRole('combobox'); await user.click(input); await waitFor(() => { const popover = baseElement.querySelector('.md-combobox__popover') as HTMLElement; expect(popover).toHaveStyle('max-height: 200px'); }); }); }); describe('toggle button', () => { it('renders toggle button', () => { const { container } = render( {}} />); expect(container.querySelector('.md-combobox__toggle')).toBeInTheDocument(); }); it('uses custom openClose label', () => { const { container } = render( {}} labels={{ openClose: 'Toggle dropdown' }} />, ); expect(container.querySelector('.md-combobox__toggle')).toHaveAttribute('aria-label', 'Toggle dropdown'); }); }); describe('ref forwarding', () => { it('forwards ref to input element', () => { const ref = React.createRef(); render( {}} />); expect(ref.current).toBeInstanceOf(HTMLInputElement); }); }); describe('props forwarding', () => { it('applies custom id', () => { const { container } = render( {}} />, ); expect(container.querySelector('[id^="custom-combobox"]')).toBeInTheDocument(); }); }); describe('dynamic padding (ResizeObserver)', () => { afterEach(() => { vi.unstubAllGlobals(); }); it('attaches ResizeObserver to the after section on mount', () => { const observeMock = vi.fn(); vi.stubGlobal( 'ResizeObserver', vi.fn().mockImplementation(() => ({ observe: observeMock, disconnect: vi.fn() })), ); const { container } = render( {}} />); const afterDiv = container.querySelector('.md-combobox__input--after'); expect(observeMock).toHaveBeenCalledWith(afterDiv); }); it('sets input padding-right to afterWidth + 12px when ResizeObserver fires', async () => { vi.stubGlobal( 'ResizeObserver', vi.fn().mockImplementation((cb: ResizeObserverCallback) => ({ observe: vi.fn().mockImplementation((el: Element) => { Object.defineProperty(el, 'offsetWidth', { value: 68, configurable: true }); cb([], {} as ResizeObserver); }), disconnect: vi.fn(), })), ); render( {}} allowReset />); await waitFor(() => { expect(screen.getByRole('combobox')).toHaveStyle('padding-right: 80px'); }); }); it('does not apply inline padding-right before ResizeObserver fires', () => { vi.stubGlobal( 'ResizeObserver', vi.fn().mockImplementation(() => ({ observe: vi.fn(), disconnect: vi.fn() })), ); render( {}} />); expect((screen.getByRole('combobox') as HTMLElement).style.paddingRight).toBe(''); }); it('disconnects ResizeObserver on unmount', () => { const disconnectMock = vi.fn(); vi.stubGlobal( 'ResizeObserver', vi.fn().mockImplementation(() => ({ observe: vi.fn(), disconnect: disconnectMock })), ); const { unmount } = render( {}} />); unmount(); expect(disconnectMock).toHaveBeenCalled(); }); }); });