Files
cc-switch/tests/components/AddProviderDialog.test.tsx
Jason 7d56aed543 fix(providers): preserve custom endpoints in meta during add/edit operations
Fixed two critical data loss bugs where user-added custom endpoints were discarded:

1. **AddProviderDialog**: Form submission ignored values.meta from ProviderForm and
   re-inferred URLs only from presets/config, causing loss of endpoints added via
   speed test modal. Now prioritizes form-collected meta and uses fallback inference
   only when custom_endpoints is missing.

2. **ProviderForm**: Edit mode always returned initialData.meta, discarding any
   changes made in the speed test modal. Now uses mergeProviderMeta to properly
   merge customEndpointsMap with existing meta fields.

Changes:
- Extract mergeProviderMeta utility to handle meta field merging logic
- Preserve other meta fields (e.g., usage_script) during endpoint updates
- Unify new/edit code paths to use consistent meta handling
- Add comprehensive unit tests for meta merging scenarios
- Add integration tests for AddProviderDialog submission flow

Impact:
- Third-party and custom providers can now reliably manage multiple endpoints
- Edit operations correctly reflect user modifications
- No data loss for existing meta fields like usage_script
2025-10-28 20:28:11 +08:00

123 lines
3.3 KiB
TypeScript

import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { AddProviderDialog } from "@/components/providers/AddProviderDialog";
import type { ProviderFormValues } from "@/components/providers/forms/ProviderForm";
vi.mock("@/components/ui/dialog", () => ({
Dialog: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
DialogContent: ({ children }: { children: React.ReactNode }) => (
<div>{children}</div>
),
DialogHeader: ({ children }: { children: React.ReactNode }) => (
<div>{children}</div>
),
DialogTitle: ({ children }: { children: React.ReactNode }) => (
<h1>{children}</h1>
),
DialogDescription: ({ children }: { children: React.ReactNode }) => (
<p>{children}</p>
),
DialogFooter: ({ children }: { children: React.ReactNode }) => (
<div>{children}</div>
),
}));
let mockFormValues: ProviderFormValues;
vi.mock("@/components/providers/forms/ProviderForm", () => ({
ProviderForm: ({ onSubmit }: { onSubmit: (values: ProviderFormValues) => void }) => (
<form
id="provider-form"
onSubmit={(event) => {
event.preventDefault();
onSubmit(mockFormValues);
}}
/>
),
}));
describe("AddProviderDialog", () => {
beforeEach(() => {
mockFormValues = {
name: "Test Provider",
websiteUrl: "https://provider.example.com",
settingsConfig: JSON.stringify({ env: {}, config: {} }),
meta: {
custom_endpoints: {
"https://api.new-endpoint.com": {
url: "https://api.new-endpoint.com",
addedAt: 1,
},
},
},
};
});
it("使用 ProviderForm 返回的自定义端点", async () => {
const handleSubmit = vi.fn().mockResolvedValue(undefined);
const handleOpenChange = vi.fn();
render(
<AddProviderDialog
open
onOpenChange={handleOpenChange}
appType="claude"
onSubmit={handleSubmit}
/>,
);
fireEvent.click(
screen.getByRole("button", {
name: "common.add",
}),
);
await waitFor(() => expect(handleSubmit).toHaveBeenCalledTimes(1));
const submitted = handleSubmit.mock.calls[0][0];
expect(submitted.meta?.custom_endpoints).toEqual(
mockFormValues.meta?.custom_endpoints,
);
expect(handleOpenChange).toHaveBeenCalledWith(false);
});
it("在缺少自定义端点时回退到配置中的 baseUrl", async () => {
const handleSubmit = vi.fn().mockResolvedValue(undefined);
mockFormValues = {
name: "Base URL Provider",
websiteUrl: "",
settingsConfig: JSON.stringify({
env: { ANTHROPIC_BASE_URL: "https://claude.base" },
config: {},
}),
};
render(
<AddProviderDialog
open
onOpenChange={vi.fn()}
appType="claude"
onSubmit={handleSubmit}
/>,
);
fireEvent.click(
screen.getByRole("button", {
name: "common.add",
}),
);
await waitFor(() => expect(handleSubmit).toHaveBeenCalledTimes(1));
const submitted = handleSubmit.mock.calls[0][0];
expect(submitted.meta?.custom_endpoints).toEqual({
"https://claude.base": {
url: "https://claude.base",
addedAt: expect.any(Number),
lastUsed: undefined,
},
});
});
});