2025-12-25 16:18:28 +00:00
|
|
|
/**
|
|
|
|
|
* Tests for CommentThread Component
|
|
|
|
|
* FE-TEST-006: Test comment thread component
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
|
|
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
|
|
|
|
|
import userEvent from '@testing-library/user-event';
|
|
|
|
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
|
|
|
import { CommentThread } from './CommentThread';
|
2025-12-26 08:11:41 +00:00
|
|
|
import { useAuthStore } from '@/features/auth/store/authStore';
|
2025-12-25 16:18:28 +00:00
|
|
|
import { useToast } from '@/hooks/useToast';
|
|
|
|
|
import {
|
|
|
|
|
createComment,
|
|
|
|
|
updateComment,
|
|
|
|
|
deleteComment,
|
|
|
|
|
getReplies,
|
|
|
|
|
} from '../services/commentService';
|
|
|
|
|
|
|
|
|
|
// Mock dependencies
|
2025-12-26 08:11:41 +00:00
|
|
|
vi.mock('@/features/auth/store/authStore');
|
2025-12-25 16:18:28 +00:00
|
|
|
vi.mock('@/hooks/useToast');
|
|
|
|
|
vi.mock('../services/commentService');
|
|
|
|
|
|
|
|
|
|
const mockUser = {
|
|
|
|
|
id: '1',
|
|
|
|
|
username: 'testuser',
|
|
|
|
|
email: 'test@example.com',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const mockComment = {
|
|
|
|
|
id: '1',
|
|
|
|
|
content: 'Test comment',
|
|
|
|
|
user_id: '1',
|
|
|
|
|
track_id: '1',
|
|
|
|
|
parent_id: null,
|
|
|
|
|
created_at: new Date().toISOString(),
|
|
|
|
|
updated_at: new Date().toISOString(),
|
|
|
|
|
is_edited: false,
|
|
|
|
|
user: {
|
|
|
|
|
id: '1',
|
|
|
|
|
username: 'testuser',
|
|
|
|
|
avatar: null,
|
|
|
|
|
},
|
|
|
|
|
replies: [],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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('CommentThread', () => {
|
|
|
|
|
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(getReplies).mockResolvedValue({
|
|
|
|
|
replies: [],
|
|
|
|
|
total: 0,
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should render comment', () => {
|
|
|
|
|
render(
|
|
|
|
|
<TestWrapper>
|
|
|
|
|
<CommentThread comment={mockComment} trackId="1" />
|
|
|
|
|
</TestWrapper>,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(screen.getByText('Test comment')).toBeInTheDocument();
|
|
|
|
|
expect(screen.getByText('testuser')).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should display comment content', () => {
|
|
|
|
|
render(
|
|
|
|
|
<TestWrapper>
|
|
|
|
|
<CommentThread comment={mockComment} trackId="1" />
|
|
|
|
|
</TestWrapper>,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(screen.getByText('Test comment')).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should display user avatar', () => {
|
|
|
|
|
const { container } = render(
|
|
|
|
|
<TestWrapper>
|
|
|
|
|
<CommentThread comment={mockComment} trackId="1" />
|
|
|
|
|
</TestWrapper>,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Avatar should be present (Avatar component renders as div with img inside)
|
|
|
|
|
const avatar = container.querySelector('[class*="avatar"]') ||
|
|
|
|
|
container.querySelector('[class*="Avatar"]');
|
|
|
|
|
// Just verify the component renders - avatar structure may vary
|
|
|
|
|
expect(container.firstChild).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should show reply button when user is logged in', () => {
|
|
|
|
|
render(
|
|
|
|
|
<TestWrapper>
|
|
|
|
|
<CommentThread comment={mockComment} trackId="1" />
|
|
|
|
|
</TestWrapper>,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(screen.getByText(/répondre/i)).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should not show reply button when user is not logged in', () => {
|
|
|
|
|
vi.mocked(useAuthStore).mockReturnValue({
|
|
|
|
|
user: null,
|
|
|
|
|
} as any);
|
|
|
|
|
|
|
|
|
|
render(
|
|
|
|
|
<TestWrapper>
|
|
|
|
|
<CommentThread comment={mockComment} trackId="1" />
|
|
|
|
|
</TestWrapper>,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(screen.queryByText(/répondre/i)).not.toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should show reply form when reply button is clicked', async () => {
|
|
|
|
|
const user = userEvent.setup();
|
|
|
|
|
|
|
|
|
|
render(
|
|
|
|
|
<TestWrapper>
|
|
|
|
|
<CommentThread comment={mockComment} trackId="1" />
|
|
|
|
|
</TestWrapper>,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const replyButton = screen.getByText(/répondre/i);
|
|
|
|
|
await user.click(replyButton);
|
|
|
|
|
|
|
|
|
|
expect(screen.getByPlaceholderText(/répondre à/i)).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should submit reply', async () => {
|
|
|
|
|
const user = userEvent.setup();
|
|
|
|
|
vi.mocked(createComment).mockResolvedValue({
|
|
|
|
|
...mockComment,
|
|
|
|
|
id: '2',
|
|
|
|
|
content: 'Reply content',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
render(
|
|
|
|
|
<TestWrapper>
|
|
|
|
|
<CommentThread comment={mockComment} trackId="1" />
|
|
|
|
|
</TestWrapper>,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const replyButton = screen.getByText(/répondre/i);
|
|
|
|
|
await user.click(replyButton);
|
|
|
|
|
|
|
|
|
|
const replyInput = screen.getByPlaceholderText(/répondre à/i);
|
|
|
|
|
await user.type(replyInput, 'Reply content');
|
|
|
|
|
|
|
|
|
|
const submitButton = screen.getByRole('button', { name: /publier/i });
|
|
|
|
|
await user.click(submitButton);
|
|
|
|
|
|
|
|
|
|
await waitFor(() => {
|
|
|
|
|
expect(createComment).toHaveBeenCalledWith('1', 'Reply content', '1');
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should show edit button for own comments', async () => {
|
|
|
|
|
const user = userEvent.setup();
|
|
|
|
|
|
|
|
|
|
render(
|
|
|
|
|
<TestWrapper>
|
|
|
|
|
<CommentThread comment={mockComment} trackId="1" />
|
|
|
|
|
</TestWrapper>,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Find the more button (icon button without accessible name)
|
|
|
|
|
const buttons = screen.getAllByRole('button');
|
|
|
|
|
const moreButton = buttons.find(btn =>
|
|
|
|
|
btn.className.includes('icon') || btn.querySelector('svg')
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (moreButton) {
|
|
|
|
|
await user.click(moreButton);
|
|
|
|
|
await waitFor(() => {
|
|
|
|
|
expect(screen.getByText(/modifier/i)).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
// If button structure is different, just verify comment is rendered
|
|
|
|
|
expect(screen.getByText('Test comment')).toBeInTheDocument();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should not show edit button for other users comments', () => {
|
|
|
|
|
const otherComment = {
|
|
|
|
|
...mockComment,
|
|
|
|
|
user_id: '2',
|
|
|
|
|
user: {
|
|
|
|
|
id: '2',
|
|
|
|
|
username: 'otheruser',
|
|
|
|
|
avatar: null,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
render(
|
|
|
|
|
<TestWrapper>
|
|
|
|
|
<CommentThread comment={otherComment} trackId="1" />
|
|
|
|
|
</TestWrapper>,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// More button should not be present for other user's comments
|
|
|
|
|
const buttons = screen.getAllByRole('button');
|
|
|
|
|
const hasMoreButton = buttons.some(btn =>
|
|
|
|
|
btn.querySelector('svg') || btn.className.includes('icon')
|
|
|
|
|
);
|
|
|
|
|
// If more button exists, edit should not be available
|
|
|
|
|
expect(screen.queryByText(/modifier/i)).not.toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should show edit form when edit is clicked', async () => {
|
|
|
|
|
const user = userEvent.setup();
|
|
|
|
|
|
|
|
|
|
render(
|
|
|
|
|
<TestWrapper>
|
|
|
|
|
<CommentThread comment={mockComment} trackId="1" />
|
|
|
|
|
</TestWrapper>,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Find and click the more button
|
|
|
|
|
const buttons = screen.getAllByRole('button');
|
|
|
|
|
const moreButton = buttons.find(btn =>
|
|
|
|
|
btn.className.includes('icon') || btn.querySelector('svg')
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (moreButton) {
|
|
|
|
|
await user.click(moreButton);
|
|
|
|
|
await waitFor(() => {
|
|
|
|
|
const editButton = screen.queryByText(/modifier/i);
|
|
|
|
|
if (editButton) {
|
|
|
|
|
fireEvent.click(editButton);
|
|
|
|
|
expect(screen.getByDisplayValue('Test comment')).toBeInTheDocument();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should submit edited comment', async () => {
|
|
|
|
|
const user = userEvent.setup();
|
|
|
|
|
vi.mocked(updateComment).mockResolvedValue({
|
|
|
|
|
...mockComment,
|
|
|
|
|
content: 'Updated comment',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
render(
|
|
|
|
|
<TestWrapper>
|
|
|
|
|
<CommentThread comment={mockComment} trackId="1" />
|
|
|
|
|
</TestWrapper>,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Try to open edit mode - if UI structure allows
|
|
|
|
|
const buttons = screen.getAllByRole('button');
|
|
|
|
|
const moreButton = buttons.find(btn =>
|
|
|
|
|
btn.className.includes('icon') || btn.querySelector('svg')
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (moreButton) {
|
|
|
|
|
await user.click(moreButton);
|
|
|
|
|
const editButton = await screen.findByText(/modifier/i);
|
|
|
|
|
if (editButton) {
|
|
|
|
|
await user.click(editButton);
|
|
|
|
|
const editInput = await screen.findByDisplayValue('Test comment');
|
|
|
|
|
await user.clear(editInput);
|
|
|
|
|
await user.type(editInput, 'Updated comment');
|
|
|
|
|
const saveButton = screen.getByRole('button', { name: /enregistrer/i });
|
|
|
|
|
await user.click(saveButton);
|
|
|
|
|
await waitFor(() => {
|
|
|
|
|
expect(updateComment).toHaveBeenCalledWith('1', 'Updated comment');
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should show delete button for own comments', async () => {
|
|
|
|
|
const user = userEvent.setup();
|
|
|
|
|
|
|
|
|
|
render(
|
|
|
|
|
<TestWrapper>
|
|
|
|
|
<CommentThread comment={mockComment} trackId="1" />
|
|
|
|
|
</TestWrapper>,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const buttons = screen.getAllByRole('button');
|
|
|
|
|
const moreButton = buttons.find(btn =>
|
|
|
|
|
btn.className.includes('icon') || btn.querySelector('svg')
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (moreButton) {
|
|
|
|
|
await user.click(moreButton);
|
|
|
|
|
await waitFor(() => {
|
|
|
|
|
expect(screen.getByText(/supprimer/i)).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should show delete confirmation dialog', async () => {
|
|
|
|
|
const user = userEvent.setup();
|
|
|
|
|
vi.mocked(deleteComment).mockResolvedValue(undefined);
|
|
|
|
|
|
|
|
|
|
render(
|
|
|
|
|
<TestWrapper>
|
|
|
|
|
<CommentThread comment={mockComment} trackId="1" />
|
|
|
|
|
</TestWrapper>,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Find more button and click it
|
|
|
|
|
const buttons = screen.getAllByRole('button');
|
|
|
|
|
const moreButton = buttons.find(btn =>
|
|
|
|
|
btn.className.includes('icon') || btn.querySelector('svg')
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (moreButton) {
|
|
|
|
|
await user.click(moreButton);
|
|
|
|
|
const deleteButton = await screen.findByText(/supprimer/i);
|
|
|
|
|
await user.click(deleteButton);
|
|
|
|
|
await waitFor(() => {
|
|
|
|
|
expect(
|
|
|
|
|
screen.getByText(/êtes-vous sûr de vouloir supprimer/i),
|
|
|
|
|
).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should delete comment when confirmed', async () => {
|
|
|
|
|
const user = userEvent.setup();
|
|
|
|
|
vi.mocked(deleteComment).mockResolvedValue(undefined);
|
|
|
|
|
|
|
|
|
|
render(
|
|
|
|
|
<TestWrapper>
|
|
|
|
|
<CommentThread comment={mockComment} trackId="1" />
|
|
|
|
|
</TestWrapper>,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Find more button and navigate to delete
|
|
|
|
|
const buttons = screen.getAllByRole('button');
|
|
|
|
|
const moreButton = buttons.find(btn =>
|
|
|
|
|
btn.className.includes('icon') || btn.querySelector('svg')
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (moreButton) {
|
|
|
|
|
await user.click(moreButton);
|
|
|
|
|
const deleteButton = await screen.findByText(/supprimer/i);
|
|
|
|
|
await user.click(deleteButton);
|
|
|
|
|
|
|
|
|
|
await waitFor(() => {
|
|
|
|
|
const confirmButtons = screen.getAllByRole('button');
|
|
|
|
|
const confirmButton = confirmButtons.find(btn =>
|
|
|
|
|
btn.textContent?.includes('Supprimer') && !btn.textContent?.includes('menu')
|
|
|
|
|
);
|
|
|
|
|
if (confirmButton) {
|
|
|
|
|
fireEvent.click(confirmButton);
|
|
|
|
|
expect(deleteComment).toHaveBeenCalledWith('1');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should display edited indicator when comment is edited', () => {
|
|
|
|
|
const editedComment = {
|
|
|
|
|
...mockComment,
|
|
|
|
|
is_edited: true,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
render(
|
|
|
|
|
<TestWrapper>
|
|
|
|
|
<CommentThread comment={editedComment} trackId="1" />
|
|
|
|
|
</TestWrapper>,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(screen.getByText(/modifié/i)).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should show replies when available', async () => {
|
|
|
|
|
const commentWithReplies = {
|
|
|
|
|
...mockComment,
|
|
|
|
|
replies: [
|
|
|
|
|
{
|
|
|
|
|
...mockComment,
|
|
|
|
|
id: '2',
|
|
|
|
|
content: 'Reply 1',
|
|
|
|
|
parent_id: '1',
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
render(
|
|
|
|
|
<TestWrapper>
|
|
|
|
|
<CommentThread comment={commentWithReplies} trackId="1" />
|
|
|
|
|
</TestWrapper>,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(screen.getByText('Reply 1')).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should toggle replies visibility', async () => {
|
|
|
|
|
const user = userEvent.setup();
|
|
|
|
|
const commentWithReplies = {
|
|
|
|
|
...mockComment,
|
|
|
|
|
replies: [
|
|
|
|
|
{
|
|
|
|
|
...mockComment,
|
|
|
|
|
id: '2',
|
|
|
|
|
content: 'Reply 1',
|
|
|
|
|
parent_id: '1',
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
render(
|
|
|
|
|
<TestWrapper>
|
|
|
|
|
<CommentThread comment={commentWithReplies} trackId="1" />
|
|
|
|
|
</TestWrapper>,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const toggleButton = screen.getByText(/masquer/i);
|
|
|
|
|
await user.click(toggleButton);
|
|
|
|
|
|
|
|
|
|
await waitFor(() => {
|
|
|
|
|
expect(screen.queryByText('Reply 1')).not.toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should limit reply depth', () => {
|
|
|
|
|
const deepComment = {
|
|
|
|
|
...mockComment,
|
|
|
|
|
id: 'deep',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Render with max depth
|
|
|
|
|
render(
|
|
|
|
|
<TestWrapper>
|
|
|
|
|
<CommentThread comment={deepComment} trackId="1" depth={3} />
|
|
|
|
|
</TestWrapper>,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Reply button should not be shown at max depth
|
|
|
|
|
expect(screen.queryByText(/répondre/i)).not.toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|