veza/apps/web/src/features/playlists/components/PlaylistFollowButton.test.tsx
senke 7ee21e7d28 [FE-TEST-007] test: Add component tests for playlist components
- Created comprehensive tests for CollaboratorManagement component
- Created comprehensive tests for PlaylistHeader component
- Created comprehensive tests for AddCollaboratorModal component
- Created comprehensive tests for PlaylistFollowButton component

All 51 tests pass. These components are essential for playlist detail and collaboration functionality.

Phase: PHASE-5
Priority: P2
Progress: 244/267 (91.39%)
2025-12-25 17:21:59 +01:00

375 lines
9.6 KiB
TypeScript

/**
* 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 (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
};
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(
<TestWrapper>
<PlaylistFollowButton playlistId="1" initialFollowing={false} />
</TestWrapper>,
);
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(
<TestWrapper>
<PlaylistFollowButton playlistId="1" initialFollowing={true} />
</TestWrapper>,
);
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(
<TestWrapper>
<PlaylistFollowButton playlistId="1" />
</TestWrapper>,
);
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(
<TestWrapper>
<PlaylistFollowButton playlistId="1" />
</TestWrapper>,
);
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(
<TestWrapper>
<PlaylistFollowButton playlistId="1" initialFollowing={false} />
</TestWrapper>,
);
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(
<TestWrapper>
<PlaylistFollowButton playlistId="1" initialFollowing={true} />
</TestWrapper>,
);
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(
<TestWrapper>
<PlaylistFollowButton playlistId="1" initialFollowing={false} />
</TestWrapper>,
);
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(
<TestWrapper>
<PlaylistFollowButton playlistId="1" initialFollowing={true} />
</TestWrapper>,
);
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(
<TestWrapper>
<PlaylistFollowButton playlistId="1" initialFollowing={false} />
</TestWrapper>,
);
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(
<TestWrapper>
<PlaylistFollowButton
playlistId="1"
initialFollowing={false}
initialFollowerCount={25}
showCount={true}
/>
</TestWrapper>,
);
await waitFor(() => {
expect(screen.getByText('(25)')).toBeInTheDocument();
});
});
it('should update follower count optimistically', async () => {
const user = userEvent.setup();
vi.mocked(followPlaylist).mockResolvedValue({});
render(
<TestWrapper>
<PlaylistFollowButton
playlistId="1"
initialFollowing={false}
initialFollowerCount={10}
showCount={true}
/>
</TestWrapper>,
);
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(
<TestWrapper>
<PlaylistFollowButton
playlistId="1"
initialFollowing={false}
onFollowChange={mockOnFollowChange}
/>
</TestWrapper>,
);
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(
<TestWrapper>
<PlaylistFollowButton playlistId="1" initialFollowing={false} />
</TestWrapper>,
);
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();
});
});
});