Files
adopt-a-street/frontend/src/__tests__/Leaderboard.test.js
T
William Valentin 3e4c730860 feat: implement comprehensive gamification, analytics, and leaderboard system
This commit adds a complete gamification system with analytics dashboards,
leaderboards, and enhanced badge tracking functionality.

Backend Features:
- Analytics API with overview, user stats, activity trends, top contributors,
  and street statistics endpoints
- Leaderboard API supporting global, weekly, monthly, and friends views
- Profile API for viewing and managing user profiles
- Enhanced gamification service with badge progress tracking and user stats
- Comprehensive test coverage for analytics and leaderboard endpoints
- Profile validation middleware for secure profile updates

Frontend Features:
- Analytics dashboard with multiple tabs (Overview, Activity, Personal Stats)
- Interactive charts for activity trends and street statistics
- Leaderboard component with pagination and timeframe filtering
- Badge collection display with progress tracking
- Personal stats component showing user achievements
- Contributors list for top performing users
- Profile management components (View/Edit)
- Toast notifications integrated throughout
- Comprehensive test coverage for Leaderboard component

Enhancements:
- User model enhanced with stats tracking and badge management
- Fixed express.Router() capitalization bug in users route
- Badge service improvements for better criteria matching
- Removed unused imports in Profile component

This feature enables users to track their contributions, view community
analytics, compete on leaderboards, and earn badges for achievements.

🤖 Generated with OpenCode

Co-Authored-By: AI Assistant <noreply@opencode.ai>
2025-11-03 13:53:48 -08:00

481 lines
13 KiB
JavaScript

