dify/web/app/components/base/image-uploader/__tests__/chat-image-uploader.spec.tsx
Saumya Talwani f50e44b24a
test: improve coverage for some test files (#32916)
Signed-off-by: edvatar <88481784+toroleapinc@users.noreply.github.com>
Signed-off-by: -LAN- <laipz8200@outlook.com>
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: majiayu000 <1835304752@qq.com>
Co-authored-by: Poojan <poojan@infocusp.com>
Co-authored-by: sahil-infocusp <73810410+sahil-infocusp@users.noreply.github.com>
Co-authored-by: 非法操作 <hjlarry@163.com>
Co-authored-by: Pandaaaa906 <ye.pandaaaa906@gmail.com>
Co-authored-by: Asuka Minato <i@asukaminato.eu.org>
Co-authored-by: heyszt <270985384@qq.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Ijas <ijas.ahmd.ap@gmail.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: 木之本澪 <kinomotomiovo@gmail.com>
Co-authored-by: KinomotoMio <200703522+KinomotoMio@users.noreply.github.com>
Co-authored-by: 不做了睡大觉 <64798754+stakeswky@users.noreply.github.com>
Co-authored-by: User <user@example.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: edvatar <88481784+toroleapinc@users.noreply.github.com>
Co-authored-by: -LAN- <laipz8200@outlook.com>
Co-authored-by: Leilei <138381132+Inlei@users.noreply.github.com>
Co-authored-by: HaKu <104669497+haku-ink@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: wangxiaolei <fatelei@gmail.com>
Co-authored-by: Varun Chawla <34209028+veeceey@users.noreply.github.com>
Co-authored-by: Stephen Zhou <38493346+hyoban@users.noreply.github.com>
Co-authored-by: yyh <yuanyouhuilyz@gmail.com>
Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com>
Co-authored-by: tda <95275462+tda1017@users.noreply.github.com>
Co-authored-by: root <root@DESKTOP-KQLO90N>
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Co-authored-by: Niels Kaspers <153818647+nielskaspers@users.noreply.github.com>
Co-authored-by: hj24 <mambahj24@gmail.com>
Co-authored-by: Tyson Cung <45380903+tysoncung@users.noreply.github.com>
Co-authored-by: Stephen Zhou <hi@hyoban.cc>
Co-authored-by: FFXN <31929997+FFXN@users.noreply.github.com>
Co-authored-by: slegarraga <64795732+slegarraga@users.noreply.github.com>
Co-authored-by: 99 <wh2099@pm.me>
Co-authored-by: Br1an <932039080@qq.com>
Co-authored-by: L1nSn0w <l1nsn0w@qq.com>
Co-authored-by: Yunlu Wen <yunlu.wen@dify.ai>
Co-authored-by: akkoaya <151345394+akkoaya@users.noreply.github.com>
Co-authored-by: 盐粒 Yanli <yanli@dify.ai>
Co-authored-by: lif <1835304752@qq.com>
Co-authored-by: weiguang li <codingpunk@gmail.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com>
Co-authored-by: HanWenbo <124024253+hwb96@users.noreply.github.com>
Co-authored-by: Coding On Star <447357187@qq.com>
Co-authored-by: CodingOnStar <hanxujiang@dify.com>
Co-authored-by: Stable Genius <stablegenius043@gmail.com>
Co-authored-by: Stable Genius <259448942+stablegenius49@users.noreply.github.com>
Co-authored-by: ふるい <46769295+Echo0ff@users.noreply.github.com>
Co-authored-by: Xiyuan Chen <52963600+GareArc@users.noreply.github.com>
2026-03-06 18:59:16 +08:00

286 lines
9.8 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 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()
})
})
})