Files
diegosouzapw--omniroute/tests/unit/ui/edit-connection-modal-free-models.test.tsx
2026-07-13 13:39:12 +08:00

236 lines
7.7 KiB
TypeScript

// @vitest-environment jsdom
import React, { act } from "react";
import { createRoot } from "react-dom/client";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
vi.mock("next/navigation", () => ({
useParams: () => ({ id: "openrouter" }),
useRouter: () => ({ push: vi.fn(), replace: vi.fn() }),
}));
vi.mock("next-intl", () => ({
useTranslations: () => (key: string) => key,
}));
const { default: EditConnectionModal } =
await import("../../../src/app/(dashboard)/dashboard/providers/[id]/components/modals/EditConnectionModal");
const FREE_TOGGLE = 'button[role="switch"][aria-label="importFreeModelsOnlyLabel"]';
const containers: Array<{ root: ReturnType<typeof createRoot>; el: HTMLDivElement }> = [];
function render(props: Record<string, unknown>) {
const el = document.createElement("div");
document.body.appendChild(el);
const root = createRoot(el);
act(() => {
root.render(
<EditConnectionModal
isOpen
providerId={(props.providerId as string) || "openrouter"}
onSave={async () => undefined}
onClose={() => {}}
{...(props as any)}
/>
);
});
containers.push({ root, el });
return el;
}
function setInputValue(input: HTMLInputElement, value: string) {
const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value")!.set!;
act(() => {
setter.call(input, value);
input.dispatchEvent(new Event("input", { bubbles: true }));
});
}
async function waitFor(fn: () => boolean, timeoutMs = 2000) {
const start = Date.now();
while (!fn()) {
if (Date.now() - start > timeoutMs) throw new Error("waitFor timed out");
await new Promise((r) => setTimeout(r, 20));
}
}
beforeEach(() => {
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
vi.clearAllMocks();
vi.stubGlobal(
"fetch",
vi.fn(() =>
Promise.resolve({ ok: true, json: async () => ({}), text: async () => "" } as Response)
)
);
vi.stubGlobal("localStorage", {
getItem: () => null,
setItem: () => undefined,
removeItem: () => undefined,
clear: () => undefined,
});
});
afterEach(() => {
for (const { root, el } of containers.splice(0)) {
act(() => root.unmount());
el.remove();
}
vi.unstubAllGlobals();
});
describe("EditConnectionModal — import only free models", () => {
it("shows the free-only toggle for a connection whose provider has free models", () => {
const el = render({
connection: { id: "conn-1", provider: "openrouter", providerSpecificData: {} },
});
expect(el.querySelector(FREE_TOGGLE)).toBeTruthy();
});
it("hides the free-only toggle for a provider without free models", () => {
const el = render({
providerId: "anthropic",
connection: { id: "conn-2", provider: "anthropic", providerSpecificData: {} },
});
expect(el.querySelector(FREE_TOGGLE)).toBeNull();
});
it("reflects the persisted flag as initially checked", () => {
const el = render({
connection: {
id: "conn-3",
provider: "openrouter",
providerSpecificData: { importFreeModelsOnly: true },
},
});
expect(el.querySelector(FREE_TOGGLE)!.getAttribute("aria-checked")).toBe("true");
});
it("clears the flag (explicit false) and re-syncs when toggled off", async () => {
const onSave = vi.fn().mockResolvedValue(undefined);
const onResyncModels = vi.fn().mockResolvedValue(undefined);
const el = render({
connection: {
id: "conn-5",
provider: "openrouter",
name: "OR",
providerSpecificData: { importFreeModelsOnly: true },
},
onSave,
onResyncModels,
});
const toggle = el.querySelector<HTMLButtonElement>(FREE_TOGGLE)!;
expect(toggle.getAttribute("aria-checked")).toBe("true");
act(() => {
toggle.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
const saveBtn = Array.from(el.querySelectorAll("button")).find(
(b) => b.textContent?.trim() === "save"
)!;
act(() => {
saveBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
await waitFor(() => onSave.mock.calls.length > 0);
// Must be an explicit `false`, not `undefined` — otherwise the PUT merge keeps the old `true`.
expect(onSave.mock.calls[0][0].providerSpecificData?.importFreeModelsOnly).toBe(false);
await waitFor(() => onResyncModels.mock.calls.length > 0);
expect(onResyncModels).toHaveBeenCalledWith("conn-5");
});
it("saves the flag and triggers a re-sync when toggled on", async () => {
const onSave = vi.fn().mockResolvedValue(undefined);
const onResyncModels = vi.fn().mockResolvedValue(undefined);
const el = render({
connection: { id: "conn-4", provider: "openrouter", name: "OR", providerSpecificData: {} },
onSave,
onResyncModels,
});
const toggle = el.querySelector<HTMLButtonElement>(FREE_TOGGLE)!;
act(() => {
toggle.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
const saveBtn = Array.from(el.querySelectorAll("button")).find(
(b) => b.textContent?.trim() === "save"
)!;
act(() => {
saveBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
await waitFor(() => onSave.mock.calls.length > 0);
const payload = onSave.mock.calls[0][0];
expect(payload.providerSpecificData?.importFreeModelsOnly).toBe(true);
await waitFor(() => onResyncModels.mock.calls.length > 0);
expect(onResyncModels).toHaveBeenCalledWith("conn-4");
});
});
describe("EditConnectionModal — quota scraping fields", () => {
it("saves OpenCode Go workspace and replacement auth cookie", async () => {
const onSave = vi.fn().mockResolvedValue(undefined);
const el = render({
providerId: "opencode-go",
connection: {
id: "conn-opencode-go",
provider: "opencode-go",
name: "OpenCode Go",
authType: "apikey",
providerSpecificData: { workspaceId: "workspace-existing" },
},
onSave,
});
const workspaceInput = el.querySelector<HTMLInputElement>(
'input[name="opencodeGoWorkspaceId"]'
)!;
const cookieInput = el.querySelector<HTMLInputElement>('input[name="opencodeGoAuthCookie"]')!;
expect(workspaceInput.value).toBe("workspace-existing");
setInputValue(workspaceInput, "workspace-updated");
setInputValue(cookieInput, "auth=opencode-cookie");
const saveBtn = Array.from(el.querySelectorAll("button")).find(
(b) => b.textContent?.trim() === "save"
)!;
act(() => {
saveBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
await waitFor(() => onSave.mock.calls.length > 0);
const payload = onSave.mock.calls[0][0];
expect(payload.providerSpecificData?.opencodeGoWorkspaceId).toBe("workspace-updated");
expect(payload.providerSpecificData?.opencodeGoAuthCookie).toBe("auth=opencode-cookie");
});
it("omits Ollama Cloud usage cookie when the edit field is left blank", async () => {
const onSave = vi.fn().mockResolvedValue(undefined);
const el = render({
providerId: "ollama-cloud",
connection: {
id: "conn-ollama-cloud",
provider: "ollama-cloud",
name: "Ollama Cloud",
authType: "apikey",
providerSpecificData: {},
},
onSave,
});
expect(el.querySelector<HTMLInputElement>('input[name="ollamaCloudUsageCookie"]')).toBeTruthy();
const saveBtn = Array.from(el.querySelectorAll("button")).find(
(b) => b.textContent?.trim() === "save"
)!;
act(() => {
saveBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
await waitFor(() => onSave.mock.calls.length > 0);
const payload = onSave.mock.calls[0][0];
expect("ollamaCloudUsageCookie" in payload.providerSpecificData).toBe(false);
});
});