/** * Tests for FormSelect Component * Verifies select field rendering, accessibility, and error handling */ import React from 'react'; import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import { useForm, FormProvider } from 'react-hook-form'; import { FormSelect, type SelectOption } from '@/components/forms/FormSelect'; // Polyfill for Radix UI Select - jsdom doesn't support these browser APIs beforeAll(() => { Element.prototype.hasPointerCapture = jest.fn(() => false); Element.prototype.setPointerCapture = jest.fn(); Element.prototype.releasePointerCapture = jest.fn(); Element.prototype.scrollIntoView = jest.fn(); window.HTMLElement.prototype.scrollIntoView = jest.fn(); }); // Helper wrapper component to provide form context interface TestFormValues { model: string; category: string; } function TestWrapper({ children, defaultValues = { model: '', category: '' }, }: { children: (props: { control: ReturnType>['control']; }) => React.ReactNode; defaultValues?: Partial; }) { const form = useForm({ defaultValues: { model: '', category: '', ...defaultValues }, }); return {children({ control: form.control })}; } const mockOptions: SelectOption[] = [ { value: 'claude-opus', label: 'Claude Opus' }, { value: 'claude-sonnet', label: 'Claude Sonnet' }, { value: 'claude-haiku', label: 'Claude Haiku' }, ]; describe('FormSelect', () => { describe('Basic Rendering', () => { it('renders with label and select trigger', () => { render( {({ control }) => ( )} ); expect(screen.getByText('Primary Model')).toBeInTheDocument(); expect(screen.getByRole('combobox')).toBeInTheDocument(); }); it('renders with description', () => { render( {({ control }) => ( )} ); expect(screen.getByText('Main model used for this agent')).toBeInTheDocument(); }); it('renders with custom placeholder', () => { render( {({ control }) => ( )} ); expect(screen.getByText('Choose a model')).toBeInTheDocument(); }); it('renders default placeholder when none provided', () => { render( {({ control }) => ( )} ); expect(screen.getByText('Select primary model')).toBeInTheDocument(); }); }); describe('Required Field', () => { it('shows asterisk when required is true', () => { render( {({ control }) => ( )} ); expect(screen.getByText('*')).toBeInTheDocument(); }); it('does not show asterisk when required is false', () => { render( {({ control }) => ( )} ); expect(screen.queryByText('*')).not.toBeInTheDocument(); }); }); describe('Options Rendering', () => { it('renders all options when opened', async () => { render( {({ control }) => ( )} ); // Open the select using fireEvent (works better with Radix UI) fireEvent.click(screen.getByRole('combobox')); // Check all options are rendered await waitFor(() => { expect(screen.getByRole('option', { name: 'Claude Opus' })).toBeInTheDocument(); }); expect(screen.getByRole('option', { name: 'Claude Sonnet' })).toBeInTheDocument(); expect(screen.getByRole('option', { name: 'Claude Haiku' })).toBeInTheDocument(); }); it('selects option when clicked', async () => { render( {({ control }) => ( )} ); // Open the select and choose an option fireEvent.click(screen.getByRole('combobox')); await waitFor(() => { expect(screen.getByRole('option', { name: 'Claude Sonnet' })).toBeInTheDocument(); }); fireEvent.click(screen.getByRole('option', { name: 'Claude Sonnet' })); // The selected value should now be displayed await waitFor(() => { expect(screen.getByRole('combobox')).toHaveTextContent('Claude Sonnet'); }); }); }); describe('Disabled State', () => { it('disables select when disabled prop is true', () => { render( {({ control }) => ( )} ); expect(screen.getByRole('combobox')).toBeDisabled(); }); it('enables select when disabled prop is false', () => { render( {({ control }) => ( )} ); expect(screen.getByRole('combobox')).not.toBeDisabled(); }); }); describe('Pre-selected Value', () => { it('displays pre-selected value', () => { render( {({ control }) => ( )} ); expect(screen.getByRole('combobox')).toHaveTextContent('Claude Opus'); }); }); describe('Accessibility', () => { it('links label to select via htmlFor/id', () => { render( {({ control }) => ( )} ); const label = screen.getByText('Primary Model'); const select = screen.getByRole('combobox'); expect(label).toHaveAttribute('for', 'model'); expect(select).toHaveAttribute('id', 'model'); }); it('sets aria-describedby with description ID when description exists', () => { render( {({ control }) => ( )} ); const select = screen.getByRole('combobox'); expect(select).toHaveAttribute('aria-describedby', 'model-description'); }); }); describe('Custom ClassName', () => { it('applies custom className to wrapper', () => { const { container } = render( {({ control }) => ( )} ); expect(container.querySelector('.custom-class')).toBeInTheDocument(); }); }); describe('Error Handling', () => { it('displays error message when field has error', () => { function TestComponent() { const form = useForm({ defaultValues: { model: '', category: '' }, }); React.useEffect(() => { form.setError('model', { type: 'required', message: 'Model is required' }); }, [form]); return ( ); } render(); expect(screen.getByRole('alert')).toHaveTextContent('Model is required'); }); it('sets aria-invalid when error exists', () => { function TestComponent() { const form = useForm({ defaultValues: { model: '', category: '' }, }); React.useEffect(() => { form.setError('model', { type: 'required', message: 'Model is required' }); }, [form]); return ( ); } render(); expect(screen.getByRole('combobox')).toHaveAttribute('aria-invalid', 'true'); }); it('sets aria-describedby with error ID when error exists', () => { function TestComponent() { const form = useForm({ defaultValues: { model: '', category: '' }, }); React.useEffect(() => { form.setError('model', { type: 'required', message: 'Model is required' }); }, [form]); return ( ); } render(); expect(screen.getByRole('combobox')).toHaveAttribute('aria-describedby', 'model-error'); }); it('combines error and description IDs in aria-describedby', () => { function TestComponent() { const form = useForm({ defaultValues: { model: '', category: '' }, }); React.useEffect(() => { form.setError('model', { type: 'required', message: 'Model is required' }); }, [form]); return ( ); } render(); expect(screen.getByRole('combobox')).toHaveAttribute( 'aria-describedby', 'model-error model-description' ); }); }); });