| |
| jest.mock('recoil', () => { |
| const originalModule = jest.requireActual('recoil'); |
| return { |
| ...originalModule, |
| atom: jest.fn().mockImplementation((config) => ({ |
| key: config.key, |
| default: config.default, |
| })), |
| useRecoilValue: jest.fn(), |
| }; |
| }); |
|
|
| |
| jest.mock('~/store', () => ({ |
| modularChat: { key: 'modularChat', default: false }, |
| availableTools: { key: 'availableTools', default: [] }, |
| })); |
|
|
| import { renderHook, act } from '@testing-library/react'; |
| import { useSearchParams } from 'react-router-dom'; |
| import { useQueryClient } from '@tanstack/react-query'; |
| import { useRecoilValue } from 'recoil'; |
| import useQueryParams from './useQueryParams'; |
| import { useChatContext, useChatFormContext } from '~/Providers'; |
| import useSubmitMessage from '~/hooks/Messages/useSubmitMessage'; |
| import useDefaultConvo from '~/hooks/Conversations/useDefaultConvo'; |
| import store from '~/store'; |
|
|
| |
| jest.mock('react-router-dom', () => ({ |
| useSearchParams: jest.fn(), |
| })); |
|
|
| jest.mock('@tanstack/react-query', () => ({ |
| useQueryClient: jest.fn(), |
| useQuery: jest.fn(), |
| })); |
|
|
| jest.mock('~/Providers', () => ({ |
| useChatContext: jest.fn(), |
| useChatFormContext: jest.fn(), |
| })); |
|
|
| jest.mock('~/hooks/Messages/useSubmitMessage', () => ({ |
| __esModule: true, |
| default: jest.fn(), |
| })); |
|
|
| jest.mock('~/hooks/Conversations/useDefaultConvo', () => ({ |
| __esModule: true, |
| default: jest.fn(), |
| })); |
|
|
| jest.mock('~/hooks/AuthContext', () => ({ |
| useAuthContext: jest.fn(), |
| })); |
|
|
| jest.mock('~/hooks/Agents/useAgentsMap', () => ({ |
| __esModule: true, |
| default: jest.fn(() => ({})), |
| })); |
| jest.mock('~/hooks/Agents/useAgentDefaultPermissionLevel', () => ({ |
| __esModule: true, |
| default: jest.fn(() => ({})), |
| })); |
|
|
| jest.mock('~/utils', () => ({ |
| getConvoSwitchLogic: jest.fn(() => ({ |
| template: {}, |
| shouldSwitch: false, |
| isNewModular: false, |
| newEndpointType: null, |
| isCurrentModular: false, |
| isExistingConversation: false, |
| })), |
| getModelSpecIconURL: jest.fn(() => 'icon-url'), |
| removeUnavailableTools: jest.fn((preset) => preset), |
| logger: { log: jest.fn() }, |
| getInitialTheme: jest.fn(() => 'light'), |
| applyFontSize: jest.fn(), |
| })); |
|
|
| |
| jest.mock('librechat-data-provider', () => ({ |
| ...jest.requireActual('librechat-data-provider'), |
| tQueryParamsSchema: { |
| shape: { |
| model: { parse: jest.fn((value) => value) }, |
| endpoint: { parse: jest.fn((value) => value) }, |
| temperature: { parse: jest.fn((value) => value) }, |
| |
| }, |
| }, |
| isAgentsEndpoint: jest.fn(() => false), |
| isAssistantsEndpoint: jest.fn(() => false), |
| QueryKeys: { startupConfig: 'startupConfig', endpoints: 'endpoints' }, |
| EModelEndpoint: { custom: 'custom', assistants: 'assistants', agents: 'agents' }, |
| })); |
|
|
| |
| jest.mock('~/data-provider', () => ({ |
| useGetAgentByIdQuery: jest.fn(() => ({ |
| data: null, |
| isLoading: false, |
| error: null, |
| })), |
| useListAgentsQuery: jest.fn(() => ({ |
| data: null, |
| isLoading: false, |
| error: null, |
| })), |
| })); |
|
|
| |
| global.window = Object.create(window); |
| global.window.history = { |
| replaceState: jest.fn(), |
| pushState: jest.fn(), |
| go: jest.fn(), |
| back: jest.fn(), |
| forward: jest.fn(), |
| length: 1, |
| scrollRestoration: 'auto', |
| state: null, |
| }; |
|
|
| describe('useQueryParams', () => { |
| |
| beforeEach(() => { |
| jest.useFakeTimers(); |
|
|
| |
| jest.spyOn(window.history, 'replaceState').mockClear(); |
|
|
| |
| const dataProvider = jest.requireMock('~/data-provider'); |
| (dataProvider.useGetAgentByIdQuery as jest.Mock).mockReturnValue({ |
| data: null, |
| isLoading: false, |
| error: null, |
| }); |
|
|
| |
| const mockSearchParams = new URLSearchParams(); |
| (useSearchParams as jest.Mock).mockReturnValue([mockSearchParams, jest.fn()]); |
|
|
| const mockQueryClient = { |
| getQueryData: jest.fn().mockImplementation((key) => { |
| if (key === 'startupConfig') { |
| return { modelSpecs: { list: [] } }; |
| } |
| if (key === 'endpoints') { |
| return {}; |
| } |
| return null; |
| }), |
| }; |
| (useQueryClient as jest.Mock).mockReturnValue(mockQueryClient); |
|
|
| (useRecoilValue as jest.Mock).mockImplementation((atom) => { |
| if (atom === store.modularChat) return false; |
| if (atom === store.availableTools) return []; |
| return null; |
| }); |
|
|
| const mockConversation = { model: null, endpoint: null }; |
| const mockNewConversation = jest.fn(); |
| (useChatContext as jest.Mock).mockReturnValue({ |
| conversation: mockConversation, |
| newConversation: mockNewConversation, |
| }); |
|
|
| const mockMethods = { |
| setValue: jest.fn(), |
| getValues: jest.fn().mockReturnValue(''), |
| handleSubmit: jest.fn((callback) => () => callback({ text: 'test message' })), |
| }; |
| (useChatFormContext as jest.Mock).mockReturnValue(mockMethods); |
|
|
| const mockSubmitMessage = jest.fn(); |
| (useSubmitMessage as jest.Mock).mockReturnValue({ |
| submitMessage: mockSubmitMessage, |
| }); |
|
|
| const mockGetDefaultConversation = jest.fn().mockReturnValue({}); |
| (useDefaultConvo as jest.Mock).mockReturnValue(mockGetDefaultConversation); |
|
|
| |
| const { useAuthContext } = jest.requireMock('~/hooks/AuthContext'); |
| (useAuthContext as jest.Mock).mockReturnValue({ |
| user: { id: 'test-user-id' }, |
| isAuthenticated: true, |
| }); |
| }); |
|
|
| afterEach(() => { |
| jest.clearAllMocks(); |
| jest.useRealTimers(); |
| }); |
|
|
| |
| const setUrlParams = (params: Record<string, string>) => { |
| const searchParams = new URLSearchParams(); |
| Object.entries(params).forEach(([key, value]) => { |
| searchParams.set(key, value); |
| }); |
| (useSearchParams as jest.Mock).mockReturnValue([searchParams, jest.fn()]); |
| }; |
|
|
| |
| it('should process query parameters on initial render', () => { |
| |
| const mockSetValue = jest.fn(); |
| const mockTextAreaRef = { |
| current: { |
| focus: jest.fn(), |
| setSelectionRange: jest.fn(), |
| } as unknown as HTMLTextAreaElement, |
| }; |
|
|
| (useChatFormContext as jest.Mock).mockReturnValue({ |
| setValue: mockSetValue, |
| getValues: jest.fn().mockReturnValue(''), |
| handleSubmit: jest.fn((callback) => () => callback({ text: 'test message' })), |
| }); |
|
|
| |
| (useQueryClient as jest.Mock).mockReturnValue({ |
| getQueryData: jest.fn().mockReturnValue({ modelSpecs: { list: [] } }), |
| }); |
|
|
| setUrlParams({ q: 'hello world' }); |
|
|
| |
| renderHook(() => useQueryParams({ textAreaRef: mockTextAreaRef })); |
|
|
| |
| act(() => { |
| jest.advanceTimersByTime(100); |
| }); |
|
|
| |
| expect(mockSetValue).toHaveBeenCalledWith( |
| 'text', |
| 'hello world', |
| expect.objectContaining({ shouldValidate: true }), |
| ); |
| expect(window.history.replaceState).toHaveBeenCalled(); |
| }); |
|
|
| it('should auto-submit message when submit=true and no settings to apply', () => { |
| |
| const mockSetValue = jest.fn(); |
| const mockHandleSubmit = jest.fn((callback) => () => callback({ text: 'test message' })); |
| const mockSubmitMessage = jest.fn(); |
| const mockTextAreaRef = { |
| current: { |
| focus: jest.fn(), |
| setSelectionRange: jest.fn(), |
| } as unknown as HTMLTextAreaElement, |
| }; |
|
|
| (useChatFormContext as jest.Mock).mockReturnValue({ |
| setValue: mockSetValue, |
| getValues: jest.fn().mockReturnValue(''), |
| handleSubmit: mockHandleSubmit, |
| }); |
|
|
| (useSubmitMessage as jest.Mock).mockReturnValue({ |
| submitMessage: mockSubmitMessage, |
| }); |
|
|
| |
| (useQueryClient as jest.Mock).mockReturnValue({ |
| getQueryData: jest.fn().mockReturnValue({ modelSpecs: { list: [] } }), |
| }); |
|
|
| setUrlParams({ q: 'hello world', submit: 'true' }); |
|
|
| |
| renderHook(() => useQueryParams({ textAreaRef: mockTextAreaRef })); |
|
|
| |
| act(() => { |
| jest.advanceTimersByTime(100); |
| }); |
|
|
| |
| expect(mockSetValue).toHaveBeenCalledWith( |
| 'text', |
| 'hello world', |
| expect.objectContaining({ shouldValidate: true }), |
| ); |
| expect(mockHandleSubmit).toHaveBeenCalled(); |
| expect(mockSubmitMessage).toHaveBeenCalled(); |
| }); |
|
|
| it('should defer submission when settings need to be applied first', () => { |
| |
| const mockSetValue = jest.fn(); |
| const mockHandleSubmit = jest.fn((callback) => () => callback({ text: 'test message' })); |
| const mockSubmitMessage = jest.fn(); |
| const mockNewConversation = jest.fn(); |
| const mockTextAreaRef = { |
| current: { |
| focus: jest.fn(), |
| setSelectionRange: jest.fn(), |
| } as unknown as HTMLTextAreaElement, |
| }; |
|
|
| |
| const mockGetQueryData = jest.fn().mockImplementation((key) => { |
| if (Array.isArray(key) && key[0] === 'startupConfig') { |
| return { modelSpecs: { list: [] } }; |
| } |
| if (key === 'startupConfig') { |
| return { modelSpecs: { list: [] } }; |
| } |
| return null; |
| }); |
|
|
| (useChatFormContext as jest.Mock).mockReturnValue({ |
| setValue: mockSetValue, |
| getValues: jest.fn().mockReturnValue(''), |
| handleSubmit: mockHandleSubmit, |
| }); |
|
|
| (useSubmitMessage as jest.Mock).mockReturnValue({ |
| submitMessage: mockSubmitMessage, |
| }); |
|
|
| (useChatContext as jest.Mock).mockReturnValue({ |
| conversation: { model: null, endpoint: null }, |
| newConversation: mockNewConversation, |
| }); |
|
|
| (useQueryClient as jest.Mock).mockReturnValue({ |
| getQueryData: mockGetQueryData, |
| }); |
|
|
| setUrlParams({ q: 'hello world', submit: 'true', model: 'gpt-4' }); |
|
|
| |
| const { rerender } = renderHook(() => useQueryParams({ textAreaRef: mockTextAreaRef })); |
|
|
| |
| act(() => { |
| jest.advanceTimersByTime(100); |
| }); |
|
|
| |
| expect(mockGetQueryData).toHaveBeenCalledWith(expect.anything()); |
| expect(mockNewConversation).toHaveBeenCalled(); |
| expect(mockSubmitMessage).not.toHaveBeenCalled(); |
|
|
| |
| (useChatContext as jest.Mock).mockReturnValue({ |
| conversation: { model: 'gpt-4', endpoint: null }, |
| newConversation: mockNewConversation, |
| }); |
|
|
| |
| rerender(); |
|
|
| |
| expect(mockSetValue).toHaveBeenCalledWith( |
| 'text', |
| 'hello world', |
| expect.objectContaining({ shouldValidate: true }), |
| ); |
| expect(mockHandleSubmit).toHaveBeenCalled(); |
| expect(mockSubmitMessage).toHaveBeenCalled(); |
| }); |
|
|
| it('should submit after timeout if settings never get applied', () => { |
| |
| const mockSetValue = jest.fn(); |
| const mockHandleSubmit = jest.fn((callback) => () => callback({ text: 'test message' })); |
| const mockSubmitMessage = jest.fn(); |
| const mockNewConversation = jest.fn(); |
| const mockTextAreaRef = { |
| current: { |
| focus: jest.fn(), |
| setSelectionRange: jest.fn(), |
| } as unknown as HTMLTextAreaElement, |
| }; |
|
|
| (useChatFormContext as jest.Mock).mockReturnValue({ |
| setValue: mockSetValue, |
| getValues: jest.fn().mockReturnValue(''), |
| handleSubmit: mockHandleSubmit, |
| }); |
|
|
| (useSubmitMessage as jest.Mock).mockReturnValue({ |
| submitMessage: mockSubmitMessage, |
| }); |
|
|
| (useChatContext as jest.Mock).mockReturnValue({ |
| conversation: { model: null, endpoint: null }, |
| newConversation: mockNewConversation, |
| }); |
|
|
| |
| (useQueryClient as jest.Mock).mockReturnValue({ |
| getQueryData: jest.fn().mockImplementation((key) => { |
| if (Array.isArray(key) && key[0] === 'startupConfig') { |
| return { modelSpecs: { list: [] } }; |
| } |
| if (key === 'startupConfig') { |
| return { modelSpecs: { list: [] } }; |
| } |
| return null; |
| }), |
| }); |
|
|
| setUrlParams({ q: 'hello world', submit: 'true', model: 'non-existent-model' }); |
|
|
| |
| renderHook(() => useQueryParams({ textAreaRef: mockTextAreaRef })); |
|
|
| |
| act(() => { |
| jest.advanceTimersByTime(100); |
| }); |
|
|
| |
| expect(mockSubmitMessage).not.toHaveBeenCalled(); |
|
|
| |
| act(() => { |
| |
| jest.advanceTimersByTime(3000); |
| }); |
|
|
| |
| expect(mockSubmitMessage).toHaveBeenCalled(); |
| }); |
|
|
| it('should mark as submitted when no submit parameter is present', () => { |
| |
| const mockSetValue = jest.fn(); |
| const mockHandleSubmit = jest.fn((callback) => () => callback({ text: 'test message' })); |
| const mockSubmitMessage = jest.fn(); |
| const mockTextAreaRef = { |
| current: { |
| focus: jest.fn(), |
| setSelectionRange: jest.fn(), |
| } as unknown as HTMLTextAreaElement, |
| }; |
|
|
| (useChatFormContext as jest.Mock).mockReturnValue({ |
| setValue: mockSetValue, |
| getValues: jest.fn().mockReturnValue(''), |
| handleSubmit: mockHandleSubmit, |
| }); |
|
|
| (useSubmitMessage as jest.Mock).mockReturnValue({ |
| submitMessage: mockSubmitMessage, |
| }); |
|
|
| |
| (useQueryClient as jest.Mock).mockReturnValue({ |
| getQueryData: jest.fn().mockReturnValue({ modelSpecs: { list: [] } }), |
| }); |
|
|
| setUrlParams({ model: 'gpt-4' }); |
|
|
| |
| renderHook(() => useQueryParams({ textAreaRef: mockTextAreaRef })); |
|
|
| |
| act(() => { |
| jest.advanceTimersByTime(100); |
| }); |
|
|
| |
| expect(mockSubmitMessage).not.toHaveBeenCalled(); |
|
|
| |
| act(() => { |
| jest.advanceTimersByTime(4000); |
| }); |
|
|
| |
| expect(mockSubmitMessage).not.toHaveBeenCalled(); |
| }); |
|
|
| it('should handle empty query parameters', () => { |
| |
| const mockSetValue = jest.fn(); |
| const mockHandleSubmit = jest.fn(); |
| const mockSubmitMessage = jest.fn(); |
|
|
| |
| window.history.replaceState = jest.fn(); |
|
|
| (useChatFormContext as jest.Mock).mockReturnValue({ |
| setValue: mockSetValue, |
| getValues: jest.fn().mockReturnValue(''), |
| handleSubmit: mockHandleSubmit, |
| }); |
|
|
| (useSubmitMessage as jest.Mock).mockReturnValue({ |
| submitMessage: mockSubmitMessage, |
| }); |
|
|
| |
| (useQueryClient as jest.Mock).mockReturnValue({ |
| getQueryData: jest.fn().mockReturnValue({ modelSpecs: { list: [] } }), |
| }); |
|
|
| setUrlParams({}); |
| const mockTextAreaRef = { |
| current: { |
| focus: jest.fn(), |
| setSelectionRange: jest.fn(), |
| } as unknown as HTMLTextAreaElement, |
| }; |
|
|
| |
| renderHook(() => useQueryParams({ textAreaRef: mockTextAreaRef })); |
|
|
| act(() => { |
| jest.advanceTimersByTime(100); |
| }); |
|
|
| |
| expect(mockSetValue).not.toHaveBeenCalled(); |
| expect(mockHandleSubmit).not.toHaveBeenCalled(); |
| expect(mockSubmitMessage).not.toHaveBeenCalled(); |
| expect(window.history.replaceState).toHaveBeenCalled(); |
| }); |
| }); |
|
|