/** * Tests for PlaylistFollowButton Component * FE-TEST-007: Test playlist follow button component */ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { PlaylistFollowButton } from './PlaylistFollowButton'; import { followPlaylist, unfollowPlaylist, getPlaylist, getPlaylistFollowStatus, } from '../services/playlistService'; import { useAuthStore } from '@/stores/auth'; import { useToast } from '@/hooks/useToast'; // Mock dependencies vi.mock('../services/playlistService', () => ({ followPlaylist: vi.fn(), unfollowPlaylist: vi.fn(), getPlaylist: vi.fn(), getPlaylistFollowStatus: vi.fn(), })); vi.mock('@/stores/auth', () => ({ useAuthStore: vi.fn(), })); vi.mock('@/hooks/useToast', () => ({ useToast: vi.fn(), })); const createTestQueryClient = () => new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }); const TestWrapper = ({ children }: { children: React.ReactNode }) => { const queryClient = createTestQueryClient(); return ( {children} ); }; describe('PlaylistFollowButton', () => { const mockUser = { id: '2', username: 'testuser', email: 'test@example.com', }; const mockShowSuccess = vi.fn(); const mockShowError = vi.fn(); beforeEach(() => { vi.clearAllMocks(); vi.mocked(useAuthStore).mockReturnValue({ user: mockUser, } as any); vi.mocked(useToast).mockReturnValue({ success: mockShowSuccess, error: mockShowError, } as any); vi.mocked(getPlaylist).mockResolvedValue({ id: '1', user_id: '1', // Different from mockUser.id } as any); vi.mocked(getPlaylistFollowStatus).mockResolvedValue({ is_following: false, follower_count: 10, }); }); it('should render follow button when not following', async () => { render( , ); await waitFor(() => { expect(screen.getByText('Suivre')).toBeInTheDocument(); }); }); it('should render unfollow button when following', async () => { vi.mocked(getPlaylistFollowStatus).mockResolvedValue({ is_following: true, follower_count: 11, }); render( , ); await waitFor(() => { expect(screen.getByText('Abonné')).toBeInTheDocument(); }); }); it('should not render for playlist owner', async () => { vi.mocked(getPlaylist).mockResolvedValue({ id: '1', user_id: '2', // Same as mockUser.id } as any); vi.mocked(getPlaylistFollowStatus).mockResolvedValue({ is_following: false, follower_count: 0, }); const { container } = render( , ); await waitFor(() => { // Button should not be rendered for owner // Component returns null, so no buttons should be found const buttons = screen.queryAllByRole('button'); expect(buttons.length).toBe(0); }, { timeout: 3000 }); }); it('should not render when user is not logged in', async () => { vi.mocked(useAuthStore).mockReturnValue({ user: null, } as any); vi.mocked(getPlaylist).mockResolvedValue({ id: '1', user_id: '1', } as any); const { container } = render( , ); await waitFor(() => { // Component should return null when user is not logged in const buttons = screen.queryAllByRole('button'); expect(buttons.length).toBe(0); }); }); it('should call followPlaylist when follow button is clicked', async () => { const user = userEvent.setup(); vi.mocked(followPlaylist).mockResolvedValue({}); render( , ); await waitFor(() => { expect(screen.getByText('Suivre')).toBeInTheDocument(); }); const followButton = screen.getByText('Suivre'); await user.click(followButton); await waitFor(() => { expect(followPlaylist).toHaveBeenCalledWith('1'); }); }); it('should call unfollowPlaylist when unfollow button is clicked', async () => { const user = userEvent.setup(); vi.mocked(unfollowPlaylist).mockResolvedValue({}); vi.mocked(getPlaylistFollowStatus).mockResolvedValue({ is_following: true, follower_count: 11, }); render( , ); await waitFor(() => { expect(screen.getByText('Abonné')).toBeInTheDocument(); }); const unfollowButton = screen.getByText('Abonné'); await user.click(unfollowButton); await waitFor(() => { expect(unfollowPlaylist).toHaveBeenCalledWith('1'); }); }); it('should show success message on follow', async () => { const user = userEvent.setup(); vi.mocked(followPlaylist).mockResolvedValue({}); render( , ); await waitFor(() => { expect(screen.getByText('Suivre')).toBeInTheDocument(); }); const followButton = screen.getByText('Suivre'); await user.click(followButton); await waitFor(() => { expect(mockShowSuccess).toHaveBeenCalledWith( 'Vous suivez maintenant cette playlist', ); }); }); it('should show success message on unfollow', async () => { const user = userEvent.setup(); vi.mocked(unfollowPlaylist).mockResolvedValue({}); vi.mocked(getPlaylistFollowStatus).mockResolvedValue({ is_following: true, follower_count: 11, }); render( , ); await waitFor(() => { expect(screen.getByText('Abonné')).toBeInTheDocument(); }); const unfollowButton = screen.getByText('Abonné'); await user.click(unfollowButton); await waitFor(() => { expect(mockShowSuccess).toHaveBeenCalledWith( 'Vous ne suivez plus cette playlist', ); }); }); it('should show error message on follow failure', async () => { const user = userEvent.setup(); vi.mocked(followPlaylist).mockRejectedValue(new Error('Follow failed')); render( , ); await waitFor(() => { expect(screen.getByText('Suivre')).toBeInTheDocument(); }); const followButton = screen.getByText('Suivre'); await user.click(followButton); await waitFor(() => { expect(mockShowError).toHaveBeenCalled(); }); }); it('should show follower count when showCount is true', async () => { render( , ); await waitFor(() => { expect(screen.getByText('(25)')).toBeInTheDocument(); }); }); it('should update follower count optimistically', async () => { const user = userEvent.setup(); vi.mocked(followPlaylist).mockResolvedValue({}); render( , ); await waitFor(() => { expect(screen.getByText('Suivre')).toBeInTheDocument(); }); const followButton = screen.getByText('Suivre'); await user.click(followButton); // Follower count should increase optimistically await waitFor(() => { expect(screen.getByText('(11)')).toBeInTheDocument(); }); }); it('should call onFollowChange callback', async () => { const user = userEvent.setup(); const mockOnFollowChange = vi.fn(); vi.mocked(followPlaylist).mockResolvedValue({}); render( , ); await waitFor(() => { expect(screen.getByText('Suivre')).toBeInTheDocument(); }); const followButton = screen.getByText('Suivre'); await user.click(followButton); await waitFor(() => { expect(mockOnFollowChange).toHaveBeenCalledWith(true); }); }); it('should be disabled during update', async () => { const user = userEvent.setup(); vi.mocked(followPlaylist).mockImplementation( () => new Promise((resolve) => setTimeout(resolve, 100)), ); render( , ); await waitFor(() => { expect(screen.getByText('Suivre')).toBeInTheDocument(); }); const followButton = screen.getByText('Suivre'); await user.click(followButton); // Button should show loading state await waitFor(() => { expect(screen.getByText(/abonnement/i)).toBeInTheDocument(); expect(followButton).toBeDisabled(); }); }); });