Files
fusion/packages/dashboard/app/components/__tests__/CustomProviderForm.test.tsx
Fusion 567873bc02 feat(FN-2959): merge fusion/fn-2959-2
- Add custom provider CRUD routes (`POST/GET/PUT/DELETE /api/custom-providers`) with auth validation and storage in `~/.fusion/settings.json`; registers via `register-custom-provider-routes.ts`
- Add `CustomProviderForm` component for user-defined provider configuration (endpoint URL, API key, model ID); includes dedicated CSS
- Add custom provider section to `SettingsModal` under the "Models" tab with provider list and add/disable controls
- Add custom provider selection to `ModelOnboardingModal` onboarding flow so new users can use their own endpoints
- Add changeset (`custom-openai-anthropic-providers.md`) for `@runfusion/fusion` minor release
- Add unit tests for `CustomProviderForm`, `ModelOnboardingModal`, `SettingsModal`, custom-provider routes, and `useTasks` hook; update existing mocks

Commits merged:
- feat(FN-2959): complete Step 6 — add changeset and docs
- test(FN-2959): update settings modal mocks for custom provider API
- feat(FN-2959): complete Step 4 — add onboarding custom provider flow
- feat(FN-2959): complete Step 3 — add settings custom provider disclosure
- feat(FN-2959): complete Step 2 — add custom provider form component
- feat(FN-2959): complete Step 1 — add custom provider CRUD routes
- feat(FN-2975): merge fusion/fn-2975

Files changed:
.changeset/custom-openai-anthropic-providers.md    |   5 +
 packages/dashboard/app/api/legacy.ts               |  41 ++++
 .../app/components/CustomProviderForm.css          |  45 ++++
 .../app/components/CustomProviderForm.tsx          | 203 ++++++++++++++++
 .../app/components/ModelOnboardingModal.css        |  14 ++
 .../app/components/ModelOnboardingModal.tsx        |  62 ++++-
 .../dashboard/app/components/SettingsModal.css     |  38 +++
 .../dashboard/app/components/SettingsModal.tsx     |  91 +++++++-
 .../__tests__/CustomProviderForm.test.tsx          |  60 +++++
 .../__tests__/ModelOnboardingModal.test.tsx        |  23 ++
 .../components/__tests__/SettingsModal.test.tsx    |  13 ++
 .../__tests__/SettingsModalNodeRouting.test.tsx    |   4 +
 .../components/__tests__/settings-mobile.test.tsx  |   4 +
 .../dashboard/app/hooks/__tests__/useTasks.test.ts |  48 ++++
 packages/dashboard/app/hooks/useTasks.ts           |   4 +-
 packages/dashboard/src/auth-paths.ts               |   4 +
 packages/dashboard/src/routes.ts                   |   2 +
 .../__tests__/custom-provider-routes.test.ts       | 118 ++++++++++
 .../src/routes/register-custom-provider-routes.ts  | 254 +++++++++++++++++++++
 19 files changed, 1027 insertions(+), 6 deletions(-)

Fusion-Task-Id: FN-2959
2026-04-29 20:15:05 -07:00

61 lines
2.7 KiB
TypeScript

import { describe, it, expect, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { CustomProviderForm } from "../CustomProviderForm";
describe("CustomProviderForm", () => {
it("renders base fields", () => {
render(<CustomProviderForm onSave={vi.fn()} />);
expect(screen.getByLabelText("Provider ID")).toBeInTheDocument();
expect(screen.getByLabelText("Display Name")).toBeInTheDocument();
expect(screen.getByLabelText("Base URL")).toBeInTheDocument();
expect(screen.getByLabelText("API Type")).toBeInTheDocument();
expect(screen.getByLabelText("API Key")).toBeInTheDocument();
});
it("validates required fields and rejects built-in IDs", async () => {
const user = userEvent.setup();
render(<CustomProviderForm onSave={vi.fn()} />);
await user.click(screen.getByRole("button", { name: "Save Provider" }));
expect(screen.getByText("Provider ID is required.")).toBeInTheDocument();
await user.type(screen.getByLabelText("Provider ID"), "openai");
await user.type(screen.getByLabelText("Base URL"), "https://proxy.example.com/v1");
await user.type(screen.getByLabelText("Model ID 1"), "gpt-4o-mini");
await user.click(screen.getByRole("button", { name: "Save Provider" }));
expect(screen.getByText("Provider ID conflicts with a built-in provider.")).toBeInTheDocument();
});
it("submits valid config", async () => {
const user = userEvent.setup();
const onSave = vi.fn().mockResolvedValue(undefined);
render(<CustomProviderForm onSave={onSave} />);
await user.type(screen.getByLabelText("Provider ID"), "my-proxy");
await user.type(screen.getByLabelText("Display Name"), "My Proxy");
await user.type(screen.getByLabelText("Base URL"), "https://proxy.example.com/v1");
await user.selectOptions(screen.getByLabelText("API Type"), "openai-responses");
await user.type(screen.getByLabelText("API Key"), "MY_API_KEY");
await user.type(screen.getByLabelText("Model ID 1"), "gpt-4.1-mini");
await user.type(screen.getByLabelText("Model name 1"), "GPT 4.1 Mini");
await user.click(screen.getByRole("button", { name: "Save Provider" }));
expect(onSave).toHaveBeenCalledWith(expect.objectContaining({
id: "my-proxy",
name: "My Proxy",
baseUrl: "https://proxy.example.com/v1",
api: "openai-responses",
apiKey: "MY_API_KEY",
models: [expect.objectContaining({ id: "gpt-4.1-mini", name: "GPT 4.1 Mini" })],
}));
});
it("shows external error state", () => {
render(<CustomProviderForm onSave={vi.fn()} error="Request failed" />);
expect(screen.getByText("Request failed")).toBeInTheDocument();
});
});