📝 fix: Properly Restore Draft Text When Switching Conversations (#12384)

Right now, if you have draft text in conversation A, but no draft text in
conversation B, then switching from A -> B inserts the draft from A into B
(oops).

This was caused by a bug in the `restoreText()` logic which did not
restore *blank* text as the saved draft.

Now, it'll always restore whatever is found as a draft (or set to blank if
there is no draft).
This commit is contained in:
Daniel Lew 2026-04-03 13:08:21 -05:00 committed by GitHub
parent 261941c05f
commit 162ac9c253
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 111 additions and 5 deletions

View file

@ -0,0 +1,110 @@
jest.mock('recoil', () => ({
...jest.requireActual('recoil'),
useRecoilValue: jest.fn(),
}));
jest.mock('~/store', () => ({
saveDrafts: { key: 'saveDrafts', default: true },
}));
jest.mock('~/Providers', () => ({
useChatFormContext: jest.fn(),
}));
jest.mock('~/data-provider', () => ({
useGetFiles: jest.fn(),
}));
jest.mock('~/utils', () => ({
...jest.requireActual('~/utils'),
getDraft: jest.fn(),
setDraft: jest.fn(),
clearDraft: jest.fn(),
clearAllDrafts: jest.fn(),
}));
import React from 'react';
import { renderHook, act } from '@testing-library/react';
import { useRecoilValue } from 'recoil';
import { useChatFormContext } from '~/Providers';
import { useGetFiles } from '~/data-provider';
import { getDraft, setDraft } from '~/utils';
import store from '~/store';
import { useAutoSave } from '~/hooks';
const mockSetValue = jest.fn();
const mockGetDraft = getDraft as jest.Mock;
const mockSetDraft = setDraft as jest.Mock;
const makeTextAreaRef = (value = '') =>
({
current: { value, addEventListener: jest.fn(), removeEventListener: jest.fn() },
}) as unknown as React.RefObject<HTMLTextAreaElement>;
beforeEach(() => {
(useRecoilValue as jest.Mock).mockImplementation((atom) => {
if (atom === store.saveDrafts) return true;
return undefined;
});
(useChatFormContext as jest.Mock).mockReturnValue({ setValue: mockSetValue });
(useGetFiles as jest.Mock).mockReturnValue({ data: [] });
mockGetDraft.mockReturnValue('');
});
describe('useAutoSave — conversation switching', () => {
it('clears the textarea when switching to a conversation with no draft', () => {
const { rerender } = renderHook(
({ conversationId }: { conversationId: string }) =>
useAutoSave({
conversationId,
textAreaRef: makeTextAreaRef(),
files: new Map(),
setFiles: jest.fn(),
}),
{ initialProps: { conversationId: 'convo-1' } },
);
act(() => {
rerender({ conversationId: 'convo-2' });
});
expect(mockSetValue).toHaveBeenLastCalledWith('text', '');
});
it('restores the saved draft when switching to a conversation with one', () => {
mockGetDraft.mockImplementation((id: string) => (id === 'convo-2' ? 'Hello, world!' : ''));
const { rerender } = renderHook(
({ conversationId }: { conversationId: string }) =>
useAutoSave({
conversationId,
textAreaRef: makeTextAreaRef(),
files: new Map(),
setFiles: jest.fn(),
}),
{ initialProps: { conversationId: 'convo-1' } },
);
act(() => {
rerender({ conversationId: 'convo-2' });
});
expect(mockSetValue).toHaveBeenLastCalledWith('text', 'Hello, world!');
});
it('saves the current textarea content before switching away', () => {
const textAreaRef = makeTextAreaRef('draft in progress');
const { rerender } = renderHook(
({ conversationId }: { conversationId: string }) =>
useAutoSave({ conversationId, textAreaRef, files: new Map(), setFiles: jest.fn() }),
{ initialProps: { conversationId: 'convo-1' } },
);
act(() => {
rerender({ conversationId: 'convo-2' });
});
expect(mockSetDraft).toHaveBeenCalledWith({ id: 'convo-1', value: 'draft in progress' });
});
});

View file

@ -73,11 +73,7 @@ export const useAutoSave = ({
const restoreText = useCallback(
(id: string) => {
const savedDraft = getDraft(id);
if (!savedDraft) {
return;
}
setValue('text', savedDraft);
setValue('text', getDraft(id) ?? '');
},
[setValue],
);