import { MOCK_EARN_CONTEXT } from '@/earn/mocks/mocks.test'; import type { EarnContextType } from '@/earn/types'; import type { MakeRequired } from '@/internal/types'; import type { Call } from '@/transaction/types'; import { render, screen } from '@testing-library/react'; import { act, ReactNode } from 'react'; import type { Address } from 'viem'; import { type Mock, beforeEach, describe, expect, it, vi } from 'vitest'; import { useAccount } from 'wagmi'; import { useConnect } from 'wagmi'; import { DepositButton } from './DepositButton'; import { useEarnContext } from './EarnProvider'; import { TransactionButtonRenderParams } from '@/transaction/types'; import React, { useCallback } from 'react'; import { Token } from '@/token'; // Address required to avoid connect wallet prompt const baseContext: MakeRequired = { ...MOCK_EARN_CONTEXT, recipientAddress: '0x123' as Address, }; vi.mock('./EarnProvider', () => ({ useEarnContext: vi.fn(), })); vi.mock('@/earn/hooks/useDepositAnalytics', () => ({ useDepositAnalytics: () => ({ setTransactionState: vi.fn() }), })); vi.mock('wagmi', () => ({ useAccount: vi.fn(), useConnect: vi.fn(), useConfig: vi.fn(), useCapabilities: vi.fn(), })); // Mock functions to capture and control the render prop // eslint-disable-next-line @typescript-eslint/no-unused-vars let capturedRenderFunction: | ((params: TransactionButtonRenderParams) => ReactNode) | null = null; let currentContext = { receipt: null, errorMessage: '', isLoading: false, }; vi.mock('@/transaction', async (importOriginal) => { return { ...(await importOriginal()), TransactionLifecycleStatus: vi.fn(), TransactionButton: ({ render, disabled, }: { render: (params: TransactionButtonRenderParams) => ReactNode; disabled: boolean; }) => { // Store the render function for later use in tests capturedRenderFunction = render; const params = { context: currentContext, onSubmit: vi.fn(), onSuccess: vi.fn(), isDisabled: disabled, } as unknown as TransactionButtonRenderParams; // Return the rendered component return render(params); }, Transaction: ({ onStatus, onSuccess, children, capabilities, }: { onStatus: (status: { statusName: string }) => void; onSuccess: (response: { transactionReceipts: Array<{ status: string }>; }) => void; children: React.ReactNode; capabilities: { paymasterService: { url: string } }; }) => ( <>
{capabilities?.paymasterService?.url}
{children} ), TransactionSponsor: vi.fn(), TransactionStatus: vi.fn(), TransactionStatusAction: vi.fn(), TransactionStatusLabel: vi.fn(), }; }); vi.mock('@/internal/components/Spinner', () => ({ Spinner: () =>
Loading...
, })); describe('DepositButton Component', () => { beforeEach(() => { vi.clearAllMocks(); capturedRenderFunction = null; currentContext = { receipt: null, errorMessage: '', isLoading: false, }; }); it('renders ConnectWallet if no account is connected', () => { vi.mocked(useEarnContext).mockReturnValue({ ...baseContext, recipientAddress: undefined, }); vi.mocked(useAccount as Mock).mockReturnValue({ address: '', status: 'disconnected', }); vi.mocked(useConnect as Mock).mockReturnValue({ connectors: [{ id: 'mockConnector' }], connect: vi.fn(), status: 'idle', }); render(); const connectWalletButton = screen.getByTestId( 'ockConnectWallet_Container', ); expect(connectWalletButton).toBeInTheDocument(); }); it('renders Transaction with depositCalls from EarnProvider', () => { const mockDepositCalls: Call[] = [{ to: '0x123', data: '0x456' }]; vi.mocked(useEarnContext).mockReturnValue({ ...baseContext, depositCalls: mockDepositCalls, }); render(); const transactionElement = screen.getByTestId('transaction'); expect(transactionElement).toBeInTheDocument(); }); it('surfaces Transaction lifecycle statuses', async () => { const mockUpdateLifecycleStatus = vi.fn(); vi.mocked(useEarnContext).mockReturnValue({ ...baseContext, depositCalls: [{ to: '0x123', data: '0x456' }], updateLifecycleStatus: mockUpdateLifecycleStatus, }); render(); // Simulate different transaction states using the mock buttons await act(async () => { screen.getByText('TransactionPending').click(); }); expect(mockUpdateLifecycleStatus).toHaveBeenCalledWith({ statusName: 'transactionPending', }); await act(async () => { screen.getByText('Success').click(); }); expect(mockUpdateLifecycleStatus).toHaveBeenCalledWith({ statusName: 'success', }); await act(async () => { screen.getByText('Error').click(); }); expect(mockUpdateLifecycleStatus).toHaveBeenCalledWith({ statusName: 'error', }); }); it('clears the deposit amount after a successful transaction', async () => { const mockSetDepositAmount = vi.fn(); const mockRefetchWalletBalance = vi.fn(); vi.mocked(useEarnContext).mockReturnValue({ ...baseContext, setDepositAmount: mockSetDepositAmount, refetchWalletBalance: mockRefetchWalletBalance, }); render(); await act(async () => { screen.getByText('Success').click(); }); expect(mockSetDepositAmount).toHaveBeenCalledWith(''); expect(mockRefetchWalletBalance).toHaveBeenCalled(); }); describe('customRender function', () => { it('renders default deposit button when no special state', () => { vi.mocked(useEarnContext).mockReturnValue({ ...baseContext, depositCalls: [{ to: '0x123', data: '0x456' }], }); render(); const button = screen.getByText('Deposit'); expect(button).toBeInTheDocument(); expect(button.tagName).toBe('BUTTON'); }); it('renders loading state with spinner', () => { vi.mocked(useEarnContext).mockReturnValue({ ...baseContext, depositCalls: [{ to: '0x123', data: '0x456' }], }); const { rerender } = render(); currentContext = { ...currentContext, isLoading: true, }; rerender(); const spinner = screen.getByTestId('spinner'); expect(spinner).toBeInTheDocument(); }); it('renders error state with error message', () => { const depositAmountError = 'Invalid deposit amount'; vi.mocked(useEarnContext).mockReturnValue({ ...baseContext, depositCalls: [{ to: '0x123', data: '0x456' }], depositAmountError, }); const { rerender } = render(); currentContext = { ...currentContext, errorMessage: 'Some error occurred', }; rerender(); const errorButton = screen.getByText(depositAmountError); expect(errorButton).toBeInTheDocument(); }); it('renders receipt state with deposited amount', () => { const depositedAmount = '100'; const vaultTokenSymbol = 'ETH'; vi.mocked(useEarnContext).mockReturnValue({ ...baseContext, depositCalls: [{ to: '0x123', data: '0x456' }], vaultToken: { symbol: vaultTokenSymbol } as Token, }); const CustomRenderTest = () => { const [depositedAmt] = React.useState(depositedAmount); const params: TransactionButtonRenderParams = { context: { receipt: { hash: '0x789' }, // Receipt state errorMessage: '', isLoading: false, }, onSubmit: vi.fn(), onSuccess: vi.fn(), isDisabled: false, } as unknown as TransactionButtonRenderParams; const customRender = useCallback( ({ context }: TransactionButtonRenderParams) => { const classNames = 'mock-class-names'; if (context.receipt) { return ( ); } return ; }, [depositedAmt], ); return customRender(params); }; render(); const successMessage = `Deposited ${depositedAmount} ${vaultTokenSymbol}`; const receiptButton = screen.getByText(successMessage); expect(receiptButton).toBeInTheDocument(); }); it('disables button when depositAmountError exists', () => { vi.mocked(useEarnContext).mockReturnValue({ ...baseContext, depositCalls: [{ to: '0x123', data: '0x456' }], depositAmountError: 'Invalid amount', depositAmount: '100', }); render(); const transactionButton = screen.getByText('Invalid amount'); expect(transactionButton).toHaveAttribute('disabled'); }); it('disables button when depositAmount is empty', () => { vi.mocked(useEarnContext).mockReturnValue({ ...baseContext, depositCalls: [{ to: '0x123', data: '0x456' }], depositAmount: '', }); render(); const transactionButton = screen.getByText('Deposit'); expect(transactionButton).toHaveAttribute('disabled'); }); }); });