import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { render, screen, waitFor, fireEvent } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { TrackUpload } from './TrackUpload'; import { uploadTrack, getUploadProgress, TrackUploadError, } from '../services/trackService'; import { useToast } from '@/hooks/useToast'; // Mock dependencies vi.mock('../services/trackService'); vi.mock('@/hooks/useToast'); vi.mock('../services/chunkedUploadService', () => ({ ChunkedUploadManager: vi.fn(), calculateTotalChunks: vi.fn((size) => Math.ceil(size / (5 * 1024 * 1024))), CHUNK_SIZE: 5 * 1024 * 1024, })); describe('TrackUpload', () => { const mockToast = { success: vi.fn(), error: vi.fn(), warning: vi.fn(), info: vi.fn(), toast: vi.fn(), }; beforeEach(() => { vi.clearAllMocks(); vi.mocked(useToast).mockReturnValue(mockToast); }); afterEach(() => { vi.restoreAllMocks(); }); it('should render upload area', () => { render(); expect( screen.getByText(/glissez-déposez un fichier audio/i), ).toBeInTheDocument(); expect(screen.getByText(/sélectionner un fichier/i)).toBeInTheDocument(); }); it('should validate file format', async () => { const user = userEvent.setup(); render(); const fileInput = document.querySelector( 'input[type="file"]', ) as HTMLInputElement; const invalidFile = new File(['test'], 'test.txt', { type: 'text/plain' }); Object.defineProperty(fileInput, 'files', { value: [invalidFile], writable: false, }); fireEvent.change(fileInput); await waitFor(() => { expect(mockToast.error).toHaveBeenCalledWith( expect.stringContaining('Format non supporté'), ); }); }); it('should validate file size', async () => { const user = userEvent.setup(); render(); const fileInput = document.querySelector( 'input[type="file"]', ) as HTMLInputElement; const largeFile = new File(['x'.repeat(101 * 1024 * 1024)], 'large.mp3', { type: 'audio/mpeg', }); Object.defineProperty(fileInput, 'files', { value: [largeFile], writable: false, }); fireEvent.change(fileInput); await waitFor(() => { expect(mockToast.error).toHaveBeenCalledWith( expect.stringContaining('Fichier trop volumineux'), ); }); }); it('should upload valid file successfully', async () => { const mockTrack = { id: 1, user_id: 123, title: 'test', artist: 'Test Artist', duration: 180, file_path: '/uploads/tracks/test.mp3', file_size: 1024, format: 'MP3', is_public: true, play_count: 0, like_count: 0, status: 'uploading' as const, created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-01T00:00:00Z', }; vi.mocked(uploadTrack).mockResolvedValue(mockTrack); vi.mocked(getUploadProgress).mockResolvedValue({ track_id: 1, status: 'completed', progress: 100, message: 'Upload completed', }); render(); const fileInput = document.querySelector( 'input[type="file"]', ) as HTMLInputElement; const validFile = new File(['test audio'], 'test.mp3', { type: 'audio/mpeg', }); Object.defineProperty(fileInput, 'files', { value: [validFile], writable: false, }); fireEvent.change(fileInput); await waitFor(() => { expect(uploadTrack).toHaveBeenCalledWith(validFile, expect.any(Function)); }); // Wait for polling to complete await waitFor( () => { expect(getUploadProgress).toHaveBeenCalled(); }, { timeout: 3000 }, ); }); it('should show file preview after selection', async () => { render(); const fileInput = document.querySelector( 'input[type="file"]', ) as HTMLInputElement; const validFile = new File(['test audio'], 'test.mp3', { type: 'audio/mpeg', }); vi.mocked(uploadTrack).mockResolvedValue({ id: 1, user_id: 123, title: 'test', artist: '', duration: 0, file_path: '', file_size: 1024, format: 'MP3', is_public: true, play_count: 0, like_count: 0, status: 'uploading', created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-01T00:00:00Z', }); Object.defineProperty(fileInput, 'files', { value: [validFile], writable: false, }); fireEvent.change(fileInput); await waitFor(() => { expect(screen.getByText('test.mp3')).toBeInTheDocument(); }); }); it('should display progress bar during upload', async () => { const mockTrack = { id: 1, user_id: 123, title: 'test', artist: '', duration: 0, file_path: '', file_size: 1024, format: 'MP3', is_public: true, play_count: 0, like_count: 0, status: 'uploading' as const, created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-01T00:00:00Z', }; let progressCallback: ((progress: number) => void) | undefined; vi.mocked(uploadTrack).mockImplementation(async (file, onProgress) => { progressCallback = onProgress; // Simulate progress if (onProgress) { onProgress(50); } return mockTrack; }); render(); const fileInput = document.querySelector( 'input[type="file"]', ) as HTMLInputElement; const validFile = new File(['test audio'], 'test.mp3', { type: 'audio/mpeg', }); Object.defineProperty(fileInput, 'files', { value: [validFile], writable: false, }); fireEvent.change(fileInput); await waitFor(() => { expect(screen.getByRole('progressbar')).toBeInTheDocument(); }); }); it('should handle upload errors', async () => { const errorMessage = 'Upload failed'; vi.mocked(uploadTrack).mockRejectedValue(new Error(errorMessage)); render(); const fileInput = document.querySelector( 'input[type="file"]', ) as HTMLInputElement; const validFile = new File(['test audio'], 'test.mp3', { type: 'audio/mpeg', }); Object.defineProperty(fileInput, 'files', { value: [validFile], writable: false, }); fireEvent.change(fileInput); await waitFor(() => { expect(mockToast.error).toHaveBeenCalledWith(errorMessage); }); }); it('should call onUploadComplete callback when upload completes', async () => { const onUploadComplete = vi.fn(); const mockTrack = { id: 1, user_id: 123, title: 'test', artist: '', duration: 0, file_path: '', file_size: 1024, format: 'MP3', is_public: true, play_count: 0, like_count: 0, status: 'completed' as const, created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-01T00:00:00Z', }; vi.mocked(uploadTrack).mockResolvedValue(mockTrack); vi.mocked(getUploadProgress).mockResolvedValue({ track_id: 1, status: 'completed', progress: 100, message: 'Upload completed', }); render(); const fileInput = document.querySelector( 'input[type="file"]', ) as HTMLInputElement; const validFile = new File(['test audio'], 'test.mp3', { type: 'audio/mpeg', }); Object.defineProperty(fileInput, 'files', { value: [validFile], writable: false, }); fireEvent.change(fileInput); await waitFor( () => { expect(getUploadProgress).toHaveBeenCalled(); }, { timeout: 3000 }, ); // Wait for callback to be called await waitFor( () => { expect(onUploadComplete).toHaveBeenCalled(); }, { timeout: 5000 }, ); }); it('should display upload speed and time remaining during upload', async () => { const mockTrack = { id: 1, user_id: 123, title: 'test', artist: '', duration: 0, file_path: '', file_size: 10 * 1024 * 1024, // 10MB format: 'MP3', is_public: true, play_count: 0, like_count: 0, status: 'uploading' as const, created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-01T00:00:00Z', }; let progressCallback: ((progress: number) => void) | undefined; vi.mocked(uploadTrack).mockImplementation(async (file, onProgress) => { progressCallback = onProgress; // Simulate progress if (onProgress) { setTimeout(() => onProgress(50), 100); setTimeout(() => onProgress(100), 200); } return mockTrack; }); vi.mocked(getUploadProgress).mockResolvedValue({ track_id: 1, status: 'completed', progress: 100, message: 'Upload completed', }); render(); const fileInput = document.querySelector( 'input[type="file"]', ) as HTMLInputElement; const validFile = new File(['test audio'], 'test.mp3', { type: 'audio/mpeg', }); Object.defineProperty(validFile, 'size', { value: 10 * 1024 * 1024 }); // 10MB Object.defineProperty(fileInput, 'files', { value: [validFile], writable: false, }); fireEvent.change(fileInput); await waitFor(() => { expect(uploadTrack).toHaveBeenCalled(); }); // Wait for progress updates await waitFor( () => { // Check that progress bar is displayed expect(screen.getByRole('progressbar')).toBeInTheDocument(); }, { timeout: 500 }, ); }); it('should display current step (uploading/processing)', async () => { const mockTrack = { id: 1, user_id: 123, title: 'test', artist: '', duration: 0, file_path: '', file_size: 1024, format: 'MP3', is_public: true, play_count: 0, like_count: 0, status: 'processing' as const, created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-01T00:00:00Z', }; vi.mocked(uploadTrack).mockResolvedValue(mockTrack); vi.mocked(getUploadProgress).mockResolvedValue({ track_id: 1, status: 'processing', progress: 100, message: 'Processing track', }); render(); const fileInput = document.querySelector( 'input[type="file"]', ) as HTMLInputElement; const validFile = new File(['test audio'], 'test.mp3', { type: 'audio/mpeg', }); Object.defineProperty(fileInput, 'files', { value: [validFile], writable: false, }); fireEvent.change(fileInput); await waitFor( () => { expect(getUploadProgress).toHaveBeenCalled(); }, { timeout: 3000 }, ); }); it('should handle drag and drop', async () => { const mockTrack = { id: 1, user_id: 123, title: 'test', artist: '', duration: 0, file_path: '', file_size: 1024, format: 'MP3', is_public: true, play_count: 0, like_count: 0, status: 'uploading' as const, created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-01T00:00:00Z', }; vi.mocked(uploadTrack).mockResolvedValue(mockTrack); render(); const dropZone = screen .getByText(/glissez-déposez un fichier audio/i) .closest('div'); const validFile = new File(['test audio'], 'test.mp3', { type: 'audio/mpeg', }); fireEvent.dragOver(dropZone!, { dataTransfer: { files: [validFile], }, }); fireEvent.drop(dropZone!, { dataTransfer: { files: [validFile], }, }); await waitFor(() => { expect(uploadTrack).toHaveBeenCalled(); }); }); it('should validate empty file', async () => { render(); const fileInput = document.querySelector( 'input[type="file"]', ) as HTMLInputElement; const emptyFile = new File([], 'empty.mp3', { type: 'audio/mpeg' }); Object.defineProperty(emptyFile, 'size', { value: 0 }); Object.defineProperty(fileInput, 'files', { value: [emptyFile], writable: false, }); fireEvent.change(fileInput); await waitFor(() => { expect(mockToast.error).toHaveBeenCalledWith( expect.stringContaining('Le fichier est vide'), ); }); }); it('should handle TrackUploadError with retry', async () => { const networkError = new TrackUploadError('Erreur réseau', 'NETWORK', true); // First attempt fails, second succeeds vi.mocked(uploadTrack) .mockRejectedValueOnce(networkError) .mockResolvedValueOnce({ id: 1, user_id: 123, title: 'test', artist: '', duration: 0, file_path: '', file_size: 1024, format: 'MP3', is_public: true, play_count: 0, like_count: 0, status: 'uploading' as const, created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-01T00:00:00Z', }); vi.mocked(getUploadProgress).mockResolvedValue({ track_id: 1, status: 'completed', progress: 100, message: 'Upload completed', }); render(); const fileInput = document.querySelector( 'input[type="file"]', ) as HTMLInputElement; const validFile = new File(['test audio'], 'test.mp3', { type: 'audio/mpeg', }); Object.defineProperty(fileInput, 'files', { value: [validFile], writable: false, }); fireEvent.change(fileInput); // Wait for retry await waitFor( () => { expect(mockToast.warning).toHaveBeenCalled(); }, { timeout: 3000 }, ); // Eventually succeeds await waitFor( () => { expect(uploadTrack).toHaveBeenCalledTimes(2); }, { timeout: 5000 }, ); }); it('should handle TrackUploadError without retry (validation error)', async () => { const validationError = new TrackUploadError( 'Format invalide', 'VALIDATION', false, ); vi.mocked(uploadTrack).mockRejectedValue(validationError); render(); const fileInput = document.querySelector( 'input[type="file"]', ) as HTMLInputElement; const validFile = new File(['test audio'], 'test.mp3', { type: 'audio/mpeg', }); Object.defineProperty(fileInput, 'files', { value: [validFile], writable: false, }); fireEvent.change(fileInput); await waitFor(() => { expect(mockToast.error).toHaveBeenCalledWith( expect.stringContaining('Format invalide'), ); }); // Should not retry expect(uploadTrack).toHaveBeenCalledTimes(1); }); it('should display retry button after error', async () => { const error = new TrackUploadError('Erreur upload', 'SERVER', false); vi.mocked(uploadTrack).mockRejectedValue(error); render(); const fileInput = document.querySelector( 'input[type="file"]', ) as HTMLInputElement; const validFile = new File(['test audio'], 'test.mp3', { type: 'audio/mpeg', }); Object.defineProperty(fileInput, 'files', { value: [validFile], writable: false, }); fireEvent.change(fileInput); await waitFor(() => { expect(screen.getByText(/réessayer/i)).toBeInTheDocument(); }); }); it('should reset upload state', async () => { render(); const fileInput = document.querySelector( 'input[type="file"]', ) as HTMLInputElement; const validFile = new File(['test audio'], 'test.mp3', { type: 'audio/mpeg', }); vi.mocked(uploadTrack).mockResolvedValue({ id: 1, user_id: 123, title: 'test', artist: '', duration: 0, file_path: '', file_size: 1024, format: 'MP3', is_public: true, play_count: 0, like_count: 0, status: 'uploading' as const, created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-01T00:00:00Z', }); Object.defineProperty(fileInput, 'files', { value: [validFile], writable: false, }); fireEvent.change(fileInput); await waitFor(() => { expect(screen.getByText('test.mp3')).toBeInTheDocument(); }); // Find and click reset button const resetButton = screen.getByRole('button', { name: /x/i }); fireEvent.click(resetButton); await waitFor(() => { expect(screen.queryByText('test.mp3')).not.toBeInTheDocument(); }); }); it('should handle polling errors gracefully', async () => { const mockTrack = { id: 1, user_id: 123, title: 'test', artist: '', duration: 0, file_path: '', file_size: 1024, format: 'MP3', is_public: true, play_count: 0, like_count: 0, status: 'uploading' as const, created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-01T00:00:00Z', }; vi.mocked(uploadTrack).mockResolvedValue(mockTrack); vi.mocked(getUploadProgress).mockRejectedValue(new Error('Network error')); render(); const fileInput = document.querySelector( 'input[type="file"]', ) as HTMLInputElement; const validFile = new File(['test audio'], 'test.mp3', { type: 'audio/mpeg', }); Object.defineProperty(fileInput, 'files', { value: [validFile], writable: false, }); fireEvent.change(fileInput); // Should not crash even if polling fails await waitFor( () => { expect(getUploadProgress).toHaveBeenCalled(); }, { timeout: 3000 }, ); }); it('should handle failed status from polling', async () => { const mockTrack = { id: 1, user_id: 123, title: 'test', artist: '', duration: 0, file_path: '', file_size: 1024, format: 'MP3', is_public: true, play_count: 0, like_count: 0, status: 'uploading' as const, created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-01T00:00:00Z', }; vi.mocked(uploadTrack).mockResolvedValue(mockTrack); vi.mocked(getUploadProgress).mockResolvedValue({ track_id: 1, status: 'failed', progress: 0, message: 'Upload failed', }); render(); const fileInput = document.querySelector( 'input[type="file"]', ) as HTMLInputElement; const validFile = new File(['test audio'], 'test.mp3', { type: 'audio/mpeg', }); Object.defineProperty(fileInput, 'files', { value: [validFile], writable: false, }); fireEvent.change(fileInput); await waitFor( () => { expect(getUploadProgress).toHaveBeenCalled(); }, { timeout: 3000 }, ); // Should stop polling and show error await waitFor( () => { expect(screen.getByText(/upload failed/i)).toBeInTheDocument(); }, { timeout: 5000 }, ); }); });