Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions backend/src/routes/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,4 +205,55 @@ export const actionsRoutes = (
});
return page(result.items.map(serialize), { nextCursor: result.nextCursor, limit: q.limit });
});

app.get("/actions/leaderboard", async () => {
const data = [
{
rank: 1,
previousRank: 2,
walletAddress: "GABCD1234567890STUVWXWXYZ1234567890ALPHA",
displayName: "GABC...LPHA",
vaultId: "v_usdc_stable",
vaultName: "USDC Savings Sprint",
depositedAmount: 12500,
asset: "USDC",
ticketsCount: 250,
prizeWins: 3,
score: 9840,
state: "rising",
lastActivity: "10 minutes ago"
},
{
rank: 2,
previousRank: 1,
walletAddress: "GBBDU9876543210ZYXWVUTSRQPONMLKJIHGBETA",
displayName: "GBBD...BETA",
vaultId: "v_xlm_drip",
vaultName: "XLM Drip Vault",
depositedAmount: 8200,
asset: "XLM",
ticketsCount: 164,
prizeWins: 1,
score: 8120,
state: "holding",
lastActivity: "1 hour ago"
},
{
rank: 3,
previousRank: 5,
walletAddress: "GC4AK9Q2345678901234567890123456789GAMMA",
displayName: "GC4A...AMMA",
vaultId: "v_student_quest",
vaultName: "Student Saver Quest",
depositedAmount: 3400,
asset: "USDC",
ticketsCount: 68,
prizeWins: 2,
score: 6540,
state: "rising",
lastActivity: "3 hours ago"
}
];
return ok(data);
});
};
157 changes: 157 additions & 0 deletions components/app/VaultLeaderboard.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import React from "react";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { describe, expect, it, vi, beforeEach } from "vitest";
import { VaultLeaderboard } from "./VaultLeaderboardPlaceholder";
import { formatPrivacyAddress, sortLeaderboardEntries } from "@/services/leaderboardService";

vi.mock("@/services/leaderboardService", async (importOriginal) => {
const actual = await importOriginal();
return {
...actual,
getLeaderboardData: vi.fn(),
};
});

import { getLeaderboardData } from "@/services/leaderboardService";

const MOCK_ENTRIES = [
{
rank: 1,
previousRank: 2,
walletAddress: "GABCD1234567890STUVWXWXYZ1234567890ALPHA",
displayName: "GABC...LPHA",
vaultId: "v_usdc",
vaultName: "USDC Vault",
depositedAmount: 5000,
asset: "USDC",
ticketsCount: 100,
prizeWins: 2,
score: 9500,
state: "rising",
lastActivity: "5m ago",
},
{
rank: 2,
previousRank: 1,
walletAddress: "GBBDU9876543210ZYXWVUTSRQPONMLKJIHGBETA",
displayName: "GBBD...BETA",
vaultId: "v_xlm",
vaultName: "XLM Vault",
depositedAmount: 3000,
asset: "XLM",
ticketsCount: 60,
prizeWins: 0,
score: 7200,
state: "holding",
lastActivity: "1h ago",
},
];

describe("VaultLeaderboard Component & Service", () => {
beforeEach(() => {
vi.clearAllMocks();
});

describe("formatPrivacyAddress", () => {
it("formats Stellar public key into privacy-conscious string", () => {
const address = "GABCD1234567890STUVWXWXYZ1234567890ALPHA";
expect(formatPrivacyAddress(address)).toBe("GABC...LPHA");
});

it("formats EVM address into privacy-conscious string", () => {
const address = "0x1234567890abcdef1234567890abcdef12345678";
expect(formatPrivacyAddress(address)).toBe("0x12...5678");
});

it("handles null or short input gracefully", () => {
expect(formatPrivacyAddress(null)).toBe("Anonymous Saver");
expect(formatPrivacyAddress("12345")).toBe("12345");
});
});

describe("sortLeaderboardEntries", () => {
it("sorts entries by score descending", () => {
const unsorted = [
{ ...MOCK_ENTRIES[1], score: 5000 },
{ ...MOCK_ENTRIES[0], score: 9000 },
];
const sorted = sortLeaderboardEntries(unsorted);
expect(sorted[0].score).toBe(9000);
expect(sorted[1].score).toBe(5000);
});
});

describe("VaultLeaderboard UI component", () => {
it("renders initial populated data when provided", () => {
render(<VaultLeaderboard initialData={MOCK_ENTRIES} />);

expect(screen.getByText("Saver Leaderboard")).toBeInTheDocument();
expect(screen.getByText("GABC...LPHA")).toBeInTheDocument();
expect(screen.getByText("GBBD...BETA")).toBeInTheDocument();
expect(screen.getByText("9,500 pts")).toBeInTheDocument();
expect(screen.getByText("7,200 pts")).toBeInTheDocument();
});

it("fetches data and displays populated state on success", async () => {
getLeaderboardData.mockResolvedValueOnce({
success: true,
data: MOCK_ENTRIES,
total: 2,
updatedAt: "2026-07-20T12:00:00Z",
source: "indexed",
});

render(<VaultLeaderboard />);

await waitFor(() => {
expect(screen.getByText("Live indexed data")).toBeInTheDocument();
});

expect(screen.getByText("GABC...LPHA")).toBeInTheDocument();
});

it("renders error state and handles retry action", async () => {
getLeaderboardData.mockRejectedValueOnce(new Error("Network Error"));

render(<VaultLeaderboard />);

await waitFor(() => {
expect(screen.getByTestId("leaderboard-error")).toBeInTheDocument();
});

expect(screen.getByText("Leaderboard Unavailable")).toBeInTheDocument();

getLeaderboardData.mockResolvedValueOnce({
success: true,
data: MOCK_ENTRIES,
total: 2,
updatedAt: "2026-07-20T12:00:00Z",
source: "indexed",
});

fireEvent.click(screen.getByRole("button", { name: /retry/i }));

await waitFor(() => {
expect(screen.getByText("GABC...LPHA")).toBeInTheDocument();
});
});

it("renders empty state when no entries are returned", async () => {
getLeaderboardData.mockResolvedValueOnce({
success: true,
data: [],
total: 0,
updatedAt: "2026-07-20T12:00:00Z",
source: "indexed",
});

render(<VaultLeaderboard />);

await waitFor(() => {
expect(screen.getByTestId("leaderboard-empty")).toBeInTheDocument();
});

expect(screen.getByText("No leaderboard activity yet")).toBeInTheDocument();
});
});
});
Loading
Loading