This commit is contained in:
Danilo Reyes
2025-11-02 14:03:01 -06:00
parent f85ae4d417
commit 3700ba02ea
14 changed files with 3103 additions and 19 deletions

View File

@@ -0,0 +1,997 @@
/**
* 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<typeof vi.fn>;
let uploadZipFile: ReturnType<typeof vi.fn>;
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<typeof vi.fn>;
let uploadZipFile: ReturnType<typeof vi.fn>;
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();
});
});
});