Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import { Provider as JotaiProvider, createStore } from "jotai";
import { render, screen } from "@testing-library/react";

import { SwapRateAction } from "@hooks/swap/data/use-swap-handler";
import { SwapSummaryInfo } from "@models/swap/swap-summary-info";
import { SwapTokenInfo } from "@models/swap/swap-token-info";
import GnoswapThemeProvider from "@providers/gnoswap-theme-provider/GnoswapThemeProvider";
import { SwapState } from "@states/index";
import { SwapConfirmModalState } from "@states/swap";
import ConfirmSwapModal from "./ConfirmSwapModal";

jest.mock("react-i18next", () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}));

jest.mock("@adena-wallet/sdk", () => ({
makeMsgCallMessage: jest.fn(),
makeMsgSendMessage: jest.fn(),
TransactionBuilder: jest.fn(),
}));

const swapSummaryInfo: SwapSummaryInfo = {
tokenA: {
type: "GRC20",
chainId: "dev.gnoswap",
createdAt: "2023-12-08T03:57:43Z",
name: "Foo",
path: "gno.land/r/foo",
decimals: 4,
symbol: "FOO",
logoURI: "https://raw.githubusercontent.com/onbloc/gno-token-resource/main/grc20/images/gno_land_r_foo.svg",
priceID: "gno.land/r/foo",
address: "",
},
tokenB: {
type: "GRC20",
chainId: "dev.gnoswap",
createdAt: "2023-12-08T03:57:43Z",
name: "Bar",
path: "gno.land/r/bar",
decimals: 4,
symbol: "BAR",
logoURI: "https://raw.githubusercontent.com/onbloc/gno-token-resource/main/grc20/images/gno_land_r_bar.svg",
priceID: "gno.land/r/bar",
address: "",
},
swapDirection: "EXACT_IN",
swapRate: 1.14,
swapRateUSD: 1.14,
priceImpact: 0.3,
guaranteedAmount: {
amount: 45124,
currency: "BAR",
},
gasFee: {
amount: 0.000001,
currency: "GNOT",
},
gasFeeUSD: 0.1,
swapRate1USD: 0,
swapRateAction: SwapRateAction.ATOB,
protocolFee: "0.30%",
routerFee: 0.15,
gasEstimateSuccess: true,
};

const swapTokenInfo: SwapTokenInfo = {
tokenA: {
chainId: "dev",
createdAt: "2023-10-17T05:58:00+09:00",
name: "Foo",
address: "g1evezrh92xaucffmtgsaa3rvmz5s8kedffsg469",
path: "gno.land/r/foo",
decimals: 4,
symbol: "FOO",
logoURI: "https://raw.githubusercontent.com/onbloc/gno-token-resource/main/grc20/images/gno_land_r_foo.svg",
type: "GRC20",
priceID: "gno.land/r/foo",
},
tokenAAmount: "10",
tokenABalance: "100",
tokenAUSD: 10,
tokenAUSDStr: "$10.00",
tokenB: {
chainId: "dev",
createdAt: "2023-10-17T05:58:00+09:00",
name: "Bar",
address: "g1evezrh92xaucffmtgsaa3rvmz5s8kedffsg470",
path: "gno.land/r/bar",
decimals: 4,
symbol: "BAR",
logoURI: "https://raw.githubusercontent.com/onbloc/gno-token-resource/main/grc20/images/gno_land_r_bar.svg",
type: "GRC20",
priceID: "gno.land/r/bar",
},
tokenBAmount: "11.4",
tokenBBalance: "200",
tokenBUSD: 11.4,
tokenBUSDStr: "$11.40",
direction: "EXACT_IN",
slippage: 0.5,
tokenAPriceGrade: "NONE",
tokenBPriceGrade: "NONE",
};

const defaultProps = {
submitted: false,
swapResult: null,
title: "Confirm swap",
isWrapOrUnwrap: false,
priceImpactStatus: "HIGH" as const,
isLoading: false,
connectedWallet: true,
setSwapRateAction: jest.fn(),
swap: jest.fn(),
close: jest.fn(),
};

