Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 113 additions & 0 deletions frontend/src/__tests__/TransactionPendingOverlay.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/**
* Unit Tests for TransactionPendingOverlay Component
*/

import { describe, test, expect, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { TransactionPendingOverlay } from '../components/TransactionPendingOverlay';

describe('TransactionPendingOverlay', () => {
test('renders nothing when isVisible is false', () => {
render(<TransactionPendingOverlay isVisible={false} />);
expect(screen.queryByText('Broadcasted to Stellar')).not.toBeInTheDocument();
});

test('displays pending state with default message', () => {
render(<TransactionPendingOverlay isVisible={true} status="pending" />);

expect(screen.getByText('Broadcasted to Stellar')).toBeInTheDocument();
expect(
screen.getByText('Your transaction is being processed on-chain. This may take a few seconds.')
).toBeInTheDocument();
expect(screen.getByText('Settling on Stellar network...')).toBeInTheDocument();
});

test('displays pending state with custom message', () => {
render(
<TransactionPendingOverlay
isVisible={true}
status="pending"
message="Custom Pending Message"
subMessage="Custom sub message"
/>
);

expect(screen.getByText('Custom Pending Message')).toBeInTheDocument();
expect(screen.getByText('Custom sub message')).toBeInTheDocument();
});

test('displays success state with default message', () => {
render(<TransactionPendingOverlay isVisible={true} status="success" />);

expect(screen.getByText('Transaction Confirmed')).toBeInTheDocument();
expect(
screen.getByText('Your transaction has been successfully processed.')
).toBeInTheDocument();
});

test('displays success state with txHash and explorer link', () => {
const txHash = 'abc123def456789';
render(<TransactionPendingOverlay isVisible={true} status="success" txHash={txHash} />);

expect(screen.getByText('Transaction Confirmed')).toBeInTheDocument();

const explorerLink = screen.getByLabelText('View transaction on explorer');
expect(explorerLink).toBeInTheDocument();
expect(explorerLink).toHaveAttribute('href', expect.stringContaining(txHash));

expect(screen.getByText('Transaction Hash')).toBeInTheDocument();
});

test('displays error state with default message', () => {
render(<TransactionPendingOverlay isVisible={true} status="error" />);

expect(screen.getByText('Transaction Failed')).toBeInTheDocument();
expect(screen.getByText('There was an issue processing your transaction.')).toBeInTheDocument();
});

test('shows dismiss button on success state', () => {
const onDismiss = vi.fn();
render(<TransactionPendingOverlay isVisible={true} status="success" onDismiss={onDismiss} />);

const dismissButton = screen.getByText('Dismiss');
expect(dismissButton).toBeInTheDocument();

fireEvent.click(dismissButton);
expect(onDismiss).toHaveBeenCalledTimes(1);
});

test('shows dismiss button on error state', () => {
const onDismiss = vi.fn();
render(<TransactionPendingOverlay isVisible={true} status="error" onDismiss={onDismiss} />);

const dismissButton = screen.getByText('Dismiss');
expect(dismissButton).toBeInTheDocument();

fireEvent.click(dismissButton);
expect(onDismiss).toHaveBeenCalledTimes(1);
});

test('does not show dismiss button during pending state', () => {
const onDismiss = vi.fn();
render(<TransactionPendingOverlay isVisible={true} status="pending" onDismiss={onDismiss} />);

expect(screen.queryByText('Dismiss')).not.toBeInTheDocument();
});

test('has proper accessibility attributes', () => {
render(<TransactionPendingOverlay isVisible={true} status="pending" />);

const overlay = screen.getByRole('dialog');
expect(overlay).toBeInTheDocument();
expect(overlay).toHaveAttribute('aria-live', 'polite');
expect(overlay).toHaveAttribute('aria-label', 'Broadcasted to Stellar');
});

test('truncates long txHash correctly', () => {
const longTxHash = 'a'.repeat(64);
render(<TransactionPendingOverlay isVisible={true} status="success" txHash={longTxHash} />);

const truncatedHash = screen.getByText(/^aaaaaaaaaaaa/);
expect(truncatedHash).toBeInTheDocument();
});
});
19 changes: 19 additions & 0 deletions frontend/src/pages/CrossAssetPayment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import {
import { ContractErrorPanel } from '../components/ContractErrorPanel';
import { IssuerMultisigBanner } from '../components/IssuerMultisigBanner';
import { parseContractError, type ContractErrorDetail } from '../utils/contractErrorParser';
import { TransactionPendingOverlay } from '../components/TransactionPendingOverlay';

type OverlayStatus = 'pending' | 'success' | 'error';

export default function CrossAssetPayment() {
const { notifyError, notifyPaymentSuccess, notifyPaymentFailure, notifyApiError } =
Expand All @@ -32,6 +35,8 @@ export default function CrossAssetPayment() {
const [liveStatusMessage, setLiveStatusMessage] = useState<string>('Waiting for submission...');
const [status, setStatus] = useState<string>('idle');
const [contractError, setContractError] = useState<ContractErrorDetail | null>(null);
const [overlayVisible, setOverlayVisible] = useState(false);
const [overlayStatus, setOverlayStatus] = useState<OverlayStatus>('pending');

const selectedPath = useMemo(
() => paths.find((path) => path.id === selectedPathId) || null,
Expand Down Expand Up @@ -90,7 +95,11 @@ export default function CrossAssetPayment() {
setStatus(nextStatus);
setLiveStatusMessage(`Live update: ${nextStatus}`);
if (nextStatus === 'completed' || nextStatus === 'confirmed') {
setOverlayStatus('success');
notifyPaymentSuccess(txHash, 'Cross-asset payment completed');
setTimeout(() => {
setOverlayVisible(false);
}, 3000);
}
};

Expand Down Expand Up @@ -123,6 +132,8 @@ export default function CrossAssetPayment() {

setStatus('submitting');
setContractError(null);
setOverlayVisible(true);
setOverlayStatus('pending');
try {
await contractService.initialize();
const contractId =
Expand All @@ -149,6 +160,7 @@ export default function CrossAssetPayment() {
notifyPaymentSuccess(result.txHash, 'Payment submitted');
} catch (error) {
setStatus('error');
setOverlayStatus('error');
const parsed = parseContractError(
undefined,
error instanceof Error ? error.message : 'An unexpected error occurred.'
Expand Down Expand Up @@ -452,6 +464,13 @@ export default function CrossAssetPayment() {
</div>
</div>
</div>

<TransactionPendingOverlay
isVisible={overlayVisible}
status={overlayStatus}
txHash={submissionTxHash ?? undefined}
onDismiss={() => setOverlayVisible(false)}
/>
</div>
);
}
28 changes: 28 additions & 0 deletions frontend/src/pages/PayrollScheduler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ interface PendingClaim {
status: string;
}

type OverlayStatus = 'pending' | 'success' | 'error';

// Mock employer secret key for simulation purposes
const MOCK_EMPLOYER_SECRET = 'SD3X5K7G7XV4K5V3M2G5QXH434M3VX6O5P3QVQO3L2PQSQQQQQQQQQQQ';

Expand Down Expand Up @@ -83,6 +85,10 @@ export default function PayrollScheduler() {
const [formErrors, setFormErrors] = useState<PayrollFormErrors>({});
const [isBroadcasting, setIsBroadcasting] = useState(false);
const [isWizardOpen, setIsWizardOpen] = useState(false);

const [overlayVisible, setOverlayVisible] = useState(false);
const [overlayStatus, setOverlayStatus] = useState<OverlayStatus>('pending');
const [overlayTxHash, setOverlayTxHash] = useState<string | undefined>();
const [activeSchedule, setActiveSchedule] = useState<SchedulingConfig | null>(null);
const [nextRunDate, setNextRunDate] = useState<Date | null>(null);
const [contractError, setContractError] = useState<ContractErrorDetail | null>(null);
Expand Down Expand Up @@ -234,6 +240,10 @@ export default function PayrollScheduler() {
const handleBroadcast = async () => {
setIsBroadcasting(true);
setContractError(null);
setOverlayVisible(true);
setOverlayStatus('pending');
setOverlayTxHash(undefined);

try {
const mockRecipientPublicKey = generateWallet().publicKey;

Expand Down Expand Up @@ -269,6 +279,16 @@ export default function PayrollScheduler() {
// Subscribe to updates for this new claim
subscribeToTransaction(newClaim.id);

// Show success overlay
setOverlayStatus('success');
const mockTxHash = `broadcast_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
setOverlayTxHash(mockTxHash);

// Auto-dismiss after 3 seconds
setTimeout(() => {
setOverlayVisible(false);
}, 3000);

notifySuccess(
'Broadcast successful!',
`Claimable balance created for ${formData.employeeName}`
Expand Down Expand Up @@ -304,6 +324,7 @@ export default function PayrollScheduler() {
);
setContractError(parsed);
notifyPaymentFailure(parsed.message);
setOverlayStatus('error');
} finally {
setIsBroadcasting(false);
}
Expand Down Expand Up @@ -628,6 +649,13 @@ export default function PayrollScheduler() {
<div className="w-full">
<BulkPaymentStatusTracker organizationId={1} />
</div>

<TransactionPendingOverlay
isVisible={overlayVisible}
status={overlayStatus}
txHash={overlayTxHash}
onDismiss={() => setOverlayVisible(false)}
/>
</div>
);
}