import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { TrackCard } from './TrackCard'; import type { Track } from '../../player/types'; vi.mock('./LikeButton', () => ({ LikeButton: ({ trackId: _trackId, initialIsLiked = false, }: { trackId: string; initialIsLiked?: boolean; }) => ( ), })); const mockTrack: Track = { id: '1', title: 'Test Track', artist: 'Test Artist', album: 'Test Album', duration: 180, url: 'https://example.com/track.mp3', cover: 'https://example.com/cover.jpg', genre: 'Rock', }; describe('TrackCard', () => { const mockOnPlay = vi.fn(); const mockOnLike = vi.fn(); const mockOnMore = vi.fn(); const mockOnClick = vi.fn(); beforeEach(() => { vi.clearAllMocks(); }); it('should render track card', () => { render(); expect(screen.getByText('Test Track')).toBeInTheDocument(); }); it('should display track title', () => { render(); expect(screen.getByText('Test Track')).toBeInTheDocument(); }); it('should display track artist', () => { render(); expect(screen.getByText('Test Artist')).toBeInTheDocument(); }); it('should display cover image', () => { render(); const image = screen.getByAltText('Cover de Test Track'); expect(image).toBeInTheDocument(); expect(image).toHaveAttribute('src', mockTrack.cover); }); it('should display placeholder when cover is missing', () => { const trackWithoutCover = { ...mockTrack, cover: undefined }; const { container } = render(); // Should show Music icon placeholder (lucide-react icons have aria-hidden="true") const musicIcon = container.querySelector('svg'); expect(musicIcon).toBeInTheDocument(); }); it('should display placeholder when cover image fails to load', async () => { render(); const image = screen.getByAltText('Cover de Test Track'); image.dispatchEvent(new Event('error')); await waitFor(() => { const placeholder = image.nextElementSibling as HTMLElement; expect(placeholder).not.toHaveClass('hidden'); }); }); it('should display duration when showDuration is true', () => { render(); expect(screen.getByText('3:00')).toBeInTheDocument(); }); it('should not display duration when showDuration is false', () => { render(); expect(screen.queryByText('3:00')).not.toBeInTheDocument(); }); it('should call onPlay when play button is clicked', async () => { const user = userEvent.setup(); render(); const card = screen.getByRole('button', { name: /Piste:/ }); await user.hover(card); await waitFor(() => { const playButton = screen.getByLabelText('Lire Test Track'); expect(playButton).toBeInTheDocument(); }); const playButton = screen.getByLabelText('Lire Test Track'); await user.click(playButton); expect(mockOnPlay).toHaveBeenCalledWith(mockTrack); }); it('should render like button when showActions is true', () => { render(); const likeButton = screen.getByLabelText(/Ajouter.*favoris/); expect(likeButton).toBeInTheDocument(); }); it('should call onMore when more button is clicked', async () => { const user = userEvent.setup(); render(); const moreButton = screen.getByLabelText(/More options/); await user.click(moreButton); expect(mockOnMore).toHaveBeenCalledWith(mockTrack); }); it('should call onClick when card is clicked', async () => { const user = userEvent.setup(); render(); const card = screen.getByRole('button', { name: /Piste:/ }); await user.click(card); expect(mockOnClick).toHaveBeenCalledWith(mockTrack); }); it('should not call onClick when like button is clicked', async () => { const user = userEvent.setup(); render(); const likeButton = screen.getByLabelText(/Ajouter.*favoris/); await user.click(likeButton); expect(mockOnClick).not.toHaveBeenCalled(); }); it('should show liked state when isLiked is true', () => { render(); const likeButton = screen.getByLabelText(/Retirer.*favoris/); expect(likeButton).toHaveAttribute('aria-pressed', 'true'); }); it('should show playing state when isPlaying is true', () => { render( , ); const card = screen.getByRole('button', { name: /Piste:/ }); // Play button should be visible when playing expect(card).toBeInTheDocument(); }); it('should hide actions when showActions is false', () => { render( , ); expect(screen.queryByLabelText(/favoris/)).not.toBeInTheDocument(); expect(screen.queryByLabelText(/More options/)).not.toBeInTheDocument(); }); it('should apply custom className', () => { const { container } = render( , ); const card = container.querySelector('[role="button"]'); expect(card).toHaveClass('custom-class'); }); it('should support different sizes', () => { const { rerender } = render(); expect(screen.getByText('Test Track')).toBeInTheDocument(); rerender(); expect(screen.getByText('Test Track')).toBeInTheDocument(); rerender(); expect(screen.getByText('Test Track')).toBeInTheDocument(); }); it('should have accessible attributes', () => { render(); const card = screen.getByRole('button', { name: /Piste:/ }); expect(card).toHaveAttribute('tabIndex', '0'); expect(card).toHaveAttribute('aria-label'); }); it('should not be clickable when onClick is not provided', () => { render(); const card = screen.getByRole('button', { name: /Piste:/ }); expect(card).toHaveAttribute('tabIndex', '-1'); }); it('should handle track without artist', () => { const trackWithoutArtist = { ...mockTrack, artist: undefined }; render(); expect(screen.getByText('Test Track')).toBeInTheDocument(); expect(screen.queryByText('Test Artist')).not.toBeInTheDocument(); }); it('should show hover overlay on cover when hovered', async () => { const user = userEvent.setup(); const { container } = render( , ); const card = container.querySelector('button') as HTMLElement; await user.hover(card); // Vérifier que l'overlay est présent const overlay = container.querySelector('.bg-gradient-to-t'); expect(overlay).toBeInTheDocument(); }); it('should show play button on hover', async () => { const user = userEvent.setup(); const { container } = render( , ); const card = container.querySelector('button') as HTMLElement; await user.hover(card); await waitFor(() => { // Le bouton play devrait être visible au hover const playButton = container.querySelector('button[aria-label*="Lire"]'); expect(playButton).toBeInTheDocument(); }); }); it('should show actions on hover with animation', async () => { const user = userEvent.setup(); const { container } = render( , ); const card = container.querySelector('button') as HTMLElement; await user.hover(card); await waitFor(() => { // Les actions devraient être visibles au hover const actionsContainer = container.querySelector( '.group-hover\\:opacity-100', ); expect(actionsContainer).toBeInTheDocument(); }); }); it('should apply hover and active animation classes', () => { const { container } = render(); const card = container.querySelector('[role="button"]') as HTMLElement; expect(card).toHaveClass('hover:-translate-y-1'); expect(card).toHaveClass('active:translate-y-0'); }); it('should render cover image with object-cover', () => { const { container } = render(); const image = container.querySelector('img[alt*="Cover"]'); expect(image).toBeInTheDocument(); expect(image).toHaveClass('object-cover'); }); it('should show playing animation when isPlaying is true', () => { render( , ); // When playing, button shows "Pause" label const playButton = screen.getByLabelText(/Pause Test Track/); expect(playButton).toBeInTheDocument(); // Should have playing state (visible, not hidden) expect(playButton).toHaveClass('opacity-100'); }); });