From 936a1d175b3101c82f223d3d8111daad815d86c4 Mon Sep 17 00:00:00 2001 From: Alissa Crane Date: Mon, 16 Dec 2024 11:01:19 -0800 Subject: [PATCH] add test coverage --- src/buy/components/BuyOnrampItem.test.tsx | 87 +++++++++ src/buy/components/BuyTokenItem.test.tsx | 110 ++++++++++++ src/buy/utils/getBuyQuote.test.ts | 207 ++++++++++++++++++++++ src/internal/svg/appleSvg.tsx | 1 + src/internal/svg/cardSvg.tsx | 1 + src/internal/svg/coinbaseLogoSvg.tsx | 1 + 6 files changed, 407 insertions(+) create mode 100644 src/buy/components/BuyOnrampItem.test.tsx create mode 100644 src/buy/components/BuyTokenItem.test.tsx create mode 100644 src/buy/utils/getBuyQuote.test.ts diff --git a/src/buy/components/BuyOnrampItem.test.tsx b/src/buy/components/BuyOnrampItem.test.tsx new file mode 100644 index 0000000000..aca971234e --- /dev/null +++ b/src/buy/components/BuyOnrampItem.test.tsx @@ -0,0 +1,87 @@ +import { render, screen, fireEvent } from '@testing-library/react'; +import { beforeEach, describe, expect, it, Mock, vi } from 'vitest'; +import { BuyOnrampItem } from './BuyOnrampItem'; +import { useBuyContext } from './BuyProvider'; + +vi.mock('./BuyProvider', () => ({ + useBuyContext: vi.fn(), +})); + +vi.mock('../../internal/svg', () => ({ + appleSvg: , + cardSvg: , + coinbaseLogoSvg: , +})); + +describe('BuyOnrampItem', () => { + const mockSetIsDropdownOpen = vi.fn(); + const mockOnClick = vi.fn(); + + beforeEach(() => { + vi.clearAllMocks(); + (useBuyContext as Mock).mockReturnValue({ + setIsDropdownOpen: mockSetIsDropdownOpen, + }); + }); + + it('renders correctly with provided props', () => { + render( + , + ); + + expect(screen.getByRole('button')).toBeInTheDocument(); + expect(screen.getByText('Apple Pay')).toBeInTheDocument(); + expect(screen.getByText('Fast and secure payments.')).toBeInTheDocument(); + expect(screen.getByTestId('appleSvg')).toBeInTheDocument(); + }); + + it('handles icon rendering based on the icon prop', () => { + render( + , + ); + + expect(screen.getByTestId('cardSvg')).toBeInTheDocument(); + }); + + it('triggers onClick and closes dropdown on button click', () => { + render( + , + ); + + const button = screen.getByRole('button'); + fireEvent.click(button); + + expect(mockSetIsDropdownOpen).toHaveBeenCalledWith(false); + expect(mockOnClick).toHaveBeenCalledTimes(1); + }); + + it('applies correct styling and attributes to the button', () => { + render( + , + ); + + const button = screen.getByRole('button'); + expect(button).toHaveClass('flex items-center gap-2 rounded-lg p-2'); + expect(button).toHaveAttribute('type', 'button'); + }); +}); diff --git a/src/buy/components/BuyTokenItem.test.tsx b/src/buy/components/BuyTokenItem.test.tsx new file mode 100644 index 0000000000..7496138e48 --- /dev/null +++ b/src/buy/components/BuyTokenItem.test.tsx @@ -0,0 +1,110 @@ +import { render, screen, fireEvent } from '@testing-library/react'; +import { beforeEach, describe, expect, it, Mock, vi } from 'vitest'; +import { BuyTokenItem } from './BuyTokenItem'; +import { useBuyContext } from './BuyProvider'; +import { getRoundedAmount } from '../../core/utils/getRoundedAmount'; +import { ethToken } from '../../token/constants'; + +vi.mock('./BuyProvider', () => ({ + useBuyContext: vi.fn(), +})); + +vi.mock('../../core/utils/getRoundedAmount', () => ({ + getRoundedAmount: vi.fn((value) => value), +})); + +const ethSwapUnit = { + token: ethToken, + amount: '10.5', + balance: '20', + amountUSD: '10.5', + loading: false, + setAmount: vi.fn(), + setAmountUSD: vi.fn(), + setLoading: vi.fn(), +}; + +describe('BuyTokenItem', () => { + const mockHandleSubmit = vi.fn(); + const mockSetIsDropdownOpen = vi.fn(); + + beforeEach(() => { + vi.clearAllMocks(); + (useBuyContext as Mock).mockReturnValue({ + handleSubmit: mockHandleSubmit, + setIsDropdownOpen: mockSetIsDropdownOpen, + }); + }); + + it('renders null when swapUnit is undefined or has no token', () => { + const { container } = render(); + expect(container.firstChild).toBeNull(); + expect(screen.queryByRole('button')).not.toBeInTheDocument(); + }); + + it('renders correctly with valid swapUnit', () => { + render(); + + expect(screen.getByRole('button')).toBeInTheDocument(); + expect(screen.getByText('10.5 ETH')).toBeInTheDocument(); + expect(screen.getByText('Balance: 20')).toBeInTheDocument(); + const button = screen.getByRole('button'); + expect(button).toHaveClass('hover:bg-[var(--ock-bg-inverse)]', { + exact: false, + }); + }); + + it('disables button and applies muted styling when balance is insufficient', () => { + const swapUnit = { + token: ethToken, + amount: '10.5', + balance: '5', + amountUSD: '10.5', + loading: false, + setAmount: vi.fn(), + setAmountUSD: vi.fn(), + setLoading: vi.fn(), + }; + + render(); + + const button = screen.getByRole('button'); + expect(button).toBeDisabled(); + expect(button).not.toHaveClass('hover:bg-[var(--ock-bg-inverse)]', { + exact: false, + }); + expect(screen.getByText('Balance: 5')).toHaveClass('text-xs'); + }); + + it('triggers handleSubmit and closes dropdown on click', () => { + render(); + + const button = screen.getByRole('button'); + fireEvent.click(button); + + expect(mockSetIsDropdownOpen).toHaveBeenCalledWith(false); + expect(mockHandleSubmit).toHaveBeenCalledWith(ethSwapUnit); + }); + + it('formats amount and balance using getRoundedAmount', () => { + const swapUnit = { + token: ethToken, + amount: '10.5678', + balance: '20.1234', + amountUSD: '10.5', + loading: false, + setAmount: vi.fn(), + setAmountUSD: vi.fn(), + setLoading: vi.fn(), + }; + + (getRoundedAmount as Mock).mockImplementation((value) => value.slice(0, 4)); + + render(); + + expect(getRoundedAmount).toHaveBeenCalledWith('10.5678', 10); + expect(getRoundedAmount).toHaveBeenCalledWith('20.1234', 10); + expect(screen.getByText('10.5 ETH')).toBeInTheDocument(); + expect(screen.getByText('Balance: 20.1')).toBeInTheDocument(); + }); +}); diff --git a/src/buy/utils/getBuyQuote.test.ts b/src/buy/utils/getBuyQuote.test.ts new file mode 100644 index 0000000000..45aeec94e5 --- /dev/null +++ b/src/buy/utils/getBuyQuote.test.ts @@ -0,0 +1,207 @@ +import { getSwapQuote } from '@/core/api/getSwapQuote'; +import { formatTokenAmount } from '@/core/utils/formatTokenAmount'; +import type { Token } from '@/token/types'; +import { base } from 'viem/chains'; +import { type Mock, beforeEach, describe, expect, it, vi } from 'vitest'; +import { isSwapError } from '../../swap/utils/isSwapError'; +import { getBuyQuote } from './getBuyQuote'; + +vi.mock('@/core/api/getSwapQuote'); +vi.mock('@/core/utils/formatTokenAmount'); +vi.mock('../../swap/utils/isSwapError'); + +const toToken: Token = { + name: 'DEGEN', + address: '0x4ed4e862860bed51a9570b96d89af5e1b0efefed', + symbol: 'DEGEN', + decimals: 18, + image: + 'https://d3r81g40ycuhqg.cloudfront.net/wallet/wais/3b/bf/3bbf118b5e6dc2f9e7fc607a6e7526647b4ba8f0bea87125f971446d57b296d2-MDNmNjY0MmEtNGFiZi00N2I0LWIwMTItMDUyMzg2ZDZhMWNm', + chainId: base.id, +}; + +const fromToken: Token = { + name: 'ETH', + address: '', + symbol: 'ETH', + decimals: 18, + image: + 'https://wallet-api-production.s3.amazonaws.com/uploads/tokens/eth_288.png', + chainId: base.id, +}; + +const mockResponse = { + from: fromToken, + to: toToken, + fromAmount: '100000000000000000', + toAmount: '16732157880511600003860', + amountReference: 'from', + priceImpact: '0.07', + chainId: 8453, + hasHighPriceImpact: false, + slippage: '3', + fromAmountUSD: '100', +}; + +const mockEmptyResponse = { + from: fromToken, + to: toToken, + fromAmount: '', + toAmount: '16732157880511600003860', + amountReference: 'from', + priceImpact: '0.07', + chainId: 8453, + hasHighPriceImpact: false, + slippage: '3', + fromAmountUSD: '', +}; + +const mockFromSwapUnit = { + setAmountUSD: vi.fn(), + setAmount: vi.fn(), + amount: '1', + amountUSD: '1', + loading: false, + setLoading: vi.fn(), + token: fromToken, +}; + +describe('getBuyQuote', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should return default values if `from` token is not provided', async () => { + const result = await getBuyQuote({ + amount: '1', + amountReference: 'exactIn', + maxSlippage: '0.5', + to: toToken, + useAggregator: true, + fromSwapUnit: mockFromSwapUnit, + }); + + expect(result).toEqual({ + response: undefined, + formattedFromAmount: '', + error: undefined, + }); + }); + + it('should call `getSwapQuote` if `from` and `to` tokens are different', async () => { + (getSwapQuote as Mock).mockResolvedValue(mockResponse); + (isSwapError as unknown as Mock).mockReturnValue(false); + (formatTokenAmount as Mock).mockReturnValue('1.0'); + + const result = await getBuyQuote({ + amount: '1', + amountReference: 'exactIn', + from: fromToken, + maxSlippage: '0.5', + to: toToken, + useAggregator: true, + fromSwapUnit: mockFromSwapUnit, + }); + + expect(getSwapQuote).toHaveBeenCalledWith({ + amount: '1', + amountReference: 'exactIn', + from: fromToken, + maxSlippage: '0.5', + to: toToken, + useAggregator: true, + }); + + expect(formatTokenAmount).toHaveBeenCalledWith('100000000000000000', 18); + expect(mockFromSwapUnit.setAmountUSD).toHaveBeenCalledWith('100'); + expect(mockFromSwapUnit.setAmount).toHaveBeenCalledWith('1.0'); + + expect(result).toEqual({ + response: mockResponse, + formattedFromAmount: '1.0', + error: undefined, + }); + }); + + it('should handle case where amount values are undefined', async () => { + (getSwapQuote as Mock).mockResolvedValue(mockEmptyResponse); + (isSwapError as unknown as Mock).mockReturnValue(false); + (formatTokenAmount as Mock).mockReturnValue('1.0'); + + const result = await getBuyQuote({ + amount: '1', + amountReference: 'exactIn', + from: fromToken, + maxSlippage: '0.5', + to: toToken, + useAggregator: true, + fromSwapUnit: mockFromSwapUnit, + }); + + expect(getSwapQuote).toHaveBeenCalledWith({ + amount: '1', + amountReference: 'exactIn', + from: fromToken, + maxSlippage: '0.5', + to: toToken, + useAggregator: true, + }); + + expect(formatTokenAmount).not.toHaveBeenCalled(); + expect(mockFromSwapUnit.setAmountUSD).toHaveBeenCalledWith(''); + expect(mockFromSwapUnit.setAmount).toHaveBeenCalledWith(''); + + expect(result).toEqual({ + response: mockEmptyResponse, + formattedFromAmount: '', + error: undefined, + }); + }); + + it('should handle swap errors correctly', async () => { + const mockError = { + code: 'UNCAUGHT_SWAP_QUOTE_ERROR', + error: 'Something went wrong', + message: '', + }; + + (getSwapQuote as Mock).mockResolvedValue(mockError); + (isSwapError as unknown as Mock).mockReturnValue(true); + + const result = await getBuyQuote({ + amount: '1', + amountReference: 'exactIn', + from: fromToken, + maxSlippage: '0.5', + to: toToken, + useAggregator: true, + fromSwapUnit: mockFromSwapUnit, + }); + + expect(isSwapError).toHaveBeenCalledWith(mockError); + expect(result).toEqual({ + response: undefined, + formattedFromAmount: '', + error: mockError, + }); + }); + + it('should not call `getSwapQuote` if `from` and `to` tokens are the same', async () => { + const result = await getBuyQuote({ + amount: '1', + amountReference: 'exactIn', + from: fromToken, + maxSlippage: '0.5', + to: fromToken, + useAggregator: true, + fromSwapUnit: mockFromSwapUnit, + }); + + expect(getSwapQuote).not.toHaveBeenCalled(); + expect(result).toEqual({ + response: undefined, + formattedFromAmount: '', + error: undefined, + }); + }); +}); diff --git a/src/internal/svg/appleSvg.tsx b/src/internal/svg/appleSvg.tsx index 46986e59d7..274767452e 100644 --- a/src/internal/svg/appleSvg.tsx +++ b/src/internal/svg/appleSvg.tsx @@ -4,6 +4,7 @@ export const appleSvg = ( viewBox="0 -29.75 165.5 165.5" preserveAspectRatio="xMidYMid meet" id="Artwork" + data-testid="appleSvg" > AppleSvg CardSvg diff --git a/src/internal/svg/coinbaseLogoSvg.tsx b/src/internal/svg/coinbaseLogoSvg.tsx index 437c7282a8..01f8eb3e63 100644 --- a/src/internal/svg/coinbaseLogoSvg.tsx +++ b/src/internal/svg/coinbaseLogoSvg.tsx @@ -8,6 +8,7 @@ export const coinbaseLogoSvg = ( fill="none" xmlns="http://www.w3.org/2000/svg" className={cn(icon.foreground)} + data-testid="coinbaseLogoSvg" > CoinbaseLogoSvg