Files
flyer-crawler.projectium.com/src/components/Leaderboard.test.tsx

146 lines
5.3 KiB
TypeScript

// src/components/Leaderboard.test.tsx
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach, type Mocked } from 'vitest';
import Leaderboard from './Leaderboard';
import * as apiClient from '../services/apiClient';
import { LeaderboardUser } from '../types';
import { createMockLeaderboardUser } from '../tests/utils/mockFactories';
import { createMockLogger } from '../tests/utils/mockLogger';
// Mock the apiClient
vi.mock('../services/apiClient'); // This was correct
const mockedApiClient = apiClient as Mocked<typeof apiClient>;
// Mock the logger
vi.mock('../services/logger', () => ({
logger: createMockLogger(),
}));
// Mock lucide-react icons to prevent rendering errors in the test environment
vi.mock('lucide-react', () => ({
Award: () => <div data-testid="award-icon" />,
Crown: () => <div data-testid="crown-icon" />,
ShieldAlert: () => <div data-testid="shield-alert-icon" />,
}));
const mockLeaderboardData: LeaderboardUser[] = [
createMockLeaderboardUser({ user_id: 'user-1', full_name: 'Alice', points: 1000, rank: '1' }),
createMockLeaderboardUser({
user_id: 'user-2',
full_name: 'Bob',
avatar_url: 'http://example.com/bob.jpg',
points: 950,
rank: '2',
}),
createMockLeaderboardUser({ user_id: 'user-3', full_name: 'Charlie', points: 900, rank: '3' }),
createMockLeaderboardUser({ user_id: 'user-4', full_name: 'Diana', points: 850, rank: '4' }),
];
describe('Leaderboard', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should display a loading message initially', () => {
// Mock a pending promise that never resolves to keep it in the loading state
mockedApiClient.fetchLeaderboard.mockReturnValue(new Promise(() => {}));
render(<Leaderboard />);
expect(screen.getByText('Loading Leaderboard...')).toBeInTheDocument();
});
it('should display an error message if the API call fails', async () => {
mockedApiClient.fetchLeaderboard.mockResolvedValue(new Response(null, { status: 500 }));
render(<Leaderboard />);
await waitFor(() => {
expect(screen.getByRole('alert')).toBeInTheDocument();
expect(screen.getByText('Error: Failed to fetch leaderboard data.')).toBeInTheDocument();
});
});
it('should display a generic error for unknown error types', async () => {
const unknownError = 'A string error';
mockedApiClient.fetchLeaderboard.mockRejectedValue(unknownError);
render(<Leaderboard />);
await waitFor(() => {
expect(screen.getByRole('alert')).toBeInTheDocument();
expect(screen.getByText('Error: An unknown error occurred.')).toBeInTheDocument();
});
});
it('should display a message when the leaderboard is empty', async () => {
mockedApiClient.fetchLeaderboard.mockResolvedValue(new Response(JSON.stringify([])));
render(<Leaderboard />);
await waitFor(() => {
expect(
screen.getByText('The leaderboard is currently empty. Be the first to earn points!'),
).toBeInTheDocument();
});
});
it('should render the leaderboard with user data on successful fetch', async () => {
mockedApiClient.fetchLeaderboard.mockResolvedValue(
new Response(JSON.stringify(mockLeaderboardData)),
);
render(<Leaderboard />);
await waitFor(() => {
expect(screen.getByRole('heading', { name: 'Top Users' })).toBeInTheDocument();
// Check for user details
expect(screen.getByText('Alice')).toBeInTheDocument();
expect(screen.getByText('1000 pts')).toBeInTheDocument();
expect(screen.getByText('Bob')).toBeInTheDocument();
expect(screen.getByText('950 pts')).toBeInTheDocument();
// Check for correct avatar URLs
const bobAvatar = screen.getByAltText('Bob') as HTMLImageElement;
expect(bobAvatar.src).toBe('http://example.com/bob.jpg');
const aliceAvatar = screen.getByAltText('Alice') as HTMLImageElement;
expect(aliceAvatar.src).toContain('api.dicebear.com'); // Check for fallback avatar
});
});
it('should render the correct rank icons', async () => {
mockedApiClient.fetchLeaderboard.mockResolvedValue(
new Response(JSON.stringify(mockLeaderboardData)),
);
render(<Leaderboard />);
await waitFor(() => {
// Rank 1, 2, and 3 should have a crown icon
const crownIcons = screen.getAllByTestId('crown-icon');
expect(crownIcons).toHaveLength(3);
// Rank 4 should display the number
expect(screen.getByText('4')).toBeInTheDocument();
});
});
it('should handle users with missing names correctly', async () => {
const dataWithMissingNames: LeaderboardUser[] = [
createMockLeaderboardUser({ user_id: 'user-anon', full_name: null, points: 500, rank: '5' }),
];
mockedApiClient.fetchLeaderboard.mockResolvedValue(
new Response(JSON.stringify(dataWithMissingNames)),
);
render(<Leaderboard />);
await waitFor(() => {
// Check for fallback name
expect(screen.getByText('Anonymous User')).toBeInTheDocument();
// Check for fallback avatar alt text and URL
const avatar = screen.getByAltText('User Avatar') as HTMLImageElement;
expect(avatar).toBeInTheDocument();
expect(avatar.src).toContain('api.dicebear.com');
expect(avatar.src).toContain('seed=user-anon');
});
});
});