Skip to content

Commit 9e9e1bb

Browse files
authored
Merge pull request #871 from dmystical-coder/feat/666-error-boundary-stellar-chat-interface
feat(frontend): add top-level error boundary to StellarChatInterface
2 parents c0d2f3c + b19a73d commit 9e9e1bb

2 files changed

Lines changed: 185 additions & 10 deletions

File tree

dex_with_fiat_frontend/src/components/StellarChatInterface.tsx

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ type HealthStatus = 'checking' | 'ok' | 'degraded';
6161

6262
const HEALTH_POLL_INTERVAL_MS = 60_000;
6363

64-
export default function StellarChatInterface() {
64+
function StellarChatInterfaceContent() {
6565
const { t } = useTranslation();
6666
const {
6767
connection,
@@ -430,7 +430,6 @@ export default function StellarChatInterface() {
430430
},
431431
}[healthStatus];
432432
// ───────────────────────────────────────────────────────────────────────────
433-
//fhj
434433
const withdrawalQueueTone =
435434
withdrawalQueueDepth === null
436435
? isDarkMode
@@ -443,13 +442,7 @@ export default function StellarChatInterface() {
443442
: 'bg-red-500/15 text-red-400';
444443

445444
return (
446-
<ErrorBoundary
447-
isDarkMode={isDarkMode}
448-
title={t('common.error_boundary_title') || 'Interface Error'}
449-
message={t('common.error_boundary_message') || 'The interface encountered an unexpected error.'}
450-
onRetry={() => window.location.reload()}
451-
>
452-
<div className="theme-app flex h-screen w-screen overflow-hidden transition-colors duration-300">
445+
<div className="theme-app flex h-screen w-screen overflow-hidden transition-colors duration-300">
453446
{/* Desktop sidebar - only rendered on lg+ viewports or when toggled */}
454447
{!isMobile && (
455448
<div
@@ -1028,6 +1021,21 @@ export default function StellarChatInterface() {
10281021
</div>
10291022
)}
10301023
</div>
1024+
);
1025+
}
1026+
1027+
/** Top-level error boundary: wraps the full interface tree so render errors are contained. */
1028+
export default function StellarChatInterface() {
1029+
const { isDarkMode } = useTheme();
1030+
const { t } = useTranslation();
1031+
return (
1032+
<ErrorBoundary
1033+
isDarkMode={isDarkMode}
1034+
title={t('common.error_boundary_title') || 'Interface Error'}
1035+
message={t('common.error_boundary_message') || 'The interface encountered an unexpected error.'}
1036+
onRetry={() => window.location.reload()}
1037+
>
1038+
<StellarChatInterfaceContent />
10311039
</ErrorBoundary>
10321040
);
1033-
}
1041+
}
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2+
import { render, screen, cleanup } from '@testing-library/react';
3+
import StellarChatInterface from '@/components/StellarChatInterface';
4+
5+
vi.mock('@/contexts/ThemeContext', () => ({
6+
useTheme: () => ({ isDarkMode: false, toggleDarkMode: vi.fn() }),
7+
}));
8+
9+
vi.mock('@/contexts/TranslationContext', () => ({
10+
useTranslation: () => ({
11+
t: (key: string) => key,
12+
}),
13+
}));
14+
15+
vi.mock('@/contexts/UserPreferencesContext', () => ({
16+
useUserPreferences: () => ({ fiatCurrency: 'NGN' }),
17+
}));
18+
19+
vi.mock('@/contexts/StellarWalletContext', () => ({
20+
EXPECTED_NETWORK: 'Test',
21+
useStellarWallet: () => ({
22+
connection: {
23+
address: '',
24+
publicKey: '',
25+
isConnected: false,
26+
network: 'TEST',
27+
},
28+
accounts: [] as { address: string; name?: string }[],
29+
selectedAccountIndex: 0,
30+
selectAccount: vi.fn(),
31+
connect: vi.fn(),
32+
disconnect: vi.fn(),
33+
signTx: vi.fn(),
34+
isFreighterInstalled: true,
35+
isLoading: false,
36+
error: null,
37+
sessionExpired: false,
38+
clearSessionExpired: vi.fn(),
39+
mockConnect: vi.fn(),
40+
isNetworkMismatch: false,
41+
}),
42+
}));
43+
44+
vi.mock('@/hooks/useChat', () => ({
45+
default: () => ({
46+
messages: [] as { id: string; role: string; content: string; timestamp: Date }[],
47+
isLoading: false,
48+
sendMessage: vi.fn(),
49+
cancelPendingRequest: vi.fn(),
50+
clearChat: vi.fn(),
51+
loadChatSession: vi.fn(),
52+
currentSessionId: null as string | null,
53+
setTransactionReadyCallback: vi.fn(),
54+
setIsAdmin: vi.fn(),
55+
}),
56+
}));
57+
58+
vi.mock('@/hooks/useBridgeStats', () => ({
59+
default: () => ({
60+
balance: null,
61+
limit: null,
62+
totalDeposited: null,
63+
loading: false,
64+
error: null,
65+
refetchStats: vi.fn(),
66+
refresh: vi.fn(),
67+
}),
68+
}));
69+
70+
vi.mock('@/hooks/useTxHistory', () => ({
71+
useTxHistory: () => ({
72+
entries: [],
73+
clearEntries: vi.fn(),
74+
updateEntry: vi.fn(),
75+
}),
76+
}));
77+
78+
vi.mock('@/hooks/useChatHistory', () => ({
79+
useChatHistory: () => ({
80+
sessions: [],
81+
}),
82+
}));
83+
84+
vi.mock('@/hooks/useSplitView', () => ({
85+
useSplitView: () => ({
86+
state: {
87+
isOpen: false,
88+
leftSessionId: null,
89+
rightSessionId: null,
90+
selectedMessageId: null,
91+
},
92+
open: vi.fn(),
93+
close: vi.fn(),
94+
setLeftSession: vi.fn(),
95+
setRightSession: vi.fn(),
96+
swapSessions: vi.fn(),
97+
selectMessage: vi.fn(),
98+
leftSession: null,
99+
rightSession: null,
100+
}),
101+
}));
102+
103+
vi.mock('@/hooks/usePaystackWebhookStatus', () => ({
104+
usePaystackWebhookStatus: () => undefined,
105+
}));
106+
107+
vi.mock('@/lib/networkQueue', () => ({
108+
getQueuedReadRequestsCount: () => 0,
109+
subscribeToQueue: () => () => undefined,
110+
processQueue: vi.fn(),
111+
}));
112+
113+
vi.mock('@/lib/stellarContract', () => ({
114+
getAdmin: vi.fn().mockResolvedValue(null),
115+
getWithdrawalQueueDepth: vi.fn().mockResolvedValue(0),
116+
stroopsToDisplay: (n: string | number) => String(n),
117+
}));
118+
119+
vi.mock('@/components/ChatHistorySidebar', () => ({ default: () => null }));
120+
vi.mock('@/components/ChatInput', () => ({ default: () => null }));
121+
vi.mock('@/components/ChatMessages', () => ({ default: () => null }));
122+
vi.mock('@/components/StellarFiatModal', () => ({ default: () => null }));
123+
vi.mock('@/components/BankDetailsModal', () => ({ default: () => null }));
124+
vi.mock('@/components/UserSettings', () => ({ default: () => null }));
125+
vi.mock('@/components/WalletConnectionTimeline', () => ({ default: () => null }));
126+
vi.mock('@/components/ReceiptDrawerWrapper', () => ({ default: () => null }));
127+
vi.mock('@/components/SplitViewComparison', () => ({ default: () => null }));
128+
vi.mock('@/components/ChatSearchPanel', () => ({ default: () => null }));
129+
vi.mock('@/components/ui/skeleton/SkeletonChat', () => ({ default: () => null }));
130+
vi.mock('@/components/ui/skeleton/SkeletonSidebar', () => ({ default: () => null }));
131+
vi.mock('@/components/NotificationsCenter', () => ({
132+
default: function NotificationsBoom() {
133+
throw new Error('notifications test throw');
134+
},
135+
}));
136+
137+
describe('StellarChatInterface', () => {
138+
beforeEach(() => {
139+
global.fetch = vi.fn().mockResolvedValue({
140+
ok: true,
141+
json: async () => ({}),
142+
} as Response);
143+
Object.defineProperty(window, 'innerWidth', {
144+
writable: true,
145+
configurable: true,
146+
value: 1200,
147+
});
148+
});
149+
150+
afterEach(() => {
151+
cleanup();
152+
vi.clearAllMocks();
153+
});
154+
155+
it('shows the top-level interface error UI when a header child throws', () => {
156+
const consoleErrorSpy = vi
157+
.spyOn(console, 'error')
158+
.mockImplementation(() => undefined);
159+
160+
render(<StellarChatInterface />);
161+
162+
expect(screen.getByText('common.error_boundary_title')).toBeTruthy();
163+
expect(screen.getByText('common.error_boundary_message')).toBeTruthy();
164+
expect(screen.getByRole('button', { name: 'Reload' })).toBeTruthy();
165+
expect(consoleErrorSpy).toHaveBeenCalled();
166+
});
167+
});

0 commit comments

Comments
 (0)