diff --git a/package.json b/package.json index 1f5a517..d9f6bba 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ "test": "vitest run", "lint": "biome check . && tsc", "format": "biome format --write .", - "generate-client-factory": "tsx --env-file .env scripts/generate-client-factory.ts" + "generate-client-factory": "tsx --env-file .env scripts/generate-client-factory.ts", + "generate-routes": "tsr generate" }, "dependencies": { "@base-ui/react": "^1.1.0", @@ -46,6 +47,7 @@ "@biomejs/biome": "2.3.12", "@effect/language-service": "^0.72.0", "@tanstack/devtools-vite": "^0.4.1", + "@tanstack/router-cli": "^1.157.15", "@testing-library/dom": "^10.4.0", "@testing-library/react": "^16.3.2", "@tim-smart/openapi-gen": "^0.4.13", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bd3fd7b..bf4f414 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -57,7 +57,7 @@ importers: version: 1.157.8(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@tanstack/react-router-devtools': specifier: ^1.157.8 - version: 1.157.8(@tanstack/react-router@1.157.8(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@tanstack/router-core@1.157.8)(csstype@3.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 1.157.8(@tanstack/react-router@1.157.8(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@tanstack/router-core@1.157.15)(csstype@3.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@tanstack/react-virtual': specifier: ^3.13.18 version: 3.13.18(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -110,6 +110,9 @@ importers: '@tanstack/devtools-vite': specifier: ^0.4.1 version: 0.4.1(bufferutil@4.1.0)(utf-8-validate@5.0.10)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)) + '@tanstack/router-cli': + specifier: ^1.157.15 + version: 1.157.15 '@testing-library/dom': specifier: ^10.4.0 version: 10.4.1 @@ -1858,6 +1861,15 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + '@tanstack/router-cli@1.157.15': + resolution: {integrity: sha512-a3ni8JUh9svDd4IenJx+9za/Gd1gHzYi4iH36Lf/hgf+zPl6ZBK6cGsveVo2xRVz94iWgpDSWOkgXC0d9fuJ7w==} + engines: {node: '>=12'} + hasBin: true + + '@tanstack/router-core@1.157.15': + resolution: {integrity: sha512-KaYz6s+wYcg92kRQ7HXlTJLhBaBXOYiiqRBv5tsRbKRIqqhWNyeGz5+NfDwaYFHg5XLSDs3DvN0elMtxcj4dTg==} + engines: {node: '>=12'} + '@tanstack/router-core@1.157.8': resolution: {integrity: sha512-2fxuhHIZ3yBXN9rxcDOgCsezuwk1VoWvprIDMe+HDi6FBdpWgEREMp0CWEYn33Lxla44iwaTpwvVcFDk8QC/rA==} engines: {node: '>=12'} @@ -1872,6 +1884,10 @@ packages: csstype: optional: true + '@tanstack/router-generator@1.157.15': + resolution: {integrity: sha512-zGac6tyRFz/X86fk9/CAmS6z8lyZf4p9lhAqLBCKVkFiFPmU4eAJp1ODvs81EtV0uJdRL1/rb+uvgHLGUsmQ0g==} + engines: {node: '>=12'} + '@tanstack/router-generator@1.157.8': resolution: {integrity: sha512-P0uvoFkhqrkmn/npgJJ02aGGsw2CMtoEAWQkiJGg00aXyA+026Ny1G/4FPuYsGK11c6yMsOl6FHk/sipNY+2YA==} engines: {node: '>=12'} @@ -6301,14 +6317,14 @@ snapshots: '@tanstack/query-core': 5.90.20 react: 19.2.3 - '@tanstack/react-router-devtools@1.157.8(@tanstack/react-router@1.157.8(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@tanstack/router-core@1.157.8)(csstype@3.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@tanstack/react-router-devtools@1.157.8(@tanstack/react-router@1.157.8(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@tanstack/router-core@1.157.15)(csstype@3.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@tanstack/react-router': 1.157.8(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@tanstack/router-devtools-core': 1.157.8(@tanstack/router-core@1.157.8)(csstype@3.2.3) + '@tanstack/router-devtools-core': 1.157.8(@tanstack/router-core@1.157.15)(csstype@3.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) optionalDependencies: - '@tanstack/router-core': 1.157.8 + '@tanstack/router-core': 1.157.15 transitivePeerDependencies: - csstype @@ -6336,6 +6352,24 @@ snapshots: react: 19.2.3 react-dom: 19.2.3(react@19.2.3) + '@tanstack/router-cli@1.157.15': + dependencies: + '@tanstack/router-generator': 1.157.15 + chokidar: 3.6.0 + yargs: 17.7.2 + transitivePeerDependencies: + - supports-color + + '@tanstack/router-core@1.157.15': + dependencies: + '@tanstack/history': 1.154.14 + '@tanstack/store': 0.8.0 + cookie-es: 2.0.0 + seroval: 1.5.0 + seroval-plugins: 1.5.0(seroval@1.5.0) + tiny-invariant: 1.3.3 + tiny-warning: 1.0.3 + '@tanstack/router-core@1.157.8': dependencies: '@tanstack/history': 1.154.14 @@ -6346,15 +6380,28 @@ snapshots: tiny-invariant: 1.3.3 tiny-warning: 1.0.3 - '@tanstack/router-devtools-core@1.157.8(@tanstack/router-core@1.157.8)(csstype@3.2.3)': + '@tanstack/router-devtools-core@1.157.8(@tanstack/router-core@1.157.15)(csstype@3.2.3)': dependencies: - '@tanstack/router-core': 1.157.8 + '@tanstack/router-core': 1.157.15 clsx: 2.1.1 goober: 2.1.18(csstype@3.2.3) tiny-invariant: 1.3.3 optionalDependencies: csstype: 3.2.3 + '@tanstack/router-generator@1.157.15': + dependencies: + '@tanstack/router-core': 1.157.15 + '@tanstack/router-utils': 1.154.7 + '@tanstack/virtual-file-routes': 1.154.7 + prettier: 3.8.1 + recast: 0.23.11 + source-map: 0.7.6 + tsx: 4.21.0 + zod: 3.25.76 + transitivePeerDependencies: + - supports-color + '@tanstack/router-generator@1.157.8': dependencies: '@tanstack/router-core': 1.157.8 diff --git a/src/components/modules/Home/index.tsx b/src/components/modules/Home/index.tsx index 174e35f..cb267be 100644 --- a/src/components/modules/Home/index.tsx +++ b/src/components/modules/Home/index.tsx @@ -8,7 +8,10 @@ import { } from "lucide-react"; import { useState } from "react"; import hyperliquid from "@/assets/hyperliquid.png"; -import { selectedProviderBalancesAtom } from "@/atoms/portfolio-atoms"; +import { + positionsAtom, + selectedProviderBalancesAtom, +} from "@/atoms/portfolio-atoms"; import { providersAtom, selectedProviderAtom } from "@/atoms/providers-atoms"; import { walletAtom } from "@/atoms/wallet-atom"; import { AssetList } from "@/components/modules/Home/AssetList"; @@ -49,6 +52,22 @@ const ProviderBalancesDisplay = ({ wallet }: { wallet: WalletConnected }) => { return null; }; +const PositionsTabLabel = ({ wallet }: { wallet: WalletConnected }) => { + const positionsResult = useAtomValue( + positionsAtom(wallet.currentAccount.address), + ); + const positionsCount = positionsResult.pipe( + Result.map((positions) => positions.length), + Result.getOrElse(() => 0), + ); + + return ( + + Positions ({positionsCount}) + + ); +}; + export const Home = () => { const [activeTab, setActiveTab] = useState<"trade" | "positions">("trade"); const wallet = useAtomValue(walletAtom).pipe(Result.getOrElse(() => null)); @@ -118,9 +137,13 @@ export const Home = () => { - - Positions - + {walletConnected ? ( + + ) : ( + + Positions (0) + + )} diff --git a/src/components/modules/Order/Overview/index.tsx b/src/components/modules/Order/Overview/index.tsx index 703d86a..a7b8bbe 100644 --- a/src/components/modules/Order/Overview/index.tsx +++ b/src/components/modules/Order/Overview/index.tsx @@ -207,6 +207,7 @@ function OrderContent({ entryPrice={currentPrice} currentPrice={currentPrice} liquidationPrice={calculations.liquidationPrice} + side={side} isLiquidationPriceEstimate >
- {formatTPOrSLSettings(tpOrSLSettings)} + {formatTPOrSLSettings(tpOrSLSettings, side)} diff --git a/src/components/modules/Order/Overview/leverage-dialog.tsx b/src/components/modules/Order/Overview/leverage-dialog.tsx index cd16af0..f304b19 100644 --- a/src/components/modules/Order/Overview/leverage-dialog.tsx +++ b/src/components/modules/Order/Overview/leverage-dialog.tsx @@ -1,6 +1,7 @@ import type { DialogRootActions } from "@base-ui/react/dialog"; import { X } from "lucide-react"; import { useRef, useState } from "react"; +import { ToggleGroup } from "@/components/molecules/toggle-group"; import { Button } from "@/components/ui/button"; import { Dialog } from "@/components/ui/dialog"; import { Divider } from "@/components/ui/divider"; @@ -33,7 +34,7 @@ export const LeverageDialog = (props: LeverageDialogProps) => { return ( - {props.children} + @@ -93,8 +94,7 @@ export function LeverageDialogContent({ getPriceChangePercentToLiquidation({ currentPrice, liquidationPrice }), ); - const handleLeverageClick = (leverage: number) => setLocalLeverage(leverage); - const handleStopClick = (stopValue: number) => setLocalLeverage(stopValue); + const leverageButtons = generateLeverageButtons(maxLeverage); const handleConfirm = () => { onLeverageChange(localLeverage); @@ -168,72 +168,30 @@ export function LeverageDialogContent({
{/* Leverage Slider */} -
-
- { - const v = Array.isArray(value) ? value[0] : value; - const leverageValue = Math.round( - MIN_LEVERAGE + (v / 100) * (maxLeverage - MIN_LEVERAGE), - ); - setLocalLeverage(leverageValue); - }} - min={0} - max={100} - stops={leverageStops.map( - (stop) => - ((stop - MIN_LEVERAGE) / (maxLeverage - MIN_LEVERAGE)) * 100, - )} - onStopClick={(stopPercent) => { - const leverageValue = Math.round( - MIN_LEVERAGE + - (stopPercent / 100) * (maxLeverage - MIN_LEVERAGE), - ); - setLocalLeverage(leverageValue); - }} - /> -
- - {/* Leverage Labels */} -
- {leverageStops.map((stop, index) => ( - - ))} -
-
- - {/* Leverage Buttons */} -
- {generateLeverageButtons(maxLeverage).map((leverage) => ( - - ))} +
+ { + const v = Array.isArray(value) ? value[0] : value; + const leverageValue = Math.round( + MIN_LEVERAGE + (v / 100) * (maxLeverage - MIN_LEVERAGE), + ); + setLocalLeverage(leverageValue); + }} + min={0} + max={100} + stops={leverageStops} + /> + + {/* Leverage Buttons */} + ({ + value: stop.toString(), + label: `${stop}x`, + }))} + value={localLeverage.toString()} + onValueChange={(value) => setLocalLeverage(Number(value))} + />
{/* Confirm Button */} diff --git a/src/components/modules/Order/Overview/limit-price-dialog.tsx b/src/components/modules/Order/Overview/limit-price-dialog.tsx index 5fd4347..8e87f25 100644 --- a/src/components/modules/Order/Overview/limit-price-dialog.tsx +++ b/src/components/modules/Order/Overview/limit-price-dialog.tsx @@ -31,7 +31,7 @@ export const LimitPriceDialog = (props: LimitPriceDialogProps) => { return ( - {props.children} + diff --git a/src/components/modules/Order/Overview/utils.ts b/src/components/modules/Order/Overview/utils.ts index bb645c5..40534f7 100644 --- a/src/components/modules/Order/Overview/utils.ts +++ b/src/components/modules/Order/Overview/utils.ts @@ -1,16 +1,23 @@ import { Option } from "effect"; import type { TPOrSLSettings } from "@/components/molecules/tp-sl-dialog"; -export function formatTPOrSLSettings(settings: TPOrSLSettings): string { +export function formatTPOrSLSettings( + settings: TPOrSLSettings, + side: "long" | "short" = "long", +): string { const tp = Option.fromNullable(settings.takeProfit.percentage).pipe( Option.filter((percentage) => percentage !== 0), - Option.map((percentage) => `TP +${percentage}%`), + Option.map((percentage) => + side === "short" ? `TP -${percentage}%` : `TP +${percentage}%`, + ), Option.getOrElse(() => "TP Off"), ); const sl = Option.fromNullable(settings.stopLoss.percentage).pipe( Option.filter((percentage) => percentage !== 0), - Option.map((percentage) => `SL -${percentage}%`), + Option.map((percentage) => + side === "short" ? `SL +${percentage}%` : `SL -${percentage}%`, + ), Option.getOrElse(() => "SL Off"), ); diff --git a/src/components/modules/PositionDetails/Overview/Position/index.tsx b/src/components/modules/PositionDetails/Overview/Position/index.tsx index 26a6bf8..a102574 100644 --- a/src/components/modules/PositionDetails/Overview/Position/index.tsx +++ b/src/components/modules/PositionDetails/Overview/Position/index.tsx @@ -46,11 +46,13 @@ function PositionCardContent({ amount: tpSlOrders.takeProfit?.triggerPrice ?? undefined, entryPrice: position.entryPrice, tpOrSl: "takeProfit", + side: position.side, }), stopLoss: getTPOrSLConfigurationFromPosition({ amount: tpSlOrders.stopLoss?.triggerPrice ?? undefined, entryPrice: position.entryPrice, tpOrSl: "stopLoss", + side: position.side, }), }; @@ -205,6 +207,7 @@ function PositionCardContent({ entryPrice={position.entryPrice} currentPrice={position.markPrice} liquidationPrice={position.liquidationPrice} + side={position.side} mode="takeProfit" >

- {isSingleMode - ? `Pick a percentage ${mode === "takeProfit" ? "gain" : "loss"}, or enter a custom trigger price to automatically close your position.` - : "Pick a percentage gain or loss, or enter a custom trigger price to automatically close your position."} + {Match.value(isSingleMode).pipe( + Match.when( + true, + () => + `Pick a percentage ${singleModeDescription}, or enter a custom trigger price to automatically close your position.`, + ), + Match.orElse( + () => + "Pick a percentage gain or loss, or enter a custom trigger price to automatically close your position.", + ), + )}

@@ -218,11 +279,10 @@ function TPOrSLDialogContent({ position="middle" /> "Est. liquidation price"), + Match.orElse(() => "Liquidation price"), + )} value={liquidationPrice} position="last" /> @@ -237,6 +297,7 @@ function TPOrSLDialogContent({ label="Take profit" percentPlaceholder="% Profit" configuration={localSettings.takeProfit} + side={side} onOptionChange={(option) => handleTPOrSLOptionChange(option, "takeProfit") } @@ -256,6 +317,7 @@ function TPOrSLDialogContent({ label="Stop loss" percentPlaceholder="% Loss" configuration={localSettings.stopLoss} + side={side} onOptionChange={(option) => handleTPOrSLOptionChange(option, "stopLoss") } @@ -306,6 +368,7 @@ function TPOrSLSection({ label, percentPlaceholder, configuration, + side, onOptionChange, onTriggerPriceChange, onPercentChange, @@ -314,6 +377,7 @@ function TPOrSLSection({ label: string; percentPlaceholder: string; configuration: TPOrSLConfiguration; + side: "long" | "short"; onOptionChange: (option: TPOrSLPercentageOption) => void; onTriggerPriceChange: (value: string) => void; onPercentChange: (value: string) => void; @@ -331,6 +395,7 @@ function TPOrSLSection({ selectedOption={configuration.option} onOptionChange={onOptionChange} tpOrSl={tpOrSl} + side={side} /> void; tpOrSl: TPOrSLOption; + side: "long" | "short"; }) { return (
@@ -360,15 +427,29 @@ function OptionButtons({ key={option} type="button" onClick={() => onOptionChange(option)} - className={`flex-1 flex items-center justify-center h-9 rounded-[10px] text-sm font-normal tracking-[-0.42px] transition-colors cursor-pointer ${ - selectedOption === option - ? "bg-white text-black" - : "bg-white/5 text-gray-2 hover:bg-white/10" - }`} + className={`flex-1 flex items-center justify-center h-9 rounded-[10px] text-sm font-normal tracking-[-0.42px] transition-colors cursor-pointer ${Match.value( + selectedOption === option, + ).pipe( + Match.when(true, () => "bg-white text-black"), + Match.orElse(() => "bg-white/5 text-gray-2 hover:bg-white/10"), + )}`} > - {option === 0 - ? "Off" - : `${tpOrSl === "takeProfit" ? "+" : "-"}${option}%`} + {Match.value(option === 0).pipe( + Match.when(true, () => "Off"), + Match.orElse( + () => + `${Match.value({ side, tpOrSl }).pipe( + Match.when( + { side: "short", tpOrSl: "takeProfit" }, + () => "-", + ), + Match.when({ side: "short", tpOrSl: "stopLoss" }, () => "+"), + Match.when({ side: "long", tpOrSl: "takeProfit" }, () => "+"), + Match.when({ side: "long", tpOrSl: "stopLoss" }, () => "-"), + Match.exhaustive, + )}${option}%`, + ), + )} ))}
@@ -394,7 +475,10 @@ function TPOrSLInputFields({ ""), + Match.orElse((value) => value.toFixed(2)), + )} onChange={(e) => onTriggerPriceChange(e.target.value)} placeholder="Trigger price" className="w-full h-full bg-transparent text-white text-sm font-normal tracking-[-0.42px] pl-4 pr-10 outline-none placeholder:text-gray-2" @@ -407,7 +491,11 @@ function TPOrSLInputFields({ ""), + Match.when(0, () => ""), + Match.orElse((value) => value.toFixed(2)), + )} onChange={(e) => onPercentChange(e.target.value)} placeholder={percentPlaceholder} className="w-full h-full bg-transparent text-white text-sm font-normal tracking-[-0.42px] pl-4 pr-8 outline-none placeholder:text-gray-2" @@ -427,17 +515,42 @@ export const getTPOrSLConfigurationFromPosition = ({ amount, entryPrice, tpOrSl, + side = "long", }: { entryPrice: number; amount: number | undefined; tpOrSl: TPOrSLOption; + side?: "long" | "short"; }): TPOrSLConfiguration => { - const percentage = amount - ? tpOrSl === "takeProfit" - ? ((amount - entryPrice) / entryPrice) * 100 - : ((entryPrice - amount) / entryPrice) * 100 - : null; - const option = percentage ? findMatchingOption(percentage) : null; + const percentage = Match.value(amount).pipe( + Match.when(undefined, () => null), + Match.orElse((value) => + Match.value({ side, tpOrSl }).pipe( + Match.when( + { side: "short", tpOrSl: "takeProfit" }, + () => ((entryPrice - value) / entryPrice) * 100, + ), + Match.when( + { side: "short", tpOrSl: "stopLoss" }, + () => ((value - entryPrice) / entryPrice) * 100, + ), + Match.when( + { side: "long", tpOrSl: "takeProfit" }, + () => ((value - entryPrice) / entryPrice) * 100, + ), + Match.when( + { side: "long", tpOrSl: "stopLoss" }, + () => ((entryPrice - value) / entryPrice) * 100, + ), + Match.exhaustive, + ), + ), + ); + const option = Match.value(percentage).pipe( + Match.when(null, () => null), + Match.when(0, () => null), + Match.orElse((value) => findMatchingOption(value)), + ); return { option, diff --git a/tests/components/leverage-dialog.test.tsx b/tests/components/leverage-dialog.test.tsx index a702a4c..e6bc2f1 100644 --- a/tests/components/leverage-dialog.test.tsx +++ b/tests/components/leverage-dialog.test.tsx @@ -129,33 +129,11 @@ describe("LeverageDialog", () => { await userEvent.click(screen.getByText("Set Leverage")); // generateLeverageButtons(40) returns [2, 5, 10, 20, 40] - await expect.element(screen.getByTestId("leverage-button-2")).toBeVisible(); - await expect.element(screen.getByTestId("leverage-button-5")).toBeVisible(); - await expect - .element(screen.getByTestId("leverage-button-10")) - .toBeVisible(); - await expect - .element(screen.getByTestId("leverage-button-20")) - .toBeVisible(); - await expect - .element(screen.getByTestId("leverage-button-40")) - .toBeVisible(); - }); - - test("displays leverage stop labels", async () => { - const screen = await render( - - Set Leverage - , - { wrapper: TestWrapper }, - ); - - await userEvent.click(screen.getByText("Set Leverage")); - - // leverageStops = [MIN_LEVERAGE, maxLeverage/2, maxLeverage] = [1, 20, 40] - await expect.element(screen.getByTestId("leverage-stop-1")).toBeVisible(); - await expect.element(screen.getByTestId("leverage-stop-20")).toBeVisible(); - await expect.element(screen.getByTestId("leverage-stop-40")).toBeVisible(); + await expect.element(screen.getByLabelText("2x")).toBeVisible(); + await expect.element(screen.getByLabelText("5x")).toBeVisible(); + await expect.element(screen.getByLabelText("10x")).toBeVisible(); + await expect.element(screen.getByLabelText("20x")).toBeVisible(); + await expect.element(screen.getByLabelText("40x")).toBeVisible(); }); test("displays confirm button with correct leverage", async () => { @@ -182,7 +160,7 @@ describe("LeverageDialog", () => { await userEvent.click(screen.getByText("Set Leverage")); // Click the 20x button - await userEvent.click(screen.getByTestId("leverage-button-20")); + await userEvent.click(screen.getByLabelText("20x")); // Large display should update await expect @@ -192,22 +170,6 @@ describe("LeverageDialog", () => { await expect.element(screen.getByText("Set 20x")).toBeVisible(); }); - test("clicking stop label updates leverage", async () => { - const screen = await render( - - Set Leverage - , - { wrapper: TestWrapper }, - ); - - await userEvent.click(screen.getByText("Set Leverage")); - - // Click the 1x stop label (first stop) - await userEvent.click(screen.getByTestId("leverage-stop-1")); - - await expect.element(screen.getByText("Set 1x")).toBeVisible(); - }); - test("confirm button calls onLeverageChange with selected leverage", async () => { const onLeverageChange = vi.fn(); const screen = await render( @@ -222,7 +184,7 @@ describe("LeverageDialog", () => { ); await userEvent.click(screen.getByText("Set Leverage")); - await userEvent.click(screen.getByTestId("leverage-button-20")); + await userEvent.click(screen.getByLabelText("20x")); await userEvent.click(screen.getByText("Set 20x")); expect(onLeverageChange).toHaveBeenCalledWith(20); @@ -276,10 +238,10 @@ describe("LeverageDialog", () => { await userEvent.click(screen.getByText("Set Leverage")); - // The 10x button should have the selected style (bg-white text-black) - const selectedButton = screen.getByTestId("leverage-button-10"); - await expect.element(selectedButton).toHaveClass("bg-white"); - await expect.element(selectedButton).toHaveClass("text-black"); + const selectedButton = screen.getByLabelText("10x"); + await expect + .element(selectedButton) + .toHaveAttribute("aria-pressed", "true"); }); test("different max leverage generates correct buttons", async () => { @@ -293,14 +255,10 @@ describe("LeverageDialog", () => { await userEvent.click(screen.getByText("Set Leverage")); // generateLeverageButtons(20) returns [2, 5, 10, 20] - await expect.element(screen.getByTestId("leverage-button-2")).toBeVisible(); - await expect.element(screen.getByTestId("leverage-button-5")).toBeVisible(); - await expect - .element(screen.getByTestId("leverage-button-10")) - .toBeVisible(); - await expect - .element(screen.getByTestId("leverage-button-20")) - .toBeVisible(); + await expect.element(screen.getByLabelText("2x")).toBeVisible(); + await expect.element(screen.getByLabelText("5x")).toBeVisible(); + await expect.element(screen.getByLabelText("10x")).toBeVisible(); + await expect.element(screen.getByLabelText("20x")).toBeVisible(); }); test("low max leverage shows limited buttons", async () => { @@ -314,8 +272,8 @@ describe("LeverageDialog", () => { await userEvent.click(screen.getByText("Set Leverage")); // generateLeverageButtons(5) returns [2, 5] - await expect.element(screen.getByTestId("leverage-button-2")).toBeVisible(); - await expect.element(screen.getByTestId("leverage-button-5")).toBeVisible(); + await expect.element(screen.getByLabelText("2x")).toBeVisible(); + await expect.element(screen.getByLabelText("5x")).toBeVisible(); }); test("calculates correct liquidation price for long position", async () => { @@ -379,7 +337,7 @@ describe("LeverageDialog", () => { await expect.element(screen.getByText("45,000")).toBeVisible(); // Change to 20x leverage - await userEvent.click(screen.getByTestId("leverage-button-20")); + await userEvent.click(screen.getByLabelText("20x")); // New: 50000 * (1 - 1/20) = 50000 * 0.95 = 47500 await expect.element(screen.getByText("47,500")).toBeVisible(); @@ -401,9 +359,9 @@ describe("LeverageDialog", () => { await userEvent.click(screen.getByText("Set Leverage")); // Click multiple leverage buttons - await userEvent.click(screen.getByTestId("leverage-button-5")); - await userEvent.click(screen.getByTestId("leverage-button-20")); - await userEvent.click(screen.getByTestId("leverage-button-2")); + await userEvent.click(screen.getByLabelText("5x")); + await userEvent.click(screen.getByLabelText("20x")); + await userEvent.click(screen.getByLabelText("2x")); // Confirm await userEvent.click(screen.getByText("Set 2x")); @@ -477,15 +435,17 @@ describe("LeverageDialogContent", () => { expect(onClose).toHaveBeenCalledTimes(1); }); - test("leverage stop labels include min, mid, and max values", async () => { + test("renders leverage toggle options", async () => { const screen = await render( , { wrapper: TestWrapper }, ); - // MIN_LEVERAGE = 1, maxLeverage/2 = 20, maxLeverage = 40 - await expect.element(screen.getByTestId("leverage-stop-1")).toBeVisible(); - await expect.element(screen.getByTestId("leverage-stop-20")).toBeVisible(); - await expect.element(screen.getByTestId("leverage-stop-40")).toBeVisible(); + // generateLeverageButtons(40) returns [2, 5, 10, 20, 40] + await expect.element(screen.getByLabelText("2x")).toBeVisible(); + await expect.element(screen.getByLabelText("5x")).toBeVisible(); + await expect.element(screen.getByLabelText("10x")).toBeVisible(); + await expect.element(screen.getByLabelText("20x")).toBeVisible(); + await expect.element(screen.getByLabelText("40x")).toBeVisible(); }); }); diff --git a/tests/components/tp-sl-dialog.test.tsx b/tests/components/tp-sl-dialog.test.tsx index 4e601fe..a79e7b6 100644 --- a/tests/components/tp-sl-dialog.test.tsx +++ b/tests/components/tp-sl-dialog.test.tsx @@ -136,6 +136,26 @@ describe("TPOrSLDialog", () => { await expect.element(screen.getByText("-100%")).toBeVisible(); }); + test("renders short side options with inverted signs", async () => { + const screen = await render( + + Open Dialog + , + { wrapper: TestWrapper }, + ); + + await userEvent.click(screen.getByText("Open Dialog")); + + await expect.element(screen.getByText("-10%")).toBeVisible(); + await expect.element(screen.getByText("-25%")).toBeVisible(); + await expect.element(screen.getByText("-50%")).toBeVisible(); + await expect.element(screen.getByText("-100%")).toBeVisible(); + await expect.element(screen.getByText("+10%")).toBeVisible(); + await expect.element(screen.getByText("+25%")).toBeVisible(); + await expect.element(screen.getByText("+50%")).toBeVisible(); + await expect.element(screen.getByText("+100%")).toBeVisible(); + }); + test("selects take profit percentage option and calculates trigger price", async () => { const onSettingsChange = vi.fn(); const screen = await render( @@ -160,6 +180,34 @@ describe("TPOrSLDialog", () => { ); }); + test("selects short take profit percentage option and calculates trigger price", async () => { + const onSettingsChange = vi.fn(); + const screen = await render( + + Open Dialog + , + { wrapper: TestWrapper }, + ); + + await userEvent.click(screen.getByText("Open Dialog")); + await userEvent.click(screen.getByText("-25%")); + await userEvent.click(screen.getByText("Done")); + + expect(onSettingsChange).toHaveBeenCalledWith( + expect.objectContaining({ + takeProfit: { + option: 25, + triggerPrice: 75, + percentage: 25, + }, + }), + ); + }); + test("selects stop loss percentage option and calculates trigger price", async () => { const onSettingsChange = vi.fn(); const screen = await render( @@ -184,6 +232,34 @@ describe("TPOrSLDialog", () => { ); }); + test("selects short stop loss percentage option and calculates trigger price", async () => { + const onSettingsChange = vi.fn(); + const screen = await render( + + Open Dialog + , + { wrapper: TestWrapper }, + ); + + await userEvent.click(screen.getByText("Open Dialog")); + await userEvent.click(screen.getByText("+25%")); + await userEvent.click(screen.getByText("Done")); + + expect(onSettingsChange).toHaveBeenCalledWith( + expect.objectContaining({ + stopLoss: { + option: 25, + triggerPrice: 125, + percentage: 25, + }, + }), + ); + }); + test('selects "Off" option to clear configuration', async () => { const onSettingsChange = vi.fn(); const settingsWithTakeProfit: TPOrSLSettings = { @@ -362,6 +438,19 @@ describe("getTPOrSLConfigurationFromPosition", () => { expect(result.option).toBe(25); }); + test("calculates short take profit configuration correctly", () => { + const result = getTPOrSLConfigurationFromPosition({ + entryPrice: 100, + amount: 75, + tpOrSl: "takeProfit", + side: "short", + }); + + expect(result.triggerPrice).toBe(75); + expect(result.percentage).toBe(25); + expect(result.option).toBe(25); + }); + test("calculates stop loss configuration correctly", () => { const result = getTPOrSLConfigurationFromPosition({ entryPrice: 100, @@ -374,6 +463,19 @@ describe("getTPOrSLConfigurationFromPosition", () => { expect(result.option).toBe(25); }); + test("calculates short stop loss configuration correctly", () => { + const result = getTPOrSLConfigurationFromPosition({ + entryPrice: 100, + amount: 125, + tpOrSl: "stopLoss", + side: "short", + }); + + expect(result.triggerPrice).toBe(125); + expect(result.percentage).toBe(25); + expect(result.option).toBe(25); + }); + test("returns null option for non-matching percentage", () => { const result = getTPOrSLConfigurationFromPosition({ entryPrice: 100,