diff --git a/src/components/usage/PricingEditModal.tsx b/src/components/usage/PricingEditModal.tsx index d27738560..8354cb479 100644 --- a/src/components/usage/PricingEditModal.tsx +++ b/src/components/usage/PricingEditModal.tsx @@ -16,6 +16,8 @@ interface PricingEditModalProps { onClose: () => void; } +const PRICE_INPUT_STEP = "0.0001"; + export function PricingEditModal({ open, model, @@ -151,7 +153,7 @@ export function PricingEditModal({ @@ -168,7 +170,7 @@ export function PricingEditModal({ @@ -188,7 +190,7 @@ export function PricingEditModal({ @@ -208,7 +210,7 @@ export function PricingEditModal({ diff --git a/tests/components/PricingEditModal.test.tsx b/tests/components/PricingEditModal.test.tsx new file mode 100644 index 000000000..7c7167293 --- /dev/null +++ b/tests/components/PricingEditModal.test.tsx @@ -0,0 +1,93 @@ +import { render, screen, fireEvent } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; + +import { PricingEditModal } from "@/components/usage/PricingEditModal"; +import type { ModelPricing } from "@/types/usage"; + +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ + t: (key: string, options?: string | { defaultValue?: string }) => + typeof options === "string" ? options : options?.defaultValue ?? key, + }), +})); + +vi.mock("sonner", () => ({ + toast: { + success: vi.fn(), + error: vi.fn(), + }, +})); + +vi.mock("@/lib/query/usage", () => ({ + useUpdateModelPricing: () => ({ + mutateAsync: vi.fn(), + isPending: false, + }), +})); + +vi.mock("@/components/common/FullScreenPanel", () => ({ + FullScreenPanel: ({ + children, + footer, + }: { + children: React.ReactNode; + footer?: React.ReactNode; + }) => ( +
+ {children} + {footer} +
+ ), +})); + +const model: ModelPricing = { + modelId: "deepseek-v4", + displayName: "DeepSeek V4", + inputCostPerMillion: "1", + outputCostPerMillion: "3", + cacheReadCostPerMillion: "0.0028", + cacheCreationCostPerMillion: "0", +}; + +const PRICE_FIELDS = [ + { id: "inputCost", label: "输入成本" }, + { id: "outputCost", label: "输出成本" }, + { id: "cacheReadCost", label: "缓存读取成本" }, + { id: "cacheCreationCost", label: "缓存写入成本" }, +] as const; + +describe("PricingEditModal", () => { + it("all price inputs have step=0.0001", () => { + render( {}} />); + + for (const { id } of PRICE_FIELDS) { + const input = screen.getByLabelText(/每百万 tokens/ as unknown as string, { + selector: `#${id}`, + }) as HTMLInputElement; + expect(input).toHaveAttribute("step", "0.0001"); + } + }); + + it("accepts precise cache read cost like 0.0028", () => { + render( {}} />); + + const cacheReadInput = document.getElementById( + "cacheReadCost", + ) as HTMLInputElement; + expect(cacheReadInput.value).toBe("0.0028"); + expect(cacheReadInput.checkValidity()).toBe(true); + }); + + it("allows user to input sub-cent prices via change event", () => { + render( + {}} isNew />, + ); + + const cacheReadInput = document.getElementById( + "cacheReadCost", + ) as HTMLInputElement; + + fireEvent.change(cacheReadInput, { target: { value: "0.0015" } }); + expect(cacheReadInput.value).toBe("0.0015"); + }); +});