/** * Tests for FormTextarea Component * Verifies textarea field rendering, accessibility, and error handling */ import { render, screen } from '@testing-library/react'; import { FormTextarea } from '@/components/forms/FormTextarea'; import type { FieldError } from 'react-hook-form'; describe('FormTextarea', () => { describe('Basic Rendering', () => { it('renders with label and textarea', () => { render(); expect(screen.getByLabelText('Description')).toBeInTheDocument(); expect(screen.getByRole('textbox')).toBeInTheDocument(); }); it('renders with description', () => { render( ); expect(screen.getByText("Define the agent's personality and behavior")).toBeInTheDocument(); }); it('renders description before textarea', () => { const { container } = render( ); const description = container.querySelector('#description-description'); const textarea = container.querySelector('textarea'); // Get positions const descriptionRect = description?.getBoundingClientRect(); const textareaRect = textarea?.getBoundingClientRect(); // Description should appear (both should exist) expect(description).toBeInTheDocument(); expect(textarea).toBeInTheDocument(); // In the DOM order, description comes before textarea expect(descriptionRect).toBeDefined(); expect(textareaRect).toBeDefined(); }); }); describe('Required Field', () => { it('shows asterisk when required is true', () => { render(); expect(screen.getByText('*')).toBeInTheDocument(); }); it('does not show asterisk when required is false', () => { render(); expect(screen.queryByText('*')).not.toBeInTheDocument(); }); }); describe('Error Handling', () => { it('displays error message when error prop is provided', () => { const error: FieldError = { type: 'required', message: 'Description is required', }; render(); expect(screen.getByText('Description is required')).toBeInTheDocument(); }); it('sets aria-invalid when error exists', () => { const error: FieldError = { type: 'required', message: 'Description is required', }; render(); const textarea = screen.getByRole('textbox'); expect(textarea).toHaveAttribute('aria-invalid', 'true'); }); it('sets aria-describedby with error ID when error exists', () => { const error: FieldError = { type: 'required', message: 'Description is required', }; render(); const textarea = screen.getByRole('textbox'); expect(textarea).toHaveAttribute('aria-describedby', 'description-error'); }); it('renders error with role="alert"', () => { const error: FieldError = { type: 'required', message: 'Description is required', }; render(); const errorElement = screen.getByRole('alert'); expect(errorElement).toHaveTextContent('Description is required'); }); }); describe('Accessibility', () => { it('links label to textarea via htmlFor/id', () => { render(); const label = screen.getByText('Description'); const textarea = screen.getByRole('textbox'); expect(label).toHaveAttribute('for', 'description'); expect(textarea).toHaveAttribute('id', 'description'); }); it('sets aria-describedby with description ID when description exists', () => { render( ); const textarea = screen.getByRole('textbox'); expect(textarea).toHaveAttribute('aria-describedby', 'description-description'); }); it('combines error and description IDs in aria-describedby', () => { const error: FieldError = { type: 'required', message: 'Description is required', }; render( ); const textarea = screen.getByRole('textbox'); expect(textarea).toHaveAttribute( 'aria-describedby', 'description-error description-description' ); }); }); describe('Textarea Props Forwarding', () => { it('forwards textarea props correctly', () => { render( ); const textarea = screen.getByRole('textbox'); expect(textarea).toHaveAttribute('placeholder', 'Enter description'); expect(textarea).toHaveAttribute('rows', '5'); expect(textarea).toBeDisabled(); }); it('accepts register() props via registration', () => { const registerProps = { name: 'description', onChange: jest.fn(), onBlur: jest.fn(), ref: jest.fn(), }; render(); const textarea = screen.getByRole('textbox'); expect(textarea).toBeInTheDocument(); expect(textarea).toHaveAttribute('id', 'description'); }); it('extracts name from spread props', () => { const spreadProps = { name: 'content', onChange: jest.fn(), }; render(); const textarea = screen.getByRole('textbox'); expect(textarea).toHaveAttribute('id', 'content'); }); }); describe('Error Cases', () => { it('throws error when name is not provided', () => { // Suppress console.error for this test const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {}); expect(() => { render(); }).toThrow('FormTextarea: name must be provided either explicitly or via register()'); consoleError.mockRestore(); }); }); describe('Layout and Styling', () => { it('applies correct spacing classes', () => { const { container } = render(); const wrapper = container.firstChild as HTMLElement; expect(wrapper).toHaveClass('space-y-2'); }); it('applies correct error styling', () => { const error: FieldError = { type: 'required', message: 'Description is required', }; render(); const errorElement = screen.getByRole('alert'); expect(errorElement).toHaveClass('text-sm', 'text-destructive'); }); it('applies correct description styling', () => { const { container } = render( ); const description = container.querySelector('#description-description'); expect(description).toHaveClass('text-sm', 'text-muted-foreground'); }); }); describe('Name Priority', () => { it('uses explicit name over registration name', () => { const registerProps = { name: 'fromRegister', onChange: jest.fn(), onBlur: jest.fn(), ref: jest.fn(), }; render(); const textarea = screen.getByRole('textbox'); expect(textarea).toHaveAttribute('id', 'explicit'); }); it('uses registration name when explicit name not provided', () => { const registerProps = { name: 'fromRegister', onChange: jest.fn(), onBlur: jest.fn(), ref: jest.fn(), }; render(); const textarea = screen.getByRole('textbox'); expect(textarea).toHaveAttribute('id', 'fromRegister'); }); }); });