| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241 |
- import fs from "node:fs";
- import path from "node:path";
- import type { ReactNode } from "react";
- import { act } from "react";
- import { createRoot } from "react-dom/client";
- import { NextIntlClientProvider } from "next-intl";
- import { beforeEach, describe, expect, test, vi } from "vitest";
- import { SystemSettingsForm } from "@/app/[locale]/settings/config/_components/system-settings-form";
- import { DEFAULT_IP_EXTRACTION_CONFIG } from "@/types/ip-extraction";
- import type { SystemSettings } from "@/types/system-config";
- vi.mock("next/navigation", () => ({
- useRouter: () => ({ refresh: vi.fn() }),
- }));
- const systemConfigActionMocks = vi.hoisted(() => ({
- saveSystemSettings: vi.fn(async () => ({ ok: true })),
- }));
- vi.mock("@/actions/system-config", () => systemConfigActionMocks);
- const sonnerMocks = vi.hoisted(() => ({
- toast: {
- success: vi.fn(),
- error: vi.fn(),
- },
- }));
- vi.mock("sonner", () => sonnerMocks);
- function loadMessages() {
- const base = path.join(process.cwd(), "messages/en/settings");
- const read = (name: string) => JSON.parse(fs.readFileSync(path.join(base, name), "utf8"));
- return {
- settings: {
- common: read("common.json"),
- config: read("config.json"),
- },
- };
- }
- const baseSettings = {
- siteTitle: "Claude Code Hub",
- allowGlobalUsageView: true,
- currencyDisplay: "USD",
- billingModelSource: "redirected",
- codexPriorityBillingSource: "requested",
- timezone: "UTC",
- verboseProviderError: false,
- enableHttp2: true,
- enableHighConcurrencyMode: false,
- interceptAnthropicWarmupRequests: true,
- enableThinkingSignatureRectifier: true,
- enableBillingHeaderRectifier: true,
- enableResponseInputRectifier: true,
- enableThinkingBudgetRectifier: true,
- enableCodexSessionIdCompletion: true,
- enableClaudeMetadataUserIdInjection: true,
- enableResponseFixer: true,
- responseFixerConfig: {
- fixEncoding: true,
- fixSseFormat: true,
- fixTruncatedJson: true,
- },
- quotaDbRefreshIntervalSeconds: 10,
- quotaLeasePercent5h: 0.05,
- quotaLeasePercentDaily: 0.05,
- quotaLeasePercentWeekly: 0.05,
- quotaLeasePercentMonthly: 0.05,
- quotaLeaseCapUsd: null,
- ipGeoLookupEnabled: true,
- ipExtractionConfig: null,
- } satisfies Pick<
- SystemSettings,
- | "siteTitle"
- | "allowGlobalUsageView"
- | "currencyDisplay"
- | "billingModelSource"
- | "codexPriorityBillingSource"
- | "timezone"
- | "verboseProviderError"
- | "enableHttp2"
- | "enableHighConcurrencyMode"
- | "interceptAnthropicWarmupRequests"
- | "enableThinkingSignatureRectifier"
- | "enableBillingHeaderRectifier"
- | "enableResponseInputRectifier"
- | "enableThinkingBudgetRectifier"
- | "enableCodexSessionIdCompletion"
- | "enableClaudeMetadataUserIdInjection"
- | "enableResponseFixer"
- | "responseFixerConfig"
- | "quotaDbRefreshIntervalSeconds"
- | "quotaLeasePercent5h"
- | "quotaLeasePercentDaily"
- | "quotaLeasePercentWeekly"
- | "quotaLeasePercentMonthly"
- | "quotaLeaseCapUsd"
- | "ipGeoLookupEnabled"
- | "ipExtractionConfig"
- >;
- function buildSettings(
- overrides: Partial<Pick<SystemSettings, keyof typeof baseSettings>> = {}
- ): Pick<SystemSettings, keyof typeof baseSettings> {
- return {
- ...baseSettings,
- ...overrides,
- };
- }
- function render(node: ReactNode) {
- const container = document.createElement("div");
- document.body.appendChild(container);
- const root = createRoot(container);
- act(() => {
- root.render(
- <NextIntlClientProvider locale="en" messages={loadMessages()} timeZone="UTC">
- {node}
- </NextIntlClientProvider>
- );
- });
- return {
- container,
- unmount: () => {
- act(() => root.unmount());
- container.remove();
- },
- };
- }
- function renderForm(settings: Pick<SystemSettings, keyof typeof baseSettings> = baseSettings) {
- return render(<SystemSettingsForm initialSettings={settings} />);
- }
- function getIpExtractionTextarea() {
- const textarea = document.getElementById("ip-extraction-config") as HTMLTextAreaElement | null;
- if (!textarea) throw new Error("未找到 IP 提取配置输入框");
- return textarea;
- }
- function setTextareaValue(textarea: HTMLTextAreaElement, value: string) {
- act(() => {
- const valueSetter = Object.getOwnPropertyDescriptor(
- HTMLTextAreaElement.prototype,
- "value"
- )?.set;
- valueSetter?.call(textarea, value);
- textarea.dispatchEvent(new Event("input", { bubbles: true }));
- });
- }
- function clickButtonByText(text: string) {
- const button = Array.from(document.body.querySelectorAll("button")).find((element) =>
- (element.textContent || "").includes(text)
- );
- if (!button) throw new Error(`未找到按钮: ${text}`);
- act(() => {
- button.dispatchEvent(new MouseEvent("click", { bubbles: true }));
- });
- }
- async function submitForm() {
- const form = document.body.querySelector("form");
- if (!form) throw new Error("未找到系统设置表单");
- await act(async () => {
- form.dispatchEvent(new Event("submit", { bubbles: true, cancelable: true }));
- await Promise.resolve();
- await new Promise((resolve) => setTimeout(resolve, 0));
- });
- }
- describe("SystemSettingsForm IP 提取配置 JSON 输入框", () => {
- beforeEach(() => {
- document.body.innerHTML = "";
- vi.clearAllMocks();
- });
- test("未配置自定义链时直接显示格式化后的内置默认 JSON", () => {
- const { unmount } = renderForm(buildSettings({ ipExtractionConfig: null }));
- const textarea = getIpExtractionTextarea();
- const formattedDefault = JSON.stringify(DEFAULT_IP_EXTRACTION_CONFIG, null, 2);
- expect(textarea.value).toBe(formattedDefault);
- expect(textarea.placeholder).toBe(formattedDefault);
- unmount();
- });
- test("恢复默认只把默认 JSON 插入输入框,不会立即保存", () => {
- const { unmount } = renderForm(
- buildSettings({
- ipExtractionConfig: {
- headers: [{ name: "cf-connecting-ip" }],
- },
- })
- );
- const textarea = getIpExtractionTextarea();
- setTextareaValue(textarea, '{"headers":[]}');
- clickButtonByText("Reset to default");
- expect(textarea.value).toBe(JSON.stringify(DEFAULT_IP_EXTRACTION_CONFIG, null, 2));
- expect(systemConfigActionMocks.saveSystemSettings).not.toHaveBeenCalled();
- unmount();
- });
- test("直接保存默认 JSON 时提交默认对象", async () => {
- const { unmount } = renderForm(buildSettings({ ipExtractionConfig: null }));
- await submitForm();
- expect(systemConfigActionMocks.saveSystemSettings).toHaveBeenCalledWith(
- expect.objectContaining({
- ipExtractionConfig: DEFAULT_IP_EXTRACTION_CONFIG,
- })
- );
- unmount();
- });
- test("用户显式清空输入框后保存仍提交 null", async () => {
- const { unmount } = renderForm(buildSettings({ ipExtractionConfig: null }));
- setTextareaValue(getIpExtractionTextarea(), "");
- await submitForm();
- expect(systemConfigActionMocks.saveSystemSettings).toHaveBeenCalledWith(
- expect.objectContaining({
- ipExtractionConfig: null,
- })
- );
- unmount();
- });
- });
|