import { fireEvent, render, screen, waitFor } from '@testing-library/react' import { sleep } from '@/utils' import AutoHeightTextarea from './index' vi.mock('@/utils', async () => { const actual = await vi.importActual('@/utils') return { ...actual, sleep: vi.fn(), } }) describe('AutoHeightTextarea', () => { beforeEach(() => { vi.clearAllMocks() }) describe('Rendering', () => { it('should render without crashing', () => { const { container } = render() const textarea = container.querySelector('textarea') expect(textarea).toBeInTheDocument() }) it('should render with placeholder when value is empty', () => { render() expect(screen.getByPlaceholderText('Enter text')).toBeInTheDocument() }) it('should render with value', () => { render() const textarea = screen.getByDisplayValue('Hello World') expect(textarea).toBeInTheDocument() }) }) describe('Props', () => { it('should apply custom className to textarea', () => { const { container } = render() const textarea = container.querySelector('textarea') expect(textarea).toHaveClass('custom-class') }) it('should apply custom wrapperClassName to wrapper div', () => { const { container } = render() const wrapper = container.querySelector('div.relative') expect(wrapper).toHaveClass('wrapper-class') }) it('should apply minHeight and maxHeight styles to hidden div', () => { const { container } = render() const hiddenDiv = container.querySelector('div.invisible') expect(hiddenDiv).toHaveStyle({ minHeight: '50px', maxHeight: '200px' }) }) it('should use default minHeight and maxHeight when not provided', () => { const { container } = render() const hiddenDiv = container.querySelector('div.invisible') expect(hiddenDiv).toHaveStyle({ minHeight: '36px', maxHeight: '96px' }) }) it('should set autoFocus on textarea', () => { const focusSpy = vi.spyOn(HTMLTextAreaElement.prototype, 'focus') render() expect(focusSpy).toHaveBeenCalled() focusSpy.mockRestore() }) }) describe('User Interactions', () => { it('should call onChange when textarea value changes', () => { const handleChange = vi.fn() render() const textarea = screen.getByRole('textbox') fireEvent.change(textarea, { target: { value: 'new value' } }) expect(handleChange).toHaveBeenCalledTimes(1) }) it('should call onKeyDown when key is pressed', () => { const handleKeyDown = vi.fn() render() const textarea = screen.getByRole('textbox') fireEvent.keyDown(textarea, { key: 'Enter' }) expect(handleKeyDown).toHaveBeenCalledTimes(1) }) it('should call onKeyUp when key is released', () => { const handleKeyUp = vi.fn() render() const textarea = screen.getByRole('textbox') fireEvent.keyUp(textarea, { key: 'Enter' }) expect(handleKeyUp).toHaveBeenCalledTimes(1) }) }) describe('Edge Cases', () => { it('should handle empty string value', () => { render() const textarea = screen.getByRole('textbox') expect(textarea).toHaveValue('') }) it('should handle whitespace-only value', () => { render() const textarea = screen.getByRole('textbox') expect(textarea).toHaveValue(' ') }) it('should handle very long text (>10000 chars)', () => { const longText = 'a'.repeat(10001) render() const textarea = screen.getByDisplayValue(longText) expect(textarea).toBeInTheDocument() }) it('should handle newlines in value', () => { const textWithNewlines = 'line1\nline2\nline3' render() const textarea = screen.getByRole('textbox') expect(textarea).toHaveValue(textWithNewlines) }) it('should handle special characters in value', () => { const specialChars = '!@#$%^&*()_+-=[]{}|;:,.<>?' render() const textarea = screen.getByDisplayValue(specialChars) expect(textarea).toBeInTheDocument() }) }) describe('Ref forwarding', () => { it('should accept ref and allow focusing', () => { const ref = { current: null as HTMLTextAreaElement | null } render(} value="" onChange={vi.fn()} />) expect(ref.current).not.toBeNull() expect(ref.current?.tagName).toBe('TEXTAREA') }) }) describe('controlFocus prop', () => { it('should call focus when controlFocus changes', () => { const focusSpy = vi.spyOn(HTMLTextAreaElement.prototype, 'focus') const { rerender } = render() expect(focusSpy).toHaveBeenCalledTimes(1) rerender() expect(focusSpy).toHaveBeenCalledTimes(2) focusSpy.mockRestore() }) it('should retry focus recursively when ref is not ready during autoFocus', async () => { const delayedRef = {} as React.RefObject let assignedNode: HTMLTextAreaElement | null = null let exposedNode: HTMLTextAreaElement | null = null Object.defineProperty(delayedRef, 'current', { get: () => exposedNode, set: (value: HTMLTextAreaElement | null) => { assignedNode = value }, }) const sleepMock = vi.mocked(sleep) let sleepCalls = 0 sleepMock.mockImplementation(async () => { sleepCalls += 1 if (sleepCalls === 2) exposedNode = assignedNode }) const focusSpy = vi.spyOn(HTMLTextAreaElement.prototype, 'focus') const setSelectionRangeSpy = vi.spyOn(HTMLTextAreaElement.prototype, 'setSelectionRange') render() await waitFor(() => { expect(sleepMock).toHaveBeenCalledTimes(2) expect(focusSpy).toHaveBeenCalled() expect(setSelectionRangeSpy).toHaveBeenCalledTimes(1) }) focusSpy.mockRestore() setSelectionRangeSpy.mockRestore() }) }) describe('displayName', () => { it('should have displayName set', () => { expect(AutoHeightTextarea.displayName).toBe('AutoHeightTextarea') }) }) })