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
+}