veza/apps/web/src/features/playlists/pages/PlaylistDetailPage.test.tsx

401 lines
11 KiB
TypeScript

/**
* Tests pour PlaylistDetailPage
* T0460: Create Playlist Detail Page
* T0475: Create Playlist Track Management Integration
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { BrowserRouter } from 'react-router-dom';
import { PlaylistDetailPage } from './PlaylistDetailPage';
import { usePlaylist, useCollaborators } from '../hooks/usePlaylist';
import { usePlaylistPermissions } from '../hooks/usePlaylistPermissions';
import type { Playlist } from '../types';
import type { Track } from '@/features/tracks/types/track';
// Mock usePlaylist hook
vi.mock('../hooks/usePlaylist', () => ({
usePlaylist: vi.fn(),
useCollaborators: vi.fn(),
}));
// Mock usePlaylistPermissions hook
vi.mock('../hooks/usePlaylistPermissions', () => ({
usePlaylistPermissions: vi.fn(),
}));
// Mock player store
const mockPlay = vi.fn();
vi.mock('@/features/player/store/playerStore', () => ({
usePlayerStore: vi.fn(() => ({
play: mockPlay,
currentTrack: null,
isPlaying: false,
})),
}));
// Mock PlaylistTrackList
vi.mock('../components/PlaylistTrackList', () => ({
PlaylistTrackList: ({ tracks, onTrackPlay, onTrackRemoved }: any) => (
<div data-testid="playlist-track-list">
{tracks.map((track: Track) => (
<div key={track.id} data-testid={`track-${track.id}`}>
{track.title}
<button onClick={() => onTrackPlay?.(track)}>Play</button>
<button onClick={() => onTrackRemoved?.()}>Remove</button>
</div>
))}
</div>
),
}));
// Mock AddTrackToPlaylistModal
const mockOnClose = vi.fn();
const mockOnTrackAdded = vi.fn();
vi.mock('../components/AddTrackToPlaylistModal', () => ({
AddTrackToPlaylistModal: ({ open, onClose, onTrackAdded }: any) => {
if (open) {
return (
<div data-testid="add-track-modal">
<button onClick={onClose}>Close</button>
<button onClick={onTrackAdded}>Add Track</button>
</div>
);
}
return null;
},
}));
// Mock PlaylistHeader
vi.mock('../components/PlaylistHeader', () => ({
PlaylistHeader: ({ playlist }: any) => (
<div data-testid="playlist-header">{playlist.title}</div>
),
}));
// Mock PlaylistActions
vi.mock('../components/PlaylistActions', () => ({
PlaylistActions: ({ onShareClick }: any) => (
<div data-testid="playlist-actions">
Actions
{onShareClick && <button onClick={onShareClick}>Share</button>}
</div>
),
}));
// Mock SharePlaylistModal
vi.mock('../components/SharePlaylistModal', () => ({
SharePlaylistModal: ({ open, onClose }: any) => {
if (open) {
return (
<div data-testid="share-playlist-modal">
<button onClick={onClose}>Close Share</button>
</div>
);
}
return null;
},
}));
// Mock CollaboratorList
vi.mock('../components/CollaboratorList', () => ({
CollaboratorList: ({ collaborators }: any) => (
<div data-testid="collaborator-list">
{collaborators.map((c: any) => (
<div key={c.id}>{c.user?.username || 'Collaborator'}</div>
))}
</div>
),
}));
function createWrapper() {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
return ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>
<BrowserRouter>{children}</BrowserRouter>
</QueryClientProvider>
);
}
const mockTrack: Track = {
id: '1',
creator_id: '1',
title: 'Test Track',
artist: 'Test Artist',
duration: 180,
file_path: '/tracks/1.mp3',
file_size: 5000000,
format: 'MP3',
is_public: true,
play_count: 0,
like_count: 0,
created_at: '2024-01-01T00:00:00Z',
updated_at: '2024-01-01T00:00:00Z',
};
const mockPlaylist: Playlist = {
id: 1,
user_id: 1,
title: 'Test Playlist',
description: 'Test Description',
is_public: true,
track_count: 1,
created_at: '2024-01-01T00:00:00Z',
updated_at: '2024-01-01T00:00:00Z',
tracks: [
{
id: 1,
playlist_id: 1,
track_id: 1,
position: 1,
added_at: '2024-01-01T00:00:00Z',
track: mockTrack,
},
],
};
describe('PlaylistDetailPage', () => {
const mockRefetch = vi.fn();
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(usePlaylist).mockReturnValue({
data: mockPlaylist,
isLoading: false,
error: null,
refetch: mockRefetch,
} as any);
vi.mocked(useCollaborators).mockReturnValue({
data: [],
isLoading: false,
isError: false,
error: null,
refetch: vi.fn(),
} as any);
vi.mocked(usePlaylistPermissions).mockReturnValue({
canEdit: true,
canDelete: true,
canAddTracks: true,
canRemoveTracks: true,
canManageCollaborators: true,
canRead: true,
isOwner: true,
});
});
it('should render playlist header', () => {
render(<PlaylistDetailPage />, { wrapper: createWrapper() });
expect(screen.getByTestId('playlist-header')).toBeInTheDocument();
expect(screen.getByText('Test Playlist')).toBeInTheDocument();
});
it('should render playlist actions', () => {
render(<PlaylistDetailPage />, { wrapper: createWrapper() });
expect(screen.getByTestId('playlist-actions')).toBeInTheDocument();
});
it('should render tracks list', () => {
render(<PlaylistDetailPage />, { wrapper: createWrapper() });
expect(screen.getByTestId('playlist-track-list')).toBeInTheDocument();
expect(screen.getByText('Test Track')).toBeInTheDocument();
});
it('should show add track button', () => {
render(<PlaylistDetailPage />, { wrapper: createWrapper() });
expect(screen.getByText('Ajouter des tracks')).toBeInTheDocument();
});
it('should open add track modal when button is clicked', async () => {
const user = userEvent.setup();
render(<PlaylistDetailPage />, { wrapper: createWrapper() });
const addButton = screen.getByText('Ajouter des tracks');
await user.click(addButton);
expect(screen.getByTestId('add-track-modal')).toBeInTheDocument();
});
it('should close add track modal when close button is clicked', async () => {
const user = userEvent.setup();
render(<PlaylistDetailPage />, { wrapper: createWrapper() });
const addButton = screen.getByText('Ajouter des tracks');
await user.click(addButton);
expect(screen.getByTestId('add-track-modal')).toBeInTheDocument();
const closeButton = screen.getByText('Close');
await user.click(closeButton);
await waitFor(() => {
expect(screen.queryByTestId('add-track-modal')).not.toBeInTheDocument();
});
});
it('should call play when track play button is clicked', async () => {
const user = userEvent.setup();
render(<PlaylistDetailPage />, { wrapper: createWrapper() });
const playButton = screen.getByText('Play');
await user.click(playButton);
expect(mockPlay).toHaveBeenCalledWith({
id: mockTrack.id,
title: mockTrack.title,
artist: mockTrack.artist,
album: mockTrack.album,
duration: mockTrack.duration,
file_path: mockTrack.file_path,
cover: mockTrack.cover_art_path,
});
});
it('should refetch playlist when track is removed', async () => {
const user = userEvent.setup();
render(<PlaylistDetailPage />, { wrapper: createWrapper() });
const removeButton = screen.getByText('Remove');
await user.click(removeButton);
expect(mockRefetch).toHaveBeenCalled();
});
it('should refetch playlist when track is added', async () => {
const user = userEvent.setup();
render(<PlaylistDetailPage />, { wrapper: createWrapper() });
const addButton = screen.getByText('Ajouter des tracks');
await user.click(addButton);
const addTrackButton = screen.getByText('Add Track');
await user.click(addTrackButton);
expect(mockRefetch).toHaveBeenCalled();
});
it('should show loading state', () => {
vi.mocked(usePlaylist).mockReturnValue({
data: undefined,
isLoading: true,
error: null,
refetch: mockRefetch,
} as any);
render(<PlaylistDetailPage />, { wrapper: createWrapper() });
expect(screen.getByText('Loading...')).toBeInTheDocument();
});
it('should show error state', () => {
vi.mocked(usePlaylist).mockReturnValue({
data: undefined,
isLoading: false,
error: new Error('Failed to load playlist'),
refetch: mockRefetch,
} as any);
render(<PlaylistDetailPage />, { wrapper: createWrapper() });
expect(screen.getByText('Error loading playlist')).toBeInTheDocument();
});
it('should show not found state', () => {
vi.mocked(usePlaylist).mockReturnValue({
data: undefined,
isLoading: false,
error: null,
refetch: mockRefetch,
} as any);
render(<PlaylistDetailPage />, { wrapper: createWrapper() });
expect(screen.getByText('Playlist not found')).toBeInTheDocument();
});
it('should show share button when user can manage collaborators', () => {
render(<PlaylistDetailPage />, { wrapper: createWrapper() });
expect(screen.getByText('Partager')).toBeInTheDocument();
});
it('should open share modal when share button is clicked', async () => {
const user = userEvent.setup();
render(<PlaylistDetailPage />, { wrapper: createWrapper() });
const shareButton = screen.getByText('Partager');
await user.click(shareButton);
expect(screen.getByTestId('share-playlist-modal')).toBeInTheDocument();
});
it('should show collaborators section when user can read', () => {
render(<PlaylistDetailPage />, { wrapper: createWrapper() });
expect(screen.getByText('Collaborateurs')).toBeInTheDocument();
expect(screen.getByTestId('collaborator-list')).toBeInTheDocument();
});
it('should not show add track button when user cannot add tracks', () => {
vi.mocked(usePlaylistPermissions).mockReturnValue({
canEdit: false,
canDelete: false,
canAddTracks: false,
canRemoveTracks: false,
canManageCollaborators: false,
canRead: true,
isOwner: false,
});
render(<PlaylistDetailPage />, { wrapper: createWrapper() });
expect(screen.queryByText('Ajouter des tracks')).not.toBeInTheDocument();
});
it('should not show collaborators section when user cannot read', () => {
vi.mocked(usePlaylistPermissions).mockReturnValue({
canEdit: false,
canDelete: false,
canAddTracks: false,
canRemoveTracks: false,
canManageCollaborators: false,
canRead: false,
isOwner: false,
});
render(<PlaylistDetailPage />, { wrapper: createWrapper() });
expect(screen.queryByText('Collaborateurs')).not.toBeInTheDocument();
});
it('should not show share button when user cannot manage collaborators', () => {
vi.mocked(usePlaylistPermissions).mockReturnValue({
canEdit: false,
canDelete: false,
canAddTracks: false,
canRemoveTracks: false,
canManageCollaborators: false,
canRead: true,
isOwner: false,
});
render(<PlaylistDetailPage />, { wrapper: createWrapper() });
// Le bouton share dans la section collaborateurs ne devrait pas être visible
const shareButtons = screen.queryAllByText('Partager');
expect(shareButtons.length).toBe(0);
});
});