diff --git a/__tests__/components/SlippageSelector.test.tsx b/__tests__/components/SlippageSelector.test.tsx new file mode 100644 index 0000000..530f168 --- /dev/null +++ b/__tests__/components/SlippageSelector.test.tsx @@ -0,0 +1,36 @@ +/** + * @jest-environment jsdom + */ + +import { fireEvent, render, screen } from "@testing-library/react" +import "@testing-library/jest-dom" +import { SlippageSelector } from "@/components/SlippageSelector" + +describe("SlippageSelector", () => { + it("rejects malformed custom slippage values without applying partial parses", () => { + const onChange = jest.fn() + render() + + const custom = screen.getByPlaceholderText("Custom") + for (const value of ["1abc", "1,5", "0.5%", "1e2", "0", "51"]) { + fireEvent.change(custom, { target: { value } }) + + expect(onChange).not.toHaveBeenCalled() + expect(custom).toHaveAttribute("aria-invalid", "true") + expect(screen.getByText("Invalid custom slippage")).toBeInTheDocument() + } + }) + + it("applies clean decimal custom slippage values", () => { + const onChange = jest.fn() + render() + + const custom = screen.getByPlaceholderText("Custom") + fireEvent.change(custom, { target: { value: "1.5" } }) + fireEvent.change(custom, { target: { value: ".5" } }) + + expect(onChange).toHaveBeenNthCalledWith(1, 1.5) + expect(onChange).toHaveBeenNthCalledWith(2, 0.5) + expect(custom).toHaveAttribute("aria-invalid", "false") + }) +}) diff --git a/components/SlippageSelector.tsx b/components/SlippageSelector.tsx index a60ba13..2a3f179 100644 --- a/components/SlippageSelector.tsx +++ b/components/SlippageSelector.tsx @@ -3,6 +3,7 @@ import { useState } from "react" const PRESETS = [0.1, 0.5, 1.0] // percent +const CUSTOM_SLIPPAGE_PATTERN = /^(?:\d+|\d*\.\d+)$/ interface SlippageSelectorProps { value: number // percent, e.g. 0.5 @@ -12,11 +13,12 @@ interface SlippageSelectorProps { export function SlippageSelector({ value, onChange }: SlippageSelectorProps) { const [custom, setCustom] = useState("") const isCustom = !PRESETS.includes(value) + const isInvalidCustom = custom !== "" && parseCustomSlippage(custom) === null function handleCustom(raw: string) { setCustom(raw) - const n = parseFloat(raw) - if (!isNaN(n) && n > 0 && n <= 50) { + const n = parseCustomSlippage(raw) + if (n !== null) { onChange(n) } } @@ -40,22 +42,32 @@ export function SlippageSelector({ value, onChange }: SlippageSelectorProps) { ))}
handleCustom(e.target.value)} - className="w-16 bg-transparent text-xs text-white placeholder-gray-600 outline-none" + aria-invalid={isInvalidCustom} + className={`w-16 bg-transparent text-xs placeholder-gray-600 outline-none ${ + isInvalidCustom ? "text-red-400" : "text-white" + }`} /> %
+ {isInvalidCustom && ( +

Invalid custom slippage

+ )} + {value > 5 && (

High slippage — your trade may be front-run. @@ -64,3 +76,14 @@ export function SlippageSelector({ value, onChange }: SlippageSelectorProps) { ) } + +function parseCustomSlippage(raw: string): number | null { + const value = raw.trim() + + if (!CUSTOM_SLIPPAGE_PATTERN.test(value)) { + return null + } + + const parsed = Number(value) + return Number.isFinite(parsed) && parsed > 0 && parsed <= 50 ? parsed : null +}