Files
OmniRoute/tests/e2e/analytics-tabs.spec.ts
Owen 9227964cb6 feat(analytics): add subscription utilization analytics (#847)
- 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
2026-03-31 00:12:27 -03:00

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();
});
});