mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
- quota-combo-balancing / quota-multiprovider: eliminate the per-test full SQLite migration (migrate once at module load; resetStorage now DELETEs rows instead of rmSync+re-migrate) so it no longer races --test-force-exit under concurrency; drain the fire-and-forget syncQuotaCombosGuarded dispatched by createPool/ updatePool (flushPendingSyncs via setImmediate) so assertions see deterministic combo state; assert the GROUP-slug combo name (combos are named by group, like quota-combo-groups) and seed the group. Validated on CI shards 5/8 + 8/8 (7 runs). - playground-compare: wait for CompareTab (dynamic import) to mount, and use expect().toBeVisible() instead of locator.isVisible() (which no longer waits in Playwright 1.50+). - group-b-quota-plans-config: drop the unreliable raw-HTML "500" substring check (Next.js chunk hashes contain "500"); keep the real error-boundary text check.
155 lines
5.7 KiB
TypeScript
155 lines
5.7 KiB
TypeScript
/**
|
|
* Group B — Quota Plans Config E2E spec.
|
|
*
|
|
* The originally planned standalone page /dashboard/costs/quota-share/plans does not
|
|
* exist in the current codebase (Group B plan 22 F9 implemented plans via the
|
|
* PoolWizard inside /dashboard/costs/quota-share, not a separate route).
|
|
*
|
|
* Tests are corrected to navigate to the existing /dashboard/costs/quota-share page
|
|
* which contains the group <select> element (QuotaSharePageClient.tsx line ~362).
|
|
* Backend is mocked so this spec does not require a running upstream.
|
|
*/
|
|
|
|
import { test, expect } from "@playwright/test";
|
|
import { gotoDashboardRoute } from "./helpers/dashboardAuth";
|
|
|
|
test.describe("Group B — Quota Plans Config", () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
// Mock the plans list endpoint
|
|
await page.route("**/api/quota/plans**", async (route) => {
|
|
const url = new URL(route.request().url());
|
|
const pathParts = url.pathname.split("/");
|
|
const lastPart = pathParts[pathParts.length - 1];
|
|
|
|
if (lastPart === "plans") {
|
|
// List all plans
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify([
|
|
{
|
|
connectionId: null,
|
|
provider: "codex",
|
|
dimensions: [
|
|
{ unit: "percent", window: "5h", limit: 100 },
|
|
{ unit: "percent", window: "weekly", limit: 100 },
|
|
],
|
|
source: "auto",
|
|
},
|
|
]),
|
|
});
|
|
} else {
|
|
// Single plan by connectionId
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({
|
|
connectionId: lastPart,
|
|
provider: "codex",
|
|
dimensions: [
|
|
{ unit: "percent", window: "5h", limit: 100 },
|
|
{ unit: "percent", window: "weekly", limit: 100 },
|
|
],
|
|
source: "auto",
|
|
}),
|
|
});
|
|
}
|
|
});
|
|
|
|
// Mock pools list — QuotaSharePageClient uses usePools() which fetches /api/quota/pools
|
|
await page.route("**/api/quota/pools**", async (route) => {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify([]),
|
|
});
|
|
});
|
|
|
|
// Mock pool groups list
|
|
await page.route("**/api/quota/groups**", async (route) => {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify([]),
|
|
});
|
|
});
|
|
|
|
// Mock provider connections list
|
|
await page.route("**/api/providers/client**", async (route) => {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify([]),
|
|
});
|
|
});
|
|
|
|
// Mock API keys list
|
|
await page.route("**/api/keys**", async (route) => {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify([]),
|
|
});
|
|
});
|
|
|
|
// Mock quota-store settings
|
|
await page.route("**/api/settings/quota-store**", async (route) => {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({ driver: "sqlite", redisUrl: null }),
|
|
});
|
|
});
|
|
});
|
|
|
|
test("quota share page exists and returns 200", async ({ page }) => {
|
|
// /dashboard/costs/quota-share/plans does not exist as a standalone route;
|
|
// the plans wizard is embedded in /dashboard/costs/quota-share.
|
|
const response = await page.goto(
|
|
"http://localhost:20128/dashboard/costs/quota-share",
|
|
{ waitUntil: "domcontentloaded" }
|
|
);
|
|
expect(response?.status()).not.toBe(404);
|
|
expect(response?.status()).not.toBe(500);
|
|
});
|
|
|
|
test("quota plans config page renders provider selector", async ({ page }) => {
|
|
// The standalone /plans sub-page was never created; the group <select> element
|
|
// that allows filtering pools lives directly in /dashboard/costs/quota-share
|
|
// (QuotaSharePageClient.tsx). Navigate there instead.
|
|
await gotoDashboardRoute(page, "/dashboard/costs/quota-share");
|
|
|
|
// Group selector (a <select> element) should be visible
|
|
const providerSelector = page.locator(
|
|
"select, [role='combobox'], [data-testid='provider-selector']"
|
|
);
|
|
await expect(providerSelector.first()).toBeVisible({ timeout: 15000 });
|
|
});
|
|
|
|
test("selecting codex provider shows dimension rows", async ({ page }) => {
|
|
// Navigate to the real quota-share page (plans are embedded, not a standalone route)
|
|
await gotoDashboardRoute(page, "/dashboard/costs/quota-share");
|
|
|
|
// The group selector is a <select> element in QuotaSharePageClient
|
|
const selector = page.locator("select, [role='combobox']").first();
|
|
await expect(selector).toBeVisible({ timeout: 15000 });
|
|
|
|
// Select codex if the option is available (it will only appear if the mock
|
|
// returns a group named "codex" — the current mock returns an empty groups list,
|
|
// so the selector will only have the "All groups" option).
|
|
const codexOption = page.getByRole("option", { name: /codex/i });
|
|
if (await codexOption.isVisible({ timeout: 3000 }).catch(() => false)) {
|
|
await selector.selectOption({ label: /codex/i });
|
|
}
|
|
|
|
// After selection, the page should not be in a broken state.
|
|
// Note: page.content() includes the full HTML source, which contains Next.js
|
|
// chunk filenames — those hashes can legitimately contain the string "500".
|
|
// Checking for "500" in raw HTML is unreliable; instead check for the actual
|
|
// error boundary text that OmniRoute renders on unrecoverable errors
|
|
// (src/app/error.tsx heading: "Internal Server Error").
|
|
const pageContent = await page.content();
|
|
expect(pageContent).not.toContain("Internal Server Error");
|
|
});
|
|
});
|