mirror of
https://github.com/langgenius/dify.git
synced 2026-03-28 07:39:51 +08:00
292 lines
10 KiB
TypeScript
292 lines
10 KiB
TypeScript
import type { ImageFile } from '@/types/app'
|
|
import { fireEvent, render, screen } from '@testing-library/react'
|
|
import userEvent from '@testing-library/user-event'
|
|
import { TransferMethod } from '@/types/app'
|
|
import ImageList from './image-list'
|
|
|
|
const createLocalFile = (overrides: Partial<ImageFile> = {}): ImageFile => ({
|
|
type: TransferMethod.local_file,
|
|
_id: `local-${Date.now()}-${Math.random()}`,
|
|
fileId: 'file-id',
|
|
progress: 100,
|
|
url: '',
|
|
base64Url: 'data:image/png;base64,abc123',
|
|
...overrides,
|
|
})
|
|
|
|
const createRemoteFile = (overrides: Partial<ImageFile> = {}): ImageFile => ({
|
|
type: TransferMethod.remote_url,
|
|
_id: `remote-${Date.now()}-${Math.random()}`,
|
|
fileId: '',
|
|
progress: 100,
|
|
url: 'https://example.com/image.png',
|
|
...overrides,
|
|
})
|
|
|
|
describe('ImageList', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
describe('Rendering', () => {
|
|
it('should render without crashing with empty list', () => {
|
|
render(<ImageList list={[]} />)
|
|
expect(screen.getByTestId('image-list')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render images for each item in the list', () => {
|
|
const list = [
|
|
createLocalFile({ _id: 'file-1' }),
|
|
createLocalFile({ _id: 'file-2' }),
|
|
]
|
|
render(<ImageList list={list} />)
|
|
|
|
const images = screen.getAllByRole('img')
|
|
expect(images).toHaveLength(2)
|
|
})
|
|
|
|
it('should use base64Url as src for local files', () => {
|
|
const list = [createLocalFile({ _id: 'file-1', base64Url: 'data:image/png;base64,xyz' })]
|
|
render(<ImageList list={list} />)
|
|
|
|
expect(screen.getByRole('img')).toHaveAttribute('src', 'data:image/png;base64,xyz')
|
|
})
|
|
|
|
it('should use url as src for remote files', () => {
|
|
const list = [createRemoteFile({ _id: 'file-1', url: 'https://example.com/img.jpg' })]
|
|
render(<ImageList list={list} />)
|
|
|
|
expect(screen.getByRole('img')).toHaveAttribute('src', 'https://example.com/img.jpg')
|
|
})
|
|
|
|
it('should set alt attribute from file name', () => {
|
|
const file = new File(['test'], 'my-image.png', { type: 'image/png' })
|
|
const list = [createLocalFile({ _id: 'file-1', file })]
|
|
render(<ImageList list={list} />)
|
|
|
|
expect(screen.getByRole('img')).toHaveAttribute('alt', 'my-image.png')
|
|
})
|
|
})
|
|
|
|
describe('Props', () => {
|
|
it('should show remove buttons when not readonly', () => {
|
|
const list = [createLocalFile({ _id: 'file-1' })]
|
|
render(<ImageList list={list} onRemove={vi.fn()} />)
|
|
|
|
expect(screen.getByRole('button')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should not show remove buttons when readonly', () => {
|
|
const list = [createLocalFile({ _id: 'file-1' })]
|
|
render(<ImageList list={list} readonly onRemove={vi.fn()} />)
|
|
|
|
expect(screen.queryByRole('button')).not.toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('Local File Progress', () => {
|
|
it('should show progress percentage when local file is uploading', () => {
|
|
const list = [createLocalFile({ _id: 'file-1', progress: 45 })]
|
|
render(<ImageList list={list} />)
|
|
|
|
expect(screen.getByText(/^45\s*%$/)).toBeInTheDocument()
|
|
})
|
|
|
|
it('should not show progress overlay when local file is complete', () => {
|
|
const list = [createLocalFile({ _id: 'file-1', progress: 100 })]
|
|
render(<ImageList list={list} />)
|
|
|
|
expect(screen.queryByText(/\d+\s*%/)).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('should show retry icon when local file upload fails (progress -1)', () => {
|
|
const onReUpload = vi.fn()
|
|
const list = [createLocalFile({ _id: 'file-1', progress: -1 })]
|
|
render(<ImageList list={list} onReUpload={onReUpload} />)
|
|
|
|
expect(screen.getByTestId('retry-icon')).toBeInTheDocument()
|
|
expect(screen.queryByText(/\d+\s*%/)).not.toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('Remote URL Progress', () => {
|
|
it('should show loading spinner when remote file is loading (progress 0)', () => {
|
|
const list = [createRemoteFile({ _id: 'file-1', progress: 0 })]
|
|
render(<ImageList list={list} />)
|
|
|
|
// Loading spinner has animate-spin class
|
|
expect(screen.getByTestId('image-loader')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should not show loading state when remote file is loaded (progress 100)', () => {
|
|
const list = [createRemoteFile({ _id: 'file-1', progress: 100 })]
|
|
render(<ImageList list={list} />)
|
|
|
|
expect(screen.queryByTestId('image-loader')).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('should show error indicator when remote file fails (progress -1)', () => {
|
|
const list = [createRemoteFile({ _id: 'file-1', progress: -1 })]
|
|
render(<ImageList list={list} />)
|
|
expect(screen.getByTestId('image-error-container')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('User Interactions', () => {
|
|
it('should call onRemove when remove button is clicked', async () => {
|
|
const user = userEvent.setup()
|
|
const onRemove = vi.fn()
|
|
const list = [createLocalFile({ _id: 'file-1' })]
|
|
render(<ImageList list={list} onRemove={onRemove} />)
|
|
|
|
await user.click(screen.getByRole('button'))
|
|
|
|
expect(onRemove).toHaveBeenCalledTimes(1)
|
|
expect(onRemove).toHaveBeenCalledWith('file-1')
|
|
})
|
|
|
|
it('should call onReUpload when retry icon is clicked on failed local file', async () => {
|
|
const user = userEvent.setup()
|
|
const onReUpload = vi.fn()
|
|
const list = [createLocalFile({ _id: 'file-1', progress: -1 })]
|
|
render(<ImageList list={list} onReUpload={onReUpload} />)
|
|
const retryIcon = screen.getByTestId('retry-icon')
|
|
await user.click(retryIcon)
|
|
expect(onReUpload).toHaveBeenCalledWith('file-1')
|
|
})
|
|
|
|
it('should open image preview when clicking a completed image', async () => {
|
|
const user = userEvent.setup()
|
|
const list = [createRemoteFile({ _id: 'file-1', progress: 100, url: 'https://example.com/img.png' })]
|
|
render(<ImageList list={list} />)
|
|
|
|
await user.click(screen.getByRole('img'))
|
|
|
|
const preview = screen.getByTestId('image-preview-container')
|
|
expect(preview).toBeInTheDocument()
|
|
})
|
|
|
|
it('should not open image preview when clicking an in-progress image', async () => {
|
|
const user = userEvent.setup()
|
|
const list = [createLocalFile({ _id: 'file-1', progress: 50 })]
|
|
render(<ImageList list={list} />)
|
|
|
|
await user.click(screen.getByRole('img'))
|
|
|
|
expect(screen.queryByTestId('image-preview-container')).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('should close image preview when cancel is clicked', async () => {
|
|
const user = userEvent.setup()
|
|
const list = [createRemoteFile({ _id: 'file-1', progress: 100 })]
|
|
render(<ImageList list={list} />)
|
|
|
|
// Open preview
|
|
await user.click(screen.getByRole('img'))
|
|
expect(screen.queryByTestId('image-preview-container')).toBeInTheDocument()
|
|
|
|
// Close preview
|
|
const closeButton = screen.getByTestId('image-preview-close-button')
|
|
await user.click(closeButton)
|
|
expect(screen.queryByTestId('image-preview-container')).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('should open preview with base64Url for completed local file', async () => {
|
|
const user = userEvent.setup()
|
|
const list = [createLocalFile({ _id: 'file-1', progress: 100, base64Url: 'data:image/png;base64,localdata' })]
|
|
render(<ImageList list={list} />)
|
|
|
|
await user.click(screen.getByRole('img'))
|
|
|
|
const previewImage = screen.getByTestId('image-preview-image')
|
|
expect(previewImage).toBeInTheDocument()
|
|
expect(previewImage).toHaveAttribute('src', 'data:image/png;base64,localdata')
|
|
})
|
|
})
|
|
|
|
describe('Image Load Events', () => {
|
|
it('should call onImageLinkLoadSuccess for remote URL on load when progress is not -1', () => {
|
|
const onImageLinkLoadSuccess = vi.fn()
|
|
const list = [createRemoteFile({ _id: 'file-1', progress: 0 })]
|
|
render(<ImageList list={list} onImageLinkLoadSuccess={onImageLinkLoadSuccess} />)
|
|
|
|
const img = screen.getByRole('img')
|
|
fireEvent.load(img)
|
|
expect(onImageLinkLoadSuccess).toHaveBeenCalledWith('file-1')
|
|
})
|
|
|
|
it('should not call onImageLinkLoadSuccess for remote URL when progress is -1', () => {
|
|
const onImageLinkLoadSuccess = vi.fn()
|
|
const list = [createRemoteFile({ _id: 'file-1', progress: -1 })]
|
|
render(<ImageList list={list} onImageLinkLoadSuccess={onImageLinkLoadSuccess} />)
|
|
|
|
const img = screen.getByRole('img')
|
|
fireEvent.load(img)
|
|
|
|
expect(onImageLinkLoadSuccess).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('should not call onImageLinkLoadSuccess for local file type', () => {
|
|
const onImageLinkLoadSuccess = vi.fn()
|
|
const list = [createLocalFile({ _id: 'file-1', progress: 50 })]
|
|
render(<ImageList list={list} onImageLinkLoadSuccess={onImageLinkLoadSuccess} />)
|
|
|
|
const img = screen.getByRole('img')
|
|
fireEvent.load(img)
|
|
|
|
expect(onImageLinkLoadSuccess).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('should call onImageLinkLoadError for remote URL on error', () => {
|
|
const onImageLinkLoadError = vi.fn()
|
|
const list = [createRemoteFile({ _id: 'file-1', progress: 0 })]
|
|
render(<ImageList list={list} onImageLinkLoadError={onImageLinkLoadError} />)
|
|
|
|
const img = screen.getByRole('img')
|
|
fireEvent.error(img)
|
|
|
|
expect(onImageLinkLoadError).toHaveBeenCalledWith('file-1')
|
|
})
|
|
|
|
it('should not call onImageLinkLoadError for local file type', () => {
|
|
const onImageLinkLoadError = vi.fn()
|
|
const list = [createLocalFile({ _id: 'file-1', progress: 50 })]
|
|
render(<ImageList list={list} onImageLinkLoadError={onImageLinkLoadError} />)
|
|
|
|
const img = screen.getByRole('img')
|
|
fireEvent.error(img)
|
|
|
|
expect(onImageLinkLoadError).not.toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
describe('Edge Cases', () => {
|
|
it('should handle list with mixed local and remote files', () => {
|
|
const list = [
|
|
createLocalFile({ _id: 'local-1' }),
|
|
createRemoteFile({ _id: 'remote-1' }),
|
|
]
|
|
render(<ImageList list={list} />)
|
|
|
|
expect(screen.getAllByRole('img')).toHaveLength(2)
|
|
})
|
|
|
|
it('should handle item without file property for alt attribute', () => {
|
|
const list = [createLocalFile({ _id: 'file-1', file: undefined })]
|
|
render(<ImageList list={list} />)
|
|
|
|
const img = screen.getByRole('img')
|
|
expect(img).toBeInTheDocument()
|
|
})
|
|
|
|
it('should handle onRemove not provided gracefully', async () => {
|
|
const user = userEvent.setup()
|
|
const list = [createLocalFile({ _id: 'file-1' })]
|
|
render(<ImageList list={list} />)
|
|
|
|
// Button exists, clicking it should not throw
|
|
await user.click(screen.getByRole('button'))
|
|
})
|
|
})
|
|
})
|