dify/web/app/components/base/auto-height-textarea/index.spec.tsx
2026-02-24 16:22:43 +08:00

202 lines
7.3 KiB
TypeScript

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(<AutoHeightTextarea value="" onChange={vi.fn()} />)
const textarea = container.querySelector('textarea')
expect(textarea).toBeInTheDocument()
})
it('should render with placeholder when value is empty', () => {
render(<AutoHeightTextarea placeholder="Enter text" value="" onChange={vi.fn()} />)
expect(screen.getByPlaceholderText('Enter text')).toBeInTheDocument()
})
it('should render with value', () => {
render(<AutoHeightTextarea value="Hello World" onChange={vi.fn()} />)
const textarea = screen.getByDisplayValue('Hello World')
expect(textarea).toBeInTheDocument()
})
})
describe('Props', () => {
it('should apply custom className to textarea', () => {
const { container } = render(<AutoHeightTextarea value="" onChange={vi.fn()} className="custom-class" />)
const textarea = container.querySelector('textarea')
expect(textarea).toHaveClass('custom-class')
})
it('should apply custom wrapperClassName to wrapper div', () => {
const { container } = render(<AutoHeightTextarea value="" onChange={vi.fn()} wrapperClassName="wrapper-class" />)
const wrapper = container.querySelector('div.relative')
expect(wrapper).toHaveClass('wrapper-class')
})
it('should apply minHeight and maxHeight styles to hidden div', () => {
const { container } = render(<AutoHeightTextarea value="" onChange={vi.fn()} minHeight={50} maxHeight={200} />)
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(<AutoHeightTextarea value="" onChange={vi.fn()} />)
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(<AutoHeightTextarea value="" onChange={vi.fn()} autoFocus />)
expect(focusSpy).toHaveBeenCalled()
focusSpy.mockRestore()
})
})
describe('User Interactions', () => {
it('should call onChange when textarea value changes', () => {
const handleChange = vi.fn()
render(<AutoHeightTextarea value="" onChange={handleChange} />)
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(<AutoHeightTextarea value="" onChange={vi.fn()} onKeyDown={handleKeyDown} />)
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(<AutoHeightTextarea value="" onChange={vi.fn()} onKeyUp={handleKeyUp} />)
const textarea = screen.getByRole('textbox')
fireEvent.keyUp(textarea, { key: 'Enter' })
expect(handleKeyUp).toHaveBeenCalledTimes(1)
})
})
describe('Edge Cases', () => {
it('should handle empty string value', () => {
render(<AutoHeightTextarea value="" onChange={vi.fn()} />)
const textarea = screen.getByRole('textbox')
expect(textarea).toHaveValue('')
})
it('should handle whitespace-only value', () => {
render(<AutoHeightTextarea value=" " onChange={vi.fn()} />)
const textarea = screen.getByRole('textbox')
expect(textarea).toHaveValue(' ')
})
it('should handle very long text (>10000 chars)', () => {
const longText = 'a'.repeat(10001)
render(<AutoHeightTextarea value={longText} onChange={vi.fn()} />)
const textarea = screen.getByDisplayValue(longText)
expect(textarea).toBeInTheDocument()
})
it('should handle newlines in value', () => {
const textWithNewlines = 'line1\nline2\nline3'
render(<AutoHeightTextarea value={textWithNewlines} onChange={vi.fn()} />)
const textarea = screen.getByRole('textbox')
expect(textarea).toHaveValue(textWithNewlines)
})
it('should handle special characters in value', () => {
const specialChars = '!@#$%^&*()_+-=[]{}|;:,.<>?'
render(<AutoHeightTextarea value={specialChars} onChange={vi.fn()} />)
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(<AutoHeightTextarea ref={ref as React.RefObject<HTMLTextAreaElement>} 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(<AutoHeightTextarea value="" onChange={vi.fn()} controlFocus={1} />)
expect(focusSpy).toHaveBeenCalledTimes(1)
rerender(<AutoHeightTextarea value="" onChange={vi.fn()} controlFocus={2} />)
expect(focusSpy).toHaveBeenCalledTimes(2)
focusSpy.mockRestore()
})
it('should retry focus recursively when ref is not ready during autoFocus', async () => {
const delayedRef = {} as React.RefObject<HTMLTextAreaElement>
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(<AutoHeightTextarea ref={delayedRef} value="" onChange={vi.fn()} autoFocus />)
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')
})
})
})