veza/apps/web/src/features/tracks/components/TrackCard.test.tsx
senke 597a3f7cee test: fix and improve unit tests across multiple features
Fix mocking issues, add missing test cases, and align tests with
current component APIs for analytics, chat, marketplace, player,
playlists, settings, tracks, and auth features.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 23:34:42 +01:00

300 lines
9.8 KiB
TypeScript

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;
}) => (
<button
aria-label={initialIsLiked ? 'Retirer des favoris' : 'Ajouter aux favoris'}
aria-pressed={initialIsLiked}
data-testid="like-button"
onClick={(e) => e.stopPropagation()}
>
Like
</button>
),
}));
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(<TrackCard track={mockTrack} />);
expect(screen.getByText('Test Track')).toBeInTheDocument();
});
it('should display track title', () => {
render(<TrackCard track={mockTrack} />);
expect(screen.getByText('Test Track')).toBeInTheDocument();
});
it('should display track artist', () => {
render(<TrackCard track={mockTrack} />);
expect(screen.getByText('Test Artist')).toBeInTheDocument();
});
it('should display cover image', () => {
render(<TrackCard track={mockTrack} />);
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(<TrackCard track={trackWithoutCover} />);
// 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(<TrackCard track={mockTrack} />);
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(<TrackCard track={mockTrack} showDuration={true} />);
expect(screen.getByText('3:00')).toBeInTheDocument();
});
it('should not display duration when showDuration is false', () => {
render(<TrackCard track={mockTrack} showDuration={false} />);
expect(screen.queryByText('3:00')).not.toBeInTheDocument();
});
it('should call onPlay when play button is clicked', async () => {
const user = userEvent.setup();
render(<TrackCard track={mockTrack} onPlay={mockOnPlay} />);
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(<TrackCard track={mockTrack} />);
const likeButton = screen.getByLabelText(/Ajouter.*favoris/);
expect(likeButton).toBeInTheDocument();
});
it('should call onMore when more button is clicked', async () => {
const user = userEvent.setup();
render(<TrackCard track={mockTrack} onMore={mockOnMore} />);
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(<TrackCard track={mockTrack} onClick={mockOnClick} />);
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(<TrackCard track={mockTrack} onClick={mockOnClick} />);
const likeButton = screen.getByLabelText(/Ajouter.*favoris/);
await user.click(likeButton);
expect(mockOnClick).not.toHaveBeenCalled();
});
it('should show liked state when isLiked is true', () => {
render(<TrackCard track={mockTrack} onLike={mockOnLike} isLiked={true} />);
const likeButton = screen.getByLabelText(/Retirer.*favoris/);
expect(likeButton).toHaveAttribute('aria-pressed', 'true');
});
it('should show playing state when isPlaying is true', () => {
render(
<TrackCard track={mockTrack} onPlay={mockOnPlay} isPlaying={true} />,
);
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(
<TrackCard
track={mockTrack}
onLike={mockOnLike}
onMore={mockOnMore}
showActions={false}
/>,
);
expect(screen.queryByLabelText(/favoris/)).not.toBeInTheDocument();
expect(screen.queryByLabelText(/More options/)).not.toBeInTheDocument();
});
it('should apply custom className', () => {
const { container } = render(
<TrackCard track={mockTrack} className="custom-class" onClick={mockOnClick} />,
);
const card = container.querySelector('[role="button"]');
expect(card).toHaveClass('custom-class');
});
it('should support different sizes', () => {
const { rerender } = render(<TrackCard track={mockTrack} size="sm" />);
expect(screen.getByText('Test Track')).toBeInTheDocument();
rerender(<TrackCard track={mockTrack} size="md" />);
expect(screen.getByText('Test Track')).toBeInTheDocument();
rerender(<TrackCard track={mockTrack} size="lg" />);
expect(screen.getByText('Test Track')).toBeInTheDocument();
});
it('should have accessible attributes', () => {
render(<TrackCard track={mockTrack} onClick={mockOnClick} />);
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(<TrackCard track={mockTrack} />);
const card = screen.getByRole('button', { name: /Piste:/ });
expect(card).toHaveAttribute('tabIndex', '-1');
});
it('should handle track without artist', () => {
const trackWithoutArtist = { ...mockTrack, artist: undefined };
render(<TrackCard track={trackWithoutArtist} />);
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(
<TrackCard track={mockTrack} onPlay={mockOnPlay} />,
);
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(
<TrackCard track={mockTrack} onPlay={mockOnPlay} />,
);
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(
<TrackCard track={mockTrack} onLike={mockOnLike} onMore={mockOnMore} />,
);
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(<TrackCard track={mockTrack} onClick={mockOnClick} />);
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(<TrackCard track={mockTrack} />);
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(
<TrackCard track={mockTrack} isPlaying={true} onPlay={mockOnPlay} />,
);
// 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');
});
});