/** * Component tests for upload components * Tests FilePicker, DropZone, ProgressBar, and ErrorDisplay Svelte components */ import { render, fireEvent, screen, waitFor } from '@testing-library/svelte'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import FilePicker from '$lib/components/upload/FilePicker.svelte'; import DropZone from '$lib/components/upload/DropZone.svelte'; import ProgressBar from '$lib/components/upload/ProgressBar.svelte'; import ErrorDisplay from '$lib/components/upload/ErrorDisplay.svelte'; import type { ImageUploadProgress } from '$lib/types/images'; // Mock the image store functions vi.mock('$lib/stores/images', () => ({ uploadSingleImage: vi.fn(), uploadZipFile: vi.fn(), uploadProgress: { update: vi.fn(), }, })); describe('FilePicker', () => { let uploadSingleImage: ReturnType; let uploadZipFile: ReturnType; beforeEach(async () => { const imageStore = await import('$lib/stores/images'); uploadSingleImage = imageStore.uploadSingleImage; uploadZipFile = imageStore.uploadZipFile; vi.clearAllMocks(); }); describe('Rendering', () => { it('renders the file picker button', () => { render(FilePicker); const button = screen.getByRole('button', { name: /choose files/i }); expect(button).toBeInTheDocument(); expect(button).not.toBeDisabled(); }); it('renders with custom accept attribute', () => { render(FilePicker, { props: { accept: 'image/png,.jpg' } }); const button = screen.getByRole('button'); expect(button).toBeInTheDocument(); }); it('renders with multiple attribute by default', () => { const { container } = render(FilePicker); const fileInput = container.querySelector('input[type="file"]'); expect(fileInput).toHaveAttribute('multiple'); }); it('can disable multiple file selection', () => { const { container } = render(FilePicker, { props: { multiple: false } }); const fileInput = container.querySelector('input[type="file"]'); expect(fileInput).not.toHaveAttribute('multiple'); }); it('hides the file input element', () => { const { container } = render(FilePicker); const fileInput = container.querySelector('input[type="file"]') as HTMLElement; expect(fileInput).toHaveStyle({ display: 'none' }); }); }); describe('File Selection', () => { it('opens file picker when button is clicked', async () => { const { container } = render(FilePicker); const button = screen.getByRole('button', { name: /choose files/i }); const fileInput = container.querySelector('input[type="file"]') as HTMLInputElement; const clickSpy = vi.fn(); fileInput.click = clickSpy; await fireEvent.click(button); expect(clickSpy).toHaveBeenCalledTimes(1); }); it('handles single image file upload', async () => { uploadSingleImage.mockResolvedValue({ success: true }); const { container, component } = render(FilePicker); const uploadCompleteHandler = vi.fn(); component.$on('upload-complete', uploadCompleteHandler); const fileInput = container.querySelector('input[type="file"]') as HTMLInputElement; const file = new File(['image content'], 'test.jpg', { type: 'image/jpeg' }); await fireEvent.change(fileInput, { target: { files: [file] } }); await waitFor(() => { expect(uploadSingleImage).toHaveBeenCalledWith(file); }); expect(uploadCompleteHandler).toHaveBeenCalledTimes(1); expect(uploadCompleteHandler.mock.calls[0][0].detail).toEqual({ fileCount: 1 }); }); it('handles multiple image file uploads', async () => { uploadSingleImage.mockResolvedValue({ success: true }); const { container, component } = render(FilePicker); const uploadCompleteHandler = vi.fn(); component.$on('upload-complete', uploadCompleteHandler); const fileInput = container.querySelector('input[type="file"]') as HTMLInputElement; const files = [ new File(['image1'], 'test1.jpg', { type: 'image/jpeg' }), new File(['image2'], 'test2.png', { type: 'image/png' }), new File(['image3'], 'test3.gif', { type: 'image/gif' }), ]; await fireEvent.change(fileInput, { target: { files } }); await waitFor(() => { expect(uploadSingleImage).toHaveBeenCalledTimes(3); }); expect(uploadCompleteHandler).toHaveBeenCalledTimes(1); expect(uploadCompleteHandler.mock.calls[0][0].detail).toEqual({ fileCount: 3 }); }); it('handles ZIP file upload', async () => { uploadZipFile.mockResolvedValue({ success: true }); const { container, component } = render(FilePicker); const uploadCompleteHandler = vi.fn(); component.$on('upload-complete', uploadCompleteHandler); const fileInput = container.querySelector('input[type="file"]') as HTMLInputElement; const file = new File(['zip content'], 'images.zip', { type: 'application/zip' }); await fireEvent.change(fileInput, { target: { files: [file] } }); await waitFor(() => { expect(uploadZipFile).toHaveBeenCalledWith(file); }); expect(uploadSingleImage).not.toHaveBeenCalled(); expect(uploadCompleteHandler).toHaveBeenCalledTimes(1); }); it('handles mixed image and ZIP file uploads', async () => { uploadSingleImage.mockResolvedValue({ success: true }); uploadZipFile.mockResolvedValue({ success: true }); const { container, component } = render(FilePicker); const uploadCompleteHandler = vi.fn(); component.$on('upload-complete', uploadCompleteHandler); const fileInput = container.querySelector('input[type="file"]') as HTMLInputElement; const files = [ new File(['image'], 'test.jpg', { type: 'image/jpeg' }), new File(['zip'], 'archive.zip', { type: 'application/zip' }), new File(['image'], 'test.png', { type: 'image/png' }), ]; await fireEvent.change(fileInput, { target: { files } }); await waitFor(() => { expect(uploadSingleImage).toHaveBeenCalledTimes(2); expect(uploadZipFile).toHaveBeenCalledTimes(1); }); expect(uploadCompleteHandler).toHaveBeenCalledTimes(1); expect(uploadCompleteHandler.mock.calls[0][0].detail).toEqual({ fileCount: 3 }); }); it('resets file input after upload', async () => { uploadSingleImage.mockResolvedValue({ success: true }); const { container } = render(FilePicker); const fileInput = container.querySelector('input[type="file"]') as HTMLInputElement; const file = new File(['image'], 'test.jpg', { type: 'image/jpeg' }); await fireEvent.change(fileInput, { target: { files: [file] } }); await waitFor(() => { expect(uploadSingleImage).toHaveBeenCalled(); }); expect(fileInput.value).toBe(''); }); }); describe('Loading State', () => { it('shows loading state during upload', async () => { uploadSingleImage.mockImplementation( () => new Promise((resolve) => setTimeout(() => resolve({ success: true }), 100)) ); const { container } = render(FilePicker); const button = screen.getByRole('button'); const fileInput = container.querySelector('input[type="file"]') as HTMLInputElement; const file = new File(['image'], 'test.jpg', { type: 'image/jpeg' }); await fireEvent.change(fileInput, { target: { files: [file] } }); // During upload expect(button).toBeDisabled(); expect(screen.getByText(/uploading/i)).toBeInTheDocument(); // Wait for upload to complete await waitFor(() => { expect(button).not.toBeDisabled(); }); expect(screen.queryByText(/uploading/i)).not.toBeInTheDocument(); }); it('disables button during upload', async () => { uploadSingleImage.mockImplementation( () => new Promise((resolve) => setTimeout(() => resolve({ success: true }), 100)) ); const { container } = render(FilePicker); const button = screen.getByRole('button'); const fileInput = container.querySelector('input[type="file"]') as HTMLInputElement; const file = new File(['image'], 'test.jpg', { type: 'image/jpeg' }); expect(button).not.toBeDisabled(); await fireEvent.change(fileInput, { target: { files: [file] } }); expect(button).toBeDisabled(); await waitFor(() => { expect(button).not.toBeDisabled(); }); }); it('shows spinner during upload', async () => { uploadSingleImage.mockImplementation( () => new Promise((resolve) => setTimeout(() => resolve({ success: true }), 100)) ); const { container } = render(FilePicker); const fileInput = container.querySelector('input[type="file"]') as HTMLInputElement; const file = new File(['image'], 'test.jpg', { type: 'image/jpeg' }); await fireEvent.change(fileInput, { target: { files: [file] } }); const spinner = container.querySelector('.spinner'); expect(spinner).toBeInTheDocument(); await waitFor(() => { expect(container.querySelector('.spinner')).not.toBeInTheDocument(); }); }); }); describe('Error Handling', () => { it('dispatches upload-error event on upload failure', async () => { uploadSingleImage.mockRejectedValue(new Error('Upload failed')); const { container, component } = render(FilePicker); const uploadErrorHandler = vi.fn(); component.$on('upload-error', uploadErrorHandler); const fileInput = container.querySelector('input[type="file"]') as HTMLInputElement; const file = new File(['image'], 'test.jpg', { type: 'image/jpeg' }); await fireEvent.change(fileInput, { target: { files: [file] } }); await waitFor(() => { expect(uploadErrorHandler).toHaveBeenCalledTimes(1); }); expect(uploadErrorHandler.mock.calls[0][0].detail).toEqual({ error: 'Upload failed' }); }); it('re-enables button after error', async () => { uploadSingleImage.mockRejectedValue(new Error('Upload failed')); const { container } = render(FilePicker); const button = screen.getByRole('button'); const fileInput = container.querySelector('input[type="file"]') as HTMLInputElement; const file = new File(['image'], 'test.jpg', { type: 'image/jpeg' }); await fireEvent.change(fileInput, { target: { files: [file] } }); await waitFor(() => { expect(button).not.toBeDisabled(); }); }); it('handles no files selected gracefully', async () => { const { container } = render(FilePicker); const fileInput = container.querySelector('input[type="file"]') as HTMLInputElement; await fireEvent.change(fileInput, { target: { files: null } }); expect(uploadSingleImage).not.toHaveBeenCalled(); expect(uploadZipFile).not.toHaveBeenCalled(); }); }); }); describe('DropZone', () => { let uploadSingleImage: ReturnType; let uploadZipFile: ReturnType; beforeEach(async () => { const imageStore = await import('$lib/stores/images'); uploadSingleImage = imageStore.uploadSingleImage; uploadZipFile = imageStore.uploadZipFile; vi.clearAllMocks(); }); describe('Rendering', () => { it('renders the drop zone', () => { render(DropZone); expect(screen.getByText(/drag and drop images here/i)).toBeInTheDocument(); expect(screen.getByText(/or use the file picker above/i)).toBeInTheDocument(); }); it('shows default state initially', () => { const { container } = render(DropZone); const dropZone = container.querySelector('.drop-zone'); expect(dropZone).not.toHaveClass('dragging'); expect(dropZone).not.toHaveClass('uploading'); }); }); describe('Drag and Drop', () => { it('shows dragging state on drag enter', async () => { const { container } = render(DropZone); const dropZone = container.querySelector('.drop-zone') as HTMLElement; await fireEvent.dragEnter(dropZone); expect(dropZone).toHaveClass('dragging'); expect(screen.getByText(/drop files here/i)).toBeInTheDocument(); }); it('removes dragging state on drag leave', async () => { const { container } = render(DropZone); const dropZone = container.querySelector('.drop-zone') as HTMLElement; await fireEvent.dragEnter(dropZone); expect(dropZone).toHaveClass('dragging'); await fireEvent.dragLeave(dropZone); expect(dropZone).not.toHaveClass('dragging'); }); it('handles drag over event', async () => { const { container } = render(DropZone); const dropZone = container.querySelector('.drop-zone') as HTMLElement; const dragOverEvent = new Event('dragover', { bubbles: true, cancelable: true }); const preventDefaultSpy = vi.spyOn(dragOverEvent, 'preventDefault'); dropZone.dispatchEvent(dragOverEvent); expect(preventDefaultSpy).toHaveBeenCalled(); }); it('handles single image file drop', async () => { uploadSingleImage.mockResolvedValue({ success: true }); const { container, component } = render(DropZone); const uploadCompleteHandler = vi.fn(); component.$on('upload-complete', uploadCompleteHandler); const dropZone = container.querySelector('.drop-zone') as HTMLElement; const file = new File(['image'], 'test.jpg', { type: 'image/jpeg' }); const dropEvent = new DragEvent('drop', { bubbles: true, cancelable: true, dataTransfer: new DataTransfer(), }); Object.defineProperty(dropEvent, 'dataTransfer', { value: { files: [file], }, }); await fireEvent(dropZone, dropEvent); await waitFor(() => { expect(uploadSingleImage).toHaveBeenCalledWith(file); }); expect(uploadCompleteHandler).toHaveBeenCalledTimes(1); expect(uploadCompleteHandler.mock.calls[0][0].detail).toEqual({ fileCount: 1 }); }); it('handles multiple image files drop', async () => { uploadSingleImage.mockResolvedValue({ success: true }); const { container, component } = render(DropZone); const uploadCompleteHandler = vi.fn(); component.$on('upload-complete', uploadCompleteHandler); const dropZone = container.querySelector('.drop-zone') as HTMLElement; const files = [ new File(['image1'], 'test1.jpg', { type: 'image/jpeg' }), new File(['image2'], 'test2.png', { type: 'image/png' }), ]; const dropEvent = new DragEvent('drop', { bubbles: true, cancelable: true }); Object.defineProperty(dropEvent, 'dataTransfer', { value: { files }, }); await fireEvent(dropZone, dropEvent); await waitFor(() => { expect(uploadSingleImage).toHaveBeenCalledTimes(2); }); expect(uploadCompleteHandler).toHaveBeenCalledTimes(1); }); it('handles ZIP file drop', async () => { uploadZipFile.mockResolvedValue({ success: true }); const { container, component } = render(DropZone); const uploadCompleteHandler = vi.fn(); component.$on('upload-complete', uploadCompleteHandler); const dropZone = container.querySelector('.drop-zone') as HTMLElement; const file = new File(['zip'], 'images.zip', { type: 'application/zip' }); const dropEvent = new DragEvent('drop', { bubbles: true, cancelable: true }); Object.defineProperty(dropEvent, 'dataTransfer', { value: { files: [file] }, }); await fireEvent(dropZone, dropEvent); await waitFor(() => { expect(uploadZipFile).toHaveBeenCalledWith(file); }); expect(uploadSingleImage).not.toHaveBeenCalled(); }); it('filters out invalid file types', async () => { const { container, component } = render(DropZone, { props: { accept: 'image/*,.zip' } }); const uploadErrorHandler = vi.fn(); component.$on('upload-error', uploadErrorHandler); const dropZone = container.querySelector('.drop-zone') as HTMLElement; const files = [new File(['text'], 'document.txt', { type: 'text/plain' })]; const dropEvent = new DragEvent('drop', { bubbles: true, cancelable: true }); Object.defineProperty(dropEvent, 'dataTransfer', { value: { files }, }); await fireEvent(dropZone, dropEvent); await waitFor(() => { expect(uploadErrorHandler).toHaveBeenCalledTimes(1); }); expect(uploadErrorHandler.mock.calls[0][0].detail).toEqual({ error: 'No valid image files found', }); expect(uploadSingleImage).not.toHaveBeenCalled(); expect(uploadZipFile).not.toHaveBeenCalled(); }); it('removes dragging state after drop', async () => { uploadSingleImage.mockResolvedValue({ success: true }); const { container } = render(DropZone); const dropZone = container.querySelector('.drop-zone') as HTMLElement; await fireEvent.dragEnter(dropZone); expect(dropZone).toHaveClass('dragging'); const file = new File(['image'], 'test.jpg', { type: 'image/jpeg' }); const dropEvent = new DragEvent('drop', { bubbles: true, cancelable: true }); Object.defineProperty(dropEvent, 'dataTransfer', { value: { files: [file] }, }); await fireEvent(dropZone, dropEvent); expect(dropZone).not.toHaveClass('dragging'); }); }); describe('Loading State', () => { it('shows uploading state during upload', async () => { uploadSingleImage.mockImplementation( () => new Promise((resolve) => setTimeout(() => resolve({ success: true }), 100)) ); const { container } = render(DropZone); const dropZone = container.querySelector('.drop-zone') as HTMLElement; const file = new File(['image'], 'test.jpg', { type: 'image/jpeg' }); const dropEvent = new DragEvent('drop', { bubbles: true, cancelable: true }); Object.defineProperty(dropEvent, 'dataTransfer', { value: { files: [file] }, }); await fireEvent(dropZone, dropEvent); expect(dropZone).toHaveClass('uploading'); expect(screen.getByText(/uploading/i)).toBeInTheDocument(); await waitFor(() => { expect(dropZone).not.toHaveClass('uploading'); }); }); it('shows spinner during upload', async () => { uploadSingleImage.mockImplementation( () => new Promise((resolve) => setTimeout(() => resolve({ success: true }), 100)) ); const { container } = render(DropZone); const dropZone = container.querySelector('.drop-zone') as HTMLElement; const file = new File(['image'], 'test.jpg', { type: 'image/jpeg' }); const dropEvent = new DragEvent('drop', { bubbles: true, cancelable: true }); Object.defineProperty(dropEvent, 'dataTransfer', { value: { files: [file] }, }); await fireEvent(dropZone, dropEvent); const spinner = container.querySelector('.spinner-large'); expect(spinner).toBeInTheDocument(); await waitFor(() => { expect(container.querySelector('.spinner-large')).not.toBeInTheDocument(); }); }); }); describe('Error Handling', () => { it('dispatches upload-error event on upload failure', async () => { uploadSingleImage.mockRejectedValue(new Error('Network error')); const { container, component } = render(DropZone); const uploadErrorHandler = vi.fn(); component.$on('upload-error', uploadErrorHandler); const dropZone = container.querySelector('.drop-zone') as HTMLElement; const file = new File(['image'], 'test.jpg', { type: 'image/jpeg' }); const dropEvent = new DragEvent('drop', { bubbles: true, cancelable: true }); Object.defineProperty(dropEvent, 'dataTransfer', { value: { files: [file] }, }); await fireEvent(dropZone, dropEvent); await waitFor(() => { expect(uploadErrorHandler).toHaveBeenCalledTimes(1); }); expect(uploadErrorHandler.mock.calls[0][0].detail).toEqual({ error: 'Network error' }); }); it('returns to normal state after error', async () => { uploadSingleImage.mockRejectedValue(new Error('Upload failed')); const { container } = render(DropZone); const dropZone = container.querySelector('.drop-zone') as HTMLElement; const file = new File(['image'], 'test.jpg', { type: 'image/jpeg' }); const dropEvent = new DragEvent('drop', { bubbles: true, cancelable: true }); Object.defineProperty(dropEvent, 'dataTransfer', { value: { files: [file] }, }); await fireEvent(dropZone, dropEvent); await waitFor(() => { expect(dropZone).not.toHaveClass('uploading'); }); }); it('handles drop event with no files', async () => { const { container } = render(DropZone); const dropZone = container.querySelector('.drop-zone') as HTMLElement; const dropEvent = new DragEvent('drop', { bubbles: true, cancelable: true }); Object.defineProperty(dropEvent, 'dataTransfer', { value: { files: null }, }); await fireEvent(dropZone, dropEvent); expect(uploadSingleImage).not.toHaveBeenCalled(); expect(uploadZipFile).not.toHaveBeenCalled(); }); }); }); describe('ProgressBar', () => { describe('Rendering', () => { it('renders progress item with filename', () => { const item: ImageUploadProgress = { filename: 'test-image.jpg', status: 'uploading', progress: 50, }; render(ProgressBar, { props: { item } }); expect(screen.getByText('test-image.jpg')).toBeInTheDocument(); }); it('shows progress bar for uploading status', () => { const item: ImageUploadProgress = { filename: 'test.jpg', status: 'uploading', progress: 75, }; const { container } = render(ProgressBar, { props: { item } }); expect(screen.getByText('75%')).toBeInTheDocument(); const progressBar = container.querySelector('.progress-bar-fill') as HTMLElement; expect(progressBar).toHaveStyle({ width: '75%' }); }); it('shows progress bar for processing status', () => { const item: ImageUploadProgress = { filename: 'test.jpg', status: 'processing', progress: 90, }; render(ProgressBar, { props: { item } }); expect(screen.getByText('90%')).toBeInTheDocument(); }); it('shows success message for complete status', () => { const item: ImageUploadProgress = { filename: 'test.jpg', status: 'complete', progress: 100, }; render(ProgressBar, { props: { item } }); expect(screen.getByText(/upload complete/i)).toBeInTheDocument(); expect(screen.queryByText(/\d+%/)).not.toBeInTheDocument(); }); it('shows error message for error status', () => { const item: ImageUploadProgress = { filename: 'test.jpg', status: 'error', progress: 0, error: 'File too large', }; render(ProgressBar, { props: { item } }); expect(screen.getByText('File too large')).toBeInTheDocument(); }); it('shows close button for complete status', () => { const item: ImageUploadProgress = { filename: 'test.jpg', status: 'complete', progress: 100, }; render(ProgressBar, { props: { item } }); const closeButton = screen.getByRole('button', { name: /remove/i }); expect(closeButton).toBeInTheDocument(); }); it('shows close button for error status', () => { const item: ImageUploadProgress = { filename: 'test.jpg', status: 'error', progress: 0, error: 'Failed', }; render(ProgressBar, { props: { item } }); const closeButton = screen.getByRole('button', { name: /remove/i }); expect(closeButton).toBeInTheDocument(); }); it('hides close button for uploading status', () => { const item: ImageUploadProgress = { filename: 'test.jpg', status: 'uploading', progress: 50, }; render(ProgressBar, { props: { item } }); const closeButton = screen.queryByRole('button', { name: /remove/i }); expect(closeButton).not.toBeInTheDocument(); }); }); describe('Status Icons', () => { it('shows correct icon for uploading status', () => { const item: ImageUploadProgress = { filename: 'test.jpg', status: 'uploading', progress: 50, }; const { container } = render(ProgressBar, { props: { item } }); const statusIcon = container.querySelector('.status-icon'); expect(statusIcon).toHaveTextContent('⟳'); }); it('shows correct icon for processing status', () => { const item: ImageUploadProgress = { filename: 'test.jpg', status: 'processing', progress: 90, }; const { container } = render(ProgressBar, { props: { item } }); const statusIcon = container.querySelector('.status-icon'); expect(statusIcon).toHaveTextContent('⟳'); }); it('shows correct icon for complete status', () => { const item: ImageUploadProgress = { filename: 'test.jpg', status: 'complete', progress: 100, }; const { container } = render(ProgressBar, { props: { item } }); const statusIcon = container.querySelector('.status-icon'); expect(statusIcon).toHaveTextContent('✓'); }); it('shows correct icon for error status', () => { const item: ImageUploadProgress = { filename: 'test.jpg', status: 'error', progress: 0, error: 'Failed', }; const { container } = render(ProgressBar, { props: { item } }); const statusIcon = container.querySelector('.status-icon'); expect(statusIcon).toHaveTextContent('✗'); }); }); describe('Remove Functionality', () => { it('removes item from store when close button is clicked', async () => { const imageStore = await import('$lib/stores/images'); const updateFn = vi.fn((callback) => callback([])); imageStore.uploadProgress.update = updateFn; const item: ImageUploadProgress = { filename: 'test.jpg', status: 'complete', progress: 100, }; render(ProgressBar, { props: { item } }); const closeButton = screen.getByRole('button', { name: /remove/i }); await fireEvent.click(closeButton); expect(updateFn).toHaveBeenCalled(); }); }); describe('Progress Display', () => { it('shows progress percentage correctly', () => { const testCases = [0, 25, 50, 75, 100]; testCases.forEach((progress) => { const item: ImageUploadProgress = { filename: 'test.jpg', status: 'uploading', progress, }; const { unmount, container } = render(ProgressBar, { props: { item } }); expect(screen.getByText(`${progress}%`)).toBeInTheDocument(); const progressBar = container.querySelector('.progress-bar-fill') as HTMLElement; expect(progressBar).toHaveStyle({ width: `${progress}%` }); unmount(); }); }); it('truncates long filenames', () => { const item: ImageUploadProgress = { filename: 'very-long-filename-that-should-be-truncated-with-ellipsis.jpg', status: 'uploading', progress: 50, }; const { container } = render(ProgressBar, { props: { item } }); const filenameElement = container.querySelector('.filename') as HTMLElement; expect(filenameElement).toHaveStyle({ overflow: 'hidden', 'text-overflow': 'ellipsis', 'white-space': 'nowrap', }); }); }); }); describe('ErrorDisplay', () => { describe('Rendering', () => { it('renders error message', () => { render(ErrorDisplay, { props: { error: 'Upload failed' } }); expect(screen.getByText('Upload failed')).toBeInTheDocument(); }); it('renders with error icon', () => { const { container } = render(ErrorDisplay, { props: { error: 'Test error' } }); const icon = container.querySelector('.error-icon svg'); expect(icon).toBeInTheDocument(); }); it('has proper ARIA role', () => { render(ErrorDisplay, { props: { error: 'Test error' } }); const errorDisplay = screen.getByRole('alert'); expect(errorDisplay).toBeInTheDocument(); }); it('shows dismiss button by default', () => { render(ErrorDisplay, { props: { error: 'Test error' } }); const dismissButton = screen.getByRole('button', { name: /dismiss error/i }); expect(dismissButton).toBeInTheDocument(); }); it('hides dismiss button when dismissible is false', () => { render(ErrorDisplay, { props: { error: 'Test error', dismissible: false } }); const dismissButton = screen.queryByRole('button', { name: /dismiss error/i }); expect(dismissButton).not.toBeInTheDocument(); }); }); describe('Dismiss Functionality', () => { it('dispatches dismiss event when button is clicked', async () => { const { component } = render(ErrorDisplay, { props: { error: 'Test error' } }); const dismissHandler = vi.fn(); component.$on('dismiss', dismissHandler); const dismissButton = screen.getByRole('button', { name: /dismiss error/i }); await fireEvent.click(dismissButton); expect(dismissHandler).toHaveBeenCalledTimes(1); }); it('does not dispatch dismiss event when dismissible is false', () => { const { component } = render(ErrorDisplay, { props: { error: 'Test error', dismissible: false }, }); const dismissHandler = vi.fn(); component.$on('dismiss', dismissHandler); // No dismiss button should exist const dismissButton = screen.queryByRole('button', { name: /dismiss error/i }); expect(dismissButton).not.toBeInTheDocument(); }); }); describe('Error Messages', () => { it('handles short error messages', () => { render(ErrorDisplay, { props: { error: 'Error' } }); expect(screen.getByText('Error')).toBeInTheDocument(); }); it('handles long error messages', () => { const longError = 'This is a very long error message that contains detailed information about what went wrong during the upload process. It should be displayed correctly with proper line wrapping.'; render(ErrorDisplay, { props: { error: longError } }); expect(screen.getByText(longError)).toBeInTheDocument(); }); it('handles error messages with special characters', () => { const errorWithSpecialChars = "File 'test.jpg' couldn't be uploaded: size > 50MB"; render(ErrorDisplay, { props: { error: errorWithSpecialChars } }); expect(screen.getByText(errorWithSpecialChars)).toBeInTheDocument(); }); it('handles empty error messages', () => { render(ErrorDisplay, { props: { error: '' } }); const errorMessage = screen.getByRole('alert'); expect(errorMessage).toBeInTheDocument(); }); }); describe('Styling', () => { it('applies error styling classes', () => { const { container } = render(ErrorDisplay, { props: { error: 'Test error' } }); const errorDisplay = container.querySelector('.error-display'); expect(errorDisplay).toBeInTheDocument(); expect(errorDisplay).toHaveClass('error-display'); }); it('has proper visual hierarchy', () => { const { container } = render(ErrorDisplay, { props: { error: 'Test error' } }); const errorIcon = container.querySelector('.error-icon'); const errorContent = container.querySelector('.error-content'); const dismissButton = container.querySelector('.dismiss-button'); expect(errorIcon).toBeInTheDocument(); expect(errorContent).toBeInTheDocument(); expect(dismissButton).toBeInTheDocument(); }); }); });