import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { fireEvent, render, renderHook, screen, waitFor, } from '@testing-library/react'; import React, { act, useCallback } from 'react'; import type { TransactionReceipt } from 'viem'; import { type Mock, afterEach, beforeEach, describe, expect, it, vi, } from 'vitest'; import { http, WagmiProvider, createConfig, useAccount, useChainId, useSwitchChain, } from 'wagmi'; import { waitForTransactionReceipt } from 'wagmi/actions'; import { base } from 'wagmi/chains'; import { mock } from 'wagmi/connectors'; import { useSendCalls } from 'wagmi/experimental'; import { buildSwapTransaction } from '../../api/buildSwapTransaction'; import type { GetSwapQuoteResponse } from '../../api/types'; import { useAnalytics } from '../../core/analytics/hooks/useAnalytics'; import { BuyEvent } from '../../core/analytics/types'; import { useCapabilitiesSafe } from '../../internal/hooks/useCapabilitiesSafe'; import type { LifecycleStatus, SwapError, SwapUnit } from '../../swap/types'; import { getSwapErrorCode } from '../../swap/utils/getSwapErrorCode'; import { daiToken, degenToken, ethToken, usdcToken, } from '../../token/constants'; import { useOnchainKit } from '../../useOnchainKit'; import { useBuyTokens } from '../hooks/useBuyTokens'; import { getBuyQuote } from '../utils/getBuyQuote'; import { validateQuote } from '../utils/validateQuote'; import { BuyProvider, useBuyContext } from './BuyProvider'; const mockResetFunction = vi.fn(); vi.mock('../hooks/useResetBuyInputs', () => ({ useResetBuyInputs: () => useCallback(mockResetFunction, []), })); vi.mock('../utils/getBuyQuote', () => ({ getBuyQuote: vi.fn(), })); vi.mock('../utils/validateQuote', () => ({ validateQuote: vi.fn(), })); vi.mock('../hooks/useBuyTokens', () => ({ useBuyTokens: vi.fn(), })); vi.mock('@/api/buildSwapTransaction', () => ({ buildSwapTransaction: vi .fn() .mockRejectedValue(new Error('buildSwapTransaction')), })); vi.mock('../../swap/utils/processSwapTransaction', () => ({ processSwapTransaction: vi.fn(), })); vi.mock('@/useOnchainKit', () => ({ useOnchainKit: vi.fn(), })); const mockSwitchChain = vi.fn(); vi.mock('wagmi', async (importOriginal) => { return { ...(await importOriginal()), useAccount: vi.fn(), useChainId: vi.fn(), useSwitchChain: vi.fn(), }; }); const mockAwaitCalls = vi.fn(); vi.mock('../../swap/hooks/useAwaitCalls', () => ({ useAwaitCalls: () => useCallback(mockAwaitCalls, []), })); vi.mock('@/internal/hooks/useCapabilitiesSafe', () => ({ useCapabilitiesSafe: vi.fn(), })); vi.mock('wagmi/actions', () => ({ waitForTransactionReceipt: vi.fn(), })); vi.mock('wagmi/experimental', () => ({ useSendCalls: vi.fn(), })); vi.mock('../path/to/maxSlippageModule', () => ({ getMaxSlippage: vi.fn().mockReturnValue(10), })); vi.mock('../../core/analytics/hooks/useAnalytics', () => ({ useAnalytics: vi.fn(() => ({ sendAnalytics: vi.fn(), })), })); const queryClient = new QueryClient(); const mockFromDai: SwapUnit = { balance: '100', amount: '', setAmount: vi.fn(), setAmountUSD: vi.fn(), token: daiToken, loading: false, setLoading: vi.fn(), error: undefined, } as unknown as SwapUnit; const mockToDegen: SwapUnit = { balance: '100', amount: '', setAmount: vi.fn(), setAmountUSD: vi.fn(), token: degenToken, loading: false, setLoading: vi.fn(), error: undefined, } as unknown as SwapUnit; const mockFromUsdc: SwapUnit = { balance: '100', amount: '', setAmount: vi.fn(), setAmountUSD: vi.fn(), token: usdcToken, loading: false, setLoading: vi.fn(), error: undefined, } as unknown as SwapUnit; const mockFromEth: SwapUnit = { balance: '100', amount: '50', setAmount: vi.fn(), setAmountUSD: vi.fn(), token: ethToken, loading: false, setLoading: vi.fn(), error: undefined, } as unknown as SwapUnit; const mockTransactionReceipt = { blockHash: '0xblock', blockNumber: 1n, contractAddress: '0xcontract', cumulativeGasUsed: 1n, effectiveGasPrice: 1n, from: '0xfrom', gasUsed: 1n, logs: [], logsBloom: '0xbloom', status: 'success', to: '0xto', transactionHash: '0x123', transactionIndex: 1, type: 'eip1559', } as TransactionReceipt; const accountConfig = createConfig({ chains: [base], connectors: [ mock({ accounts: [ '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', '0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC', ], }), ], transports: { [base.id]: http(), }, }); const wrapper = ({ children }: { children: React.ReactNode }) => ( {children} ); const renderWithProviders = ({ Component, onError = vi.fn(), onStatus = vi.fn(), onSuccess = vi.fn(), }: { Component: () => React.ReactNode; onError?: (error: SwapError) => void; onStatus?: (status: LifecycleStatus) => void; onSuccess?: (transactionReceipt?: TransactionReceipt) => void; }) => { const config = { maxSlippage: 10 }; const mockExperimental = { useAggregator: true }; (useOnchainKit as Mock).mockReturnValue({ projectId: 'mock-project-id', config: { paymaster: undefined, }, }); return render( , ); }; const TestSwapComponent = () => { const context = useBuyContext(); const handleStatusError = async () => { context.updateLifecycleStatus({ statusName: 'error', statusData: { code: 'code', error: 'error_long_messages', message: '', }, }); }; const handleStatusAmountChange = async () => { context.updateLifecycleStatus({ statusName: 'amountChange', statusData: { amountFrom: '', amountTo: '', }, }); }; const handleStatusTransactionPending = async () => { context.updateLifecycleStatus({ statusName: 'transactionPending', }); }; const handleStatusTransactionApproved = async () => { context.updateLifecycleStatus({ statusName: 'transactionApproved', statusData: { transactionHash: '0x123', transactionType: 'ERC20', }, }); }; const handleStatusSuccess = async () => { context.updateLifecycleStatus({ statusName: 'success', statusData: { transactionReceipt: { transactionHash: '0x123' }, }, } as unknown as LifecycleStatus); }; return (
{context.lifecycleStatus.statusName} {context.lifecycleStatus.statusName === 'error' && ( {context.lifecycleStatus.statusData.code} )}
); }; describe('useBuyContext', () => { beforeEach(async () => { (useOnchainKit as Mock).mockReturnValue({ projectId: 'mock-project-id', config: { paymaster: undefined, }, }); vi.resetAllMocks(); (useAccount as ReturnType).mockReturnValue({ address: '0x123', }); (useChainId as ReturnType).mockReturnValue(8453); (useSendCalls as ReturnType).mockReturnValue({ status: 'idle', sendCallsAsync: vi.fn(), }); (useSwitchChain as ReturnType).mockReturnValue({ switchChainAsync: mockSwitchChain, }); (validateQuote as Mock).mockReturnValue({ isValid: true, }); (useBuyTokens as Mock).mockReturnValue({ from: mockFromDai, to: mockToDegen, fromETH: mockFromEth, fromUSDC: mockFromUsdc, }); await act(async () => { renderWithProviders({ Component: () => null }); }); }); afterEach(() => { vi.clearAllMocks(); }); it('should throw an error when used outside of BuyProvider', () => { const TestComponent = () => { useBuyContext(); return null; }; // Suppress console.error for this test to avoid noisy output const originalError = console.error; console.error = vi.fn(); expect(() => { render(); }).toThrow('useBuyContext must be used within a Buy component'); // Restore console.error console.error = originalError; }); it('should provide context when used within BuyProvider', async () => { const TestComponent = () => { const context = useBuyContext(); expect(context).toBeDefined(); expect(context.from).toBeDefined(); expect(context.to).toBeDefined(); expect(context.handleAmountChange).toBeDefined(); return null; }; await act(async () => { renderWithProviders({ Component: TestComponent }); }); }); }); describe('BuyProvider', () => { beforeEach(async () => { vi.resetAllMocks(); (useAccount as ReturnType).mockReturnValue({ address: '0x123', }); (useChainId as ReturnType).mockReturnValue(8453); (useSendCalls as ReturnType).mockReturnValue({ status: 'idle', sendCallsAsync: vi.fn(), }); (useSwitchChain as ReturnType).mockReturnValue({ switchChainAsync: mockSwitchChain, }); (useCapabilitiesSafe as ReturnType).mockReturnValue({}); (useOnchainKit as Mock).mockReturnValue({ projectId: 'mock-project-id', config: { paymaster: undefined, }, }); (useBuyTokens as Mock).mockReturnValue({ from: mockFromDai, to: mockToDegen, fromETH: mockFromEth, fromUSDC: mockFromUsdc, }); (useAnalytics as Mock).mockImplementation(() => ({ sendAnalytics: vi.fn(), })); }); it('should call validateQuote with responses', async () => { const mockResponse = { response: { amountUsd: '10' }, } as unknown as GetSwapQuoteResponse; vi.mocked(getBuyQuote).mockResolvedValue({ response: mockResponse }); const { result } = renderHook(() => useBuyContext(), { wrapper }); await act(async () => { result.current.handleAmountChange('10'); }); expect(validateQuote).toHaveBeenCalledWith({ to: mockToDegen, responseETH: mockResponse, responseUSDC: mockResponse, responseFrom: mockResponse, updateLifecycleStatus: expect.any(Function), }); }); it('should not set lifecycle status to amountChange with invalid quote', async () => { const mockResponse = { response: { amountUsd: '10' }, } as unknown as GetSwapQuoteResponse; vi.mocked(getBuyQuote).mockResolvedValue({ response: mockResponse }); (validateQuote as Mock).mockReturnValue({ isValid: false, }); const { result } = renderHook(() => useBuyContext(), { wrapper }); await act(async () => { result.current.handleAmountChange('10'); }); expect(result.current.lifecycleStatus).not.toEqual({ statusName: 'amountChange', statusData: expect.objectContaining({ isMissingRequiredField: false, }), }); }); it('should set lifecycle status to amountChange with valid quote', async () => { const mockResponse = { response: { amountUsd: '10' }, } as unknown as GetSwapQuoteResponse; vi.mocked(getBuyQuote).mockResolvedValue({ response: mockResponse, formattedFromAmount: '20', }); (validateQuote as Mock).mockReturnValue({ isValid: true, }); const { result } = renderHook(() => useBuyContext(), { wrapper }); await act(async () => { result.current.handleAmountChange('10'); }); expect(result.current.lifecycleStatus).toEqual({ statusName: 'amountChange', statusData: expect.objectContaining({ amountETH: '20', amountUSDC: '20', amountFrom: '20', amountTo: '10', }), }); }); it('should set lifecycle status to amountChange with valid quote and empty formattedFromAmount', async () => { const mockResponse = { response: { amountUsd: '10' }, } as unknown as GetSwapQuoteResponse; vi.mocked(getBuyQuote).mockResolvedValue({ response: mockResponse, formattedFromAmount: '', }); (validateQuote as Mock).mockReturnValue({ isValid: true, }); const { result } = renderHook(() => useBuyContext(), { wrapper }); await act(async () => { result.current.handleAmountChange('10'); }); expect(result.current.lifecycleStatus).toEqual({ statusName: 'amountChange', statusData: expect.objectContaining({ amountETH: '', amountUSDC: '', amountFrom: '', amountTo: '10', }), }); }); it('should reset inputs when setLifecycleStatus is called with success', async () => { const { result } = renderHook(() => useBuyContext(), { wrapper }); await act(async () => { result.current.updateLifecycleStatus({ statusName: 'success', statusData: { transactionReceipt: { transactionHash: '0x123' }, }, } as unknown as LifecycleStatus); }); await waitFor( () => { expect(mockResetFunction).toHaveBeenCalled(); }, { timeout: 5000 }, ); expect(mockResetFunction).toHaveBeenCalledTimes(1); }); it('should handle batched transactions', async () => { const { result } = renderHook(() => useBuyContext(), { wrapper }); (useCapabilitiesSafe as ReturnType).mockReturnValue({ atomicBatch: { supported: true }, paymasterService: { supported: true }, auxiliaryFunds: { supported: true }, }); (waitForTransactionReceipt as ReturnType).mockResolvedValue({ transactionHash: 'receiptHash', }); await act(async () => { result.current.updateLifecycleStatus({ statusName: 'transactionApproved', statusData: { transactionType: 'Batched', }, }); }); await waitFor(() => { expect(mockAwaitCalls).toHaveBeenCalled(); }); expect(mockAwaitCalls).toHaveBeenCalledTimes(1); }); it('should emit onError when setLifecycleStatus is called with error', async () => { const onErrorMock = vi.fn(); renderWithProviders({ Component: TestSwapComponent, onError: onErrorMock }); const button = screen.getByText('setLifecycleStatus.error'); fireEvent.click(button); expect(onErrorMock).toHaveBeenCalled(); }); it('should emit onStatus when setLifecycleStatus is called with amountChange', async () => { const onStatusMock = vi.fn(); renderWithProviders({ Component: TestSwapComponent, onStatus: onStatusMock, }); const button = screen.getByText('setLifecycleStatus.amountChange'); fireEvent.click(button); expect(onStatusMock).toHaveBeenCalled(); }); it('should persist statusData when upodating lifecycle status', async () => { const onStatusMock = vi.fn(); renderWithProviders({ Component: TestSwapComponent, onStatus: onStatusMock, }); fireEvent.click(screen.getByText('setLifecycleStatus.transactionPending')); expect(onStatusMock).toHaveBeenLastCalledWith({ statusName: 'transactionPending', statusData: { isMissingRequiredField: true, maxSlippage: 10, }, }); fireEvent.click(screen.getByText('setLifecycleStatus.transactionApproved')); expect(onStatusMock).toHaveBeenLastCalledWith({ statusName: 'transactionApproved', statusData: { transactionHash: '0x123', transactionType: 'ERC20', isMissingRequiredField: true, maxSlippage: 10, }, }); }); it('should not persist error when updating lifecycle status', async () => { const onStatusMock = vi.fn(); renderWithProviders({ Component: TestSwapComponent, onStatus: onStatusMock, }); fireEvent.click(screen.getByText('setLifecycleStatus.error')); expect(onStatusMock).toHaveBeenLastCalledWith({ statusName: 'error', statusData: { code: 'code', error: 'error_long_messages', message: '', isMissingRequiredField: true, maxSlippage: 10, }, }); fireEvent.click(screen.getByText('setLifecycleStatus.transactionPending')); expect(onStatusMock).toHaveBeenLastCalledWith({ statusName: 'transactionPending', statusData: { isMissingRequiredField: true, maxSlippage: 10, }, }); }); it('should emit onStatus when setLifecycleStatus is called with transactionPending', async () => { const onStatusMock = vi.fn(); renderWithProviders({ Component: TestSwapComponent, onStatus: onStatusMock, }); const button = screen.getByText('setLifecycleStatus.transactionPending'); fireEvent.click(button); expect(onStatusMock).toHaveBeenCalled(); }); it('should emit onStatus when setLifecycleStatus is called with transactionApproved', async () => { const onStatusMock = vi.fn(); renderWithProviders({ Component: TestSwapComponent, onStatus: onStatusMock, }); const button = screen.getByText('setLifecycleStatus.transactionApproved'); fireEvent.click(button); expect(onStatusMock).toHaveBeenCalled(); }); it('should emit onSuccess when setLifecycleStatus is called with success', async () => { const onSuccessMock = vi.fn(); renderWithProviders({ Component: TestSwapComponent, onSuccess: onSuccessMock, }); const button = screen.getByText('setLifecycleStatus.success'); fireEvent.click(button); expect(onSuccessMock).toHaveBeenCalled(); }); it('should reset status to init when setLifecycleStatus is called with success', async () => { const onStatusMock = vi.fn(); renderWithProviders({ Component: TestSwapComponent, onStatus: onStatusMock, }); const button = screen.getByText('setLifecycleStatus.success'); fireEvent.click(button); await waitFor(() => { expect(onStatusMock).toHaveBeenCalledWith( expect.objectContaining({ statusName: 'init', statusData: { isMissingRequiredField: true, maxSlippage: 10, }, }), ); }); }); it('should emit onStatus when setLifecycleStatus is called with error', async () => { const onStatusMock = vi.fn(); renderWithProviders({ Component: TestSwapComponent, onStatus: onStatusMock, }); const button = screen.getByText('setLifecycleStatus.error'); fireEvent.click(button); expect(onStatusMock).toHaveBeenCalled(); }); it('should pass the correct slippage to getBuyQuote', async () => { const TestComponent = () => { const { handleAmountChange } = useBuyContext(); React.useEffect(() => { const initializeSwap = () => { handleAmountChange('5'); }; initializeSwap(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return null; }; await act(async () => { renderWithProviders({ Component: TestComponent }); }); expect(getBuyQuote).toHaveBeenCalledWith( expect.objectContaining({ maxSlippage: '10', amount: '5', from: ethToken, to: degenToken, useAggregator: true, }), ); }); it('should set lifecycle status to amountChange with missing required fields when to token is undefined', async () => { (useBuyTokens as Mock).mockReturnValue({ from: mockFromDai, to: { ...mockToDegen, token: undefined }, fromETH: mockFromEth, fromUSDC: mockFromUsdc, }); const { result } = renderHook(() => useBuyContext(), { wrapper }); await act(async () => { result.current.handleAmountChange('10'); }); expect(result.current.lifecycleStatus).toEqual({ statusName: 'amountChange', statusData: expect.objectContaining({ isMissingRequiredField: true, }), }); }); it('should handle undefined in input', async () => { const TestComponent = () => { const { handleAmountChange } = useBuyContext(); React.useEffect(() => { const initializeSwap = () => { handleAmountChange('100'); }; initializeSwap(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return null; }; await act(async () => { renderWithProviders({ Component: TestComponent }); }); }); it('should initialize with empty values', () => { const { result } = renderHook(() => useBuyContext(), { wrapper }); expect(result.current.from?.amount).toBe(''); expect(result.current.to?.amount).toBe(''); expect(result.current.to?.amount).toBe(''); }); it('should update amount and trigger quote', async () => { const { result } = renderHook(() => useBuyContext(), { wrapper }); await act(async () => { result.current.handleAmountChange('10'); }); expect(getBuyQuote).toHaveBeenCalled(); expect(result.current.to?.loading).toBe(false); }); it('should handle empty amount input', async () => { const { result } = renderHook(() => useBuyContext(), { wrapper }); await act(async () => { await result.current.handleAmountChange(''); }); expect(result.current.to?.amount).toBe(''); }); it('should handle zero amount input', async () => { const { result } = renderHook(() => useBuyContext(), { wrapper }); await act(async () => { await result.current.handleAmountChange('0'); }); expect(result.current.to?.amount).toBe(''); }); it('should not setLifecycleStatus to error when getBuyQuote throws an error', async () => { const mockError = new Error('Test error'); vi.mocked(getBuyQuote).mockRejectedValueOnce(mockError); const { result } = renderHook(() => useBuyContext(), { wrapper }); await act(async () => { result.current.handleAmountChange('10'); }); expect(result.current.lifecycleStatus).toEqual({ statusName: 'error', statusData: expect.objectContaining({ code: 'TmBPc02', error: JSON.stringify(mockError), message: '', }), }); }); it('logs an error when projectId is not provided', async () => { const consoleErrorSpy = vi .spyOn(console, 'error') .mockImplementation(() => {}); (useOnchainKit as Mock).mockReturnValue({ projectId: undefined, config: { paymaster: undefined, }, }); renderHook(() => useBuyContext(), { wrapper }); await waitFor(() => { expect(consoleErrorSpy).toHaveBeenCalledWith( 'Project ID is required for this component, please set the projectId in the OnchainKitProvider', ); }); consoleErrorSpy.mockRestore(); }); it('should handle submit correctly', async () => { (useBuyTokens as Mock).mockReturnValue({ from: mockFromDai, to: { ...mockToDegen, amount: '50' }, fromETH: { ...mockFromEth, amount: '100' }, fromUSDC: mockFromUsdc, }); await act(async () => { renderWithProviders({ Component: TestSwapComponent }); }); await act(async () => { fireEvent.click(screen.getByText('Swap')); }); expect(buildSwapTransaction).toBeCalledTimes(1); }); it('should not call buildSwapTransaction when missing required fields', async () => { const TestComponent = () => { const context = useBuyContext(); return ( ); }; renderWithProviders({ Component: TestComponent }); fireEvent.click(screen.getByText('Swap')); expect(buildSwapTransaction).not.toBeCalled(); }); it('should setLifecycleStatus to error when buildSwapTransaction throws an "User rejected the request." error', async () => { const mockError = { shortMessage: 'User rejected the request.', }; vi.mocked(buildSwapTransaction).mockRejectedValueOnce(mockError); renderWithProviders({ Component: TestSwapComponent }); fireEvent.click(screen.getByText('Swap')); await waitFor(() => { expect( screen.getByTestId('context-value-lifecycleStatus-statusName') .textContent, ).toBe('error'); expect( screen.getByTestId('context-value-lifecycleStatus-statusData-code') .textContent, ).toBe('TmBPc03'); }); }); it('should setLifecycleStatus to error when buildSwapTransaction throws an error', async () => { const mockError = new Error('Test error'); vi.mocked(buildSwapTransaction).mockRejectedValueOnce(mockError); renderWithProviders({ Component: TestSwapComponent }); fireEvent.click(screen.getByText('Swap')); await waitFor(() => { expect( screen.getByTestId('context-value-lifecycleStatus-statusName') .textContent, ).toBe('error'); expect( screen.getByTestId('context-value-lifecycleStatus-statusData-code') .textContent, ).toBe('TmBPc03'); }); }); it('should setLifecycleStatus to error when buildSwapTransaction returns an error', async () => { vi.mocked(buildSwapTransaction).mockResolvedValueOnce({ code: getSwapErrorCode('uncaught-swap'), error: 'Something went wrong', message: '', }); renderWithProviders({ Component: TestSwapComponent }); fireEvent.click(screen.getByText('Swap')); await waitFor(() => { expect( screen.getByTestId('context-value-lifecycleStatus-statusName') .textContent, ).toBe('error'); expect( screen.getByTestId('context-value-lifecycleStatus-statusData-code') .textContent, ).toBe('UNCAUGHT_SWAP_ERROR'); }); }); it('should use default maxSlippage when not provided in experimental', () => { const useTestHook = () => { const { lifecycleStatus } = useBuyContext(); return lifecycleStatus; }; const config = { maxSlippage: 3 }; const wrapper = ({ children }: { children: React.ReactNode }) => ( {children} ); const { result } = renderHook(() => useTestHook(), { wrapper }); expect(result.current.statusName).toBe('init'); if (result.current.statusName === 'init') { expect(result.current.statusData.maxSlippage).toBe(3); } }); describe('analytics', () => { let sendAnalytics: Mock; beforeEach(() => { sendAnalytics = vi.fn(); (useAnalytics as Mock).mockImplementation(() => ({ sendAnalytics, })); (useAccount as ReturnType).mockReturnValue({ address: '0x123', }); (useChainId as ReturnType).mockReturnValue(8453); (useBuyTokens as Mock).mockReturnValue({ from: mockFromDai, to: mockToDegen, fromETH: mockFromEth, fromUSDC: mockFromUsdc, }); }); it('should track BuySuccess event on successful swap', async () => { const { result } = renderHook(() => useBuyContext(), { wrapper }); await act(async () => { result.current.updateLifecycleStatus({ statusName: 'transactionApproved', statusData: { transactionHash: '0x123', transactionType: 'ERC20', }, }); result.current.updateLifecycleStatus({ statusName: 'success', statusData: { transactionReceipt: mockTransactionReceipt, }, } as unknown as LifecycleStatus); }); expect(sendAnalytics).toHaveBeenCalledWith(BuyEvent.BuySuccess, { address: '0x123', amount: 0, from: '0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb', paymaster: false, to: '0x4ed4E862860beD51a9570b96d89aF5E1B0Efefed', transactionHash: '0x123', }); }); it('should not track BuyInitiated event when getting quote', async () => { const { result } = renderHook(() => useBuyContext(), { wrapper }); await act(async () => { result.current.handleAmountChange('10'); }); expect(sendAnalytics).not.toHaveBeenCalledWith(BuyEvent.BuyInitiated, { amount: 10, token: degenToken.symbol, }); }); it('should track BuyInitiated event when submitting swap', async () => { renderWithProviders({ Component: TestSwapComponent }); await act(async () => { fireEvent.click(screen.getByText('Swap')); }); expect(sendAnalytics).toHaveBeenCalledWith(BuyEvent.BuyInitiated, { amount: Number(mockFromEth.amount), token: mockFromEth.token?.symbol, }); }); it('should track BuyFailure event when swap fails', async () => { const mockError = new Error('Test error'); vi.mocked(buildSwapTransaction).mockRejectedValueOnce(mockError); renderWithProviders({ Component: TestSwapComponent }); await act(async () => { fireEvent.click(screen.getByText('Swap')); }); await waitFor(() => { expect(sendAnalytics).toHaveBeenCalledWith(BuyEvent.BuyFailure, { error: mockError.message, metadata: { token: ethToken.symbol, amount: '50', }, }); }); }); it('should track BuyFailure event when quote fails', async () => { const mockError = new Error('Quote error'); vi.mocked(getBuyQuote).mockRejectedValueOnce(mockError); const { result } = renderHook(() => useBuyContext(), { wrapper }); await act(async () => { result.current.handleAmountChange('10'); }); expect(sendAnalytics).toHaveBeenCalledWith(BuyEvent.BuyFailure, { error: mockError.message, metadata: { amount: '10' }, }); }); it('should track BuySuccess event with empty values when fields are undefined', async () => { // Mock tokens as undefined (useBuyTokens as Mock).mockReturnValue({ from: { ...mockFromDai, token: undefined }, to: { ...mockToDegen, token: undefined }, fromETH: { ...mockFromEth, token: undefined }, fromUSDC: { ...mockFromUsdc, token: undefined }, }); const { result } = renderHook(() => useBuyContext(), { wrapper }); await act(async () => { result.current.updateLifecycleStatus({ statusName: 'success', statusData: { transactionReceipt: { ...mockTransactionReceipt, transactionHash: undefined, }, }, } as unknown as LifecycleStatus); }); expect(sendAnalytics).toHaveBeenCalledWith(BuyEvent.BuySuccess, { address: '0x123', amount: 0, from: undefined, paymaster: false, to: undefined, transactionHash: undefined, }); }); it('should track BuyInitiated event with empty token when token is undefined', async () => { (useBuyTokens as Mock).mockReturnValue({ from: mockFromDai, to: mockToDegen, fromETH: { ...mockFromEth, token: undefined }, fromUSDC: mockFromUsdc, }); renderWithProviders({ Component: TestSwapComponent }); await act(async () => { fireEvent.click(screen.getByText('Swap')); }); expect(sendAnalytics).toHaveBeenCalledWith(BuyEvent.BuyInitiated, { amount: Number(mockFromEth.amount), token: 'ETH', }); }); it('should track BuyFailure event with empty metadata when not provided', async () => { const mockError = new Error('Test error'); vi.mocked(getBuyQuote).mockRejectedValueOnce(mockError); const { result } = renderHook(() => useBuyContext(), { wrapper }); await act(async () => { result.current.handleAmountChange('10'); }); expect(sendAnalytics).toHaveBeenCalledWith(BuyEvent.BuyFailure, { error: mockError.message, metadata: { amount: '10' }, }); }); it('should handle non-Error objects in error handling', async () => { const nonErrorObject = { message: 'Custom error object' }; vi.mocked(getBuyQuote).mockRejectedValueOnce(nonErrorObject); const { result } = renderHook(() => useBuyContext(), { wrapper }); await act(async () => { result.current.handleAmountChange('10'); }); expect(sendAnalytics).toHaveBeenCalledWith(BuyEvent.BuyFailure, { error: String(nonErrorObject), metadata: { amount: '10' }, }); }); it('should handle string errors in error handling', async () => { const stringError = 'String error message'; vi.mocked(getBuyQuote).mockRejectedValueOnce(stringError); const { result } = renderHook(() => useBuyContext(), { wrapper }); await act(async () => { result.current.handleAmountChange('10'); }); expect(sendAnalytics).toHaveBeenCalledWith(BuyEvent.BuyFailure, { error: stringError, metadata: { amount: '10' }, }); }); it('should handle Error objects in error handling', async () => { const errorObject = new Error('Test error message'); vi.mocked(getBuyQuote).mockRejectedValueOnce(errorObject); const { result } = renderHook(() => useBuyContext(), { wrapper }); await act(async () => { result.current.handleAmountChange('10'); }); expect(sendAnalytics).toHaveBeenCalledWith(BuyEvent.BuyFailure, { error: errorObject.message, metadata: { amount: '10' }, }); }); }); });