veza/apps/web/src/features/tracks/components/TrackUpload.test.tsx

784 lines
19 KiB
TypeScript
Raw Normal View History

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';
2025-12-13 02:34:34 +00:00
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(<TrackUpload />);
2025-12-13 02:34:34 +00:00
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(<TrackUpload />);
2025-12-13 02:34:34 +00:00
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(
2025-12-13 02:34:34 +00:00
expect.stringContaining('Format non supporté'),
);
});
});
it('should validate file size', async () => {
const user = userEvent.setup();
render(<TrackUpload />);
2025-12-13 02:34:34 +00:00
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(
2025-12-13 02:34:34 +00:00
expect.stringContaining('Fichier trop volumineux'),
);
});
});
it('should upload valid file successfully', async () => {
const mockTrack = {
id: 1,
2025-12-22 21:00:50 +00:00
creator_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(<TrackUpload />);
2025-12-13 02:34:34 +00:00
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
2025-12-13 02:34:34 +00:00
await waitFor(
() => {
expect(getUploadProgress).toHaveBeenCalled();
},
{ timeout: 3000 },
);
});
it('should show file preview after selection', async () => {
render(<TrackUpload />);
2025-12-13 02:34:34 +00:00
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,
2025-12-22 21:00:50 +00:00
creator_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,
2025-12-22 21:00:50 +00:00
creator_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(<TrackUpload />);
2025-12-13 02:34:34 +00:00
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(<TrackUpload />);
2025-12-13 02:34:34 +00:00
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,
2025-12-22 21:00:50 +00:00
creator_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(<TrackUpload onUploadComplete={onUploadComplete} />);
2025-12-13 02:34:34 +00:00
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);
2025-12-13 02:34:34 +00:00
await waitFor(
() => {
expect(getUploadProgress).toHaveBeenCalled();
},
{ timeout: 3000 },
);
// Wait for callback to be called
2025-12-13 02:34:34 +00:00
await waitFor(
() => {
expect(onUploadComplete).toHaveBeenCalled();
},
{ timeout: 5000 },
);
});
it('should display upload speed and time remaining during upload', async () => {
const mockTrack = {
id: 1,
2025-12-22 21:00:50 +00:00
creator_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(<TrackUpload />);
2025-12-13 02:34:34 +00:00
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
2025-12-13 02:34:34 +00:00
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,
2025-12-22 21:00:50 +00:00
creator_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(<TrackUpload />);
2025-12-13 02:34:34 +00:00
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);
2025-12-13 02:34:34 +00:00
await waitFor(
() => {
expect(getUploadProgress).toHaveBeenCalled();
},
{ timeout: 3000 },
);
});
it('should handle drag and drop', async () => {
const mockTrack = {
id: 1,
2025-12-22 21:00:50 +00:00
creator_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(<TrackUpload />);
2025-12-13 02:34:34 +00:00
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(<TrackUpload />);
2025-12-13 02:34:34 +00:00
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(
2025-12-13 02:34:34 +00:00
expect.stringContaining('Le fichier est vide'),
);
});
});
it('should handle TrackUploadError with retry', async () => {
2025-12-13 02:34:34 +00:00
const networkError = new TrackUploadError('Erreur réseau', 'NETWORK', true);
// First attempt fails, second succeeds
vi.mocked(uploadTrack)
.mockRejectedValueOnce(networkError)
.mockResolvedValueOnce({
id: 1,
2025-12-22 21:00:50 +00:00
creator_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(<TrackUpload />);
2025-12-13 02:34:34 +00:00
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
2025-12-13 02:34:34 +00:00
await waitFor(
() => {
expect(mockToast.warning).toHaveBeenCalled();
},
{ timeout: 3000 },
);
// Eventually succeeds
2025-12-13 02:34:34 +00:00
await waitFor(
() => {
expect(uploadTrack).toHaveBeenCalledTimes(2);
},
{ timeout: 5000 },
);
});
it('should handle TrackUploadError without retry (validation error)', async () => {
const validationError = new TrackUploadError(
'Format invalide',
'VALIDATION',
2025-12-13 02:34:34 +00:00
false,
);
vi.mocked(uploadTrack).mockRejectedValue(validationError);
render(<TrackUpload />);
2025-12-13 02:34:34 +00:00
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(
2025-12-13 02:34:34 +00:00
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(<TrackUpload />);
2025-12-13 02:34:34 +00:00
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(<TrackUpload />);
2025-12-13 02:34:34 +00:00
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,
2025-12-22 21:00:50 +00:00
creator_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,
2025-12-22 21:00:50 +00:00
creator_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(<TrackUpload />);
2025-12-13 02:34:34 +00:00
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
2025-12-13 02:34:34 +00:00
await waitFor(
() => {
expect(getUploadProgress).toHaveBeenCalled();
},
{ timeout: 3000 },
);
});
it('should handle failed status from polling', async () => {
const mockTrack = {
id: 1,
2025-12-22 21:00:50 +00:00
creator_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(<TrackUpload />);
2025-12-13 02:34:34 +00:00
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);
2025-12-13 02:34:34 +00:00
await waitFor(
() => {
expect(getUploadProgress).toHaveBeenCalled();
},
{ timeout: 3000 },
);
// Should stop polling and show error
2025-12-13 02:34:34 +00:00
await waitFor(
() => {
expect(screen.getByText(/upload failed/i)).toBeInTheDocument();
},
{ timeout: 5000 },
);
});
});