diff --git a/src/components/DepositForm.test.tsx b/src/components/DepositForm.test.tsx new file mode 100644 index 0000000..766768d --- /dev/null +++ b/src/components/DepositForm.test.tsx @@ -0,0 +1,71 @@ +import { afterEach, beforeEach, describe, expect, it, jest } from '@jest/globals'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import type { useVault as useVaultType } from '@/hooks/useVault'; + +jest.mock('@/hooks/useVault', () => ({ + useVault: jest.fn(), +})); + +const { DepositForm } = require('./DepositForm') as typeof import('./DepositForm'); +const { useVault } = jest.requireMock('@/hooks/useVault') as { + useVault: jest.MockedFunction; +}; + +describe('DepositForm', () => { + const deposit = jest.fn(); + + beforeEach(() => { + jest.spyOn(window, 'alert').mockImplementation(() => {}); + deposit.mockResolvedValue({ success: true }); + useVault.mockReturnValue({ + deposit, + isLoading: false, + } as ReturnType); + }); + + afterEach(() => { + jest.restoreAllMocks(); + jest.clearAllMocks(); + }); + + it('disables submit until the entered amount is valid', () => { + render(); + + const amountInput = screen.getByLabelText(/amount/i); + const submitButton = screen.getByRole('button', { name: /deposit into vault/i }); + + expect(submitButton).toBeDisabled(); + + fireEvent.change(amountInput, { target: { value: '0' } }); + + expect(screen.getByRole('alert')).toHaveTextContent('Amount must be greater than 0 XLM.'); + expect(amountInput).toHaveAttribute('aria-invalid', 'true'); + expect(submitButton).toBeDisabled(); + + fireEvent.change(amountInput, { target: { value: '12.5' } }); + + expect(screen.queryByRole('alert')).not.toBeInTheDocument(); + expect(amountInput).toHaveAttribute('aria-invalid', 'false'); + expect(submitButton).toBeEnabled(); + }); + + it('submits a valid amount in atomic units', async () => { + render(); + + fireEvent.change(screen.getByLabelText(/amount/i), { target: { value: '12.5' } }); + fireEvent.click(screen.getByRole('button', { name: /deposit into vault/i })); + + await waitFor(() => { + expect(deposit).toHaveBeenCalledWith(BigInt(125000000)); + }); + }); + + it('shows a decimal precision error', () => { + render(); + + fireEvent.change(screen.getByLabelText(/amount/i), { target: { value: '1.12345678' } }); + + expect(screen.getByRole('alert')).toHaveTextContent('Use at most 7 decimal places.'); + expect(screen.getByRole('button', { name: /deposit into vault/i })).toBeDisabled(); + }); +}); diff --git a/src/components/DepositForm.tsx b/src/components/DepositForm.tsx index 1a3b693..76df14e 100644 --- a/src/components/DepositForm.tsx +++ b/src/components/DepositForm.tsx @@ -1,6 +1,7 @@ import React, { useState } from 'react'; import { useVault } from '@/hooks/useVault'; import { ArrowUpCircle, Loader2 } from 'lucide-react'; +import { validateXlmAmount } from '@/utils/amountValidation'; /** * DepositForm Component @@ -14,6 +15,11 @@ export const DepositForm: React.FC = () => { // Access vault actions and state via custom hook const { deposit, isLoading } = useVault(); + const validation = validateXlmAmount(amount); + const showError = amount.trim().length > 0 && !validation.isValid; + const amountHelperId = 'deposit-amount-helper'; + const amountErrorId = 'deposit-amount-error'; + const submitDisabled = isLoading || !validation.isValid; /** * Handles the form submission event. @@ -24,16 +30,11 @@ export const DepositForm: React.FC = () => { // Prevent default form submission behavior (page refresh) e.preventDefault(); - // Basic validation: ensure amount is present and is a valid number - if (!amount || isNaN(Number(amount))) return; + if (!validation.isValid) return; try { - // Convert the decimal XLM amount to atomic units (7 decimal places) - // Example: 1.5 XLM -> 15,000,000 atomic units - const atomicAmount = BigInt(Math.floor(Number(amount) * 10000000)); - // Execute the deposit transaction - await deposit(atomicAmount); + await deposit(validation.atomicAmount); // Clear the input field upon successful submission setAmount(''); @@ -71,9 +72,13 @@ export const DepositForm: React.FC = () => { value={amount} onChange={(e) => setAmount(e.target.value)} placeholder="0.00" - step="0.01" + step="0.0000001" min="0" - className="input pr-16 focus:ring-protox-primary/20 transition-all" + aria-describedby={`${amountHelperId}${showError ? ` ${amountErrorId}` : ''}`} + aria-invalid={showError} + className={`input pr-16 focus:ring-protox-primary/20 transition-all ${ + showError ? 'border-red-300 bg-red-50/50 focus:border-red-400 focus:ring-red-100' : '' + }`} required /> {/* Currency suffix badge */} @@ -81,12 +86,20 @@ export const DepositForm: React.FC = () => { XLM +

+ Use up to 7 decimal places, e.g. 25.5 XLM. +

+ {showError && ( + + )} {/* Action button with loading state */}