Files
2026-07-13 13:39:12 +08:00

335 lines
9.8 KiB
TypeScript

import { test, expect } from "@playwright/test";
import { gotoDashboardRoute } from "./helpers/dashboardAuth";
function getTimeRangeSelector(page: import("@playwright/test").Page) {
return page.getByRole("tablist", { name: /select time range/i }).first();
}
async function waitForAnalyticsShell(page: import("@playwright/test").Page) {
const mainTabList = page.locator('[role="tablist"]').first();
await expect(mainTabList).toBeVisible({ timeout: 15000 });
await expect(
page
.locator("button")
.filter({
hasText: /overview/i,
})
.first()
).toBeVisible({ timeout: 15000 });
}
test.describe("Analytics Tabs UI", () => {
test.beforeEach(async ({ page }) => {
await page.route("**/api/usage/analytics", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
stats: {
totalRequests: 1234,
totalTokens: 567890,
estimatedCost: 12.34,
},
charts: {},
}),
});
});
await page.route("**/api/usage/utilization**", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
timeRange: "24h",
bucketSizeMinutes: 10,
providers: ["codex", "claude", "gemini"],
data: [
{
timestamp: new Date(Date.now() - 3600000).toISOString(),
provider: "codex",
remainingPct: 75.5,
isExhausted: false,
windowKey: "5h",
},
{
timestamp: new Date().toISOString(),
provider: "codex",
remainingPct: 72.0,
isExhausted: false,
windowKey: "5h",
},
],
}),
});
});
await page.route("**/api/usage/combo-health**", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
timeRange: "24h",
combo: {
id: "test-combo",
name: "Test Combo",
models: ["openai/gpt-4", "anthropic/claude-3"],
},
quotaHealth: {
overall: {
remainingAvg: 68.5,
trend: "stable",
},
providers: [
{
provider: "openai",
remaining: 75.0,
trend: "stable",
},
],
},
usageSkew: {
modelDistribution: [
{ model: "openai/gpt-4", count: 45, share: 0.6 },
{ model: "anthropic/claude-3", count: 30, share: 0.4 },
],
gini: 0.2,
},
performance: {
successRate: 0.98,
avgLatency: 1200,
totalCalls: 75,
},
}),
});
});
await page.route("**/api/combos", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
combos: [
{
id: "test-combo",
name: "Test Combo",
models: ["openai/gpt-4", "anthropic/claude-3"],
strategy: "priority",
isActive: true,
},
],
}),
});
});
});
test("displays all 5 analytics tabs", async ({ page }) => {
await gotoDashboardRoute(page, "/dashboard/analytics");
await waitForAnalyticsShell(page);
const mainTabList = page.locator('[role="tablist"]').first();
await expect(mainTabList).toBeVisible();
const tabButtons = page.locator(
'button[class*="segmented"], [role="tablist"] > button, [role="tablist"] div > button'
);
const count = await tabButtons.count();
expect(count).toBeGreaterThanOrEqual(4);
const tabLabels = ["overview", "evals", "search", "utilization", "combo health"];
for (const label of tabLabels) {
const tabButton = page
.locator("button")
.filter({
hasText: new RegExp(label, "i"),
})
.first();
await expect(tabButton).toBeVisible();
}
});
test("Provider Utilization tab shows TimeRangeSelector and chart", async ({ page }) => {
await gotoDashboardRoute(page, "/dashboard/analytics");
await waitForAnalyticsShell(page);
const utilizationTab = page
.locator("button")
.filter({
hasText: /utilization/i,
})
.first();
// Retry click until the tab switches, mitigating Next.js hydration race conditions
await expect(async () => {
await utilizationTab.click();
const timeRangeSelector = page
.locator('[role="tablist"][aria-label]')
.filter({ hasText: /1h/ })
.first();
await expect(timeRangeSelector).toBeVisible({ timeout: 1000 });
}).toPass({ timeout: 15000 });
const timeRangeSelector = page
.locator('[role="tablist"][aria-label]')
.filter({ hasText: /1h/ })
.first();
const timeRangeButtons = timeRangeSelector.locator('button[role="tab"]');
await expect(timeRangeButtons.first()).toBeVisible();
const chart = page
.locator('svg.recharts-surface, .recharts-wrapper, div[class*="recharts"]')
.first();
await expect(chart).toBeVisible();
});
test("Combo Health tab displays health cards and metrics", async ({ page }) => {
await gotoDashboardRoute(page, "/dashboard/analytics");
await waitForAnalyticsShell(page);
const comboHealthTab = page
.locator("button")
.filter({
hasText: /combo.*health/i,
})
.first();
await expect(async () => {
await comboHealthTab.click();
const timeRangeSelector = getTimeRangeSelector(page);
await expect(timeRangeSelector).toBeVisible({ timeout: 1000 });
}).toPass({ timeout: 15000 });
const mainContent = page.locator('main, [class*="dashboard"], div[class*="container"]').first();
await expect(mainContent).toBeVisible();
const timeRangeSelector = getTimeRangeSelector(page);
await expect(timeRangeSelector).toBeVisible();
const metricElements = page
.locator('[class*="rounded-lg"], [class*="border"], [class*="bg-black/"]')
.first();
await expect(metricElements).toBeVisible();
});
test("time range change triggers network request", async ({ page }) => {
await gotoDashboardRoute(page, "/dashboard/analytics");
await waitForAnalyticsShell(page);
const utilizationTab = page
.locator("button")
.filter({
hasText: /utilization/i,
})
.first();
await expect(async () => {
await utilizationTab.click();
const timeRangeSelector = getTimeRangeSelector(page);
await expect(timeRangeSelector).toBeVisible({ timeout: 1000 });
}).toPass({ timeout: 15000 });
let networkRequestMade = false;
page.on("request", (request) => {
if (request.url().includes("/api/usage/utilization")) {
const url = new URL(request.url());
if (url.searchParams.get("range") === "7d") {
networkRequestMade = true;
}
}
});
const timeRangeSelector = getTimeRangeSelector(page);
const sevenDayButton = timeRangeSelector
.locator('button[role="tab"]')
.filter({ hasText: "7d" })
.first();
if (await sevenDayButton.isVisible()) {
await sevenDayButton.click();
} else {
const buttons = timeRangeSelector.locator('button[role="tab"]');
const count = await buttons.count();
for (let i = 0; i < count; i++) {
const button = buttons.nth(i);
const text = await button.textContent();
if (text?.includes("7")) {
await button.click();
break;
}
}
}
await page.waitForTimeout(1000);
await expect
.poll(() => networkRequestMade, {
message: "Expected time range change to trigger network request",
})
.toBe(true);
});
test("tab switching persists state correctly", async ({ page }) => {
await gotoDashboardRoute(page, "/dashboard/analytics");
await waitForAnalyticsShell(page);
const overviewTab = page
.locator("button")
.filter({
hasText: /overview/i,
})
.first();
await expect(async () => {
await overviewTab.click();
const overviewStats = page.locator("text=Total API Requests").first();
// Overview uses UsageAnalytics, we wait for a generic evidence of overview
// Or simply just wait 300ms if click doesn't throw
})
.toPass({ timeout: 15000 })
.catch(() => {});
const utilizationTab = page
.locator("button")
.filter({
hasText: /utilization/i,
})
.first();
await expect(async () => {
await utilizationTab.click();
const chart = page
.locator('svg.recharts-surface, .recharts-wrapper, div[class*="recharts"]')
.first();
await expect(chart).toBeVisible({ timeout: 1000 });
}).toPass({ timeout: 15000 });
const chart = page
.locator('svg.recharts-surface, .recharts-wrapper, div[class*="recharts"]')
.first();
await expect(chart).toBeVisible();
const comboHealthTab = page
.locator("button")
.filter({
hasText: /combo.*health/i,
})
.first();
await expect(async () => {
await comboHealthTab.click();
const timeRangeSelector = getTimeRangeSelector(page);
await expect(timeRangeSelector).toBeVisible({ timeout: 1000 });
}).toPass({ timeout: 15000 });
const timeRangeSelector = getTimeRangeSelector(page);
await expect(timeRangeSelector).toBeVisible();
await expect(async () => {
await utilizationTab.click();
await expect(chart).toBeVisible({ timeout: 1000 });
}).toPass({ timeout: 15000 });
await expect(chart).toBeVisible();
});
});