veza/apps/web/src/features/tracks/pages/TrackDetailPage.test.tsx
2026-01-07 19:39:21 +01:00

271 lines
7.9 KiB
TypeScript

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 { TrackDetailPage } from './TrackDetailPage';
import { getTrack } from '../services/trackService';
import { TrackServiceError as TrackUploadError } from '../errors/trackErrors';
import { usePlayerStore } from '@/stores/player';
import { useToast } from '@/hooks/useToast';
import { useParams, useNavigate } from 'react-router-dom';
import type { Track } from '../types/track';
// Mock dependencies
vi.mock('../services/trackService');
vi.mock('@/stores/player');
vi.mock('@/hooks/useToast');
vi.mock('react-router-dom', async () => {
const actual = await vi.importActual('react-router-dom');
return {
...actual,
useParams: vi.fn(),
useNavigate: vi.fn(),
};
});
describe('TrackDetailPage', () => {
const mockNavigate = vi.fn();
const mockToast = {
success: vi.fn(),
error: vi.fn(),
warning: vi.fn(),
info: vi.fn(),
toast: vi.fn(),
};
const mockTrack: Track = {
id: 1,
creator_id: 123,
title: 'Test Track',
artist: 'Test Artist',
album: 'Test Album',
duration: 180,
genre: 'Rock',
year: 2024,
file_path: '/uploads/track.mp3',
file_size: 1024,
format: 'MP3',
bitrate: 320,
sample_rate: 44100,
waveform_path: '/waveforms/track.png',
cover_art_path: '/covers/track.jpg',
is_public: true,
play_count: 100,
like_count: 50,
created_at: '2024-01-01T00:00:00Z',
updated_at: '2024-01-01T00:00:00Z',
};
const mockPlayerStore = {
play: vi.fn(),
pause: vi.fn(),
currentTrack: null,
isPlaying: false,
addToQueue: vi.fn(),
};
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(useParams).mockReturnValue({ id: '1' });
vi.mocked(useNavigate).mockReturnValue(mockNavigate);
vi.mocked(useToast).mockReturnValue(mockToast);
vi.mocked(usePlayerStore).mockReturnValue(mockPlayerStore as any);
});
afterEach(() => {
vi.restoreAllMocks();
});
it('should render loading state initially', async () => {
vi.mocked(getTrack).mockImplementation(
() =>
new Promise((resolve) => {
setTimeout(() => resolve(mockTrack), 100);
}),
);
render(<TrackDetailPage />);
expect(screen.getByText(/chargement du track/i)).toBeInTheDocument();
});
it('should render track details after loading', async () => {
vi.mocked(getTrack).mockResolvedValue(mockTrack);
render(<TrackDetailPage />);
await waitFor(() => {
expect(screen.getByText('Test Track')).toBeInTheDocument();
expect(screen.getByText('Test Artist')).toBeInTheDocument();
expect(screen.getByText('Test Album')).toBeInTheDocument();
});
expect(screen.getByText('3:00')).toBeInTheDocument(); // 180 seconds
expect(screen.getByText('Rock')).toBeInTheDocument();
expect(screen.getByText('2024')).toBeInTheDocument();
expect(screen.getByText('100')).toBeInTheDocument(); // play_count
expect(screen.getByText('50')).toBeInTheDocument(); // like_count
});
it('should display error message on load failure', async () => {
const error = new TrackUploadError('Track not found', 'VALIDATION', false);
vi.mocked(getTrack).mockRejectedValue(error);
render(<TrackDetailPage />);
await waitFor(() => {
expect(screen.getByText(/track introuvable/i)).toBeInTheDocument();
expect(mockToast.error).toHaveBeenCalledWith('Track not found');
});
});
it('should handle missing track ID', async () => {
vi.mocked(useParams).mockReturnValue({ id: undefined });
render(<TrackDetailPage />);
await waitFor(() => {
expect(screen.getByText(/track id is required/i)).toBeInTheDocument();
});
});
it('should play track when play button is clicked', async () => {
vi.mocked(getTrack).mockResolvedValue(mockTrack);
render(<TrackDetailPage />);
await waitFor(() => {
expect(screen.getByText('Test Track')).toBeInTheDocument();
});
const playButton = screen.getByRole('button', { name: /play/i });
await userEvent.click(playButton);
expect(mockPlayerStore.play).toHaveBeenCalled();
});
it('should pause track when pause button is clicked if track is playing', async () => {
vi.mocked(getTrack).mockResolvedValue(mockTrack);
vi.mocked(usePlayerStore).mockReturnValue({
...mockPlayerStore,
currentTrack: { id: '1' } as any,
isPlaying: true,
} as any);
render(<TrackDetailPage />);
await waitFor(() => {
expect(screen.getByText('Test Track')).toBeInTheDocument();
});
const pauseButton = screen.getByRole('button', { name: /pause/i });
await userEvent.click(pauseButton);
expect(mockPlayerStore.pause).toHaveBeenCalled();
});
it('should add track to queue when queue button is clicked', async () => {
vi.mocked(getTrack).mockResolvedValue(mockTrack);
render(<TrackDetailPage />);
await waitFor(() => {
expect(screen.getByText('Test Track')).toBeInTheDocument();
});
const queueButtons = screen.getAllByRole('button');
const queueButton = queueButtons.find((btn) =>
btn.getAttribute('title')?.includes("file d'attente"),
);
if (queueButton) {
await userEvent.click(queueButton);
expect(mockPlayerStore.addToQueue).toHaveBeenCalled();
expect(mockToast.success).toHaveBeenCalledWith(
expect.stringContaining('ajouté à la file'),
);
}
});
it('should copy share link when share button is clicked', async () => {
vi.mocked(getTrack).mockResolvedValue(mockTrack);
const mockWriteText = vi.fn().mockResolvedValue(undefined);
Object.assign(navigator, {
clipboard: {
writeText: mockWriteText,
},
});
render(<TrackDetailPage />);
await waitFor(() => {
expect(screen.getByText('Test Track')).toBeInTheDocument();
});
const shareButtons = screen.getAllByRole('button');
const shareButton = shareButtons.find((btn) =>
btn.getAttribute('title')?.includes('Partager'),
);
if (shareButton) {
await userEvent.click(shareButton);
expect(mockWriteText).toHaveBeenCalled();
expect(mockToast.success).toHaveBeenCalledWith(
expect.stringContaining('copié'),
);
}
});
it('should navigate back when back button is clicked', async () => {
vi.mocked(getTrack).mockResolvedValue(mockTrack);
render(<TrackDetailPage />);
await waitFor(() => {
expect(screen.getByText('Test Track')).toBeInTheDocument();
});
const backButton = screen.getByRole('button', { name: /retour/i });
await userEvent.click(backButton);
expect(mockNavigate).toHaveBeenCalledWith(-1);
});
it('should display waveform if available', async () => {
vi.mocked(getTrack).mockResolvedValue(mockTrack);
render(<TrackDetailPage />);
await waitFor(() => {
const waveform = screen.getByAltText('Waveform');
expect(waveform).toBeInTheDocument();
expect(waveform).toHaveAttribute('src', '/waveforms/track.png');
});
});
it('should display cover art if available', async () => {
vi.mocked(getTrack).mockResolvedValue(mockTrack);
render(<TrackDetailPage />);
await waitFor(() => {
const coverArt = screen.getByAltText('Test Track');
expect(coverArt).toBeInTheDocument();
expect(coverArt).toHaveAttribute('src', '/covers/track.jpg');
});
});
it('should display placeholder if no cover art', async () => {
const trackWithoutCover = { ...mockTrack, cover_art_path: undefined };
vi.mocked(getTrack).mockResolvedValue(trackWithoutCover);
render(<TrackDetailPage />);
await waitFor(() => {
expect(screen.getByText('Test Track')).toBeInTheDocument();
});
// Should have Music icon placeholder
const musicIcons = screen.getAllByRole('img', { hidden: true });
expect(musicIcons.length).toBeGreaterThan(0);
});
});