401 lines
11 KiB
TypeScript
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);
|
|
});
|
|
});
|