mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 20:32:20 +03:00
- Add quota_snapshots table for time-series quota tracking - Add QuotaSnapshot DB module with CRUD + cleanup (6h gate) - Add snapshot save hook in quotaCache.setQuotaCache() - Add Provider Utilization API: GET /api/usage/utilization - Add Combo Health API: GET /api/usage/combo-health - Add ProviderUtilizationTab with recharts LineChart - Add ComboHealthTab with quota/skew/performance metrics - Add TimeRangeSelector component (1h/24h/7d/30d) - Integrate tabs into /dashboard/analytics - Add unit tests for quotaSnapshots module - Add E2E tests for analytics tabs - Add i18n keys for 33 languages
301 lines
8.7 KiB
TypeScript
301 lines
8.7 KiB
TypeScript
import { test, expect } from "@playwright/test";
|
|
|
|
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 page.goto("/dashboard/analytics");
|
|
await page.waitForLoadState("networkidle");
|
|
|
|
const redirectedToLogin = page.url().includes("/login");
|
|
test.skip(redirectedToLogin, "Authentication enabled without a login fixture.");
|
|
|
|
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 page.goto("/dashboard/analytics");
|
|
await page.waitForLoadState("networkidle");
|
|
|
|
const redirectedToLogin = page.url().includes("/login");
|
|
test.skip(redirectedToLogin, "Authentication enabled without a login fixture.");
|
|
|
|
const utilizationTab = page
|
|
.locator("button")
|
|
.filter({
|
|
hasText: /utilization/i,
|
|
})
|
|
.first();
|
|
await utilizationTab.click();
|
|
|
|
await page.waitForTimeout(500);
|
|
|
|
const timeRangeSelector = page
|
|
.locator('[role="tablist"][aria-label]')
|
|
.filter({ hasText: /1h/ })
|
|
.first();
|
|
await expect(timeRangeSelector).toBeVisible();
|
|
|
|
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 page.goto("/dashboard/analytics");
|
|
await page.waitForLoadState("networkidle");
|
|
|
|
const redirectedToLogin = page.url().includes("/login");
|
|
test.skip(redirectedToLogin, "Authentication enabled without a login fixture.");
|
|
|
|
const comboHealthTab = page
|
|
.locator("button")
|
|
.filter({
|
|
hasText: /combo.*health/i,
|
|
})
|
|
.first();
|
|
await comboHealthTab.click();
|
|
|
|
await page.waitForTimeout(500);
|
|
|
|
const mainContent = page.locator('main, [class*="dashboard"], div[class*="container"]').first();
|
|
await expect(mainContent).toBeVisible();
|
|
|
|
const timeRangeSelector = page.locator('[aria-label="시간 범위 선택"]');
|
|
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 page.goto("/dashboard/analytics");
|
|
await page.waitForLoadState("networkidle");
|
|
|
|
const redirectedToLogin = page.url().includes("/login");
|
|
test.skip(redirectedToLogin, "Authentication enabled without a login fixture.");
|
|
|
|
const utilizationTab = page
|
|
.locator("button")
|
|
.filter({
|
|
hasText: /utilization/i,
|
|
})
|
|
.first();
|
|
await utilizationTab.click();
|
|
|
|
await page.waitForTimeout(500);
|
|
|
|
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 = page.locator('[aria-label="시간 범위 선택"]');
|
|
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 page.goto("/dashboard/analytics");
|
|
await page.waitForLoadState("networkidle");
|
|
|
|
const redirectedToLogin = page.url().includes("/login");
|
|
test.skip(redirectedToLogin, "Authentication enabled without a login fixture.");
|
|
|
|
const overviewTab = page
|
|
.locator("button")
|
|
.filter({
|
|
hasText: /overview/i,
|
|
})
|
|
.first();
|
|
await overviewTab.click();
|
|
await page.waitForTimeout(300);
|
|
|
|
const utilizationTab = page
|
|
.locator("button")
|
|
.filter({
|
|
hasText: /utilization/i,
|
|
})
|
|
.first();
|
|
await utilizationTab.click();
|
|
await page.waitForTimeout(300);
|
|
|
|
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 comboHealthTab.click();
|
|
await page.waitForTimeout(300);
|
|
|
|
const timeRangeSelector = page.locator('[aria-label="시간 범위 선택"]');
|
|
await expect(timeRangeSelector).toBeVisible();
|
|
|
|
await utilizationTab.click();
|
|
await page.waitForTimeout(300);
|
|
|
|
await expect(chart).toBeVisible();
|
|
});
|
|
});
|