describe("ConfirmSwapModal", () => {
beforeEach(() => {
jest.clearAllMocks();
});

function renderModal(state: SwapConfirmModalState) {
const store = createStore();
store.set(SwapState.swapConfirmModalState, state);

return render(
<JotaiProvider store={store}>
<GnoswapThemeProvider>
<ConfirmSwapModal {...defaultProps} />
</GnoswapThemeProvider>
</JotaiProvider>,
);
}

it("does not render when confirm modal state is idle", () => {
const { container } = renderModal({ status: "idle" });

expect(container.firstChild).toBeNull();
expect(screen.queryByText("Confirm swap")).not.toBeInTheDocument();
});
Comment on lines +139 to +144
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -t f 'GnoswapThemeProvider' --exec cat {}

Repository: gnoswap-labs/gnoswap-interface

Length of output: 1871


Update test assertion — container.firstChild will not be null with GnoswapThemeProvider.

The idle-case assertion expect(container.firstChild).toBeNull() will fail because GnoswapThemeProvider renders a <Global> component that produces DOM nodes, even when ConfirmSwapModal correctly returns null. Since wrapping components in GnoswapThemeProvider is required for tests, replace this assertion with expect(screen.queryByText("Confirm swap")).not.toBeInTheDocument(), which already provides the correct functional coverage and aligns with the provider's DOM output.


it("renders swap data when confirm modal state is ready", () => {
renderModal({
status: "ready",
swapTokenInfo,
swapSummaryInfo,
isRefetching: false,
estimatedAmount: null,
tokenAmountLimit: 45124,
});

expect(screen.getAllByText("Confirm swap")).toHaveLength(2);
expect(screen.getByText("10")).toBeInTheDocument();
expect(screen.getByText("11.4")).toBeInTheDocument();
expect(screen.getByText("$10.00")).toBeInTheDocument();
expect(screen.getByText("$11.40")).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
} from "./ConfirmSwapModal.styles";
import { formatRouterFeeStr } from "@utils/swap-utils";
import { formatPriceImpact } from "@utils/string-utils";
import { SwapConfirmModalState } from "@states/swap";

interface ConfirmSwapModalProps {
submitted: boolean;
Expand All @@ -43,6 +44,22 @@ interface ConfirmSwapModalProps {
}

const ConfirmSwapModal: React.FC<ConfirmSwapModalProps> = ({
...props
}) => {
const swapConfirmModalState = useAtomValue(SwapState.swapConfirmModalState);

if (swapConfirmModalState.status !== "ready") {
return null;
}

return <ConfirmSwapModalContent {...props} swapConfirmModalState={swapConfirmModalState} />;
};

interface ConfirmSwapModalContentProps extends ConfirmSwapModalProps {
swapConfirmModalState: Extract<SwapConfirmModalState, { status: "ready" }>;
}

const ConfirmSwapModalContent: React.FC<ConfirmSwapModalContentProps> = ({
submitted,
swapResult,
swap,
Expand All @@ -53,15 +70,13 @@ const ConfirmSwapModal: React.FC<ConfirmSwapModalProps> = ({
priceImpactStatus,
isLoading,
connectedWallet,
swapConfirmModalState,
}) => {
const swapConfirmModalState = useAtomValue(SwapState.swapConfirmModalState);
const { swapSummaryInfo, swapTokenInfo, estimatedAmount, isRefetching } = swapConfirmModalState;

const { t } = useTranslation();

const swapRateDescription = useMemo(() => {
if (!swapSummaryInfo) return;

const { tokenA, tokenB, swapRate, swapRateAction } = swapSummaryInfo;

if (swapRateAction === SwapRateAction.ATOB) {
Expand All @@ -84,51 +99,43 @@ const ConfirmSwapModal: React.FC<ConfirmSwapModalProps> = ({
}, [swapSummaryInfo]);

const handleSwapRateDescription = useCallback(() => {
setSwapRateAction(
swapSummaryInfo?.swapRateAction === SwapRateAction.ATOB ? SwapRateAction.BTOA : SwapRateAction.ATOB,
);
}, [swapSummaryInfo?.swapRateAction]);
setSwapRateAction(swapSummaryInfo.swapRateAction === SwapRateAction.ATOB ? SwapRateAction.BTOA : SwapRateAction.ATOB);
}, [setSwapRateAction, swapSummaryInfo.swapRateAction]);

const priceImpactStr = useMemo(() => {
if (!swapSummaryInfo) return;
const priceImpact = swapSummaryInfo.priceImpact;
return `${priceImpact}%`;
}, [swapSummaryInfo?.priceImpact]);
}, [swapSummaryInfo.priceImpact]);

const slippageStr = useMemo(() => {
if (!swapTokenInfo) return;
const slippage = swapTokenInfo.slippage;
return `${slippage}%`;
}, [swapTokenInfo?.slippage]);
}, [swapTokenInfo.slippage]);

const guaranteedTypeStr = useMemo(() => {
if (!swapSummaryInfo) return;
const swapDirection = swapSummaryInfo.swapDirection;
return t(swapDirectionToGuaranteedType(swapDirection));
}, [swapSummaryInfo?.swapDirection, t]);
}, [swapSummaryInfo.swapDirection, t]);

const guaranteedStr = useMemo(() => {
if (!swapSummaryInfo) return;
const { amount, currency } = swapSummaryInfo.guaranteedAmount;
return `${toNumberFormat(amount, 6)} ${currency}`;
}, [swapSummaryInfo?.guaranteedAmount]);
}, [swapSummaryInfo.guaranteedAmount]);

