diff --git a/extension/src/popup/components/InternalTransaction/TokenList/__tests__/TokenList.test.tsx b/extension/src/popup/components/InternalTransaction/TokenList/__tests__/TokenList.test.tsx new file mode 100644 index 0000000000..352d00ed8c --- /dev/null +++ b/extension/src/popup/components/InternalTransaction/TokenList/__tests__/TokenList.test.tsx @@ -0,0 +1,153 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import BigNumber from "bignumber.js"; + +import { defaultBlockaidScanAssetResult } from "@shared/helpers/stellar"; + +import { TokenList } from "../index"; + +jest.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (key: string) => key }), +})); + +jest.mock("popup/components/account/AccountAssets", () => ({ + AssetIcon: () =>
, +})); + +const USDC_ISSUER = "GCK3D3V2XNLLKRFGFFFDEJXA4O2J4X36HET2FE446AV3M4U7DPHO3PEM"; +const AQUA_ISSUER = "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"; + +const USDC_CANONICAL = `USDC:${USDC_ISSUER}`; +const AQUA_CANONICAL = `AQUA:${AQUA_ISSUER}`; + +const XLM_BALANCE = { + token: { code: "XLM", type: "native" as const }, + total: new BigNumber("500"), + available: new BigNumber("500"), + blockaidData: defaultBlockaidScanAssetResult, +}; + +const USDC_BALANCE = { + token: { + code: "USDC", + issuer: { key: USDC_ISSUER }, + }, + total: new BigNumber("200"), + available: new BigNumber("200"), + blockaidData: defaultBlockaidScanAssetResult, +}; + +const AQUA_BALANCE = { + token: { + code: "AQUA", + issuer: { key: AQUA_ISSUER }, + }, + total: new BigNumber("10000"), + available: new BigNumber("10000"), + blockaidData: defaultBlockaidScanAssetResult, +}; + +const LP_BALANCE = { + liquidityPoolId: + "a468d41d8e9b8f3c7209651608b74b7db7ac9952dcae0cdf24871d1d9c7b0088", + total: new BigNumber("10"), + limit: new BigNumber("100"), +}; + +const TOKEN_PRICES = { + native: { currentPrice: "0.10" }, + [USDC_CANONICAL]: { currentPrice: "1.00" }, + [AQUA_CANONICAL]: { currentPrice: "0.005" }, +}; + +const defaultProps = { + icons: {}, + tokenPrices: TOKEN_PRICES, + onClickAsset: jest.fn(), + hiddenAssets: [] as string[], +}; + +describe("TokenList", () => { + describe("value sort ordering", () => { + it("renders tokens in descending USD value order", () => { + // USDC: 200 * 1.00 = $200 + // XLM: 500 * 0.10 = $50 + // AQUA: 10000 * 0.005 = $50 (tie with XLM, preserves input order) + const tokens = [XLM_BALANCE, USDC_BALANCE, AQUA_BALANCE] as any[]; + + render(); + + const rows = screen.getAllByTestId(/^SendRow-/); + expect(rows[0]).toHaveAttribute("data-testid", `SendRow-${USDC_CANONICAL}`); + expect(rows[1]).toHaveAttribute("data-testid", "SendRow-native"); + expect(rows[2]).toHaveAttribute("data-testid", `SendRow-${AQUA_CANONICAL}`); + }); + + it("preserves original order when tokenPrices is empty", () => { + const tokens = [XLM_BALANCE, USDC_BALANCE, AQUA_BALANCE] as any[]; + + render(); + + const rows = screen.getAllByTestId(/^SendRow-/); + expect(rows[0]).toHaveAttribute("data-testid", "SendRow-native"); + expect(rows[1]).toHaveAttribute("data-testid", `SendRow-${USDC_CANONICAL}`); + expect(rows[2]).toHaveAttribute("data-testid", `SendRow-${AQUA_CANONICAL}`); + }); + + it("sorts priced assets before unpriced ones", () => { + const partialPrices = { + [USDC_CANONICAL]: { currentPrice: "1.00" }, + }; + const tokens = [XLM_BALANCE, AQUA_BALANCE, USDC_BALANCE] as any[]; + + render( + , + ); + + const rows = screen.getAllByTestId(/^SendRow-/); + // USDC (priced) comes first + expect(rows[0]).toHaveAttribute("data-testid", `SendRow-${USDC_CANONICAL}`); + // Unpriced assets preserve input order + expect(rows[1]).toHaveAttribute("data-testid", "SendRow-native"); + expect(rows[2]).toHaveAttribute("data-testid", `SendRow-${AQUA_CANONICAL}`); + }); + }); + + describe("filtering", () => { + it("excludes LP share assets from the list", () => { + const tokens = [XLM_BALANCE, LP_BALANCE, USDC_BALANCE] as any[]; + + render(); + + const rows = screen.getAllByTestId(/^SendRow-/); + expect(rows).toHaveLength(2); + expect(rows[0]).toHaveAttribute("data-testid", `SendRow-${USDC_CANONICAL}`); + expect(rows[1]).toHaveAttribute("data-testid", "SendRow-native"); + }); + + it("excludes hidden assets from the list", () => { + const tokens = [XLM_BALANCE, USDC_BALANCE, AQUA_BALANCE] as any[]; + const hiddenAssets = [USDC_CANONICAL]; + + render( + , + ); + + const rows = screen.getAllByTestId(/^SendRow-/); + expect(rows).toHaveLength(2); + // USDC is hidden, XLM and AQUA remain (sorted by value) + expect(rows[0]).toHaveAttribute("data-testid", "SendRow-native"); + expect(rows[1]).toHaveAttribute("data-testid", `SendRow-${AQUA_CANONICAL}`); + }); + }); + + it("shows empty state when no tokens provided", () => { + render(); + + expect( + screen.getByText( + "You have no assets added. Get started by adding an asset.", + ), + ).toBeDefined(); + }); +}); diff --git a/extension/src/popup/components/InternalTransaction/TokenList/index.tsx b/extension/src/popup/components/InternalTransaction/TokenList/index.tsx index ec46737495..079685ac25 100644 --- a/extension/src/popup/components/InternalTransaction/TokenList/index.tsx +++ b/extension/src/popup/components/InternalTransaction/TokenList/index.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useMemo } from "react"; import { useTranslation } from "react-i18next"; import BigNumber from "bignumber.js"; import classnames from "classnames"; @@ -11,6 +11,7 @@ import { getCanonicalFromAsset } from "helpers/stellar"; import { ApiTokenPrices, AssetIcons } from "@shared/api/types"; import { getAvailableBalance } from "popup/helpers/soroban"; import { formatAmount, roundUsdValue } from "popup/helpers/formatters"; +import { sortBalancesByValue } from "popup/helpers/balance"; import { AssetIcon } from "popup/components/account/AccountAssets"; import { title } from "helpers/transaction"; @@ -34,6 +35,10 @@ export const TokenList = ({ isShowingHeader, }: TokenListProps) => { const { t } = useTranslation(); + const sortedTokens = useMemo( + () => sortBalancesByValue(tokens, tokenPrices), + [tokens, tokenPrices], + ); return (
- {!tokens.length ? ( + {!sortedTokens.length ? (
{`${t("You have no assets added.")} ${t("Get started by adding an asset.")}`}
@@ -50,7 +55,7 @@ export const TokenList = ({ {isShowingHeader && (
{t("Your Tokens")}
)} - {tokens + {sortedTokens .filter( ( balance,