diff --git a/README.md b/README.md index 77aadfe..ed36642 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,15 @@ The test suite includes comprehensive coverage of wallet connection handling: - Retry behavior - User interaction flows +- **Formatting Utility Tests** (`test/unit/format.test.js`) + - Currency amount formatting (`formatAmount`) with locale and currency symbol support + - Date and timestamp formatting (`formatDate`) + - Exchange rate formatting (`formatRate`) + - Input normalization (`formatCurrencyInput`) + - Percentage formatting (`formatPercent`) + - Grouped number formatting (`formatNumber`) + - Address and Stellar public key truncation (`shortenAddress`) + Integration tests cover send-money validation, successful transfer submission, pending button behavior, duplicate-submission prevention, Transfers page filter sync (search, status, and date-range presets such as last 7/30/90 days), diff --git a/eslint.config.js b/eslint.config.js index 0e9e535..e7ba7c3 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -27,6 +27,7 @@ export default [ languageOptions: { globals: { ...globals.browser, + ...globals.node, ...globals.es2021, }, parserOptions: { diff --git a/src/services/wallet.js b/src/services/wallet.js index 73e5929..64bcf2d 100644 --- a/src/services/wallet.js +++ b/src/services/wallet.js @@ -43,7 +43,7 @@ export function connectWallet() { * @param {object} payload - the transaction details to "sign" * @returns {Promise<{signature: string}>} */ -export function signTransaction(_payload) { +export function signTransaction() { return new Promise((resolve) => { setTimeout(() => { resolve({ diff --git a/test/integration/send-money-form.test.jsx b/test/integration/send-money-form.test.jsx index 08cdef6..6c946f6 100644 --- a/test/integration/send-money-form.test.jsx +++ b/test/integration/send-money-form.test.jsx @@ -13,7 +13,10 @@ import * as api from '../../src/services/api.js'; async function fillValidForm(user) { await user.type(screen.getByLabelText(/recipient/i), 'amina@example.com'); await user.type(screen.getByLabelText(/amount/i), '15'); - await user.selectOptions(screen.getByLabelText(/to/i), 'NGN'); + await user.selectOptions( + screen.getByLabelText(/to/i, { selector: 'select' }), + 'NGN', + ); } function createdTransfer(payload) { diff --git a/test/services/wallet.test.js b/test/services/wallet.test.js index e2e801d..abd7339 100644 --- a/test/services/wallet.test.js +++ b/test/services/wallet.test.js @@ -46,7 +46,7 @@ describe('connectWallet', () => { try { await connectWallet(); - } catch (err) { + } catch { // Expected to throw } diff --git a/test/touch-targets.test.js b/test/touch-targets.test.js index 34a0936..9dc93a8 100644 --- a/test/touch-targets.test.js +++ b/test/touch-targets.test.js @@ -6,8 +6,6 @@ function readCSS(filename) { return fs.readFileSync(path.resolve(process.cwd(), 'src', filename), 'utf8'); } -const MIN_TARGET = '44px'; - describe('touch target minimum sizes (44px)', () => { describe('button elements', () => { const css = readCSS(path.join('components', 'Button.css')); diff --git a/test/unit/format.test.js b/test/unit/format.test.js index 310b830..989d799 100644 --- a/test/unit/format.test.js +++ b/test/unit/format.test.js @@ -1,8 +1,12 @@ import { describe, expect, it } from 'vitest'; import { formatAmount, + formatCurrencyInput, formatDate, formatNumber, + formatPercent, + formatRate, + shortenAddress, } from '../../src/utils/format.js'; describe('formatAmount', () => { @@ -11,7 +15,6 @@ describe('formatAmount', () => { }); it('respects an explicit locale for grouping and decimal separators', () => { - // French formatting uses a comma as the decimal separator, unlike en-US. const result = formatAmount(1234.5, 'EUR', 'fr-FR'); expect(result).toContain('234,50'); expect(result).toContain('€'); @@ -26,6 +29,10 @@ describe('formatAmount', () => { expect(formatAmount('not-a-number', 'USD')).toBe('$0.00'); }); + it('handles numeric string inputs correctly', () => { + expect(formatAmount('100.5', 'USD')).toBe('$100.50'); + }); + it('defaults the locale argument itself when omitted, independent of the currency', () => { expect(formatAmount(10, 'NGN')).toBe( new Intl.NumberFormat('en-US', { @@ -51,6 +58,67 @@ describe('formatDate', () => { it('returns a placeholder for a missing value regardless of locale', () => { expect(formatDate(null, 'fr-FR')).toBe('-'); + expect(formatDate(undefined)).toBe('-'); + expect(formatDate('')).toBe('-'); + }); + + it('handles Date objects and numeric timestamps', () => { + const dateObj = new Date(value); + expect(formatDate(dateObj)).toBe('Jul 15, 2026'); + expect(formatDate(dateObj.getTime())).toBe('Jul 15, 2026'); + }); +}); + +describe('formatRate', () => { + it('formats exchange rate as a "1 FROM = X TO" string with 4 decimals', () => { + expect(formatRate(294.62, 'USD', 'NGN')).toBe('1 USD = 294.6200 NGN'); + expect(formatRate(0.0034, 'NGN', 'USD')).toBe('1 NGN = 0.0034 USD'); + }); + + it('returns a placeholder when rate is null or undefined', () => { + expect(formatRate(null, 'USD', 'NGN')).toBe('-'); + expect(formatRate(undefined, 'USD', 'NGN')).toBe('-'); + }); +}); + +describe('formatCurrencyInput', () => { + it('returns empty string for null, undefined, or empty values', () => { + expect(formatCurrencyInput(null)).toBe(''); + expect(formatCurrencyInput(undefined)).toBe(''); + expect(formatCurrencyInput('')).toBe(''); + expect(formatCurrencyInput('.')).toBe(''); + }); + + it('strips non-numeric characters and formats to two decimal places', () => { + expect(formatCurrencyInput('1,234.5')).toBe('1234.50'); + expect(formatCurrencyInput('$100.5')).toBe('100.50'); + expect(formatCurrencyInput('abc12.3xyz')).toBe('12.30'); + }); + + it('handles clean number inputs', () => { + expect(formatCurrencyInput(15)).toBe('15.00'); + expect(formatCurrencyInput('25.5')).toBe('25.50'); + }); + + it('returns empty string for non-numeric input strings', () => { + expect(formatCurrencyInput('abc')).toBe(''); + }); +}); + +describe('formatPercent', () => { + it('formats fractional ratio as percentage string with 2 decimal places default', () => { + expect(formatPercent(0.005)).toBe('0.50%'); + expect(formatPercent(0.15)).toBe('15.00%'); + }); + + it('supports custom decimal places', () => { + expect(formatPercent(0.005, 1)).toBe('0.5%'); + expect(formatPercent(0.12345, 3)).toBe('12.345%'); + }); + + it('falls back to 0 for non-numeric or missing input', () => { + expect(formatPercent('not-a-number')).toBe('0.00%'); + expect(formatPercent(null)).toBe('0.00%'); }); }); @@ -64,4 +132,31 @@ describe('formatNumber', () => { expect(result).toContain('234,5'); expect(result).not.toBe(formatNumber(1234.5, 2, 'en-US')); }); + + it('falls back to 0 for non-numeric inputs', () => { + expect(formatNumber('invalid')).toBe('0'); + }); +}); + +describe('shortenAddress', () => { + const address = 'GBQAZ7Z3X7DEMOPUBLICKEY4REMITFLOWWALLET123456789ABCDEF'; + + it('shortens long address with default head (6) and tail (4)', () => { + expect(shortenAddress(address)).toBe('GBQAZ7...CDEF'); + }); + + it('allows custom head and tail lengths', () => { + expect(shortenAddress(address, 4, 3)).toBe('GBQA...DEF'); + }); + + it('returns original value when string length is less than or equal to head + tail', () => { + expect(shortenAddress('SHORT')).toBe('SHORT'); + expect(shortenAddress('1234567890')).toBe('1234567890'); + }); + + it('returns "-" placeholder for empty, null, or undefined values', () => { + expect(shortenAddress('')).toBe('-'); + expect(shortenAddress(null)).toBe('-'); + expect(shortenAddress(undefined)).toBe('-'); + }); });