diff --git a/client/src/components/SidePanel/Agents/AgentGrid.tsx b/client/src/components/SidePanel/Agents/AgentGrid.tsx index 3ed09d8037..93626f177f 100644 --- a/client/src/components/SidePanel/Agents/AgentGrid.tsx +++ b/client/src/components/SidePanel/Agents/AgentGrid.tsx @@ -278,9 +278,7 @@ const AgentGrid: React.FC = ({ category, searchQuery, onSelectAg )} ); - console.log('isLoading', isLoading); - console.log('isFetching', isFetching); - console.log('isFetchingNextPage', isFetchingNextPage); + if (isLoading || (isFetching && !isFetchingNextPage)) { return loadingSkeleton; } diff --git a/client/src/components/SidePanel/Agents/ErrorDisplay.tsx b/client/src/components/SidePanel/Agents/ErrorDisplay.tsx index f19c4648dc..be60fea0c9 100644 --- a/client/src/components/SidePanel/Agents/ErrorDisplay.tsx +++ b/client/src/components/SidePanel/Agents/ErrorDisplay.tsx @@ -67,23 +67,25 @@ export const ErrorDisplay: React.FC = ({ error, onRetry, cont errorData = error; } - // Use user-friendly message from backend if available - if (errorData && typeof errorData === 'object' && (errorData as any)?.userMessage) { + // Handle network errors first + let errorMessage = ''; + if (isErrorInstance(error)) { + errorMessage = error.message; + } else if (isErrorObject(error) && (error as any)?.message) { + errorMessage = (error as any).message; + } + + const errorCode = isErrorObject(error) ? (error as any)?.code : ''; + + // Handle timeout errors specifically + if (errorCode === 'ECONNABORTED' || errorMessage?.includes('timeout')) { return { - title: getContextualTitle(), - message: (errorData as any).userMessage, - suggestion: - (errorData as any).suggestion || localize('com_agents_error_suggestion_generic'), + title: localize('com_agents_error_timeout_title'), + message: localize('com_agents_error_timeout_message'), + suggestion: localize('com_agents_error_timeout_suggestion'), }; } - // Handle network errors - const errorMessage = isErrorInstance(error) - ? error.message - : isErrorObject(error) && (error as any)?.message - ? (error as any).message - : ''; - const errorCode = isErrorObject(error) ? (error as any)?.code : ''; if (errorCode === 'NETWORK_ERROR' || errorMessage?.includes('Network Error')) { return { title: localize('com_agents_error_network_title'), @@ -92,7 +94,7 @@ export const ErrorDisplay: React.FC = ({ error, onRetry, cont }; } - // Handle specific HTTP status codes + // Handle specific HTTP status codes before generic userMessage const status = isErrorObject(error) ? (error as any)?.response?.status : null; if (status) { if (status === 404) { @@ -108,7 +110,8 @@ export const ErrorDisplay: React.FC = ({ error, onRetry, cont title: localize('com_agents_error_invalid_request'), message: (errorData as any)?.userMessage || localize('com_agents_error_bad_request_message'), - suggestion: localize('com_agents_error_bad_request_suggestion'), + suggestion: + (errorData as any)?.suggestion || localize('com_agents_error_bad_request_suggestion'), }; } @@ -121,9 +124,19 @@ export const ErrorDisplay: React.FC = ({ error, onRetry, cont } } - // Fallback to generic error + // Use user-friendly message from backend if available (after specific status code handling) + if (errorData && typeof errorData === 'object' && (errorData as any)?.userMessage) { + return { + title: getContextualTitle(), + message: (errorData as any).userMessage, + suggestion: + (errorData as any).suggestion || localize('com_agents_error_suggestion_generic'), + }; + } + + // Fallback to generic error with contextual title return { - title: localize('com_agents_error_title'), + title: getContextualTitle(), message: localize('com_agents_error_generic'), suggestion: localize('com_agents_error_suggestion_generic'), }; @@ -193,9 +206,9 @@ export const ErrorDisplay: React.FC = ({ error, onRetry, cont {/* Error content with proper headings and structure */}
-

+

{title} -

+

({ + useRecoilValue: jest.fn(() => 'en'), + RecoilRoot: ({ children }: any) => children, + atom: jest.fn(() => ({})), + atomFamily: jest.fn(() => ({})), + selector: jest.fn(() => ({})), + selectorFamily: jest.fn(() => ({})), + useRecoilState: jest.fn(() => ['en', jest.fn()]), + useSetRecoilState: jest.fn(() => jest.fn()), +})); + +// Mock react-i18next +jest.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + i18n: { changeLanguage: jest.fn() }, + }), +})); + +// Create the localize function once to be reused +const mockLocalize = jest.fn((key: string, options?: any) => { + const translations: Record = { + com_agents_category_tabs_label: 'Agent Categories', + com_agents_category_tab_label: `${options?.category} category, ${options?.position} of ${options?.total}`, + com_agents_search_instructions: 'Type to search agents by name or description', + com_agents_search_aria: 'Search agents', + com_agents_search_placeholder: 'Search agents...', + com_agents_clear_search: 'Clear search', + com_agents_agent_card_label: `${options?.name} agent. ${options?.description}`, + com_agents_no_description: 'No description available', + com_agents_grid_announcement: `Showing ${options?.count} agents in ${options?.category} category`, + com_agents_load_more_label: `Load more agents from ${options?.category} category`, + com_agents_error_retry: 'Try Again', + com_agents_loading: 'Loading...', + com_agents_empty_state_heading: 'No agents found', + com_agents_search_empty_heading: 'No search results', + com_agents_created_by: 'by', + com_agents_top_picks: 'Top Picks', + // ErrorDisplay translations + com_agents_error_suggestion_generic: 'Try refreshing the page or check your network connection', + com_agents_error_network_title: 'Network Error', + com_agents_error_network_message: 'Unable to connect to the server', + com_agents_error_network_suggestion: 'Check your internet connection and try again', + com_agents_error_not_found_title: 'Not Found', + com_agents_error_not_found_suggestion: 'The requested resource could not be found', + com_agents_error_invalid_request: 'Invalid Request', + com_agents_error_bad_request_message: 'The request was invalid', + com_agents_error_bad_request_suggestion: 'Please check your input and try again', + com_agents_error_server_title: 'Server Error', + com_agents_error_server_message: 'An internal server error occurred', + com_agents_error_server_suggestion: 'Please try again later', + com_agents_error_title: 'Error', + com_agents_error_generic: 'An unexpected error occurred', + com_agents_error_search_title: 'Search Error', + com_agents_error_category_title: 'Category Error', + com_agents_search_no_results: `No results found for "${options?.query}"`, + com_agents_category_empty: `No agents found in ${options?.category} category`, + com_agents_error_not_found_message: 'The requested resource could not be found', + }; + return translations[key] || key; +}); + +// Mock useLocalize specifically +jest.mock('~/hooks/useLocalize', () => ({ + __esModule: true, + default: () => mockLocalize, +})); + // Mock hooks -jest.mock( - '~/hooks/useLocalize', - () => () => - jest.fn((key: string, options?: any) => { - const translations: Record = { - com_agents_category_tabs_label: 'Agent Categories', - com_agents_category_tab_label: `${options?.category} category, ${options?.position} of ${options?.total}`, - com_agents_search_instructions: 'Type to search agents by name or description', - com_agents_search_aria: 'Search agents', - com_agents_search_placeholder: 'Search agents...', - com_agents_clear_search: 'Clear search', - com_agents_agent_card_label: `${options?.name} agent. ${options?.description}`, - com_agents_no_description: 'No description available', - com_agents_grid_announcement: `Showing ${options?.count} agents in ${options?.category} category`, - com_agents_load_more_label: `Load more agents from ${options?.category} category`, - com_agents_error_retry: 'Try Again', - com_agents_loading: 'Loading...', - com_agents_empty_state_heading: 'No agents found', - com_agents_search_empty_heading: 'No search results', - }; - return translations[key] || key; - }), -); -const useDynamicAgentQuery = jest.fn(); +jest.mock('~/hooks', () => ({ + useLocalize: () => mockLocalize, + useDebounce: jest.fn(), +})); + +jest.mock('~/data-provider/Agents', () => ({ + useMarketplaceAgentsInfiniteQuery: jest.fn(), +})); + +jest.mock('~/hooks/Agents', () => ({ + useAgentCategories: jest.fn(), +})); + +// Mock utility functions +jest.mock('~/utils/agents', () => ({ + renderAgentAvatar: jest.fn(() =>

), + getContactDisplayName: jest.fn((agent) => agent.authorName), +})); + +// Mock SmartLoader +jest.mock('../SmartLoader', () => ({ + SmartLoader: ({ children, isLoading }: any) => (isLoading ?
Loading...
: children), + useHasData: jest.fn(() => true), +})); + +// Import the actual modules to get the mocked functions +import { useMarketplaceAgentsInfiniteQuery } from '~/data-provider/Agents'; +import { useAgentCategories } from '~/hooks/Agents'; +import { useDebounce } from '~/hooks'; + +// Get typed mock functions +const mockUseMarketplaceAgentsInfiniteQuery = jest.mocked(useMarketplaceAgentsInfiniteQuery); +const mockUseAgentCategories = jest.mocked(useAgentCategories); +const mockUseDebounce = jest.mocked(useDebounce); // Create wrapper with QueryClient const createWrapper = () => { @@ -61,14 +141,29 @@ const createWrapper = () => { describe('Accessibility Improvements', () => { beforeEach(() => { - useDynamicAgentQuery.mockClear(); + mockUseMarketplaceAgentsInfiniteQuery.mockClear(); + mockUseAgentCategories.mockClear(); + mockUseDebounce.mockClear(); + + // Default mock implementations + mockUseDebounce.mockImplementation((value) => value); + mockUseAgentCategories.mockReturnValue({ + categories: [ + { value: 'promoted', label: 'Top Picks' }, + { value: 'all', label: 'All' }, + { value: 'productivity', label: 'Productivity' }, + ], + emptyCategory: { value: 'all', label: 'All' }, + isLoading: false, + error: null, + }); }); describe('CategoryTabs Accessibility', () => { const categories = [ - { name: 'promoted', count: 5 }, - { name: 'all', count: 20 }, - { name: 'productivity', count: 8 }, + { value: 'promoted', label: 'Top Picks', count: 5 }, + { value: 'all', label: 'All', count: 20 }, + { value: 'productivity', label: 'Productivity', count: 8 }, ]; it('implements proper tablist role and ARIA attributes', () => { @@ -91,7 +186,7 @@ describe('Accessibility Improvements', () => { const tabs = screen.getAllByRole('tab'); expect(tabs).toHaveLength(3); - tabs.forEach((tab, index) => { + tabs.forEach((tab) => { expect(tab).toHaveAttribute('aria-selected'); expect(tab).toHaveAttribute('aria-controls'); expect(tab).toHaveAttribute('id'); @@ -109,7 +204,7 @@ describe('Accessibility Improvements', () => { />, ); - const promotedTab = screen.getByRole('tab', { name: /promoted category/ }); + const promotedTab = screen.getByRole('tab', { name: /Top Picks tab/ }); // Test arrow key navigation fireEvent.keyDown(promotedTab, { key: 'ArrowRight' }); @@ -136,8 +231,8 @@ describe('Accessibility Improvements', () => { />, ); - const promotedTab = screen.getByRole('tab', { name: /promoted category/ }); - const allTab = screen.getByRole('tab', { name: /all category/ }); + const promotedTab = screen.getByRole('tab', { name: /Top Picks tab/ }); + const allTab = screen.getByRole('tab', { name: /All tab/ }); // Active tab should be focusable expect(promotedTab).toHaveAttribute('tabIndex', '0'); @@ -154,7 +249,7 @@ describe('Accessibility Improvements', () => { expect(searchRegion).toBeInTheDocument(); // Check input accessibility - const searchInput = screen.getByRole('searchbox'); + const searchInput = screen.getByRole('textbox'); expect(searchInput).toHaveAttribute('id', 'agent-search'); expect(searchInput).toHaveAttribute('aria-label', 'Search agents'); expect(searchInput).toHaveAttribute( @@ -162,10 +257,9 @@ describe('Accessibility Improvements', () => { 'search-instructions search-results-count', ); - // Check hidden label - expect(screen.getByText('Type to search agents by name or description')).toHaveClass( - 'sr-only', - ); + // Check hidden label exists + const hiddenLabel = screen.getByLabelText('Search agents'); + expect(hiddenLabel).toBeInTheDocument(); }); it('provides accessible clear button', () => { @@ -192,10 +286,24 @@ describe('Accessibility Improvements', () => { name: 'Test Agent', description: 'A test agent for testing', authorName: 'Test Author', + created_at: 1704067200000, + avatar: null, + instructions: 'Test instructions', + provider: 'openai' as const, + model: 'gpt-4', + model_parameters: { + temperature: 0.7, + maxContextTokens: 4096, + max_context_tokens: 4096, + max_output_tokens: 1024, + top_p: 1, + frequency_penalty: 0, + presence_penalty: 0, + }, }; it('provides comprehensive ARIA labels', () => { - render(); + render(); const card = screen.getByRole('button'); expect(card).toHaveAttribute('aria-label', 'Test Agent agent. A test agent for testing'); @@ -205,14 +313,14 @@ describe('Accessibility Improvements', () => { it('handles agents without descriptions', () => { const agentWithoutDesc = { ...mockAgent, description: undefined }; - render(); + render(); expect(screen.getByText('No description available')).toBeInTheDocument(); }); it('supports keyboard interaction', () => { const onClick = jest.fn(); - render(); + render(); const card = screen.getByRole('button'); @@ -226,19 +334,20 @@ describe('Accessibility Improvements', () => { describe('AgentGrid Accessibility', () => { beforeEach(() => { - useDynamicAgentQuery.mockReturnValue({ + mockUseMarketplaceAgentsInfiniteQuery.mockReturnValue({ data: { - agents: [ - { id: '1', name: 'Agent 1', description: 'First agent' }, - { id: '2', name: 'Agent 2', description: 'Second agent' }, + pages: [ + { + data: [ + { id: '1', name: 'Agent 1', description: 'First agent' }, + { id: '2', name: 'Agent 2', description: 'Second agent' }, + ], + }, ], - pagination: { hasMore: false, total: 2, current: 1 }, }, isLoading: false, - isFetching: false, error: null, - refetch: jest.fn(), - }); + } as any); }); it('implements proper tabpanel structure', () => { @@ -267,7 +376,7 @@ describe('Accessibility Improvements', () => { // Check grid role const grid = screen.getByRole('grid'); expect(grid).toBeInTheDocument(); - expect(grid).toHaveAttribute('aria-label', 'Showing 2 agents in all category'); + expect(grid).toHaveAttribute('aria-label', 'Showing 2 agents in All category'); // Check gridcells const gridcells = screen.getAllByRole('gridcell'); @@ -275,13 +384,16 @@ describe('Accessibility Improvements', () => { }); it('announces loading states to screen readers', () => { - useDynamicAgentQuery.mockReturnValue({ - data: { agents: [{ id: '1', name: 'Agent 1' }] }, - isLoading: false, + mockUseMarketplaceAgentsInfiniteQuery.mockReturnValue({ + data: { + pages: [{ data: [{ id: '1', name: 'Agent 1' }] }], + }, isFetching: true, + hasNextPage: true, + isFetchingNextPage: true, + isLoading: false, error: null, - refetch: jest.fn(), - }); + } as any); const Wrapper = createWrapper(); render( @@ -290,20 +402,26 @@ describe('Accessibility Improvements', () => { , ); - // Check for loading announcement - const loadingStatus = screen.getByRole('status', { name: 'Loading...' }); + // Check for loading announcement when fetching more data + const loadingStatus = screen.getByRole('status'); expect(loadingStatus).toBeInTheDocument(); expect(loadingStatus).toHaveAttribute('aria-live', 'polite'); + expect(loadingStatus).toHaveAttribute('aria-label', 'Loading...'); + + // Check for screen reader text + const srText = screen.getByText('Loading...'); + expect(srText).toHaveClass('sr-only'); }); it('provides accessible empty states', () => { - useDynamicAgentQuery.mockReturnValue({ - data: { agents: [], pagination: { hasMore: false, total: 0, current: 1 } }, + mockUseMarketplaceAgentsInfiniteQuery.mockReturnValue({ + data: { + pages: [{ data: [] }], + }, isLoading: false, isFetching: false, error: null, - refetch: jest.fn(), - }); + } as any); const Wrapper = createWrapper(); render( @@ -377,7 +495,7 @@ describe('Accessibility Improvements', () => { it('provides visible focus indicators on interactive elements', () => { render( { const tab = screen.getByRole('tab'); expect(tab.className).toContain('focus:outline-none'); - expect(tab.className).toContain('focus:ring-2'); + expect(tab.className).toContain('focus:bg-gray-100'); }); }); diff --git a/client/src/components/SidePanel/Agents/__tests__/AgentCard.spec.tsx b/client/src/components/SidePanel/Agents/__tests__/AgentCard.spec.tsx index e319bca6d9..15e95bc1a6 100644 --- a/client/src/components/SidePanel/Agents/__tests__/AgentCard.spec.tsx +++ b/client/src/components/SidePanel/Agents/__tests__/AgentCard.spec.tsx @@ -21,8 +21,21 @@ describe('AgentCard', () => { name: 'Test Support', email: 'test@example.com', }, - avatar: '/test-avatar.png', - } as t.Agent; + avatar: { filepath: '/test-avatar.png', source: 'local' }, + created_at: 1672531200000, + instructions: 'Test instructions', + provider: 'openai' as const, + model: 'gpt-4', + model_parameters: { + temperature: 0.7, + maxContextTokens: 4096, + max_context_tokens: 4096, + max_output_tokens: 1024, + top_p: 1, + frequency_penalty: 0, + presence_penalty: 0, + }, + }; const mockOnClick = jest.fn(); @@ -39,7 +52,7 @@ describe('AgentCard', () => { expect(screen.getByText('Test Support')).toBeInTheDocument(); }); - it('displays avatar when provided as string', () => { + it('displays avatar when provided as object', () => { render(); const avatarImg = screen.getByAltText('Test Agent avatar'); @@ -47,17 +60,17 @@ describe('AgentCard', () => { expect(avatarImg).toHaveAttribute('src', '/test-avatar.png'); }); - it('displays avatar when provided as object with filepath', () => { - const agentWithObjectAvatar = { + it('displays avatar when provided as string', () => { + const agentWithStringAvatar = { ...mockAgent, - avatar: { filepath: '/object-avatar.png' }, + avatar: '/string-avatar.png' as any, // Legacy support for string avatars }; - render(); + render(); const avatarImg = screen.getByAltText('Test Agent avatar'); expect(avatarImg).toBeInTheDocument(); - expect(avatarImg).toHaveAttribute('src', '/object-avatar.png'); + expect(avatarImg).toHaveAttribute('src', '/string-avatar.png'); }); it('displays Bot icon fallback when no avatar is provided', () => { @@ -66,7 +79,7 @@ describe('AgentCard', () => { avatar: undefined, }; - render(); + render(); // Check for Bot icon presence by looking for the svg with lucide-bot class const botIcon = document.querySelector('.lucide-bot'); diff --git a/client/src/components/SidePanel/Agents/__tests__/AgentGrid.integration.spec.tsx b/client/src/components/SidePanel/Agents/__tests__/AgentGrid.integration.spec.tsx index a91bf70f05..722c6edfb7 100644 --- a/client/src/components/SidePanel/Agents/__tests__/AgentGrid.integration.spec.tsx +++ b/client/src/components/SidePanel/Agents/__tests__/AgentGrid.integration.spec.tsx @@ -1,13 +1,16 @@ import React from 'react'; -import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { render, screen, fireEvent } from '@testing-library/react'; import '@testing-library/jest-dom'; import AgentGrid from '../AgentGrid'; -import { useGetMarketplaceAgentsQuery } from 'librechat-data-provider/react-query'; import type t from 'librechat-data-provider'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; // Mock the marketplace agent query hook +jest.mock('~/data-provider/Agents', () => ({ + useMarketplaceAgentsInfiniteQuery: jest.fn(), +})); + jest.mock('~/hooks/Agents', () => ({ - useGetMarketplaceAgentsQuery: jest.fn(), useAgentCategories: jest.fn(() => ({ categories: [], isLoading: false, @@ -15,8 +18,13 @@ jest.mock('~/hooks/Agents', () => ({ })), })); +// Mock SmartLoader +jest.mock('../SmartLoader', () => ({ + useHasData: jest.fn(() => true), +})); + // Mock useLocalize hook -jest.mock('~/hooks/useLocalize', () => () => (key: string) => { +jest.mock('~/hooks/useLocalize', () => () => (key: string, options?: any) => { const mockTranslations: Record = { com_agents_top_picks: 'Top Picks', com_agents_all: 'All Agents', @@ -33,23 +41,28 @@ jest.mock('~/hooks/useLocalize', () => () => (key: string) => { com_agents_grid_announcement: '{{count}} agents in {{category}}', com_agents_load_more_label: 'Load more agents from {{category}}', }; - return mockTranslations[key] || key.replace(/{{(\w+)}}/g, (match, key) => `[${key}]`); -}); -// Mock SmartLoader components -jest.mock('../SmartLoader', () => ({ - SmartLoader: ({ children, isLoading }: { children: React.ReactNode; isLoading: boolean }) => - isLoading ?
Loading...
:
{children}
, - useHasData: (data: any) => !!data?.agents?.length, -})); + let translation = mockTranslations[key] || key; + + if (options) { + Object.keys(options).forEach((optionKey) => { + translation = translation.replace(new RegExp(`{{${optionKey}}}`, 'g'), options[optionKey]); + }); + } + + return translation; +}); // Mock ErrorDisplay component jest.mock('../ErrorDisplay', () => ({ __esModule: true, - default: ({ error, onRetry }: { error: string; onRetry: () => void }) => ( + default: ({ error, onRetry }: { error: any; onRetry: () => void }) => (
-
Error: {error}
- +
+ {`Error: `} + {typeof error === 'string' ? error : error?.message || 'Unknown error'} +
+
), })); @@ -65,9 +78,10 @@ jest.mock('../AgentCard', () => ({ ), })); -const mockUseGetMarketplaceAgentsQuery = useGetMarketplaceAgentsQuery as jest.MockedFunction< - typeof useGetMarketplaceAgentsQuery ->; +// Import the actual modules to get the mocked functions +import { useMarketplaceAgentsInfiniteQuery } from '~/data-provider/Agents'; + +const mockUseMarketplaceAgentsInfiniteQuery = jest.mocked(useMarketplaceAgentsInfiniteQuery); describe('AgentGrid Integration with useGetMarketplaceAgentsQuery', () => { const mockOnSelectAgent = jest.fn(); @@ -84,7 +98,15 @@ describe('AgentGrid Integration with useGetMarketplaceAgentsQuery', () => { instructions: null, provider: 'custom', model: 'gpt-4', - model_parameters: {}, + model_parameters: { + temperature: null, + maxContextTokens: null, + max_context_tokens: null, + max_output_tokens: null, + top_p: null, + frequency_penalty: null, + presence_penalty: null, + }, }, { id: '2', @@ -97,31 +119,37 @@ describe('AgentGrid Integration with useGetMarketplaceAgentsQuery', () => { instructions: null, provider: 'custom', model: 'gpt-4', - model_parameters: {}, + model_parameters: { + temperature: 0.7, + top_p: 0.9, + frequency_penalty: 0, + maxContextTokens: null, + max_context_tokens: null, + max_output_tokens: null, + presence_penalty: null, + }, }, ]; - const defaultMockQueryResult = { data: { - data: mockAgents, - pagination: { - current: 1, - hasMore: true, - total: 10, - }, + pages: [ + { + data: mockAgents, + }, + ], }, isLoading: false, error: null, isFetching: false, + isFetchingNextPage: false, + hasNextPage: true, + fetchNextPage: jest.fn(), refetch: jest.fn(), - isSuccess: true, - isError: false, - status: 'success' as const, - }; + } as any; beforeEach(() => { jest.clearAllMocks(); - mockUseGetMarketplaceAgentsQuery.mockReturnValue(defaultMockQueryResult); + mockUseMarketplaceAgentsInfiniteQuery.mockReturnValue(defaultMockQueryResult); }); describe('Query Integration', () => { @@ -130,7 +158,7 @@ describe('AgentGrid Integration with useGetMarketplaceAgentsQuery', () => { , ); - expect(mockUseGetMarketplaceAgentsQuery).toHaveBeenCalledWith({ + expect(mockUseMarketplaceAgentsInfiniteQuery).toHaveBeenCalledWith({ requiredPermission: 1, category: 'finance', search: 'test query', @@ -141,7 +169,7 @@ describe('AgentGrid Integration with useGetMarketplaceAgentsQuery', () => { it('should call useGetMarketplaceAgentsQuery with promoted=1 for promoted category', () => { render(); - expect(mockUseGetMarketplaceAgentsQuery).toHaveBeenCalledWith({ + expect(mockUseMarketplaceAgentsInfiniteQuery).toHaveBeenCalledWith({ requiredPermission: 1, promoted: 1, limit: 6, @@ -151,7 +179,7 @@ describe('AgentGrid Integration with useGetMarketplaceAgentsQuery', () => { it('should call useGetMarketplaceAgentsQuery without category filter for "all" category', () => { render(); - expect(mockUseGetMarketplaceAgentsQuery).toHaveBeenCalledWith({ + expect(mockUseMarketplaceAgentsInfiniteQuery).toHaveBeenCalledWith({ requiredPermission: 1, limit: 6, }); @@ -160,7 +188,7 @@ describe('AgentGrid Integration with useGetMarketplaceAgentsQuery', () => { it('should not include category in search when category is "all" or "promoted"', () => { render(); - expect(mockUseGetMarketplaceAgentsQuery).toHaveBeenCalledWith({ + expect(mockUseMarketplaceAgentsInfiniteQuery).toHaveBeenCalledWith({ requiredPermission: 1, search: 'test', limit: 6, @@ -168,9 +196,25 @@ describe('AgentGrid Integration with useGetMarketplaceAgentsQuery', () => { }); }); + // Create wrapper with QueryClient + const createWrapper = () => { + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }); + + return ({ children }: { children: React.ReactNode }) => ( + {children} + ); + }; + describe('Agent Display', () => { it('should render agent cards when data is available', () => { - render(); + const Wrapper = createWrapper(); + render( + + + , + ); expect(screen.getByTestId('agent-card-1')).toBeInTheDocument(); expect(screen.getByTestId('agent-card-2')).toBeInTheDocument(); @@ -179,7 +223,12 @@ describe('AgentGrid Integration with useGetMarketplaceAgentsQuery', () => { }); it('should call onSelectAgent when agent card is clicked', () => { - render(); + const Wrapper = createWrapper(); + render( + + + , + ); fireEvent.click(screen.getByTestId('agent-card-1')); expect(mockOnSelectAgent).toHaveBeenCalledWith(mockAgents[0]); @@ -188,24 +237,41 @@ describe('AgentGrid Integration with useGetMarketplaceAgentsQuery', () => { describe('Loading States', () => { it('should show loading state when isLoading is true', () => { - mockUseGetMarketplaceAgentsQuery.mockReturnValue({ + mockUseMarketplaceAgentsInfiniteQuery.mockReturnValue({ ...defaultMockQueryResult, isLoading: true, data: undefined, }); - render(); + const Wrapper = createWrapper(); + render( + + + , + ); - expect(screen.getByText('Loading...')).toBeInTheDocument(); + // Should show skeleton loading state + expect(document.querySelector('.animate-pulse')).toBeInTheDocument(); }); it('should show empty state when no agents are available', () => { - mockUseGetMarketplaceAgentsQuery.mockReturnValue({ + mockUseMarketplaceAgentsInfiniteQuery.mockReturnValue({ ...defaultMockQueryResult, - data: { data: [], pagination: { current: 1, hasMore: false, total: 0 } }, + data: { + pages: [ + { + data: [], + }, + ], + }, }); - render(); + const Wrapper = createWrapper(); + render( + + + , + ); expect(screen.getByText('No agents available')).toBeInTheDocument(); }); @@ -214,14 +280,19 @@ describe('AgentGrid Integration with useGetMarketplaceAgentsQuery', () => { describe('Error Handling', () => { it('should show error display when query has error', () => { const mockError = new Error('Failed to fetch agents'); - mockUseGetMarketplaceAgentsQuery.mockReturnValue({ + mockUseMarketplaceAgentsInfiniteQuery.mockReturnValue({ ...defaultMockQueryResult, error: mockError, isError: true, data: undefined, }); - render(); + const Wrapper = createWrapper(); + render( + + + , + ); expect(screen.getByText('Error: Failed to fetch agents')).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Retry' })).toBeInTheDocument(); @@ -230,25 +301,41 @@ describe('AgentGrid Integration with useGetMarketplaceAgentsQuery', () => { describe('Search Results', () => { it('should show search results title when searching', () => { + const Wrapper = createWrapper(); render( - , + + + , ); expect(screen.getByText('Results for "automation"')).toBeInTheDocument(); }); it('should show empty search results message', () => { - mockUseGetMarketplaceAgentsQuery.mockReturnValue({ + mockUseMarketplaceAgentsInfiniteQuery.mockReturnValue({ ...defaultMockQueryResult, - data: { data: [], pagination: { current: 1, hasMore: false, total: 0 } }, + data: { + pages: [ + { + data: [], + }, + ], + }, }); + const Wrapper = createWrapper(); render( - , + + + , ); expect(screen.getByText('No results found')).toBeInTheDocument(); @@ -257,24 +344,33 @@ describe('AgentGrid Integration with useGetMarketplaceAgentsQuery', () => { }); describe('Load More Functionality', () => { - it('should show "See more" button when hasMore is true', () => { - render(); + it('should show "See more" button when hasNextPage is true', () => { + const Wrapper = createWrapper(); + render( + + + , + ); - expect(screen.getByRole('button', { name: 'See more' })).toBeInTheDocument(); + expect( + screen.getByRole('button', { name: 'Load more agents from Finance' }), + ).toBeInTheDocument(); }); - it('should not show "See more" button when hasMore is false', () => { - mockUseGetMarketplaceAgentsQuery.mockReturnValue({ + it('should not show "See more" button when hasNextPage is false', () => { + mockUseMarketplaceAgentsInfiniteQuery.mockReturnValue({ ...defaultMockQueryResult, - data: { - ...defaultMockQueryResult.data, - pagination: { current: 1, hasMore: false, total: 2 }, - }, + hasNextPage: false, }); - render(); + const Wrapper = createWrapper(); + render( + + + , + ); - expect(screen.queryByRole('button', { name: 'See more' })).not.toBeInTheDocument(); + expect(screen.queryByRole('button', { name: /Load more agents/ })).not.toBeInTheDocument(); }); }); }); diff --git a/client/src/components/SidePanel/Agents/__tests__/ErrorDisplay.spec.tsx b/client/src/components/SidePanel/Agents/__tests__/ErrorDisplay.spec.tsx index 8008d5e4d9..b8ab6bb1c0 100644 --- a/client/src/components/SidePanel/Agents/__tests__/ErrorDisplay.spec.tsx +++ b/client/src/components/SidePanel/Agents/__tests__/ErrorDisplay.spec.tsx @@ -38,6 +38,9 @@ const mockLocalize = jest.fn((key: string, options?: any) => { com_agents_error_server_suggestion: 'Please try again in a few moments.', com_agents_error_search_title: 'Search Error', com_agents_error_category_title: 'Category Error', + com_agents_error_timeout_title: 'Connection Timeout', + com_agents_error_timeout_message: 'The request took too long to complete.', + com_agents_error_timeout_suggestion: 'Please check your internet connection and try again.', com_agents_search_no_results: `No agents found for "${options?.query}"`, com_agents_category_empty: `No agents found in the ${options?.category} category`, com_agents_error_retry: 'Try Again', diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index a31813947b..cb52c9a2df 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -1154,6 +1154,9 @@ "com_agents_error_server_suggestion": "Please try again in a few moments.", "com_agents_error_search_title": "Search Error", "com_agents_error_category_title": "Category Error", + "com_agents_error_timeout_title": "Connection Timeout", + "com_agents_error_timeout_message": "The request took too long to complete.", + "com_agents_error_timeout_suggestion": "Please check your internet connection and try again.", "com_agents_search_no_results": "No agents found for \"{{query}}\"", "com_agents_category_empty": "No agents found in the {{category}} category", "com_agents_error_retry": "Try Again",