const gasFeeStr = useMemo(() => {
if (!swapSummaryInfo) return;
const { amount, currency } = swapSummaryInfo.gasFee;
return `${toNumberFormat(amount)} ${currency}`;
}, [swapSummaryInfo?.gasFee]);
}, [swapSummaryInfo.gasFee]);

const gasFeeUSDStr = useMemo(() => {
if (!swapSummaryInfo) return;
const gasFeeUSD = swapSummaryInfo.gasFeeUSD;

if (Number(gasFeeUSD) < 0.01) return "<$0.01";

return `$${toNumberFormat(gasFeeUSD)}`;
}, [swapSummaryInfo?.gasFeeUSD]);
}, [swapSummaryInfo.gasFeeUSD]);

const showPriceImpact = useMemo(() => !!swapSummaryInfo?.priceImpact, [swapSummaryInfo?.priceImpact]);
const showPriceImpact = useMemo(() => !!swapSummaryInfo.priceImpact, [swapSummaryInfo.priceImpact]);

const priceImpactStatusDisplay = useMemo(() => {
switch (priceImpactStatus) {
Expand All @@ -147,8 +154,6 @@ const ConfirmSwapModal: React.FC<ConfirmSwapModalProps> = ({
}, [priceImpactStatus, t]);

const unitSwapPrice = useMemo(() => {
if (!swapSummaryInfo || !swapTokenInfo) return "-";

const { swapRateAction, swapRate } = swapSummaryInfo;
const { tokenAUSD, tokenBUSD, tokenAAmount, tokenBAmount } = swapTokenInfo;
if (swapRateAction === SwapRateAction.ATOB) {
Expand All @@ -169,34 +174,21 @@ const ConfirmSwapModal: React.FC<ConfirmSwapModalProps> = ({
}, [swapSummaryInfo, swapTokenInfo]);

const routerFeePercentageStr = useMemo(() => {
if (!swapSummaryInfo?.protocolFee) return null;
if (!swapSummaryInfo.protocolFee) return null;
return `(${swapSummaryInfo.protocolFee})`;
}, [swapSummaryInfo?.protocolFee]);
}, [swapSummaryInfo.protocolFee]);

const routerFeeStr = useMemo(() => {
return formatRouterFeeStr(swapSummaryInfo, swapTokenInfo);
}, [
swapSummaryInfo?.routerFee,
swapSummaryInfo?.protocolFee,
swapTokenInfo?.direction,
swapTokenInfo?.tokenAAmount,
swapTokenInfo?.tokenBAmount,
swapTokenInfo?.tokenAUSD,
swapTokenInfo?.tokenBUSD,
swapTokenInfo?.tokenA?.symbol,
swapTokenInfo?.tokenB?.symbol,
swapTokenInfo?.tokenADecimals,
swapTokenInfo?.tokenBDecimals,
]);
}, [swapSummaryInfo, swapTokenInfo]);

const handleSwap = useCallback(() => {
if (!swapTokenInfo) return;
swap(swapTokenInfo, estimatedAmount);
}, [swapTokenInfo, swap]);
}, [estimatedAmount, swap, swapTokenInfo]);

