Skip to content

Commit b326cf3

Browse files
authored
Merge pull request #430 from Feyisara2108/feat/359-persist-draft-messages
feat(frontend): persist chat drafts with 500ms debounce
2 parents 286a5ce + 6945e91 commit b326cf3

2 files changed

Lines changed: 129 additions & 6 deletions

File tree

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import React from 'react';
2+
import { render, screen, fireEvent, waitFor, act } from '@testing-library/react';
3+
import ChatInput from '../ChatInput';
4+
import * as draftUtils from '@/lib/draftUtils';
5+
6+
// Mock the translation context
7+
jest.mock('@/contexts/TranslationContext', () => ({
8+
useTranslation: () => ({
9+
t: (key: string) => key,
10+
}),
11+
}));
12+
13+
// Mock draft utils
14+
jest.mock('@/lib/draftUtils', () => ({
15+
saveDraft: jest.fn(),
16+
getDraft: jest.fn(),
17+
clearDraft: jest.fn(),
18+
}));
19+
20+
// Mock Stellar Wallet context
21+
jest.mock('@/contexts/StellarWalletContext', () => ({
22+
useStellarWallet: () => ({
23+
connection: { isConnected: true },
24+
}),
25+
}));
26+
27+
describe('ChatInput - Draft Persistence', () => {
28+
const mockOnSendMessage = jest.fn();
29+
const sessionId = 'test-session-123';
30+
const defaultProps = {
31+
onSendMessage: mockOnSendMessage,
32+
isLoading: false,
33+
placeholder: 'Type a message...',
34+
sessionId: sessionId,
35+
};
36+
37+
beforeEach(() => {
38+
jest.clearAllMocks();
39+
jest.useFakeTimers();
40+
});
41+
42+
afterEach(() => {
43+
jest.useRealTimers();
44+
});
45+
46+
it('should restore draft from draftUtils on mount', () => {
47+
(draftUtils.getDraft as jest.Mock).mockReturnValue('Restored draft content');
48+
49+
render(<ChatInput {...defaultProps} />);
50+
51+
const textarea = screen.getByPlaceholderText('Type a message...') as HTMLTextAreaElement;
52+
expect(textarea.value).toBe('Restored draft content');
53+
expect(draftUtils.getDraft).toHaveBeenCalledWith(sessionId);
54+
});
55+
56+
it('should save draft to draft store on keystroke with 500ms debounce', async () => {
57+
render(<ChatInput {...defaultProps} />);
58+
const textarea = screen.getByPlaceholderText('Type a message...');
59+
60+
fireEvent.change(textarea, { target: { value: 'T' } });
61+
fireEvent.change(textarea, { target: { value: 'Te' } });
62+
fireEvent.change(textarea, { target: { value: 'Test' } });
63+
64+
// Should not have called saveDraft yet due to debounce
65+
expect(draftUtils.saveDraft).not.toHaveBeenCalled();
66+
67+
// Advance time by 500ms
68+
act(() => {
69+
jest.advanceTimersByTime(500);
70+
});
71+
72+
expect(draftUtils.saveDraft).toHaveBeenCalledWith(sessionId, 'Test');
73+
expect(draftUtils.saveDraft).toHaveBeenCalledTimes(1);
74+
});
75+
76+
it('should clear draft on successful send', async () => {
77+
(draftUtils.getDraft as jest.Mock).mockReturnValue('Message to send');
78+
render(<ChatInput {...defaultProps} />);
79+
80+
const submitButton = screen.getByRole('button', { name: /send message/i });
81+
82+
fireEvent.click(submitButton);
83+
84+
await waitFor(() => {
85+
expect(mockOnSendMessage).toHaveBeenCalledWith('Message to send');
86+
expect(draftUtils.clearDraft).toHaveBeenCalledWith(sessionId);
87+
});
88+
});
89+
90+
it('should persist draft across "reloads" (unmount and remount)', async () => {
91+
const { unmount } = render(<ChatInput {...defaultProps} />);
92+
const textarea = screen.getByPlaceholderText('Type a message...') as HTMLTextAreaElement;
93+
94+
// Type something
95+
fireEvent.change(textarea, { target: { value: 'Persistent message' } });
96+
97+
// Advance timers to trigger save
98+
act(() => {
99+
jest.advanceTimersByTime(500);
100+
});
101+
expect(draftUtils.saveDraft).toHaveBeenCalledWith(sessionId, 'Persistent message');
102+
103+
// Unmount (simulating page exit/reload context)
104+
unmount();
105+
106+
// Mock getDraft to return the saved value for the next mount
107+
(draftUtils.getDraft as jest.Mock).mockReturnValue('Persistent message');
108+
109+
// Remount
110+
render(<ChatInput {...defaultProps} />);
111+
const newTextarea = screen.getByPlaceholderText('Type a message...') as HTMLTextAreaElement;
112+
113+
expect(newTextarea.value).toBe('Persistent message');
114+
});
115+
});

dex_with_fiat_frontend/src/components/ChatInput.tsx

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -191,16 +191,24 @@ export default function ChatInput({
191191
if (sessionId) {
192192
const draft = getDraft(sessionId);
193193
setMessage(draft || '');
194+
} else {
195+
setMessage('');
194196
}
195197
}, [sessionId]);
196198

197-
// Save draft when message changes
199+
// Save draft when message changes (debounced 500ms)
198200
useEffect(() => {
199-
if (sessionId && message.trim()) {
200-
saveDraft(sessionId, message);
201-
} else if (sessionId && !message.trim()) {
202-
clearDraft(sessionId);
203-
}
201+
if (!sessionId) return;
202+
203+
const timer = setTimeout(() => {
204+
if (message.trim()) {
205+
saveDraft(sessionId, message);
206+
} else {
207+
clearDraft(sessionId);
208+
}
209+
}, 500);
210+
211+
return () => clearTimeout(timer);
204212
}, [message, sessionId]);
205213

206214
return (

0 commit comments

Comments
 (0)