import React from "react";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { BrowserRouter } from "react-router-dom";
import axios from "axios";
import { toast } from "react-toastify";
import Leaderboard from "../components/Leaderboard";
import { AuthContext } from "../context/AuthContext";
// Mock axios
jest.mock("axios");
// Mock react-toastify
jest.mock("react-toastify", () => ({
toast: {
success: jest.fn(),
error: jest.fn(),
warning: jest.fn(),
},
}));
// Mock leaderboard data
const mockLeaderboardData = [
{
userId: "user1",
username: "TopUser",
email: "topuser@example.com",
points: 1000,
streetsAdopted: 5,
tasksCompleted: 20,
badges: [
{ name: "Beginner", icon: "🏅" },
{ name: "Intermediate", icon: "🏆" },
],
},
{
userId: "user2",
username: "SecondUser",
email: "second@example.com",
points: 800,
streetsAdopted: 4,
tasksCompleted: 15,
badges: [{ name: "Beginner", icon: "🏅" }],
},
{
userId: "user3",
username: "ThirdUser",
email: "third@example.com",
points: 600,
streetsAdopted: 3,
tasksCompleted: 10,
badges: [],
},
];
const mockStats = {
totalUsers: 100,
totalPoints: 50000,
averagePoints: 500,
maxPoints: 1000,
minPoints: 0,
};
describe("Leaderboard Component", () => {
const mockAuthContext = {
auth: {
isAuthenticated: true,
user: {
_id: "user1",
username: "TopUser",
points: 1000,
},
},
};
const renderLeaderboard = (authContext = mockAuthContext) => {
return render(
<BrowserRouter>
<AuthContext.Provider value={authContext}>
<Leaderboard />
</AuthContext.Provider>
</BrowserRouter>
);
};
beforeEach(() => {
jest.clearAllMocks();
localStorage.setItem("token", "mock-token");
});
afterEach(() => {
localStorage.clear();
});
describe("Initial Loading", () => {
it("should display loading spinner on initial load", () => {
axios.get.mockImplementation(() => new Promise(() => {})); // Never resolves
renderLeaderboard();
expect(screen.getByText(/loading leaderboard/i)).toBeInTheDocument();
expect(screen.getByRole("status")).toBeInTheDocument();
});
it("should load global leaderboard by default", async () => {
axios.get.mockImplementation((url) => {
if (url.includes("/api/leaderboard/global")) {
return Promise.resolve({ data: mockLeaderboardData });
}
if (url.includes("/api/leaderboard/stats")) {
return Promise.resolve({ data: mockStats });
}
});
renderLeaderboard();
await waitFor(() => {
expect(screen.getByText("TopUser")).toBeInTheDocument();
});
expect(axios.get).toHaveBeenCalledWith(
expect.stringContaining("/api/leaderboard/global"),
expect.anything()
);
});
it("should load leaderboard stats", async () => {
axios.get.mockImplementation((url) => {
if (url.includes("/api/leaderboard/global")) {
return Promise.resolve({ data: mockLeaderboardData });
}
if (url.includes("/api/leaderboard/stats")) {
return Promise.resolve({ data: mockStats });
}
});
renderLeaderboard();
await waitFor(() => {
expect(screen.getByText(/total users:/i)).toBeInTheDocument();
});
expect(screen.getByText(/100/)).toBeInTheDocument();
expect(screen.getByText(/50,000/)).toBeInTheDocument();
});
});
describe("Tab Navigation", () => {
beforeEach(() => {
axios.get.mockImplementation((url) => {
if (url.includes("/api/leaderboard/")) {
return Promise.resolve({ data: mockLeaderboardData });
}
return Promise.resolve({ data: mockStats });
});
});
it("should switch to weekly leaderboard when clicking weekly tab", async () => {
renderLeaderboard();
await waitFor(() => {
expect(screen.getByText("TopUser")).toBeInTheDocument();
});
const weeklyTab = screen.getByRole("button", { name: /this week/i });
fireEvent.click(weeklyTab);
await waitFor(() => {
expect(axios.get).toHaveBeenCalledWith(
expect.stringContaining("/api/leaderboard/weekly"),
expect.anything()
);
});
});
it("should switch to monthly leaderboard when clicking monthly tab", async () => {
renderLeaderboard();
await waitFor(() => {
expect(screen.getByText("TopUser")).toBeInTheDocument();
});
const monthlyTab = screen.getByRole("button", { name: /this month/i });
fireEvent.click(monthlyTab);
await waitFor(() => {
expect(axios.get).toHaveBeenCalledWith(
expect.stringContaining("/api/leaderboard/monthly"),
expect.anything()
);
});
});
it("should switch to friends leaderboard when authenticated", async () => {
renderLeaderboard();
await waitFor(() => {
expect(screen.getByText("TopUser")).toBeInTheDocument();
});
const friendsTab = screen.getByRole("button", { name: /friends/i });
fireEvent.click(friendsTab);
await waitFor(() => {
expect(axios.get).toHaveBeenCalledWith(
expect.stringContaining("/api/leaderboard/friends"),
expect.objectContaining({
headers: { "x-auth-token": "mock-token" },
})
);
});
});
it("should show warning when trying to access friends tab without authentication", async () => {
const unauthContext = {
auth: {
isAuthenticated: false,
user: null,
},
};
axios.get.mockResolvedValue({ data: mockLeaderboardData });
renderLeaderboard(unauthContext);
await waitFor(() => {
expect(screen.getByText("TopUser")).toBeInTheDocument();
});
const friendsTab = screen.getByRole("button", { name: /friends/i });
fireEvent.click(friendsTab);
expect(toast.warning).toHaveBeenCalledWith(
"Please login to view friends leaderboard"
);
});
});
describe("User Display", () => {
beforeEach(() => {
axios.get.mockImplementation((url) => {
if (url.includes("/api/leaderboard/")) {
return Promise.resolve({ data: mockLeaderboardData });
}
return Promise.resolve({ data: mockStats });
});
});
it("should display all users in leaderboard", async () => {
renderLeaderboard();
await waitFor(() => {
expect(screen.getByText("TopUser")).toBeInTheDocument();
expect(screen.getByText("SecondUser")).toBeInTheDocument();
expect(screen.getByText("ThirdUser")).toBeInTheDocument();
});
});
it("should highlight current user", async () => {
renderLeaderboard();
await waitFor(() => {
expect(screen.getByText("TopUser")).toBeInTheDocument();
});
expect(screen.getByText("You")).toBeInTheDocument();
});
it("should display user points", async () => {
renderLeaderboard();
await waitFor(() => {
expect(screen.getByText("1,000")).toBeInTheDocument();
});
});
it("should display user statistics", async () => {
renderLeaderboard();
await waitFor(() => {
expect(screen.getByText("TopUser")).toBeInTheDocument();
});
// Check for streets and tasks counts
const statsElements = screen.getAllByText(/5|20/);
expect(statsElements.length).toBeGreaterThan(0);
});
it("should display current user points in alert", async () => {
renderLeaderboard();
await waitFor(() => {
expect(screen.getByText(/your points:/i)).toBeInTheDocument();
});
expect(screen.getByText("1000")).toBeInTheDocument();
});
});
describe("Pagination", () => {
beforeEach(() => {
axios.get.mockImplementation((url) => {
if (url.includes("/api/leaderboard/")) {
return Promise.resolve({ data: mockLeaderboardData });
}
return Promise.resolve({ data: mockStats });
});
});
it("should disable previous button on first page", async () => {
renderLeaderboard();
await waitFor(() => {
expect(screen.getByText("TopUser")).toBeInTheDocument();
});
const previousButton = screen.getByRole("button", { name: /previous/i });
expect(previousButton).toBeDisabled();
});
it("should enable next button when there are more results", async () => {
// Mock 50 results to trigger hasMore
const largeDataset = Array.from({ length: 50 }, (_, i) => ({
userId: `user${i}`,
username: `User${i}`,
points: 1000 - i * 10,
streetsAdopted: 1,
tasksCompleted: 1,
badges: [],
}));
axios.get.mockImplementation((url) => {
if (url.includes("/api/leaderboard/")) {
return Promise.resolve({ data: largeDataset });
}
return Promise.resolve({ data: mockStats });
});
renderLeaderboard();
await waitFor(() => {
expect(screen.getByText("User0")).toBeInTheDocument();
});
const nextButton = screen.getByRole("button", { name: /next/i });
expect(nextButton).not.toBeDisabled();
});
it("should load next page when clicking next button", async () => {
const largeDataset = Array.from({ length: 50 }, (_, i) => ({
userId: `user${i}`,
username: `User${i}`,
points: 1000 - i * 10,
streetsAdopted: 1,
tasksCompleted: 1,
badges: [],
}));
axios.get.mockImplementation((url) => {
if (url.includes("/api/leaderboard/")) {
return Promise.resolve({ data: largeDataset });
}
return Promise.resolve({ data: mockStats });
});
renderLeaderboard();
await waitFor(() => {
expect(screen.getByText("User0")).toBeInTheDocument();
});
const nextButton = screen.getByRole("button", { name: /next/i });
fireEvent.click(nextButton);
await waitFor(() => {
expect(axios.get).toHaveBeenCalledWith(
expect.stringContaining("offset=50"),
expect.anything()
);
});
});
});
describe("Error Handling", () => {
it("should display error message when API fails", async () => {
axios.get.mockRejectedValue({
response: { data: { msg: "Server error" } },
});
renderLeaderboard();
await waitFor(() => {
expect(screen.getByText(/error loading leaderboard/i)).toBeInTheDocument();
});
expect(toast.error).toHaveBeenCalledWith("Server error");
});
it("should show retry button on error", async () => {
axios.get.mockRejectedValue({
response: { data: { msg: "Server error" } },
});
renderLeaderboard();
await waitFor(() => {
expect(screen.getByRole("button", { name: /retry/i })).toBeInTheDocument();
});
});
it("should retry loading when clicking retry button", async () => {
axios.get
.mockRejectedValueOnce({
response: { data: { msg: "Server error" } },
})
.mockImplementation((url) => {
if (url.includes("/api/leaderboard/")) {
return Promise.resolve({ data: mockLeaderboardData });
}
return Promise.resolve({ data: mockStats });
});
renderLeaderboard();
await waitFor(() => {
expect(screen.getByRole("button", { name: /retry/i })).toBeInTheDocument();
});
const retryButton = screen.getByRole("button", { name: /retry/i });
fireEvent.click(retryButton);
await waitFor(() => {
expect(screen.getByText("TopUser")).toBeInTheDocument();
});
});
});
describe("Empty State", () => {
it("should display message when leaderboard is empty", async () => {
axios.get.mockImplementation((url) => {
if (url.includes("/api/leaderboard/")) {
return Promise.resolve({ data: [] });
}
return Promise.resolve({ data: mockStats });
});
renderLeaderboard();
await waitFor(() => {
expect(
screen.getByText(/no users to display yet/i)
).toBeInTheDocument();
});
});
it("should display friends-specific message when friends leaderboard is empty", async () => {
axios.get.mockImplementation((url) => {
if (url.includes("/api/leaderboard/friends")) {
return Promise.resolve({ data: [] });
}
if (url.includes("/api/leaderboard/")) {
return Promise.resolve({ data: mockLeaderboardData });
}
return Promise.resolve({ data: mockStats });
});
renderLeaderboard();
await waitFor(() => {
expect(screen.getByText("TopUser")).toBeInTheDocument();
});
const friendsTab = screen.getByRole("button", { name: /friends/i });
fireEvent.click(friendsTab);
await waitFor(() => {
expect(
screen.getByText(/no friends to display/i)
).toBeInTheDocument();
});
});
});
});