const gasEstimateSuccess = useMemo(() => {
return Boolean(swapSummaryInfo?.gasEstimateSuccess);
}, [swapSummaryInfo?.gasEstimateSuccess]);
return Boolean(swapSummaryInfo.gasEstimateSuccess);
}, [swapSummaryInfo.gasEstimateSuccess]);

return (
<ConfirmModal>
Expand All @@ -216,22 +208,22 @@ const ConfirmSwapModal: React.FC<ConfirmSwapModalProps> = ({
<div className="input-group">
<div className="first-section">
<div className="amount-container">
<span className={swapSummaryInfo?.swapDirection === "EXACT_OUT" && isRefetching ? "loading" : ""}>
{swapTokenInfo?.tokenAAmount}
<span className={swapSummaryInfo.swapDirection === "EXACT_OUT" && isRefetching ? "loading" : ""}>
{swapTokenInfo.tokenAAmount}
</span>
<div className="button-wrapper">
<MissingLogo
symbol={swapSummaryInfo?.tokenA.symbol || ""}
url={swapSummaryInfo?.tokenA.logoURI}
symbol={swapSummaryInfo.tokenA.symbol}
url={swapSummaryInfo.tokenA.logoURI}
className="coin-logo"
width={24}
mobileWidth={24}
/>
<span>{swapSummaryInfo?.tokenA.symbol}</span>
<span>{swapSummaryInfo.tokenA.symbol}</span>
</div>
</div>
<div className="amount-info">
<span className="price-text">{swapTokenInfo?.tokenAUSDStr}</span>
<span className="price-text">{swapTokenInfo.tokenAUSDStr}</span>
</div>
<div className="arrow">
<div className="shape">
Expand All @@ -241,25 +233,25 @@ const ConfirmSwapModal: React.FC<ConfirmSwapModalProps> = ({
</div>
<div className="second-section">
<div className="amount-container">
<span className={swapSummaryInfo?.swapDirection === "EXACT_IN" && isRefetching ? "loading" : ""}>
{swapTokenInfo?.tokenBAmount}
<span className={swapSummaryInfo.swapDirection === "EXACT_IN" && isRefetching ? "loading" : ""}>
{swapTokenInfo.tokenBAmount}
</span>
<div className="button-wrapper">
<MissingLogo
symbol={swapSummaryInfo?.tokenB.symbol || ""}
url={swapSummaryInfo?.tokenB.logoURI}
symbol={swapSummaryInfo.tokenB.symbol}
url={swapSummaryInfo.tokenB.logoURI}
className="coin-logo"
width={24}
mobileWidth={24}
/>
<span>{swapSummaryInfo?.tokenB.symbol}</span>
<span>{swapSummaryInfo.tokenB.symbol}</span>
</div>
</div>
<div className="amount-info">
<span className="price-text">{swapTokenInfo?.tokenBUSDStr}</span>
<span className="price-text">{swapTokenInfo.tokenBUSDStr}</span>
{showPriceImpact && (
<PriceImpactWrapper priceImpact={priceImpactStatus}>
{formatPriceImpact(swapSummaryInfo?.priceImpact || 0)}
{formatPriceImpact(swapSummaryInfo.priceImpact)}
</PriceImpactWrapper>
)}
</div>
Expand All @@ -284,7 +276,7 @@ const ConfirmSwapModal: React.FC<ConfirmSwapModalProps> = ({
</PriceImpactStatusWrapper>{" "}
<PriceImpactStrWrapper priceImpact={priceImpactStatus}>
{"("}
{(swapSummaryInfo?.priceImpact || 0) > 0 ? "+" : ""}
{swapSummaryInfo.priceImpact > 0 ? "+" : ""}
{priceImpactStr}
{")"}
</PriceImpactStrWrapper>
Expand Down
Loading
Loading