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 => ({ 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 => ({ 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() 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() 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() 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() 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() 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() expect(screen.getByRole('button')).toBeInTheDocument() }) it('should not show remove buttons when readonly', () => { const list = [createLocalFile({ _id: 'file-1' })] render() 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() 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() 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() 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() // 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() 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() 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() 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() 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() 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() 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() // 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() 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() 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() 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() 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() 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() 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() 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() 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() // Button exists, clicking it should not throw await user.click(screen.getByRole('button')) }) }) })