import React, { createRef } from 'react'; import { render, screen, fireEvent, waitFor, act } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; import BookmarkForm from '../BookmarkForm'; import type { TConversationTag } from 'librechat-data-provider'; const mockMutate = jest.fn(); const mockShowToast = jest.fn(); const mockGetQueryData = jest.fn(); const mockSetOpen = jest.fn(); jest.mock('~/hooks', () => ({ useLocalize: () => (key: string, params?: Record) => { const translations: Record = { com_ui_bookmarks_title: 'Title', com_ui_bookmarks_description: 'Description', com_ui_bookmarks_edit: 'Edit Bookmark', com_ui_bookmarks_new: 'New Bookmark', com_ui_bookmarks_create_exists: 'This bookmark already exists', com_ui_bookmarks_add_to_conversation: 'Add to current conversation', com_ui_bookmarks_tag_exists: 'A bookmark with this title already exists', com_ui_field_required: 'This field is required', com_ui_field_max_length: `${params?.field || 'Field'} must be less than ${params?.length || 0} characters`, }; return translations[key] || key; }, })); jest.mock('@librechat/client', () => { const ActualReact = jest.requireActual('react'); return { Checkbox: ({ checked, onCheckedChange, value, ...props }: { checked: boolean; onCheckedChange: (checked: boolean) => void; value: string; }) => ActualReact.createElement('input', { type: 'checkbox', checked, onChange: (e: React.ChangeEvent) => onCheckedChange(e.target.checked), value, ...props, }), Label: ({ children, ...props }: { children: React.ReactNode }) => ActualReact.createElement('label', props, children), TextareaAutosize: ActualReact.forwardRef< HTMLTextAreaElement, React.TextareaHTMLAttributes >((props, ref) => ActualReact.createElement('textarea', { ref, ...props })), Input: ActualReact.forwardRef>( (props, ref) => ActualReact.createElement('input', { ref, ...props }), ), useToastContext: () => ({ showToast: mockShowToast, }), }; }); jest.mock('~/Providers/BookmarkContext', () => ({ useBookmarkContext: () => ({ bookmarks: [], }), })); jest.mock('@tanstack/react-query', () => ({ useQueryClient: () => ({ getQueryData: mockGetQueryData, }), })); jest.mock('~/utils', () => ({ cn: (...classes: (string | undefined | null | boolean)[]) => classes.filter(Boolean).join(' '), logger: { log: jest.fn(), }, })); const createMockBookmark = (overrides?: Partial): TConversationTag => ({ _id: 'bookmark-1', user: 'user-1', tag: 'Test Bookmark', description: 'Test description', createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-01T00:00:00.000Z', count: 1, position: 0, ...overrides, }); const createMockMutation = (isLoading = false) => ({ mutate: mockMutate, isLoading, isError: false, isSuccess: false, data: undefined, error: null, reset: jest.fn(), mutateAsync: jest.fn(), status: 'idle' as const, variables: undefined, context: undefined, failureCount: 0, failureReason: null, isPaused: false, isIdle: true, submittedAt: 0, }); describe('BookmarkForm - Bookmark Editing', () => { const formRef = createRef(); beforeEach(() => { jest.clearAllMocks(); mockGetQueryData.mockReturnValue([]); }); describe('Editing only the description (tag unchanged)', () => { it('should allow submitting when only the description is changed', async () => { const existingBookmark = createMockBookmark({ tag: 'My Bookmark', description: 'Original description', }); mockGetQueryData.mockReturnValue([existingBookmark]); render( } setOpen={mockSetOpen} formRef={formRef} />, ); const descriptionInput = screen.getByRole('textbox', { name: /description/i }); await act(async () => { fireEvent.change(descriptionInput, { target: { value: 'Updated description' } }); }); await act(async () => { fireEvent.submit(formRef.current!); }); await waitFor(() => { expect(mockMutate).toHaveBeenCalledWith( expect.objectContaining({ tag: 'My Bookmark', description: 'Updated description', }), ); }); expect(mockShowToast).not.toHaveBeenCalled(); expect(mockSetOpen).toHaveBeenCalledWith(false); }); it('should not submit when both tag and description are unchanged', async () => { const existingBookmark = createMockBookmark({ tag: 'My Bookmark', description: 'Same description', }); mockGetQueryData.mockReturnValue([existingBookmark]); render( } setOpen={mockSetOpen} formRef={formRef} />, ); await act(async () => { fireEvent.submit(formRef.current!); }); await waitFor(() => { expect(mockMutate).not.toHaveBeenCalled(); }); expect(mockSetOpen).not.toHaveBeenCalled(); }); }); describe('Renaming a tag to an existing tag (should show error)', () => { it('should show error toast when renaming to an existing tag name (via allTags)', async () => { const existingBookmark = createMockBookmark({ tag: 'Original Tag', description: 'Description', }); const otherBookmark = createMockBookmark({ _id: 'bookmark-2', tag: 'Existing Tag', description: 'Other description', }); mockGetQueryData.mockReturnValue([existingBookmark, otherBookmark]); render( } setOpen={mockSetOpen} formRef={formRef} />, ); const tagInput = screen.getByLabelText('Edit Bookmark'); await act(async () => { fireEvent.change(tagInput, { target: { value: 'Existing Tag' } }); }); await act(async () => { fireEvent.submit(formRef.current!); }); await waitFor(() => { expect(mockShowToast).toHaveBeenCalledWith({ message: 'This bookmark already exists', status: 'warning', }); }); expect(mockMutate).not.toHaveBeenCalled(); expect(mockSetOpen).not.toHaveBeenCalled(); }); it('should show error toast when renaming to an existing tag name (via tags prop)', async () => { const existingBookmark = createMockBookmark({ tag: 'Original Tag', description: 'Description', }); mockGetQueryData.mockReturnValue([existingBookmark]); render( } setOpen={mockSetOpen} formRef={formRef} />, ); const tagInput = screen.getByLabelText('Edit Bookmark'); await act(async () => { fireEvent.change(tagInput, { target: { value: 'Existing Tag' } }); }); await act(async () => { fireEvent.submit(formRef.current!); }); await waitFor(() => { expect(mockShowToast).toHaveBeenCalledWith({ message: 'This bookmark already exists', status: 'warning', }); }); expect(mockMutate).not.toHaveBeenCalled(); expect(mockSetOpen).not.toHaveBeenCalled(); }); }); describe('Renaming a tag to a new tag (should succeed)', () => { it('should allow renaming to a completely new tag name', async () => { const existingBookmark = createMockBookmark({ tag: 'Original Tag', description: 'Description', }); mockGetQueryData.mockReturnValue([existingBookmark]); render( } setOpen={mockSetOpen} formRef={formRef} />, ); const tagInput = screen.getByLabelText('Edit Bookmark'); await act(async () => { fireEvent.change(tagInput, { target: { value: 'Brand New Tag' } }); }); await act(async () => { fireEvent.submit(formRef.current!); }); await waitFor(() => { expect(mockMutate).toHaveBeenCalledWith( expect.objectContaining({ tag: 'Brand New Tag', description: 'Description', }), ); }); expect(mockShowToast).not.toHaveBeenCalled(); expect(mockSetOpen).toHaveBeenCalledWith(false); }); it('should allow keeping the same tag name when editing (not trigger duplicate error)', async () => { const existingBookmark = createMockBookmark({ tag: 'My Bookmark', description: 'Original description', }); mockGetQueryData.mockReturnValue([existingBookmark]); render( } setOpen={mockSetOpen} formRef={formRef} />, ); const descriptionInput = screen.getByRole('textbox', { name: /description/i }); await act(async () => { fireEvent.change(descriptionInput, { target: { value: 'New description' } }); }); await act(async () => { fireEvent.submit(formRef.current!); }); await waitFor(() => { expect(mockMutate).toHaveBeenCalledWith( expect.objectContaining({ tag: 'My Bookmark', description: 'New description', }), ); }); expect(mockShowToast).not.toHaveBeenCalled(); }); }); describe('Validation interaction between different data sources', () => { it('should check both tags prop and allTags query data for duplicates', async () => { const existingBookmark = createMockBookmark({ tag: 'Original Tag', description: 'Description', }); const queryDataBookmark = createMockBookmark({ _id: 'bookmark-query', tag: 'Query Data Tag', }); mockGetQueryData.mockReturnValue([existingBookmark, queryDataBookmark]); render( } setOpen={mockSetOpen} formRef={formRef} />, ); const tagInput = screen.getByLabelText('Edit Bookmark'); await act(async () => { fireEvent.change(tagInput, { target: { value: 'Props Tag' } }); }); await act(async () => { fireEvent.submit(formRef.current!); }); await waitFor(() => { expect(mockShowToast).toHaveBeenCalledWith({ message: 'This bookmark already exists', status: 'warning', }); }); expect(mockMutate).not.toHaveBeenCalled(); }); it('should not trigger mutation when mutation is loading', async () => { const existingBookmark = createMockBookmark({ tag: 'My Bookmark', description: 'Description', }); mockGetQueryData.mockReturnValue([existingBookmark]); render( } setOpen={mockSetOpen} formRef={formRef} />, ); const descriptionInput = screen.getByRole('textbox', { name: /description/i }); await act(async () => { fireEvent.change(descriptionInput, { target: { value: 'Updated description' } }); }); await act(async () => { fireEvent.submit(formRef.current!); }); await waitFor(() => { expect(mockMutate).not.toHaveBeenCalled(); }); }); it('should handle empty allTags gracefully', async () => { const existingBookmark = createMockBookmark({ tag: 'My Bookmark', description: 'Description', }); mockGetQueryData.mockReturnValue(null); render( } setOpen={mockSetOpen} formRef={formRef} />, ); const tagInput = screen.getByLabelText('Edit Bookmark'); await act(async () => { fireEvent.change(tagInput, { target: { value: 'New Tag' } }); }); await act(async () => { fireEvent.submit(formRef.current!); }); await waitFor(() => { expect(mockMutate).toHaveBeenCalledWith( expect.objectContaining({ tag: 'New Tag', }), ); }); }); }); });