| 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<string, unknown>) => { | |
| const translations: Record<string, string> = { | |
| 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<typeof import('react')>('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<HTMLInputElement>) => onCheckedChange(e.target.checked), | |
| value, | |
| ...props, | |
| }), | |
| Label: ({ children, ...props }: { children: React.ReactNode }) => | |
| ActualReact.createElement('label', props, children), | |
| TextareaAutosize: ActualReact.forwardRef< | |
| HTMLTextAreaElement, | |
| React.TextareaHTMLAttributes<HTMLTextAreaElement> | |
| >((props, ref) => ActualReact.createElement('textarea', { ref, ...props })), | |
| Input: ActualReact.forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLInputElement>>( | |
| (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>): 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<HTMLFormElement>(); | |
| 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( | |
| <BookmarkForm | |
| bookmark={existingBookmark} | |
| mutation={ | |
| createMockMutation() as ReturnType< | |
| typeof import('~/data-provider').useConversationTagMutation | |
| > | |
| } | |
| 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( | |
| <BookmarkForm | |
| bookmark={existingBookmark} | |
| mutation={ | |
| createMockMutation() as ReturnType< | |
| typeof import('~/data-provider').useConversationTagMutation | |
| > | |
| } | |
| 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( | |
| <BookmarkForm | |
| bookmark={existingBookmark} | |
| mutation={ | |
| createMockMutation() as ReturnType< | |
| typeof import('~/data-provider').useConversationTagMutation | |
| > | |
| } | |
| 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( | |
| <BookmarkForm | |
| tags={['Existing Tag', 'Another Tag']} | |
| bookmark={existingBookmark} | |
| mutation={ | |
| createMockMutation() as ReturnType< | |
| typeof import('~/data-provider').useConversationTagMutation | |
| > | |
| } | |
| 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( | |
| <BookmarkForm | |
| bookmark={existingBookmark} | |
| mutation={ | |
| createMockMutation() as ReturnType< | |
| typeof import('~/data-provider').useConversationTagMutation | |
| > | |
| } | |
| 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( | |
| <BookmarkForm | |
| bookmark={existingBookmark} | |
| mutation={ | |
| createMockMutation() as ReturnType< | |
| typeof import('~/data-provider').useConversationTagMutation | |
| > | |
| } | |
| 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( | |
| <BookmarkForm | |
| tags={['Props Tag']} | |
| bookmark={existingBookmark} | |
| mutation={ | |
| createMockMutation() as ReturnType< | |
| typeof import('~/data-provider').useConversationTagMutation | |
| > | |
| } | |
| 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( | |
| <BookmarkForm | |
| bookmark={existingBookmark} | |
| mutation={ | |
| createMockMutation(true) as ReturnType< | |
| typeof import('~/data-provider').useConversationTagMutation | |
| > | |
| } | |
| 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( | |
| <BookmarkForm | |
| bookmark={existingBookmark} | |
| mutation={ | |
| createMockMutation() as ReturnType< | |
| typeof import('~/data-provider').useConversationTagMutation | |
| > | |
| } | |
| 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', | |
| }), | |
| ); | |
| }); | |
| }); | |
| }); | |
| }); | |