mirror of
https://github.com/langgenius/dify.git
synced 2026-04-18 04:16:28 +08:00
315 lines
11 KiB
TypeScript
315 lines
11 KiB
TypeScript
import type { useLocalFileUploader } from '../hooks'
|
|
import type { ImageFile, VisionSettings } from '@/types/app'
|
|
import { fireEvent, render, screen } from '@testing-library/react'
|
|
import userEvent from '@testing-library/user-event'
|
|
import { Resolution, TransferMethod } from '@/types/app'
|
|
import ChatImageUploader from '../chat-image-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 {
|
|
disabled: args.disabled ?? false,
|
|
handleLocalFileUpload: mocks.handleLocalFileUpload,
|
|
}
|
|
},
|
|
}))
|
|
|
|
const createSettings = (overrides: Partial<VisionSettings> = {}): VisionSettings => ({
|
|
enabled: true,
|
|
number_limits: 5,
|
|
detail: Resolution.high,
|
|
transfer_methods: [TransferMethod.local_file],
|
|
image_file_size_limit: 10,
|
|
...overrides,
|
|
})
|
|
|
|
const queryFileInput = () => {
|
|
return screen.queryByTestId('local-file-input') as HTMLInputElement | null
|
|
}
|
|
|
|
const getFileInput = () => {
|
|
const input = queryFileInput()
|
|
if (!input)
|
|
throw new Error('Expected file input to exist')
|
|
return input
|
|
}
|
|
|
|
describe('ChatImageUploader', () => {
|
|
const defaultOnUpload = vi.fn()
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
mocks.hookArgs = undefined
|
|
mocks.handleLocalFileUpload.mockImplementation((file) => {
|
|
mocks.hookArgs?.onUpload({
|
|
type: TransferMethod.local_file,
|
|
_id: 'local-upload-id',
|
|
fileId: '',
|
|
progress: 0,
|
|
url: 'data:image/png;base64,mock',
|
|
file,
|
|
} as ImageFile)
|
|
})
|
|
})
|
|
|
|
describe('Rendering', () => {
|
|
it('should render UploadOnlyFromLocal when only local_file transfer method', () => {
|
|
const settings = createSettings({
|
|
transfer_methods: [TransferMethod.local_file],
|
|
})
|
|
render(<ChatImageUploader settings={settings} onUpload={defaultOnUpload} />)
|
|
|
|
expect(queryFileInput()).toBeInTheDocument()
|
|
expect(screen.queryByRole('button')).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('should render UploaderButton when remote_url is a transfer method', () => {
|
|
const settings = createSettings({
|
|
transfer_methods: [TransferMethod.remote_url],
|
|
})
|
|
render(<ChatImageUploader settings={settings} onUpload={defaultOnUpload} />)
|
|
|
|
expect(screen.getByRole('button')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render UploaderButton when both transfer methods are present', () => {
|
|
const settings = createSettings({
|
|
transfer_methods: [TransferMethod.local_file, TransferMethod.remote_url],
|
|
})
|
|
render(<ChatImageUploader settings={settings} onUpload={defaultOnUpload} />)
|
|
|
|
expect(screen.getByRole('button')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('Props', () => {
|
|
it('should pass limit from image_file_size_limit to uploader hook', () => {
|
|
const settings = createSettings({
|
|
transfer_methods: [TransferMethod.local_file],
|
|
image_file_size_limit: 20,
|
|
})
|
|
render(<ChatImageUploader settings={settings} onUpload={defaultOnUpload} />)
|
|
|
|
expect(mocks.hookArgs?.limit).toBe(20)
|
|
})
|
|
|
|
it('should convert string image_file_size_limit to number', () => {
|
|
const settings = createSettings({
|
|
transfer_methods: [TransferMethod.local_file],
|
|
image_file_size_limit: '15',
|
|
})
|
|
render(<ChatImageUploader settings={settings} onUpload={defaultOnUpload} />)
|
|
|
|
expect(mocks.hookArgs?.limit).toBe(15)
|
|
})
|
|
|
|
it('should pass disabled prop in local-only mode', () => {
|
|
const settings = createSettings({
|
|
transfer_methods: [TransferMethod.local_file],
|
|
})
|
|
render(<ChatImageUploader settings={settings} onUpload={defaultOnUpload} disabled />)
|
|
|
|
expect(mocks.hookArgs?.disabled).toBe(true)
|
|
expect(getFileInput()).toBeDisabled()
|
|
})
|
|
|
|
it('should pass disabled prop in button mode', () => {
|
|
const settings = createSettings({
|
|
transfer_methods: [TransferMethod.remote_url],
|
|
})
|
|
render(<ChatImageUploader settings={settings} onUpload={defaultOnUpload} disabled />)
|
|
|
|
expect(screen.getByRole('button')).toBeDisabled()
|
|
})
|
|
})
|
|
|
|
describe('User Interactions', () => {
|
|
it('should call onUpload when a local file is uploaded', async () => {
|
|
const user = userEvent.setup()
|
|
const onUpload = vi.fn()
|
|
const settings = createSettings({
|
|
transfer_methods: [TransferMethod.local_file],
|
|
})
|
|
render(<ChatImageUploader settings={settings} onUpload={onUpload} />)
|
|
|
|
const input = getFileInput()
|
|
const file = new File(['hello'], 'demo.png', { type: 'image/png' })
|
|
await user.upload(input, file)
|
|
|
|
expect(mocks.handleLocalFileUpload).toHaveBeenCalledWith(file)
|
|
expect(onUpload).toHaveBeenCalledWith(expect.objectContaining({
|
|
type: TransferMethod.local_file,
|
|
}))
|
|
})
|
|
|
|
it('should open popover when uploader trigger is clicked', async () => {
|
|
const user = userEvent.setup()
|
|
const settings = createSettings({
|
|
transfer_methods: [TransferMethod.remote_url],
|
|
})
|
|
render(<ChatImageUploader settings={settings} onUpload={defaultOnUpload} />)
|
|
|
|
await user.click(screen.getByRole('button'))
|
|
|
|
expect(screen.getByRole('textbox')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should call onUpload when a remote image link is submitted', async () => {
|
|
const user = userEvent.setup()
|
|
const onUpload = vi.fn()
|
|
const settings = createSettings({
|
|
transfer_methods: [TransferMethod.remote_url],
|
|
})
|
|
render(<ChatImageUploader settings={settings} onUpload={onUpload} />)
|
|
|
|
await user.click(screen.getByRole('button'))
|
|
await user.type(screen.getByTestId('image-link-input'), 'https://example.com/image.png')
|
|
await user.click(screen.getByRole('button', { name: 'common.operation.ok' }))
|
|
|
|
expect(onUpload).toHaveBeenCalledWith(expect.objectContaining({
|
|
type: TransferMethod.remote_url,
|
|
url: 'https://example.com/image.png',
|
|
progress: 0,
|
|
}))
|
|
})
|
|
|
|
it('should not open popover when uploader trigger is disabled', async () => {
|
|
const user = userEvent.setup()
|
|
const settings = createSettings({
|
|
transfer_methods: [TransferMethod.remote_url],
|
|
})
|
|
render(<ChatImageUploader settings={settings} onUpload={defaultOnUpload} disabled />)
|
|
|
|
await user.click(screen.getByRole('button'))
|
|
|
|
expect(screen.queryByRole('textbox')).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('should keep popover closed when trigger wrapper is clicked while disabled', async () => {
|
|
const user = userEvent.setup()
|
|
const settings = createSettings({
|
|
transfer_methods: [TransferMethod.remote_url],
|
|
})
|
|
render(<ChatImageUploader settings={settings} onUpload={defaultOnUpload} disabled />)
|
|
|
|
const button = screen.getByRole('button')
|
|
const triggerWrapper = button.parentElement
|
|
if (!triggerWrapper)
|
|
throw new Error('Expected trigger wrapper to exist')
|
|
|
|
await user.click(triggerWrapper)
|
|
|
|
expect(screen.queryByRole('textbox')).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('should show OR separator and local uploader when both methods are available', async () => {
|
|
const user = userEvent.setup()
|
|
const settings = createSettings({
|
|
transfer_methods: [TransferMethod.local_file, TransferMethod.remote_url],
|
|
})
|
|
render(<ChatImageUploader settings={settings} onUpload={defaultOnUpload} />)
|
|
|
|
await user.click(screen.getByRole('button'))
|
|
|
|
expect(screen.getByText(/OR/i)).toBeInTheDocument()
|
|
expect(screen.getByRole('textbox')).toBeInTheDocument()
|
|
expect(queryFileInput()).toBeInTheDocument()
|
|
})
|
|
|
|
it('should close popover when local upload calls closePopover in mixed mode', async () => {
|
|
const user = userEvent.setup()
|
|
const settings = createSettings({
|
|
transfer_methods: [TransferMethod.local_file, TransferMethod.remote_url],
|
|
})
|
|
|
|
mocks.handleLocalFileUpload.mockImplementation((file) => {
|
|
mocks.hookArgs?.onUpload({
|
|
type: TransferMethod.local_file,
|
|
_id: 'mixed-local-upload-id',
|
|
fileId: '',
|
|
progress: 0,
|
|
url: 'data:image/png;base64,mixed',
|
|
file,
|
|
} as ImageFile)
|
|
})
|
|
|
|
render(<ChatImageUploader settings={settings} onUpload={defaultOnUpload} />)
|
|
|
|
await user.click(screen.getByRole('button'))
|
|
expect(screen.getByRole('textbox')).toBeInTheDocument()
|
|
|
|
const localInput = getFileInput()
|
|
const file = new File(['hello'], 'mixed.png', { type: 'image/png' })
|
|
await user.upload(localInput, file)
|
|
|
|
expect(screen.queryByRole('textbox')).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('should toggle local-upload hover style in mixed transfer mode', async () => {
|
|
const user = userEvent.setup()
|
|
const settings = createSettings({
|
|
transfer_methods: [TransferMethod.local_file, TransferMethod.remote_url],
|
|
})
|
|
render(<ChatImageUploader settings={settings} onUpload={defaultOnUpload} />)
|
|
|
|
await user.click(screen.getByRole('button'))
|
|
|
|
const uploadFromComputer = screen.getByText('common.imageUploader.uploadFromComputer')
|
|
expect(uploadFromComputer).not.toHaveClass('bg-primary-50')
|
|
|
|
const localInput = getFileInput()
|
|
const hoverWrapper = localInput.parentElement
|
|
if (!hoverWrapper)
|
|
throw new Error('Expected local uploader wrapper to exist')
|
|
|
|
fireEvent.mouseEnter(hoverWrapper)
|
|
expect(uploadFromComputer).toHaveClass('bg-primary-50')
|
|
|
|
fireEvent.mouseLeave(hoverWrapper)
|
|
expect(uploadFromComputer).not.toHaveClass('bg-primary-50')
|
|
})
|
|
|
|
it('should not show OR separator or local uploader when only remote_url method', async () => {
|
|
const user = userEvent.setup()
|
|
const settings = createSettings({
|
|
transfer_methods: [TransferMethod.remote_url],
|
|
})
|
|
render(<ChatImageUploader settings={settings} onUpload={defaultOnUpload} />)
|
|
|
|
await user.click(screen.getByRole('button'))
|
|
|
|
expect(screen.getByRole('textbox')).toBeInTheDocument()
|
|
expect(screen.queryByText(/OR/i)).not.toBeInTheDocument()
|
|
expect(queryFileInput()).not.toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('Edge Cases', () => {
|
|
it('should render UploaderButton for all transfer method', () => {
|
|
const settings = createSettings({
|
|
transfer_methods: [TransferMethod.all],
|
|
})
|
|
render(<ChatImageUploader settings={settings} onUpload={defaultOnUpload} />)
|
|
|
|
expect(screen.getByRole('button')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render UploaderButton when transfer_methods is empty', () => {
|
|
const settings = createSettings({
|
|
transfer_methods: [],
|
|
})
|
|
render(<ChatImageUploader settings={settings} onUpload={defaultOnUpload} />)
|
|
|
|
expect(screen.getByRole('button')).toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|