import { fireEvent, render, screen } from '@testing-library/react'; import { type Mock, beforeEach, describe, expect, it, vi } from 'vitest'; import { useAccount, useChainId } from 'wagmi'; import { getChainExplorer } from '../../core/network/getChainExplorer'; import { TransactionButton } from './TransactionButton'; import { useTransactionContext } from './TransactionProvider'; import { TransactionButtonRenderParams } from '../types'; vi.mock('./TransactionProvider', () => ({ useTransactionContext: vi.fn(), })); vi.mock('wagmi', () => ({ useChainId: vi.fn(), useAccount: vi.fn(), })); vi.mock('../../core/network/getChainExplorer', () => ({ getChainExplorer: vi.fn(), })); function customRender({ status, onSuccess, onSubmit, }: TransactionButtonRenderParams) { if (status === 'pending') { return
loading
; } if (status === 'success') { return
yay
; } if (status === 'error') { return
oops
; } return
Transact
; } const mockCustomErrorHandler = vi.fn(); function customRenderWithErrorHandler({ status, onSuccess, onSubmit, }: TransactionButtonRenderParams) { if (status === 'pending') { return
Transaction in progress
; } if (status === 'success') { return
yay
; } if (status === 'error') { return
oops
; } return
Transact
; } const mockCustomSuccessHandler = vi.fn(); function customRenderWithSuccessHandler({ status, onSubmit, }: TransactionButtonRenderParams) { if (status === 'pending') { return
Transaction in progress
; } if (status === 'success') { return
yay
; } if (status === 'error') { return
oops
; } return
Transact
; } describe('TransactionButton', () => { beforeEach(() => { (useChainId as Mock).mockReturnValue(123); (useAccount as Mock).mockReturnValue({ address: '123' }); vi.clearAllMocks(); }); it('renders correctly', () => { (useTransactionContext as Mock).mockReturnValue({ isLoading: false, lifecycleStatus: { statusName: 'init', statusData: null }, }); render(); const contentElement = screen.getByText('Transact'); expect(contentElement).toBeInTheDocument(); }); it('renders spinner correctly', () => { (useTransactionContext as Mock).mockReturnValue({ lifecycleStatus: { statusName: 'init', statusData: null }, isLoading: true, }); render(); const spinner = screen.getByTestId('ockSpinner'); expect(spinner).toBeInTheDocument(); }); it('renders view txn text when receipt exists', () => { (useTransactionContext as Mock).mockReturnValue({ isLoading: true, lifecycleStatus: { statusName: 'init', statusData: null }, receipt: '123', }); render(); const text = screen.getByText('View transaction'); expect(text).toBeInTheDocument(); }); it('renders try again when error exists', () => { (useTransactionContext as Mock).mockReturnValue({ isLoading: true, lifecycleStatus: { statusName: 'init', statusData: null }, errorMessage: '123', }); render(); const text = screen.getByText('Try again'); expect(text).toBeInTheDocument(); }); it('renders custom error text when error exists', () => { const mockErrorFunc = vi.fn(); (useTransactionContext as Mock).mockReturnValue({ lifecycleStatus: { statusName: 'init', statusData: null }, errorMessage: 'blah blah', isLoading: false, address: '123', transactions: [{}], onSubmit: mockErrorFunc, }); render(); const text = screen.getByText('oops'); expect(text).toBeInTheDocument(); const button = screen.getByText('oops'); fireEvent.click(button); expect(mockErrorFunc).toHaveBeenCalled(); }); it('should call custom error handler when error exists', () => { (useTransactionContext as Mock).mockReturnValue({ lifecycleStatus: { statusName: 'init', statusData: null }, errorMessage: 'blah blah', isLoading: false, address: '123', transactions: [{}], }); render( , ); const button = screen.getByText('oops'); fireEvent.click(button); expect(mockCustomErrorHandler).toHaveBeenCalled(); }); it('should recall onSubmit when error exists and no custom handler provided', () => { const mockOnSubmit = vi.fn(); (useTransactionContext as Mock).mockReturnValue({ lifecycleStatus: { statusName: 'init', statusData: null }, errorMessage: 'blah blah', isLoading: false, address: '123', transactions: [{}], onSubmit: mockOnSubmit, }); render(); const button = screen.getByTestId('ockTransactionButton_Button'); fireEvent.click(button); expect(mockOnSubmit).toHaveBeenCalled(); }); it('should have disabled attribute when isDisabled is true', () => { const { getByRole } = render( , ); const button = getByRole('button'); expect(button).toBeDisabled(); }); it('should have disabled attribute when txn is in progress', () => { (useTransactionContext as Mock).mockReturnValue({ isLoading: true, lifecycleStatus: { statusName: 'init', statusData: null }, }); const { getByRole } = render(); const button = getByRole('button'); expect(button).toBeDisabled(); }); it('should have disabled when transactions are missing', () => { (useTransactionContext as Mock).mockReturnValue({ transactions: undefined, lifecycleStatus: { statusName: 'init', statusData: null }, }); const { getByRole } = render(); const button = getByRole('button'); expect(button).toBeDisabled(); }); it('should open wallet.coinbase.com for smart wallets', () => { const onSubmit = vi.fn(); const chainExplorerUrl = 'https://explorer.com'; (useTransactionContext as Mock).mockReturnValue({ lifecycleStatus: { statusName: 'init', statusData: null }, receipt: 'receipt-123', transactionId: '123', transactionHash: 'hash', chainId: 1, onSubmit, }); const url = new URL('https://wallet.coinbase.com/assets/transactions'); url.searchParams.set('contentParams[txHash]', 'hash'); url.searchParams.set('contentParams[chainId]', '1'); url.searchParams.set('contentParams[fromAddress]', '123'); (getChainExplorer as Mock).mockReturnValue(chainExplorerUrl); window.open = vi.fn(); render(); const button = screen.getByText('View transaction'); fireEvent.click(button); expect(window.open).toHaveBeenCalledWith( url, '_blank', 'noopener,noreferrer', ); expect(onSubmit).not.toHaveBeenCalled(); }); it('should render custom success text when it exists', () => { (useTransactionContext as Mock).mockReturnValue({ lifecycleStatus: { statusName: 'init', statusData: null }, receipt: '123', transactionId: '456', }); render(); const button = screen.getByText('yay'); fireEvent.click(button); expect(window.open).toHaveBeenCalled(); }); it('should render custom pending text when it exists', () => { (useTransactionContext as Mock).mockReturnValue({ lifecycleStatus: { statusName: 'init', statusData: null }, transactionId: '456', isLoading: true, }); render(); const button = screen.getByText('loading'); fireEvent.click(button); expect(button).toBeDefined(); }); it('should call custom success handler when it exists', () => { (useTransactionContext as Mock).mockReturnValue({ lifecycleStatus: { statusName: 'init', statusData: null }, receipt: '123', transactionId: '456', }); render( , ); const button = screen.getByText('yay'); fireEvent.click(button); expect(mockCustomSuccessHandler).toHaveBeenCalled(); }); it('should enable button when not in progress, not missing props, and not waiting for receipt', () => { (useTransactionContext as Mock).mockReturnValue({ isLoading: false, lifecycleStatus: { statusName: 'init', statusData: null }, transactions: [], transactionId: undefined, transactionHash: undefined, receipt: undefined, }); const { getByRole } = render(); const button = getByRole('button'); expect(button).not.toBeDisabled(); }); it('should open transaction link when only receipt exists', () => { const onSubmit = vi.fn(); const chainExplorerUrl = 'https://explorer.com'; (useTransactionContext as Mock).mockReturnValue({ lifecycleStatus: { statusName: 'init', statusData: null }, receipt: 'receipt-123', transactionId: undefined, transactionHash: 'hash-789', onSubmit, }); (getChainExplorer as Mock).mockReturnValue(chainExplorerUrl); window.open = vi.fn(); render(); const button = screen.getByText('View transaction'); fireEvent.click(button); expect(window.open).toHaveBeenCalledWith( `${chainExplorerUrl}/tx/hash-789`, '_blank', 'noopener,noreferrer', ); expect(onSubmit).not.toHaveBeenCalled(); }); it('should call onSubmit when neither receipt nor transactionId exists', () => { const onSubmit = vi.fn(); (useTransactionContext as Mock).mockReturnValue({ address: '123', transactions: [{}], lifecycleStatus: { statusName: 'init', statusData: null }, onSubmit, receipt: undefined, transactionId: undefined, }); render(); const button = screen.getByText('Transact'); fireEvent.click(button); expect(onSubmit).toHaveBeenCalled(); }); });