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
21 changes: 17 additions & 4 deletions Dechat/dex_with_fiat_frontend/src/components/BankDetailsModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
Clock,
RefreshCw,
} from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import { motion, AnimatePresence, useReducedMotion } from 'framer-motion';
import { fetchLockedQuote, type LockedQuote } from '@/lib/cryptoPriceService';
import SkeletonWallet from '@/components/ui/skeleton/SkeletonWallet';
import { useNotifications } from '@/hooks/useNotifications';
Expand Down Expand Up @@ -164,6 +164,7 @@ export default function BankDetailsModal({
xlmAmount,
}: BankDetailsModalProps) {
const modalRef = useRef<HTMLDivElement>(null);
const prefersReducedMotion = useReducedMotion();
const {
beneficiaries,
isLoaded: beneficiariesLoaded,
Expand Down Expand Up @@ -665,10 +666,10 @@ export default function BankDetailsModal({
return (
<motion.div
className="theme-overlay fixed inset-0 z-50 flex items-center justify-center backdrop-blur-sm"
initial={{ opacity: 0 }}
initial={prefersReducedMotion ? false : { opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
transition={prefersReducedMotion ? { duration: 0 } : { duration: 0.2 }}
>
<motion.div
ref={modalRef}
Expand All @@ -678,10 +679,22 @@ export default function BankDetailsModal({
tabIndex={-1}
className="theme-surface theme-border relative w-full max-w-md mx-4 border rounded-2xl shadow-2xl p-6"
variants={modalVariants}
initial="hidden"
initial={prefersReducedMotion ? false : 'hidden'}
animate="visible"
exit="exit"
>
<div
role="status"
aria-live="polite"
aria-atomic="true"
className="sr-only"
>
{step === 1 && 'Bank selection step.'}
{step === 2 && 'Account verification step.'}
{step === 3 && 'Payout confirmation step.'}
{step === 4 && 'Payout status step.'}
</div>

{/* Header */}
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-2">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -942,7 +942,14 @@ function StellarChatInterfaceContent() {
{/* Messages */}
<div className="flex-1 min-h-0 flex flex-col">
{!isHydrated || (isLoading && messages.length === 0) ? (
<SkeletonChat />
<div
role="status"
aria-live="polite"
aria-atomic="true"
className="flex-1"
>
<SkeletonChat />
</div>
) : (
<ErrorBoundary
isDarkMode={isDarkMode}
Expand Down
20 changes: 16 additions & 4 deletions Dechat/dex_with_fiat_frontend/src/components/ToastProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use client';

import { useState, useEffect, useCallback, useRef, ReactNode } from 'react';
import { useReducedMotion } from 'framer-motion';
import { X, CheckCircle, AlertCircle, Info, AlertTriangle } from 'lucide-react';
import { toastStore, AppToast, ToastVariant } from '@/lib/toastStore';
import { useTheme } from '@/contexts/ThemeContext';
Expand All @@ -17,6 +18,7 @@ interface ToastItemProps {

function ToastItem({ toast, onDismiss, isDarkMode }: ToastItemProps) {
const touchStartX = useRef(0);
const prefersReducedMotion = useReducedMotion();
const touchStartTime = useRef(0);
const [offsetX, setOffsetX] = useState(0);
const [isSwiping, setIsSwiping] = useState(false);
Expand Down Expand Up @@ -89,10 +91,12 @@ function ToastItem({ toast, onDismiss, isDarkMode }: ToastItemProps) {
? { transform: `translateX(${offsetX}px)`, opacity: Math.max(0, 1 - Math.abs(offsetX) / 300) }
: { transform: 'translateX(0)', opacity: 1 };

const motionPreference = prefersReducedMotion ? { transition: 'none' } : {};

return (
<div
className={getVariantStyles(toast.variant)}
style={swipeStyle}
style={{ ...swipeStyle, ...motionPreference }}
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
Expand Down Expand Up @@ -121,10 +125,13 @@ export function ToastProvider({ children }: ToastProviderProps) {
const { isDarkMode } = useTheme();

useEffect(() => {
const unsubscribe = toastStore.subscribe(() => {
const syncToasts = () => {
const currentToasts = toastStore.getToasts();
setToasts(currentToasts.slice(0, MAX_VISIBLE_TOASTS));
});
};

syncToasts();
const unsubscribe = toastStore.subscribe(syncToasts);

return () => unsubscribe();
}, []);
Expand All @@ -136,7 +143,12 @@ export function ToastProvider({ children }: ToastProviderProps) {
return (
<>
{children}
<div className="fixed bottom-4 right-4 z-50 flex flex-col gap-2 max-w-sm">
<div
role="status"
aria-live="polite"
aria-atomic="true"
className="fixed bottom-4 right-4 z-50 flex flex-col gap-2 max-w-sm"
>
{toasts.map((toast) => (
<ToastItem
key={toast.id}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { render, screen } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import BankDetailsModal from '../BankDetailsModal';

vi.mock('@/lib/chatTelemetry', () => ({
chatTelemetry: {
fiatPayoutStep: vi.fn(),
},
}));

vi.mock('@/hooks/useNotifications', () => ({
useNotifications: () => ({
addNotification: vi.fn(),
}),
}));

vi.mock('@/hooks/useBeneficiaries', () => ({
useBeneficiaries: () => ({
beneficiaries: [],
isLoaded: true,
addBeneficiary: vi.fn(),
renameBeneficiary: vi.fn(),
deleteBeneficiary: vi.fn(),
}),
}));

vi.mock('@/hooks/useTxHistory', () => ({
useTxHistory: () => ({
addEntry: vi.fn(),
}),
}));

vi.mock('@/hooks/useIdempotentAction', () => ({
useIdempotentAction: () => ({
execute: async (fn: (key: string) => Promise<void>) => {
await fn('test-key');
return null;
},
isProcessing: false,
}),
}));

vi.mock('@/hooks/useAccessibleModal', () => ({
useAccessibleModal: vi.fn(),
}));

vi.mock('@/lib/clientSession', () => ({
getOrCreateClientSessionId: () => 'test-session-id',
}));

describe('BankDetailsModal accessibility', () => {
it('renders a polite live region for announcements', () => {
render(
<BankDetailsModal isOpen={true} onClose={() => undefined} xlmAmount={100} />,
);

const liveRegion = screen.getByRole('status');
expect(liveRegion).toHaveAttribute('aria-live', 'polite');
expect(liveRegion).toHaveAttribute('aria-atomic', 'true');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ vi.mock('@/lib/chatTelemetry', () => ({
chatTelemetry: { fiatPayoutStep: vi.fn() },
}));
vi.mock('framer-motion', () => ({
useReducedMotion: () => false,
motion: {
div: ({ children, ...props }: any) => <div {...props}>{children}</div>,
button: ({ children, ...props }: any) => <button {...props}>{children}</button>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,8 @@ describe('ChatInput - Rapid Click Protection', () => {

expect(submitButton).toHaveAttribute('title', 'Send message (Ctrl+Enter)');
expect(submitButton).toHaveAttribute('aria-keyshortcuts', 'Control+Enter');
expect(textarea).toHaveAttribute('aria-describedby', 'chat-submit-shortcut');
expect(textarea.getAttribute('aria-describedby')).toContain('chat-submit-shortcut');
expect(textarea.getAttribute('aria-describedby')).toContain('chat-input-status');
expect(screen.getByText(/send message with ctrl\+enter/i)).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,9 @@ describe('ChatInput - Wallet Disconnect Handling', () => {
fireEvent.change(textarea, { target: { value: 'Test message' } });
fireEvent.keyDown(textarea, { key: 'Enter', code: 'Enter', ctrlKey: true });

expect(screen.getByText('Wallet disconnected. Reconnect to continue.')).toBeInTheDocument();
expect(
screen.getAllByText('Wallet disconnected. Reconnect to continue.'),
).toHaveLength(2);
expect(screen.getByRole('status')).toHaveTextContent('Wallet disconnected. Reconnect to continue.');
expect(mockOnSendMessage).not.toHaveBeenCalled();
});
Expand All @@ -86,7 +88,9 @@ describe('ChatInput - Wallet Disconnect Handling', () => {
fireEvent.change(textarea, { target: { value: 'Test message' } });
fireEvent.keyDown(textarea, { key: 'Enter', code: 'Enter', ctrlKey: true });

expect(screen.getByText('Wallet disconnected. Reconnect to continue.')).toBeInTheDocument();
expect(
screen.getAllByText('Wallet disconnected. Reconnect to continue.'),
).toHaveLength(2);

// Simulate wallet reconnection
mockWalletContext.connection = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ describe('ChatSearchPanel – keyboard shortcuts (#1183)', () => {
// not just for the (synchronously-rendered) results container to exist.
// Generous timeout: real (non-fake) timers under a loaded test runner.
await screen.findByText('Bridging XLM', {}, { timeout: 3000 });
await screen.findByRole('option', { selected: true });

fireEvent.keyDown(root, { key: 'Enter' });

Expand All @@ -75,6 +76,7 @@ describe('ChatSearchPanel – keyboard shortcuts (#1183)', () => {

fireEvent.change(input, { target: { value: 'XLM' } });
await screen.findByText('Wallet setup', {}, { timeout: 3000 });
await screen.findByRole('option', { selected: true });

fireEvent.keyDown(root, { key: 'ArrowDown' });
fireEvent.keyDown(root, { key: 'Enter' });
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { render, screen, act } from '@testing-library/react';
import { describe, expect, it, beforeEach, vi } from 'vitest';
import { ToastProvider } from '../ToastProvider';
import { toastStore } from '@/lib/toastStore';

vi.mock('@/contexts/ThemeContext', () => ({
useTheme: () => ({ isDarkMode: false }),
}));

describe('ToastProvider', () => {
beforeEach(() => {
toastStore.clearToasts();
});

it('renders a polite live region for toast announcements', () => {
act(() => {
toastStore.addToast('Saved successfully', 'success');
});

render(
<ToastProvider>
<div>children</div>
</ToastProvider>,
);

const liveRegion = screen.getByRole('status');
expect(liveRegion).toHaveAttribute('aria-live', 'polite');
expect(liveRegion).toHaveAttribute('aria-atomic', 'true');
expect(screen.getByText('Saved successfully')).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ type SkeletonProps = {

export default function Skeleton({ className }: SkeletonProps) {
return (
<div className={cn('animate-pulse rounded-md bg-gray-700/40', className)} />
<div
className={cn(
'animate-pulse motion-reduce:animate-none rounded-md bg-gray-700/40',
className,
)}
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,9 @@ test.describe('BankDetailsModal — Step 3: confirm payout', () => {
});

test('shows confirm payout screen with quote details', async ({ page }) => {
await expect(page.getByText(/confirm/i)).toBeVisible();
await expect(
page.getByRole('button', { name: 'Confirm Payout' }),
).toBeVisible();
});

test('payout note field accepts text up to 160 characters', async ({ page }) => {
Expand Down
Loading