import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { LikeButton } from './LikeButton'; import { import { likeTrack, unlikeTrack, } from '../services/interactionService'; getTrackLikes, TrackUploadError, } from '../services/trackService'; import { useToast } from '@/hooks/useToast'; // Mock dependencies vi.mock('../services/trackService'); vi.mock('@/hooks/useToast'); describe('LikeButton', () => { const mockToast = { success: vi.fn(), error: vi.fn(), warning: vi.fn(), info: vi.fn(), toast: vi.fn(), }; beforeEach(() => { vi.clearAllMocks(); vi.mocked(useToast).mockReturnValue(mockToast); }); afterEach(() => { vi.restoreAllMocks(); }); it('should render like button', async () => { vi.mocked(getTrackLikes).mockResolvedValue({ count: 5, isLiked: false, }); render(); await waitFor(() => { expect(screen.getByRole('button')).toBeInTheDocument(); }); expect(getTrackLikes).toHaveBeenCalledWith(1); }); it('should display like count', async () => { vi.mocked(getTrackLikes).mockResolvedValue({ count: 10, isLiked: false, }); render(); await waitFor(() => { expect(screen.getByText('10')).toBeInTheDocument(); }); }); it('should not display count when count is 0', async () => { vi.mocked(getTrackLikes).mockResolvedValue({ count: 0, isLiked: false, }); render(); await waitFor(() => { expect(screen.getByRole('button')).toBeInTheDocument(); }); expect(screen.queryByText('0')).not.toBeInTheDocument(); }); it('should display filled heart when liked', async () => { vi.mocked(getTrackLikes).mockResolvedValue({ count: 5, isLiked: true, }); render(); await waitFor(() => { const button = screen.getByRole('button'); expect(button).toHaveClass('text-red-500'); }); }); it('should like track on click when not liked', async () => { const user = userEvent.setup(); vi.mocked(getTrackLikes).mockResolvedValue({ count: 5, isLiked: false, }); vi.mocked(likeTrack).mockResolvedValue(); render(); await waitFor(() => { expect(screen.getByRole('button')).toBeInTheDocument(); }); const button = screen.getByRole('button'); await user.click(button); await waitFor(() => { expect(likeTrack).toHaveBeenCalledWith(1); }); }); it('should unlike track on click when liked', async () => { const user = userEvent.setup(); vi.mocked(getTrackLikes).mockResolvedValue({ count: 5, isLiked: true, }); vi.mocked(unlikeTrack).mockResolvedValue(); render(); await waitFor(() => { expect(screen.getByRole('button')).toBeInTheDocument(); }); const button = screen.getByRole('button'); await user.click(button); await waitFor(() => { expect(unlikeTrack).toHaveBeenCalledWith(1); }); }); it('should increment count when liking', async () => { const user = userEvent.setup(); vi.mocked(getTrackLikes).mockResolvedValue({ count: 5, isLiked: false, }); vi.mocked(likeTrack).mockResolvedValue(); render(); await waitFor(() => { expect(screen.getByText('5')).toBeInTheDocument(); }); const button = screen.getByRole('button'); await user.click(button); await waitFor(() => { expect(screen.getByText('6')).toBeInTheDocument(); }); }); it('should decrement count when unliking', async () => { const user = userEvent.setup(); vi.mocked(getTrackLikes).mockResolvedValue({ count: 5, isLiked: true, }); vi.mocked(unlikeTrack).mockResolvedValue(); render(); await waitFor(() => { expect(screen.getByText('5')).toBeInTheDocument(); }); const button = screen.getByRole('button'); await user.click(button); await waitFor(() => { expect(screen.getByText('4')).toBeInTheDocument(); }); }); it('should not decrement below 0', async () => { const user = userEvent.setup(); vi.mocked(getTrackLikes).mockResolvedValue({ count: 0, isLiked: true, }); vi.mocked(unlikeTrack).mockResolvedValue(); render(); await waitFor(() => { expect(screen.getByRole('button')).toBeInTheDocument(); }); const button = screen.getByRole('button'); await user.click(button); await waitFor(() => { expect(screen.queryByText('0')).not.toBeInTheDocument(); }); }); it('should disable button while loading', async () => { const user = userEvent.setup(); vi.mocked(getTrackLikes).mockResolvedValue({ count: 5, isLiked: false, }); vi.mocked(likeTrack).mockImplementation( () => new Promise((resolve) => { setTimeout(() => resolve(), 100); }), ); render(); await waitFor(() => { expect(screen.getByRole('button')).toBeInTheDocument(); }); const button = screen.getByRole('button'); await user.click(button); expect(button).toBeDisabled(); }); it('should show error toast on like failure', async () => { const user = userEvent.setup(); const error = new TrackUploadError('Failed to like track', 'SERVER', true); vi.mocked(getTrackLikes).mockResolvedValue({ count: 5, isLiked: false, }); vi.mocked(likeTrack).mockRejectedValue(error); render(); await waitFor(() => { expect(screen.getByRole('button')).toBeInTheDocument(); }); const button = screen.getByRole('button'); await user.click(button); await waitFor(() => { expect(mockToast.error).toHaveBeenCalledWith('Failed to like track'); }); }); it('should show error toast on unlike failure', async () => { const user = userEvent.setup(); const error = new TrackUploadError( 'Failed to unlike track', 'SERVER', true, ); vi.mocked(getTrackLikes).mockResolvedValue({ count: 5, isLiked: true, }); vi.mocked(unlikeTrack).mockRejectedValue(error); render(); await waitFor(() => { expect(screen.getByRole('button')).toBeInTheDocument(); }); const button = screen.getByRole('button'); await user.click(button); await waitFor(() => { expect(mockToast.error).toHaveBeenCalledWith('Failed to unlike track'); }); }); it('should revert state on error', async () => { const user = userEvent.setup(); const error = new TrackUploadError('Failed to like track', 'SERVER', true); vi.mocked(getTrackLikes).mockResolvedValue({ count: 5, isLiked: false, }); vi.mocked(likeTrack).mockRejectedValue(error); render(); await waitFor(() => { expect(screen.getByText('5')).toBeInTheDocument(); }); const button = screen.getByRole('button'); await user.click(button); // Should revert to original state await waitFor(() => { expect(screen.getByText('5')).toBeInTheDocument(); expect(button).not.toHaveClass('text-red-500'); }); }); it('should reload likes when trackId changes', async () => { const { rerender } = render(); vi.mocked(getTrackLikes).mockResolvedValue({ count: 5, isLiked: false, }); await waitFor(() => { expect(getTrackLikes).toHaveBeenCalledWith(1); }); vi.clearAllMocks(); vi.mocked(getTrackLikes).mockResolvedValue({ count: 10, isLiked: true, }); rerender(); await waitFor(() => { expect(getTrackLikes).toHaveBeenCalledWith(2); }); }); it('should apply custom className', async () => { vi.mocked(getTrackLikes).mockResolvedValue({ count: 5, isLiked: false, }); render(); await waitFor(() => { const button = screen.getByRole('button'); expect(button).toHaveClass('custom-class'); }); }); it('should have correct aria-label when not liked', async () => { vi.mocked(getTrackLikes).mockResolvedValue({ count: 5, isLiked: false, }); render(); await waitFor(() => { const button = screen.getByRole('button'); expect(button).toHaveAttribute('aria-label', 'Ajouter un like'); }); }); it('should have correct aria-label when liked', async () => { vi.mocked(getTrackLikes).mockResolvedValue({ count: 5, isLiked: true, }); render(); await waitFor(() => { const button = screen.getByRole('button'); expect(button).toHaveAttribute('aria-label', 'Retirer le like'); }); }); });