diff --git a/src/app/__tests__/TransactionProgressModal.test.tsx b/src/app/__tests__/TransactionProgressModal.test.tsx new file mode 100644 index 00000000..b03b11ec --- /dev/null +++ b/src/app/__tests__/TransactionProgressModal.test.tsx @@ -0,0 +1,238 @@ +// @vitest-environment happy-dom +import { fireEvent, render, screen } from '@testing-library/react'; +import { describe, expect, it, beforeEach, vi } from 'vitest'; +import TransactionProgressModal from '../TransactionProgressModal'; + +// Mock TransactionStepTimeline so tests stay focused +vi.mock('@/components/transaction/TransactionStepTimeline', () => ({ + default: (txProps: any) => ( +
+ {(txProps.state === 'error' && txProps.txHash) && ( + + )} +
+ ) +})); + +describe('TransactionProgressModal', () => { + const mockOnClose = vi.fn(); + const mockOnRetry = vi.fn(); + const mockOnSuccessAction = vi.fn(); + + beforeEach(() => { + vi.clearAllMocks(); + // Mock clipboard + Object.defineProperty(navigator, 'clipboard', { + value: { + writeText: vi.fn().mockResolvedValue(undefined), + }, + writable: true, + }); + }); + + it('renders nothing when isOpen is false', () => { + const { container } = render( + + ); + expect(container.firstChild).toBeNull(); + }); + + it('renders nothing when state is IDLE', () => { + const { container } = render( + + ); + expect(container.firstChild).toBeNull(); + }); + + it('renders AWAITING_SIGNATURE state correctly', () => { + render( + + ); + expect(screen.getByText(/Confirm in Freighter/i)).toBeInTheDocument(); + expect(screen.getByText(/Please sign the transaction in your wallet./i)).toBeInTheDocument(); + const cancelButton = screen.getByText(/Cancel/i); + fireEvent.click(cancelButton); + expect(mockOnClose).toHaveBeenCalled(); + }); + + it('renders SUBMITTING state correctly', () => { + render( + + ); + expect(screen.getByText(/Testing Action in Progress/i)).toBeInTheDocument(); + expect(screen.getByText(/Sending to the Stellar Network.../i)).toBeInTheDocument(); + }); + + it('renders PROCESSING state correctly', () => { + render( + + ); + expect(screen.getByText(/Confirming Transaction/i)).toBeInTheDocument(); + }); + + it('renders SUCCESS state correctly', () => { + render( + + ); + expect(screen.getByText(/Testing Action Successful!/i)).toBeInTheDocument(); + expect(screen.getByText(/Custom success message/i)).toBeInTheDocument(); + const viewDetailsButton = screen.getByText(/View Details/i); + fireEvent.click(viewDetailsButton); + expect(mockOnSuccessAction).toHaveBeenCalled(); + }); + + it('calls onClose if onSuccessAction is not provided', () => { + render( + + ); + const viewDetailsButton = screen.getByText(/View Details/i); + fireEvent.click(viewDetailsButton); + expect(mockOnClose).toHaveBeenCalled(); + }); + + it('copies transaction hash in ERROR state', async () => { + render( + + ); + const copyButton = screen.getByText(/Copy hash/i); + await fireEvent.click(copyButton); + expect(navigator.clipboard.writeText).toHaveBeenCalledWith('test-hash-456'); + }); + + it('handles clipboard rejection gracefully', async () => { + (navigator.clipboard.writeText as vi.Mock).mockRejectedValueOnce(new Error('Clipboard error')); + render( + + ); + const copyButton = screen.getByText(/Copy hash/i); + await fireEvent.click(copyButton); + expect(navigator.clipboard.writeText).toHaveBeenCalledWith('test-hash-789'); + }); + + it('does not throw when txHash is missing', () => { + render( + + ); + expect(screen.queryByText(/Copy hash/i)).not.toBeInTheDocument(); + }); + + it('renders ERROR state correctly', () => { + render( + + ); + expect(screen.getByText(/Transaction Failed/i)).toBeInTheDocument(); + const tryAgainButton = screen.getByText(/Try Again/i); + fireEvent.click(tryAgainButton); + expect(mockOnRetry).toHaveBeenCalled(); + }); + + it('renders UNKNOWN_ERROR by default', () => { + render( + + ); + expect(screen.getByText(/Unexpected Error/i)).toBeInTheDocument(); + }); + + it('renders RPC_TIMEOUT error correctly', () => { + render( + + ); + expect(screen.getByText(/Network Timeout/i)).toBeInTheDocument(); + }); + + it('calls onClose when close button is clicked', () => { + render( + + ); + const closeButton = screen.getByRole('button', { name: /Close modal/i }); + fireEvent.click(closeButton); + expect(mockOnClose).toHaveBeenCalled(); + }); +});