import { describe, expect, it, vi, afterEach } from 'vitest' import { render, screen, fireEvent, cleanup } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { Input } from './Input' import { TextField } from 'react-aria-components' afterEach(() => { cleanup() }) // Wrap Input in TextField for proper React Aria context const renderInput = (props: React.ComponentProps) => { return render( ) } // Render Input standalone (without TextField) for testing Input's own behavior const renderInputStandalone = (props: React.ComponentProps) => { return render() } describe('Input', () => { describe('props', () => { it('applies required attribute', () => { renderInput({ label: 'Email', required: true }) expect(screen.getByRole('textbox')).toHaveProperty('required', true) }) it('applies readOnly attribute', () => { renderInput({ label: 'Email', readOnly: true }) expect(screen.getByRole('textbox')).toHaveProperty('readOnly', true) }) it('applies placeholder for floating label when provided', () => { renderInput({ label: 'Email', placeholder: 'Enter email' }) expect(screen.getByRole('textbox').getAttribute('placeholder')).toBe( 'Enter email' ) }) it('applies empty placeholder for top label by default', () => { renderInput({ label: 'Email', labelPosition: 'top' }) expect(screen.getByRole('textbox').getAttribute('placeholder')).toBe('') }) it('applies custom id', () => { // Use standalone render since TextField overrides id via context renderInputStandalone({ label: 'Email', id: 'custom-id' }) expect(screen.getByRole('textbox').getAttribute('id')).toBe('custom-id') }) it('applies aria-describedby', () => { renderInput({ label: 'Email', 'aria-describedby': 'error-message' }) expect(screen.getByRole('textbox').getAttribute('aria-describedby')).toBe( 'error-message' ) }) }) describe('clear content button', () => { it('does not show clear button when showClearContentIcon is false', () => { renderInput({ label: 'Email', value: 'test', onChange: vi.fn(), showClearContentIcon: false, }) expect(screen.queryByLabelText('Clear content')).toBeNull() }) it('does not show clear button when input is empty', () => { renderInput({ label: 'Email', value: '', onChange: vi.fn(), showClearContentIcon: true, }) expect(screen.queryByLabelText('Clear content')).toBeNull() }) it('shows clear button when input has value and showClearContentIcon is true', () => { renderInput({ label: 'Email', value: 'test', onChange: vi.fn(), showClearContentIcon: true, }) expect(screen.getByLabelText('Clear content')).toBeTruthy() }) }) describe('icons', () => { it('renders left icon when provided', () => { renderInput({ label: 'Search', // eslint-disable-next-line formatjs/no-literal-string-in-jsx leftIcon: 🔍, }) expect(screen.getByTestId('left-icon')).toBeTruthy() }) it('renders right icon when provided', () => { renderInput({ label: 'Password', // eslint-disable-next-line formatjs/no-literal-string-in-jsx rightIcon: 👁, }) expect(screen.getByTestId('right-icon')).toBeTruthy() }) it('hides right icon when clear button is shown', () => { renderInput({ label: 'Email', value: 'test', onChange: vi.fn(), showClearContentIcon: true, // eslint-disable-next-line formatjs/no-literal-string-in-jsx rightIcon: 👁, }) expect(screen.queryByTestId('right-icon')).toBeNull() expect(screen.getByLabelText('Clear content')).toBeTruthy() }) it('shows right icon when clear button condition not met', () => { renderInput({ label: 'Email', value: '', onChange: vi.fn(), showClearContentIcon: true, // eslint-disable-next-line formatjs/no-literal-string-in-jsx rightIcon: 👁, }) expect(screen.getByTestId('right-icon')).toBeTruthy() }) }) describe('controlled input', () => { it('displays the controlled value', () => { renderInput({ label: 'Email', value: 'test@example.com', onChange: vi.fn(), }) expect(screen.getByRole('textbox')).toHaveProperty( 'value', 'test@example.com' ) }) it('calls onChange when typing', async () => { const onChange = vi.fn() renderInput({ label: 'Email', value: '', onChange }) const input = screen.getByRole('textbox') await userEvent.type(input, 'a') expect(onChange).toHaveBeenCalled() }) it('does not change value without onChange updating it', () => { const onChange = vi.fn() renderInput({ label: 'Email', value: 'initial', onChange }) const input = screen.getByRole('textbox') fireEvent.change(input, { target: { value: 'changed' } }) // Value stays the same because it's controlled expect(input).toHaveProperty('value', 'initial') }) }) describe('ref forwarding', () => { it('forwards ref to the input element', () => { const ref = { current: null as HTMLInputElement | null } render( ) expect(ref.current).toBeInstanceOf(HTMLInputElement) }) it('allows focusing via ref', () => { const ref = { current: null as HTMLInputElement | null } render( ) ref.current?.focus() expect(document.activeElement).toBe(ref.current) }) }) })