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'; 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'); // Simulate image error const errorEvent = new Event('error'); image.dispatchEvent(errorEvent); await waitFor(() => { // After error, should show placeholder expect(screen.queryByAltText('Cover de Test Track')).not.toBeInTheDocument(); }); }); 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 call onLike when like button is clicked', async () => { const user = userEvent.setup(); render(); const likeButton = screen.getByLabelText(/Ajouter.*favoris/); await user.click(likeButton); expect(mockOnLike).toHaveBeenCalledWith(mockTrack); }); it('should call onMore when more button is clicked', async () => { const user = userEvent.setup(); render(); const moreButton = screen.getByLabelText(/Plus d'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 action button is clicked', async () => { const user = userEvent.setup(); render(); const likeButton = screen.getByLabelText(/Ajouter.*favoris/); await user.click(likeButton); expect(mockOnLike).toHaveBeenCalled(); 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(/Plus d'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('[role="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('[role="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('[role="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 scale animation on hover', () => { const { container } = render(); const card = container.querySelector('[role="button"]') as HTMLElement; expect(card).toHaveClass('hover:scale-[1.02]'); }); it('should animate cover image on hover', async () => { const user = userEvent.setup(); const { container } = render(); const card = container.querySelector('[role="button"]') as HTMLElement; await user.hover(card); await waitFor(() => { // L'image devrait avoir une classe de scale au hover const coverContainer = container.querySelector('.scale-105'); expect(coverContainer).toBeInTheDocument(); }); }); it('should show playing animation when isPlaying is true', () => { const { container } = render(); // Le bouton play devrait être visible même sans hover const playButton = container.querySelector('button[aria-label*="Lire"]'); expect(playButton).toBeInTheDocument(); // Devrait avoir l'animation de lecture const playingIndicator = container.querySelector('.animate-pulse'); expect(playingIndicator).toBeInTheDocument(); }); });