Files
CC-Switch/tests/components/ClaudeFormFields.test.tsx
T
Jason 596019505f feat(provider-form): custom User-Agent presets dropdown in advanced settings
Polish the provider-level User-Agent override UI on the Claude and Codex forms.

- Add a shared CustomUserAgentField (label + input + preset dropdown + live
  validation) so both forms stay in sync.
- Provide curated UA presets (Claude Code / Kilo Code families that pass
  coding-plan UA whitelists per #3671); the first is Claude Code's real
  `claude-cli/x (external, cli)` format. Whitelists gate on the name prefix,
  not the version, so static values stay valid across upgrades.
- Expose presets via a dropdown to the right of the input (z-[200] so it
  renders above the dialog layers) instead of inline chips.
- Move the field into the existing advanced/reasoning collapsibles.
- userAgent.ts mirrors the backend byte rule (reject only control chars;
  non-ASCII is allowed) for a non-blocking inline hint.
- i18n for all four locales (zh/en/ja/zh-TW).
2026-06-11 08:29:29 +08:00

171 lines
5.1 KiB
TypeScript

import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import type { ComponentProps, PropsWithChildren } from "react";
import { useForm } from "react-hook-form";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { ClaudeFormFields } from "@/components/providers/forms/ClaudeFormFields";
import { Form } from "@/components/ui/form";
const copilotApiMock = vi.hoisted(() => ({
copilotGetModels: vi.fn(),
copilotGetModelsForAccount: vi.fn(),
}));
const modelFetchApiMock = vi.hoisted(() => ({
fetchCodexOauthModels: vi.fn(),
fetchModelsForConfig: vi.fn(),
showFetchModelsError: vi.fn(),
}));
vi.mock("@/lib/api/copilot", () => ({
copilotGetModels: copilotApiMock.copilotGetModels,
copilotGetModelsForAccount: copilotApiMock.copilotGetModelsForAccount,
}));
vi.mock("@/lib/api/model-fetch", () => ({
fetchCodexOauthModels: modelFetchApiMock.fetchCodexOauthModels,
fetchModelsForConfig: modelFetchApiMock.fetchModelsForConfig,
showFetchModelsError: modelFetchApiMock.showFetchModelsError,
}));
vi.mock("@/components/providers/forms/CopilotAuthSection", () => ({
CopilotAuthSection: () => <div data-testid="copilot-auth-section" />,
}));
vi.mock("@/components/providers/forms/CodexOAuthSection", () => ({
CodexOAuthSection: () => <div data-testid="codex-oauth-section" />,
}));
type ClaudeFormFieldsProps = ComponentProps<typeof ClaudeFormFields>;
const FormShell = ({ children }: PropsWithChildren) => {
const form = useForm();
return <Form {...form}>{children}</Form>;
};
const renderCopilotForm = (overrides: Partial<ClaudeFormFieldsProps> = {}) => {
const props: ClaudeFormFieldsProps = {
shouldShowApiKey: false,
apiKey: "",
onApiKeyChange: vi.fn(),
category: "official",
shouldShowApiKeyLink: false,
websiteUrl: "",
isCopilotPreset: true,
usesOAuth: true,
isCopilotAuthenticated: true,
selectedGitHubAccountId: "gh-1",
onGitHubAccountSelect: vi.fn(),
isCodexOauthPreset: false,
isCodexOauthAuthenticated: false,
selectedCodexAccountId: null,
onCodexAccountSelect: vi.fn(),
codexFastMode: false,
onCodexFastModeChange: vi.fn(),
templateValueEntries: [],
templateValues: {},
templatePresetName: "",
onTemplateValueChange: vi.fn(),
shouldShowSpeedTest: false,
baseUrl: "",
onBaseUrlChange: vi.fn(),
isEndpointModalOpen: false,
onEndpointModalToggle: vi.fn(),
onCustomEndpointsChange: vi.fn(),
autoSelect: false,
onAutoSelectChange: vi.fn(),
showEndpointTools: true,
shouldShowModelSelector: true,
claudeModel: "",
defaultHaikuModel: "",
defaultHaikuModelName: "",
defaultSonnetModel: "claude-sonnet",
defaultSonnetModelName: "Claude Sonnet",
defaultOpusModel: "",
defaultOpusModelName: "",
onModelChange: vi.fn(),
speedTestEndpoints: [],
apiFormat: "anthropic",
onApiFormatChange: vi.fn(),
apiKeyField: "ANTHROPIC_AUTH_TOKEN",
onApiKeyFieldChange: vi.fn(),
isFullUrl: false,
onFullUrlChange: vi.fn(),
customUserAgent: "",
onCustomUserAgentChange: vi.fn(),
...overrides,
};
return render(
<FormShell>
<ClaudeFormFields {...props} />
</FormShell>,
);
};
const renderCodexOauthForm = (overrides: Partial<ClaudeFormFieldsProps> = {}) =>
renderCopilotForm({
isCopilotPreset: false,
isCopilotAuthenticated: false,
selectedGitHubAccountId: null,
isCodexOauthPreset: true,
isCodexOauthAuthenticated: true,
selectedCodexAccountId: "chatgpt-1",
...overrides,
});
describe("ClaudeFormFields", () => {
beforeEach(() => {
copilotApiMock.copilotGetModels.mockResolvedValue([]);
copilotApiMock.copilotGetModelsForAccount.mockResolvedValue([]);
modelFetchApiMock.fetchCodexOauthModels.mockResolvedValue([]);
modelFetchApiMock.fetchModelsForConfig.mockResolvedValue([]);
});
it("不会在 Copilot 表单打开时自动获取模型列表", () => {
renderCopilotForm();
expect(copilotApiMock.copilotGetModels).not.toHaveBeenCalled();
expect(copilotApiMock.copilotGetModelsForAccount).not.toHaveBeenCalled();
});
it("点击获取模型列表后才请求当前 Copilot 账号的模型", async () => {
renderCopilotForm();
fireEvent.click(
screen.getByRole("button", {
name: "providerForm.fetchModels",
}),
);
await waitFor(() => {
expect(copilotApiMock.copilotGetModelsForAccount).toHaveBeenCalledWith(
"gh-1",
);
});
expect(copilotApiMock.copilotGetModels).not.toHaveBeenCalled();
});
it("不会在 Codex OAuth 表单打开时自动获取模型列表", () => {
renderCodexOauthForm();
expect(modelFetchApiMock.fetchCodexOauthModels).not.toHaveBeenCalled();
});
it("点击获取模型列表后才请求当前 Codex OAuth 账号的模型", async () => {
renderCodexOauthForm();
fireEvent.click(
screen.getByRole("button", {
name: "providerForm.fetchModels",
}),
);
await waitFor(() => {
expect(modelFetchApiMock.fetchCodexOauthModels).toHaveBeenCalledWith(
"chatgpt-1",
);
});
});
});