import { beforeEach, describe, expect, it, vi } from "vitest"; import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { CustomProvidersSection } from "../CustomProvidersSection"; const mockFetchCustomProviders = vi.fn(); const mockAddCustomProvider = vi.fn(); const mockUpdateCustomProvider = vi.fn(); const mockDeleteCustomProvider = vi.fn(); vi.mock("../../api", () => ({ fetchCustomProviders: (...args: unknown[]) => mockFetchCustomProviders(...args), addCustomProvider: (...args: unknown[]) => mockAddCustomProvider(...args), updateCustomProvider: (...args: unknown[]) => mockUpdateCustomProvider(...args), deleteCustomProvider: (...args: unknown[]) => mockDeleteCustomProvider(...args), })); vi.mock("lucide-react", () => ({ AlertCircle: () => , ChevronRight: () => , Loader2: ({ className }: { className?: string }) => , Pencil: () => , Plus: () => , Search: () => , Trash2: () => , })); describe("CustomProvidersSection", () => { beforeEach(() => { vi.clearAllMocks(); Object.defineProperty(window, "matchMedia", { writable: true, value: vi.fn().mockImplementation((query: string) => ({ matches: false, media: query, onchange: null, addListener: vi.fn(), removeListener: vi.fn(), addEventListener: vi.fn(), removeEventListener: vi.fn(), dispatchEvent: vi.fn(), })), }); mockFetchCustomProviders.mockResolvedValue([]); mockAddCustomProvider.mockResolvedValue({ id: "test-id", name: "Test Provider", apiType: "openai-compatible", baseUrl: "https://api.example.com", }); mockUpdateCustomProvider.mockResolvedValue({ id: "test-id", name: "Updated", apiType: "openai-compatible", baseUrl: "https://api.example.com", }); mockDeleteCustomProvider.mockResolvedValue({ success: true }); }); it("renders collapsed disclosure by default", () => { render(); expect(screen.getByRole("button", { name: /Advanced: Custom Providers/i })).toBeTruthy(); expect(screen.queryByText("No custom providers configured.")).toBeNull(); }); it("loads providers when disclosure is expanded", async () => { render(); fireEvent.click(screen.getByRole("button", { name: /Advanced: Custom Providers/i })); await waitFor(() => { expect(mockFetchCustomProviders).toHaveBeenCalledTimes(1); expect(screen.getByText("No custom providers configured.")).toBeTruthy(); }); }); it("fetches providers on mount when embedded", async () => { render(); await waitFor(() => { expect(mockFetchCustomProviders).toHaveBeenCalledTimes(1); }); }); it("adds a provider and refreshes list", async () => { mockFetchCustomProviders .mockResolvedValueOnce([]) .mockResolvedValueOnce([ { id: "test-id", name: "Test Provider", apiType: "openai-compatible", baseUrl: "https://api.example.com", }, ]); render(); await waitFor(() => { expect(screen.getByRole("button", { name: /Add Custom Provider/i })).toBeTruthy(); }); fireEvent.click(screen.getByRole("button", { name: /Add Custom Provider/i })); fireEvent.change(screen.getByLabelText("Provider name"), { target: { value: "Test Provider" } }); fireEvent.change(screen.getByLabelText("Base URL"), { target: { value: "https://api.example.com" } }); fireEvent.click(screen.getByRole("button", { name: "Save Provider" })); await waitFor(() => { expect(mockAddCustomProvider).toHaveBeenCalledWith({ name: "Test Provider", apiType: "openai-compatible", baseUrl: "https://api.example.com", }); expect(screen.getByText("Test Provider")).toBeTruthy(); }); }); it("shows OpenAI Responses option and posts openai-responses apiType", async () => { mockFetchCustomProviders.mockResolvedValueOnce([]).mockResolvedValueOnce([]); render(); await waitFor(() => { expect(screen.getByRole("button", { name: /Add Custom Provider/i })).toBeTruthy(); }); fireEvent.click(screen.getByRole("button", { name: /Add Custom Provider/i })); const apiTypeSelect = screen.getByLabelText("API type") as HTMLSelectElement; expect(screen.getByRole("option", { name: "OpenAI Responses" })).toBeTruthy(); fireEvent.change(apiTypeSelect, { target: { value: "openai-responses" } }); expect(apiTypeSelect.value).toBe("openai-responses"); fireEvent.change(screen.getByLabelText("Provider name"), { target: { value: "Responses Provider" } }); fireEvent.change(screen.getByLabelText("Base URL"), { target: { value: "https://api.example.com/v1" } }); fireEvent.click(screen.getByRole("button", { name: "Save Provider" })); await waitFor(() => { expect(mockAddCustomProvider).toHaveBeenCalledWith({ name: "Responses Provider", apiType: "openai-responses", baseUrl: "https://api.example.com/v1", }); }); }); it("shows validation errors for empty name and invalid baseUrl", async () => { render(); await waitFor(() => { expect(screen.getByRole("button", { name: /Add Custom Provider/i })).toBeTruthy(); }); fireEvent.click(screen.getByRole("button", { name: /Add Custom Provider/i })); fireEvent.click(screen.getByRole("button", { name: "Save Provider" })); await waitFor(() => { expect(screen.getByText("Provider name is required.")).toBeTruthy(); }); fireEvent.change(screen.getByLabelText("Provider name"), { target: { value: "Name" } }); fireEvent.change(screen.getByLabelText("Base URL"), { target: { value: "ftp://example.com" } }); fireEvent.click(screen.getByRole("button", { name: "Save Provider" })); await waitFor(() => { expect(screen.getByText("Base URL must be a valid http/https URL.")).toBeTruthy(); }); }); it("normalizes legacy openai-responses api to apiType on edit", async () => { mockFetchCustomProviders .mockResolvedValueOnce([ { id: "legacy-id", name: "Legacy Responses", baseUrl: "https://legacy.example.com/v1", api: "openai-responses", models: [{ id: "r1", name: "Responses 1" }], }, ]) .mockResolvedValueOnce([ { id: "legacy-id", name: "Legacy Responses", apiType: "openai-responses", baseUrl: "https://legacy.example.com/v1", }, ]); render(); await waitFor(() => { expect(screen.getByLabelText("Edit Legacy Responses")).toBeTruthy(); }); fireEvent.click(screen.getByLabelText("Edit Legacy Responses")); fireEvent.click(screen.getByRole("button", { name: "Save Changes" })); await waitFor(() => { expect(mockUpdateCustomProvider).toHaveBeenCalledWith("legacy-id", expect.objectContaining({ apiType: "openai-responses", })); }); }); it("edits an existing provider", async () => { mockFetchCustomProviders .mockResolvedValueOnce([ { id: "test-id", name: "Test Provider", apiType: "openai-compatible", baseUrl: "https://api.example.com", }, ]) .mockResolvedValueOnce([ { id: "test-id", name: "Updated Provider", apiType: "openai-compatible", baseUrl: "https://api.updated.example.com", }, ]); render(); await waitFor(() => { expect(screen.getByLabelText("Edit Test Provider")).toBeTruthy(); }); fireEvent.click(screen.getByLabelText("Edit Test Provider")); fireEvent.change(screen.getByLabelText("Provider name"), { target: { value: "Updated Provider" } }); fireEvent.change(screen.getByLabelText("Base URL"), { target: { value: "https://api.updated.example.com" } }); fireEvent.click(screen.getByRole("button", { name: "Save Changes" })); await waitFor(() => { expect(mockUpdateCustomProvider).toHaveBeenCalledWith("test-id", { name: "Updated Provider", apiType: "openai-compatible", baseUrl: "https://api.updated.example.com", }); expect(screen.getByText("Updated Provider")).toBeTruthy(); }); }); it("deletes provider after confirmation", async () => { mockFetchCustomProviders .mockResolvedValueOnce([ { id: "test-id", name: "Test Provider", apiType: "openai-compatible", baseUrl: "https://api.example.com", }, ]) .mockResolvedValueOnce([]); vi.spyOn(window, "confirm").mockReturnValue(true); render(); await waitFor(() => { expect(screen.getByLabelText("Delete Test Provider")).toBeTruthy(); }); fireEvent.click(screen.getByLabelText("Delete Test Provider")); await waitFor(() => { expect(window.confirm).toHaveBeenCalled(); expect(mockDeleteCustomProvider).toHaveBeenCalledWith("test-id"); expect(screen.getByText("No custom providers configured.")).toBeTruthy(); }); }); it("shows load error when fetchCustomProviders fails", async () => { mockFetchCustomProviders.mockRejectedValueOnce(new Error("load failed")); render(); fireEvent.click(screen.getByRole("button", { name: /Advanced: Custom Providers/i })); await waitFor(() => { expect(screen.getByText("load failed")).toBeTruthy(); }); }); it("shows save error when addCustomProvider fails", async () => { mockAddCustomProvider.mockRejectedValueOnce(new Error("add failed")); render(); await waitFor(() => { expect(screen.getByRole("button", { name: /Add Custom Provider/i })).toBeTruthy(); }); fireEvent.click(screen.getByRole("button", { name: /Add Custom Provider/i })); fireEvent.change(screen.getByLabelText("Provider name"), { target: { value: "Test Provider" } }); fireEvent.change(screen.getByLabelText("Base URL"), { target: { value: "https://api.example.com" } }); fireEvent.click(screen.getByRole("button", { name: "Save Provider" })); await waitFor(() => { expect(screen.getByText("add failed")).toBeTruthy(); }); }); });