import { describe, it, expect, vi, beforeEach } from 'vitest' import { render, screen, waitFor, fireEvent } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { MediaUpload } from './media-upload' import * as storage from '@/lib/storage' // Mock storage module vi.mock('@/lib/storage', () => ({ uploadFile: vi.fn(), formatFileSize: vi.fn((bytes) => `${bytes} bytes`), UPLOAD_CONFIGS: { video: { bucket: 'workout-videos', maxSize: 500 * 1024 * 1024, allowedTypes: ['video/mp4', 'video/webm', 'video/quicktime'], }, thumbnail: { bucket: 'workout-thumbnails', maxSize: 10 * 1024 * 1024, allowedTypes: ['image/jpeg', 'image/png', 'image/webp'], }, avatar: { bucket: 'trainer-avatars', maxSize: 5 * 1024 * 1024, allowedTypes: ['image/jpeg', 'image/png', 'image/webp'], }, }, })) describe('MediaUpload', () => { const mockOnChange = vi.fn() beforeEach(() => { vi.clearAllMocks() }) describe('rendering', () => { it('should render upload area for thumbnail', () => { render( ) expect(screen.getByText(/click or drag file to upload/i)).toBeInTheDocument() expect(screen.getByText(/10 mb/i)).toBeInTheDocument() }) it('should render upload area for video', () => { render( ) expect(screen.getByText(/click or drag file to upload/i)).toBeInTheDocument() expect(screen.getByText(/500 mb/i)).toBeInTheDocument() }) it('should render upload area for avatar', () => { render( ) expect(screen.getByText(/click or drag file to upload/i)).toBeInTheDocument() expect(screen.getByText(/5 mb/i)).toBeInTheDocument() }) it('should show image preview after successful upload', () => { render( ) const img = screen.getByRole('img') expect(img).toBeInTheDocument() expect(img).toHaveAttribute('src', 'https://example.com/image.jpg') }) it('should show video preview after upload', () => { render( ) const video = document.querySelector('video') expect(video).toBeInTheDocument() expect(video).toHaveAttribute('src', 'https://example.com/video.mp4') }) }) describe('file upload', () => { it('should handle file selection via click', async () => { vi.mocked(storage.uploadFile).mockResolvedValue({ url: 'https://example.com/image.jpg', path: 'temp/123.jpg', }) render( ) const input = document.querySelector('input[type="file"]') as HTMLInputElement const file = new File(['test'], 'test.jpg', { type: 'image/jpeg' }) await userEvent.upload(input, file) await waitFor(() => { expect(storage.uploadFile).toHaveBeenCalled() expect(mockOnChange).toHaveBeenCalledWith('https://example.com/image.jpg') }, { timeout: 10000 }) }, 15000) it('should show error for upload failure', async () => { vi.mocked(storage.uploadFile).mockRejectedValue({ message: 'Upload failed', code: 'UPLOAD_FAILED', } as storage.UploadError) render( ) const input = document.querySelector('input[type="file"]') as HTMLInputElement const file = new File(['test'], 'test.jpg', { type: 'image/jpeg' }) await userEvent.upload(input, file) await waitFor(() => { // Error should be displayed (check for any error text in the error container) const errorContainer = document.querySelector('[class*="bg-red-500"]') || document.querySelector('[class*="text-red"]') || screen.queryByText(/failed/i) || screen.queryByText(/error/i) expect(errorContainer).toBeTruthy() }, { timeout: 5000 }) }, 10000) it('should show error for file too large', async () => { vi.mocked(storage.uploadFile).mockRejectedValue({ message: 'File too large. Maximum size is 10 MB.', code: 'SIZE_EXCEEDED', } as storage.UploadError) render( ) const input = document.querySelector('input[type="file"]') as HTMLInputElement const file = new File(['test'], 'test.jpg', { type: 'image/jpeg' }) await userEvent.upload(input, file) await waitFor(() => { const errorElement = screen.getByText(/file too large/i) expect(errorElement).toBeInTheDocument() }, { timeout: 5000 }) }, 10000) it('should show upload progress', async () => { let resolveUpload: (value: { url: string; path: string }) => void const uploadPromise = new Promise<{ url: string; path: string }>((resolve) => { resolveUpload = resolve }) vi.mocked(storage.uploadFile).mockReturnValue(uploadPromise) render( ) const input = document.querySelector('input[type="file"]') as HTMLInputElement const file = new File(['test'], 'test.jpg', { type: 'image/jpeg' }) await userEvent.upload(input, file) expect(screen.getByText(/uploading/i)).toBeInTheDocument() resolveUpload!({ url: 'https://example.com/image.jpg', path: 'temp/123.jpg' }) await waitFor(() => { expect(mockOnChange).toHaveBeenCalledWith('https://example.com/image.jpg') }, { timeout: 5000 }) }, 10000) it('should clear error when uploading new file', async () => { vi.mocked(storage.uploadFile) .mockRejectedValueOnce({ message: 'First error', code: 'UPLOAD_FAILED', } as storage.UploadError) .mockResolvedValueOnce({ url: 'https://example.com/image.jpg', path: 'temp/123.jpg', }) render( ) const input = document.querySelector('input[type="file"]') as HTMLInputElement // First upload - error await userEvent.upload(input, new File(['test'], 'test1.jpg', { type: 'image/jpeg' })) await waitFor(() => { expect(screen.getByText(/first error/i)).toBeInTheDocument() }) // Second upload - success, error should be cleared await userEvent.upload(input, new File(['test'], 'test2.jpg', { type: 'image/jpeg' })) await waitFor(() => { expect(screen.queryByText(/first error/i)).not.toBeInTheDocument() }) }, 10000) }) describe('drag and drop', () => { it('should highlight on drag over', () => { render( ) const dropZone = screen.getByText(/click or drag file to upload/i).closest('[class*="relative"]') as HTMLElement expect(dropZone).not.toHaveClass('border-orange-500') fireEvent.dragOver(dropZone) expect(dropZone).toHaveClass('border-orange-500') }) it('should remove highlight on drag leave', () => { render( ) const dropZone = screen.getByText(/click or drag file to upload/i).closest('[class*="relative"]') as HTMLElement fireEvent.dragOver(dropZone) expect(dropZone).toHaveClass('border-orange-500') fireEvent.dragLeave(dropZone) expect(dropZone).not.toHaveClass('border-orange-500') }) it('should handle file drop', async () => { vi.mocked(storage.uploadFile).mockResolvedValue({ url: 'https://example.com/image.jpg', path: 'temp/123.jpg', }) render( ) const dropZone = screen.getByText(/click or drag file to upload/i).closest('[class*="relative"]') as HTMLElement const file = new File(['test'], 'test.jpg', { type: 'image/jpeg' }) fireEvent.drop(dropZone, { dataTransfer: { files: [file], }, }) await waitFor(() => { expect(storage.uploadFile).toHaveBeenCalled() }, { timeout: 5000 }) }, 10000) it('should not handle drop when disabled', () => { render( ) const dropZone = screen.getByText(/click or drag file to upload/i).closest('[class*="relative"]') as HTMLElement const file = new File(['test'], 'test.jpg', { type: 'image/jpeg' }) fireEvent.drop(dropZone, { dataTransfer: { files: [file], }, }) expect(storage.uploadFile).not.toHaveBeenCalled() }) it('should not highlight on drag over when disabled', () => { render( ) const dropZone = screen.getByText(/click or drag file to upload/i).closest('[class*="relative"]') as HTMLElement fireEvent.dragOver(dropZone) expect(dropZone).not.toHaveClass('border-orange-500') }) }) describe('file removal', () => { it('should allow removing uploaded file', async () => { render( ) const removeButton = screen.getByRole('button', { name: /remove/i }) await userEvent.click(removeButton) expect(mockOnChange).toHaveBeenCalledWith('') }) it('should clear file input when removed', async () => { render( ) const removeButton = screen.getByRole('button', { name: /remove/i }) await userEvent.click(removeButton) // Verify onChange was called with empty string expect(mockOnChange).toHaveBeenCalledWith('') }) }) describe('disabled state', () => { it('should be disabled when disabled prop is true', () => { render( ) const dropZone = screen.getByText(/click or drag file to upload/i).closest('[class*="relative"]') as HTMLElement expect(dropZone).toHaveClass('cursor-not-allowed') expect(dropZone).toHaveClass('opacity-50') const input = document.querySelector('input[type="file"]') as HTMLInputElement expect(input).toBeDisabled() }) it('should not show remove button when disabled with value', () => { render( ) const removeButton = screen.getByRole('button', { name: /remove/i }) expect(removeButton).toBeDisabled() }) }) describe('workoutId handling', () => { it('should use workoutId for file path', async () => { vi.mocked(storage.uploadFile).mockResolvedValue({ url: 'https://example.com/image.jpg', path: 'workout-123/123.jpg', }) render( ) const input = document.querySelector('input[type="file"]') as HTMLInputElement const file = new File(['test'], 'test.jpg', { type: 'image/jpeg' }) await userEvent.upload(input, file) await waitFor(() => { expect(storage.uploadFile).toHaveBeenCalled() }, { timeout: 10000 }) // Check the third argument was 'workout-123' const call = vi.mocked(storage.uploadFile).mock.calls[0] expect(call[2]).toBe('workout-123') }, 15000) it('should use temp folder when no workoutId provided', async () => { vi.mocked(storage.uploadFile).mockResolvedValue({ url: 'https://example.com/image.jpg', path: 'temp/123.jpg', }) render( ) const input = document.querySelector('input[type="file"]') as HTMLInputElement const file = new File(['test'], 'test.jpg', { type: 'image/jpeg' }) await userEvent.upload(input, file) await waitFor(() => { expect(storage.uploadFile).toHaveBeenCalled() }, { timeout: 10000 }) // Check the third argument was 'temp' const call = vi.mocked(storage.uploadFile).mock.calls[0] expect(call[2]).toBe('temp') }, 15000) }) })