dify/web/app/components/base/image-uploader/uploader.spec.tsx
2026-02-24 21:29:28 +08:00

155 lines
4.3 KiB
TypeScript

import type { ComponentProps } from 'react'
import type { useLocalFileUploader } from './hooks'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { ALLOW_FILE_EXTENSIONS } from '@/types/app'
import Uploader from './uploader'
type LocalUploaderArgs = Parameters<typeof useLocalFileUploader>[0]
const mocks = vi.hoisted(() => ({
hookArgs: undefined as LocalUploaderArgs | undefined,
handleLocalFileUpload: vi.fn<(file: File) => void>(),
}))
vi.mock('./hooks', () => ({
useLocalFileUploader: (args: LocalUploaderArgs) => {
mocks.hookArgs = args
return {
handleLocalFileUpload: mocks.handleLocalFileUpload,
}
},
}))
const getInput = () => {
const input = screen.getByTestId('local-file-input')
return input as HTMLInputElement
}
const renderUploader = (props: Partial<ComponentProps<typeof Uploader>> = {}) => {
const onUpload = vi.fn()
const closePopover = vi.fn()
const childRenderer = vi.fn((hovering: boolean) => (
<div data-testid="hover-state">{hovering ? 'hovering' : 'idle'}</div>
))
const result = render(
<Uploader
onUpload={onUpload}
closePopover={closePopover}
limit={3}
disabled={false}
{...props}
>
{childRenderer}
</Uploader>,
)
return {
...result,
onUpload,
closePopover,
childRenderer,
}
}
describe('Uploader', () => {
beforeEach(() => {
vi.clearAllMocks()
mocks.hookArgs = undefined
})
describe('Rendering', () => {
it('should render file input and idle child content', () => {
renderUploader()
const input = getInput()
expect(screen.getByTestId('hover-state')).toHaveTextContent('idle')
expect(input).toBeInTheDocument()
})
it('should set accept attribute from allowed file extensions', () => {
renderUploader()
const input = getInput()
const expectedAccept = ALLOW_FILE_EXTENSIONS.map(ext => `.${ext}`).join(',')
expect(input).toHaveAttribute('accept', expectedAccept)
})
it('should pass hook arguments to useLocalFileUploader', () => {
const { onUpload } = renderUploader({ limit: 5, disabled: true })
expect(mocks.hookArgs).toMatchObject({
limit: 5,
disabled: true,
})
expect(mocks.hookArgs?.onUpload).toBe(onUpload)
})
})
describe('User Interactions', () => {
it('should update hovering state on mouse enter and leave', async () => {
const user = userEvent.setup()
renderUploader()
const input = getInput()
expect(screen.getByTestId('hover-state')).toHaveTextContent('idle')
await user.hover(input)
expect(screen.getByTestId('hover-state')).toHaveTextContent('hovering')
await user.unhover(input)
expect(screen.getByTestId('hover-state')).toHaveTextContent('idle')
})
it('should call handleLocalFileUpload and closePopover when file is selected', async () => {
const user = userEvent.setup()
const { closePopover } = renderUploader()
const input = getInput()
const file = new File(['hello'], 'demo.png', { type: 'image/png' })
await user.upload(input, file)
expect(mocks.handleLocalFileUpload).toHaveBeenCalledWith(file)
expect(closePopover).toHaveBeenCalledTimes(1)
})
it('should reset input value on click', async () => {
const user = userEvent.setup()
renderUploader()
const input = getInput()
const file = new File(['hello'], 'demo.png', { type: 'image/png' })
await user.upload(input, file)
expect(input.files).toHaveLength(1)
await user.click(input)
expect(input.value).toBe('')
})
it('should not upload or close popover when no file is selected', () => {
const { closePopover } = renderUploader()
const input = getInput()
Object.defineProperty(input, 'files', {
value: [] as unknown as FileList,
configurable: true,
})
input.dispatchEvent(new Event('change', { bubbles: true }))
expect(mocks.handleLocalFileUpload).not.toHaveBeenCalled()
expect(closePopover).not.toHaveBeenCalled()
})
})
describe('Props', () => {
it('should disable file input when disabled prop is true', () => {
renderUploader({ disabled: true })
const input = getInput()
expect(input).toBeDisabled()
})
})
})