-
setScope(v as LeaderboardScope)}>
-
- {t("tabs.userRanking")}
- {isAdmin && (
-
- {t("tabs.userCacheHitRateRanking")}
-
- )}
- {isAdmin && {t("tabs.providerRanking")}}
- {isAdmin && (
-
- {t("tabs.providerCacheHitRateRanking")}
-
- )}
- {isAdmin && {t("tabs.modelRanking")}}
-
-
-
- {scope === "provider" || scope === "providerCacheHitRate" ? (
+
+
+
+ {showSecondaryTabs ? (
+
+ ) : null}
+
+
+ {isProviderFamily ? (
- {(scope === "user" || scope === "userCacheHitRate") && isAdmin && (
+ {isUserFamily && isAdmin && (
{
+ let container: HTMLDivElement | null = null;
+ let root: ReturnType | null = null;
+
+ beforeEach(() => {
+ container = document.createElement("div");
+ document.body.appendChild(container);
+ root = createRoot(container);
+ });
+
+ afterEach(() => {
+ if (root) {
+ act(() => root!.unmount());
+ root = null;
+ }
+ if (container) {
+ container.remove();
+ container = null;
+ }
+ });
+
+ it("renders user provider model tabs for admin", async () => {
+ const onPrimaryChange = vi.fn();
+
+ await act(async () => {
+ root!.render(
+
+ );
+ });
+
+ const triggers = Array.from(
+ container!.querySelectorAll("[data-testid^='leaderboard-primary-tab-']")
+ );
+ expect(container!.querySelector("[data-testid='leaderboard-primary-tabs']")).not.toBeNull();
+ expect(triggers.map((node) => node.dataset.testid)).toEqual([
+ "leaderboard-primary-tab-user",
+ "leaderboard-primary-tab-provider",
+ "leaderboard-primary-tab-model",
+ ]);
+
+ await act(async () => {
+ triggers[2]?.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
+ triggers[2]?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
+ });
+
+ expect(onPrimaryChange).toHaveBeenCalledWith("model");
+ });
+
+ it("renders only user tab for non-admin", async () => {
+ await act(async () => {
+ root!.render(
+
+ );
+ });
+
+ expect(container!.querySelector("[data-testid='leaderboard-primary-tab-user']")).not.toBeNull();
+ expect(container!.querySelector("[data-testid='leaderboard-primary-tab-provider']")).toBeNull();
+ expect(container!.querySelector("[data-testid='leaderboard-primary-tab-model']")).toBeNull();
+ });
+});
diff --git a/tests/unit/dashboard/leaderboard-secondary-tabs.test.tsx b/tests/unit/dashboard/leaderboard-secondary-tabs.test.tsx
new file mode 100644
index 000000000..c5ed578cc
--- /dev/null
+++ b/tests/unit/dashboard/leaderboard-secondary-tabs.test.tsx
@@ -0,0 +1,81 @@
+/**
+ * @vitest-environment happy-dom
+ */
+import { act } from "react";
+import { createRoot } from "react-dom/client";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import { LeaderboardSecondaryTabs } from "@/app/[locale]/dashboard/leaderboard/_components/leaderboard-secondary-tabs";
+
+describe("LeaderboardSecondaryTabs", () => {
+ let container: HTMLDivElement | null = null;
+ let root: ReturnType | null = null;
+
+ beforeEach(() => {
+ container = document.createElement("div");
+ document.body.appendChild(container);
+ root = createRoot(container);
+ });
+
+ afterEach(() => {
+ if (root) {
+ act(() => root!.unmount());
+ root = null;
+ }
+ if (container) {
+ container.remove();
+ container = null;
+ }
+ });
+
+ it("renders cost and cache-hit tabs for grouped scopes", async () => {
+ const onSecondaryChange = vi.fn();
+
+ await act(async () => {
+ root!.render(
+
+ );
+ });
+
+ const triggers = Array.from(
+ container!.querySelectorAll("[data-testid^='leaderboard-secondary-tab-']")
+ );
+ expect(container!.querySelector("[data-testid='leaderboard-secondary-tabs']")).not.toBeNull();
+ expect(triggers.map((node) => node.dataset.testid)).toEqual([
+ "leaderboard-secondary-tab-cost",
+ "leaderboard-secondary-tab-cache-hit",
+ ]);
+
+ await act(async () => {
+ triggers[1]?.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
+ triggers[1]?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
+ });
+
+ expect(onSecondaryChange).toHaveBeenCalledWith("cacheHit");
+ });
+
+ it("renders no secondary tabs for model", async () => {
+ await act(async () => {
+ root!.render(
+
+ );
+ });
+
+ expect(container!.querySelector("[data-testid='leaderboard-secondary-tabs']")).toBeNull();
+ });
+});
diff --git a/tests/unit/dashboard/leaderboard-tab-groups.test.ts b/tests/unit/dashboard/leaderboard-tab-groups.test.ts
new file mode 100644
index 000000000..088a4a501
--- /dev/null
+++ b/tests/unit/dashboard/leaderboard-tab-groups.test.ts
@@ -0,0 +1,34 @@
+import { describe, expect, it } from "vitest";
+import {
+ getPrimaryTabFromScope,
+ getScopeForPrimaryTab,
+ getScopeForSecondaryTab,
+ getSecondaryTabFromScope,
+ normalizeScopeFromUrl,
+} from "@/app/[locale]/dashboard/leaderboard/_components/leaderboard-tab-groups";
+
+describe("leaderboard tab groups", () => {
+ it("maps admin leaf scopes to grouped tabs", () => {
+ expect(normalizeScopeFromUrl("providerCacheHitRate", true)).toBe("providerCacheHitRate");
+ expect(getPrimaryTabFromScope("user")).toBe("user");
+ expect(getPrimaryTabFromScope("providerCacheHitRate")).toBe("provider");
+ expect(getPrimaryTabFromScope("model")).toBe("model");
+ expect(getSecondaryTabFromScope("user")).toBe("cost");
+ expect(getSecondaryTabFromScope("providerCacheHitRate")).toBe("cacheHit");
+ expect(getSecondaryTabFromScope("model")).toBeNull();
+ expect(getScopeForPrimaryTab("provider")).toBe("provider");
+ expect(getScopeForPrimaryTab("model")).toBe("model");
+ expect(getScopeForSecondaryTab("user", "cacheHit")).toBe("userCacheHitRate");
+ expect(getScopeForSecondaryTab("provider", "cost")).toBe("provider");
+ });
+
+ it("forces non-admin scopes back to user", () => {
+ expect(normalizeScopeFromUrl("provider", false)).toBe("user");
+ expect(normalizeScopeFromUrl("providerCacheHitRate", false)).toBe("user");
+ expect(normalizeScopeFromUrl("model", false)).toBe("user");
+ expect(normalizeScopeFromUrl("userCacheHitRate", false)).toBe("user");
+ expect(normalizeScopeFromUrl("user", false)).toBe("user");
+ expect(normalizeScopeFromUrl("unknown", true)).toBe("user");
+ expect(normalizeScopeFromUrl(null, true)).toBe("user");
+ });
+});
diff --git a/tests/unit/dashboard/leaderboard-view-filter-gating.test.tsx b/tests/unit/dashboard/leaderboard-view-filter-gating.test.tsx
new file mode 100644
index 000000000..461330d6e
--- /dev/null
+++ b/tests/unit/dashboard/leaderboard-view-filter-gating.test.tsx
@@ -0,0 +1,159 @@
+/**
+ * @vitest-environment happy-dom
+ */
+import { act } from "react";
+import { createRoot } from "react-dom/client";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import { LeaderboardView } from "@/app/[locale]/dashboard/leaderboard/_components/leaderboard-view";
+
+const fetchMock = vi.fn();
+const { getAllUserTagsMock, getAllUserKeyGroupsMock } = vi.hoisted(() => ({
+ getAllUserTagsMock: vi.fn(),
+ getAllUserKeyGroupsMock: vi.fn(),
+}));
+const searchParamsState = vi.hoisted(() => ({
+ value: new URLSearchParams(),
+}));
+const tMock = vi.hoisted(() => vi.fn((key: string) => key));
+
+vi.mock("next/navigation", () => ({
+ useSearchParams: () => searchParamsState.value,
+}));
+
+vi.mock("next-intl", () => ({
+ useTranslations: () => tMock,
+}));
+
+vi.mock("@/actions/users", () => ({
+ getAllUserTags: getAllUserTagsMock,
+ getAllUserKeyGroups: getAllUserKeyGroupsMock,
+}));
+
+vi.mock("@/app/[locale]/settings/providers/_components/provider-type-filter", () => ({
+ ProviderTypeFilter: ({ value }: { value: string }) => (
+ {value}
+ ),
+}));
+
+vi.mock("@/app/[locale]/dashboard/leaderboard/_components/date-range-picker", () => ({
+ DateRangePicker: () => ,
+}));
+
+vi.mock("@/app/[locale]/dashboard/leaderboard/_components/leaderboard-table", () => ({
+ LeaderboardTable: ({ data }: { data: unknown[] }) => (
+ {JSON.stringify(data)}
+ ),
+}));
+
+vi.mock("@/components/ui/tag-input", () => ({
+ TagInput: ({ ["data-testid"]: testId }: { "data-testid"?: string }) => (
+
+ ),
+}));
+
+vi.mock("@/i18n/routing", () => ({
+ Link: ({ children, href, ...props }: any) => (
+
+ {children}
+
+ ),
+}));
+
+const globalFetch = global.fetch;
+
+async function waitForFetchCalls(expectedCalls: number) {
+ for (let i = 0; i < 20; i += 1) {
+ if (fetchMock.mock.calls.length >= expectedCalls) {
+ return;
+ }
+
+ await act(async () => {
+ await Promise.resolve();
+ });
+ }
+
+ throw new Error(`fetchMock call count did not reach ${expectedCalls}`);
+}
+
+async function flushUi() {
+ await act(async () => {
+ await Promise.resolve();
+ });
+}
+
+describe("LeaderboardView filter gating", () => {
+ let container: HTMLDivElement | null = null;
+ let root: ReturnType | null = null;
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ getAllUserTagsMock.mockResolvedValue({ ok: true, data: ["vip"] });
+ getAllUserKeyGroupsMock.mockResolvedValue({ ok: true, data: ["default"] });
+ container = document.createElement("div");
+ document.body.appendChild(container);
+ root = createRoot(container);
+
+ fetchMock.mockImplementation(
+ async () =>
+ ({
+ ok: true,
+ json: async () => [],
+ }) as Response
+ );
+
+ global.fetch = fetchMock as typeof fetch;
+ });
+
+ afterEach(() => {
+ if (root) {
+ act(() => root!.unmount());
+ root = null;
+ }
+ if (container) {
+ container.remove();
+ container = null;
+ }
+ global.fetch = globalFetch;
+ });
+
+ it("shows provider filters for provider family and preserves provider request params", async () => {
+ searchParamsState.value = new URLSearchParams("scope=provider");
+
+ await act(async () => {
+ root!.render();
+ });
+ await waitForFetchCalls(1);
+ await flushUi();
+
+ expect(container!.querySelector("[data-testid='provider-filter']")).not.toBeNull();
+ expect(fetchMock.mock.calls.at(-1)?.[0]).toContain("scope=provider");
+ expect(fetchMock.mock.calls.at(-1)?.[0]).toContain("includeModelStats=1");
+
+ searchParamsState.value = new URLSearchParams("scope=providerCacheHitRate");
+ await act(async () => {
+ root!.render();
+ });
+ await waitForFetchCalls(2);
+ await flushUi();
+
+ expect(container!.querySelector("[data-testid='provider-filter']")).not.toBeNull();
+ expect(fetchMock.mock.calls.at(-1)?.[0]).toContain("scope=providerCacheHitRate");
+ expect(fetchMock.mock.calls.at(-1)?.[0]).not.toContain("includeModelStats=1");
+ });
+
+ it("hides secondary tabs and family filters for model scope", async () => {
+ searchParamsState.value = new URLSearchParams("scope=model");
+
+ await act(async () => {
+ root!.render();
+ });
+ await waitForFetchCalls(1);
+ await flushUi();
+
+ expect(container!.querySelector("[data-testid='leaderboard-secondary-tabs']")).toBeNull();
+ expect(container!.querySelector("[data-testid='provider-filter']")).toBeNull();
+ expect(container!.querySelector("[data-testid='leaderboard-user-tag-filter']")).toBeNull();
+ expect(container!.querySelector("[data-testid='leaderboard-user-group-filter']")).toBeNull();
+ expect(fetchMock.mock.calls.at(-1)?.[0]).toContain("scope=model");
+ });
+});
diff --git a/tests/unit/dashboard/leaderboard-view-tab-grouping.test.tsx b/tests/unit/dashboard/leaderboard-view-tab-grouping.test.tsx
new file mode 100644
index 000000000..3451fed9e
--- /dev/null
+++ b/tests/unit/dashboard/leaderboard-view-tab-grouping.test.tsx
@@ -0,0 +1,235 @@
+/**
+ * @vitest-environment happy-dom
+ */
+import { act } from "react";
+import { createRoot } from "react-dom/client";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import { LeaderboardView } from "@/app/[locale]/dashboard/leaderboard/_components/leaderboard-view";
+
+const fetchMock = vi.fn();
+const { getAllUserTagsMock, getAllUserKeyGroupsMock } = vi.hoisted(() => ({
+ getAllUserTagsMock: vi.fn(),
+ getAllUserKeyGroupsMock: vi.fn(),
+}));
+const searchParamsState = vi.hoisted(() => ({
+ value: new URLSearchParams(),
+}));
+const tMock = vi.hoisted(() => vi.fn((key: string) => key));
+
+vi.mock("next/navigation", () => ({
+ useSearchParams: () => searchParamsState.value,
+}));
+
+vi.mock("next-intl", () => ({
+ useTranslations: () => tMock,
+}));
+
+vi.mock("@/actions/users", () => ({
+ getAllUserTags: getAllUserTagsMock,
+ getAllUserKeyGroups: getAllUserKeyGroupsMock,
+}));
+
+vi.mock("@/app/[locale]/settings/providers/_components/provider-type-filter", () => ({
+ ProviderTypeFilter: ({ value }: { value: string }) => (
+ {value}
+ ),
+}));
+
+vi.mock("@/app/[locale]/dashboard/leaderboard/_components/date-range-picker", () => ({
+ DateRangePicker: () => ,
+}));
+
+vi.mock("@/app/[locale]/dashboard/leaderboard/_components/leaderboard-table", () => ({
+ LeaderboardTable: ({ data }: { data: unknown[] }) => (
+ {JSON.stringify(data)}
+ ),
+}));
+
+vi.mock("@/components/ui/tag-input", () => ({
+ TagInput: ({ ["data-testid"]: testId }: { "data-testid"?: string }) => (
+
+ ),
+}));
+
+vi.mock("@/i18n/routing", () => ({
+ Link: ({ children, href, ...props }: any) => (
+
+ {children}
+
+ ),
+}));
+
+const globalFetch = global.fetch;
+
+async function waitForFetchCalls(expectedCalls: number) {
+ for (let i = 0; i < 20; i += 1) {
+ if (fetchMock.mock.calls.length >= expectedCalls) {
+ return;
+ }
+
+ await act(async () => {
+ await Promise.resolve();
+ });
+ }
+
+ throw new Error(`fetchMock call count did not reach ${expectedCalls}`);
+}
+
+async function flushUi() {
+ await act(async () => {
+ await Promise.resolve();
+ });
+}
+
+function getRequestedScopes() {
+ return fetchMock.mock.calls.map((call) => {
+ const url = new URL(String(call[0]), "http://localhost");
+ return url.searchParams.get("scope");
+ });
+}
+
+describe("LeaderboardView grouped tabs", () => {
+ let container: HTMLDivElement | null = null;
+ let root: ReturnType | null = null;
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ getAllUserTagsMock.mockResolvedValue({ ok: true, data: ["vip"] });
+ getAllUserKeyGroupsMock.mockResolvedValue({ ok: true, data: ["default"] });
+ container = document.createElement("div");
+ document.body.appendChild(container);
+ root = createRoot(container);
+
+ fetchMock.mockImplementation(
+ async () =>
+ ({
+ ok: true,
+ json: async () => [],
+ }) as Response
+ );
+
+ global.fetch = fetchMock as typeof fetch;
+ });
+
+ afterEach(() => {
+ if (root) {
+ act(() => root!.unmount());
+ root = null;
+ }
+ if (container) {
+ container.remove();
+ container = null;
+ }
+ global.fetch = globalFetch;
+ });
+
+ it("deep-links admin users to the user cost leaderboard", async () => {
+ searchParamsState.value = new URLSearchParams("scope=user");
+
+ await act(async () => {
+ root!.render();
+ });
+ await waitForFetchCalls(1);
+ await flushUi();
+
+ expect(
+ container!.querySelector("[data-testid='leaderboard-primary-tab-user'][data-state='active']")
+ ).not.toBeNull();
+ expect(
+ container!.querySelector(
+ "[data-testid='leaderboard-secondary-tab-cost'][data-state='active']"
+ )
+ ).not.toBeNull();
+ expect(getRequestedScopes()).toContain("user");
+ });
+
+ it("deep-links admin users to the cache-hit secondary tab", async () => {
+ searchParamsState.value = new URLSearchParams("scope=userCacheHitRate");
+
+ await act(async () => {
+ root!.render();
+ });
+ await waitForFetchCalls(1);
+ await flushUi();
+
+ expect(
+ container!.querySelector("[data-testid='leaderboard-primary-tab-user'][data-state='active']")
+ ).not.toBeNull();
+ expect(
+ container!.querySelector(
+ "[data-testid='leaderboard-secondary-tab-cache-hit'][data-state='active']"
+ )
+ ).not.toBeNull();
+ expect(
+ fetchMock.mock.calls.some((call) => String(call[0]).includes("scope=userCacheHitRate"))
+ ).toBe(true);
+ });
+
+ it("switching primary tab resets grouped scopes to cost leaf", async () => {
+ searchParamsState.value = new URLSearchParams("scope=userCacheHitRate");
+
+ await act(async () => {
+ root!.render();
+ });
+ await waitForFetchCalls(1);
+ await flushUi();
+
+ const providerTab = container!.querySelector(
+ "[data-testid='leaderboard-primary-tab-provider']"
+ ) as HTMLElement | null;
+ expect(providerTab).not.toBeNull();
+
+ await act(async () => {
+ providerTab!.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
+ providerTab!.dispatchEvent(new MouseEvent("click", { bubbles: true }));
+ });
+ await waitForFetchCalls(2);
+ await flushUi();
+
+ const requestedUrls = fetchMock.mock.calls.map((call) => String(call[0]));
+ expect(requestedUrls.at(-1)).toContain("scope=provider");
+ expect(requestedUrls.at(-1)).not.toContain("providerCacheHitRate");
+ expect(
+ container!.querySelector(
+ "[data-testid='leaderboard-secondary-tab-cost'][data-state='active']"
+ )
+ ).not.toBeNull();
+ });
+
+ it("renders no secondary tabs for model", async () => {
+ searchParamsState.value = new URLSearchParams("scope=model");
+
+ await act(async () => {
+ root!.render();
+ });
+ await waitForFetchCalls(1);
+ await flushUi();
+
+ expect(
+ container!.querySelector("[data-testid='leaderboard-primary-tab-model'][data-state='active']")
+ ).not.toBeNull();
+ expect(container!.querySelector("[data-testid='leaderboard-secondary-tabs']")).toBeNull();
+ expect(getRequestedScopes()).toContain("model");
+ });
+
+ it("falls back non-admin users to the user cost leaderboard", async () => {
+ searchParamsState.value = new URLSearchParams("scope=providerCacheHitRate");
+
+ await act(async () => {
+ root!.render();
+ });
+ await waitForFetchCalls(1);
+ await flushUi();
+
+ expect(
+ container!.querySelector("[data-testid='leaderboard-primary-tab-user'][data-state='active']")
+ ).not.toBeNull();
+ expect(container!.querySelector("[data-testid='leaderboard-primary-tab-provider']")).toBeNull();
+ expect(container!.querySelector("[data-testid='leaderboard-primary-tab-model']")).toBeNull();
+ expect(container!.querySelector("[data-testid='leaderboard-secondary-tabs']")).toBeNull();
+ expect(getRequestedScopes()).toContain("user");
+ expect(getRequestedScopes()).not.toContain("userCacheHitRate");
+ expect(getRequestedScopes()).not.toContain("provider");
+ expect(getRequestedScopes()).not.toContain("providerCacheHitRate");
+ });
+});
diff --git a/tests/unit/dashboard/leaderboard-view-user-cache-hit-rate.test.tsx b/tests/unit/dashboard/leaderboard-view-user-cache-hit-rate.test.tsx
index 2ac1fa7ca..90d9f72dc 100644
--- a/tests/unit/dashboard/leaderboard-view-user-cache-hit-rate.test.tsx
+++ b/tests/unit/dashboard/leaderboard-view-user-cache-hit-rate.test.tsx
@@ -123,6 +123,14 @@ describe("LeaderboardView user cache hit rate scope", () => {
(url) => url.includes("scope=userCacheHitRate") && url.includes("includeUserModelStats=1")
)
).toBe(true);
+ expect(
+ container!.querySelector("[data-testid='leaderboard-primary-tab-user'][data-state='active']")
+ ).not.toBeNull();
+ expect(
+ container!.querySelector(
+ "[data-testid='leaderboard-secondary-tab-cache-hit'][data-state='active']"
+ )
+ ).not.toBeNull();
expect(container!.textContent).toContain("cache-user");
expect(container!.textContent).toContain("50.0%");
});