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>
300 lines
9.8 KiB
TypeScript
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');
|
|
});
|
|
});
|