Fix Unit tests WIP

This commit is contained in:
Atef Bellaaj 2025-06-24 13:53:36 +02:00 committed by Danny Avila
parent 37c423eb00
commit 33c4ef03c3
No known key found for this signature in database
GPG key ID: BF31EEB2C5CA0956
7 changed files with 402 additions and 158 deletions

View file

@ -278,9 +278,7 @@ const AgentGrid: React.FC<AgentGridProps> = ({ category, searchQuery, onSelectAg
)} )}
</div> </div>
); );
console.log('isLoading', isLoading);
console.log('isFetching', isFetching);
console.log('isFetchingNextPage', isFetchingNextPage);
if (isLoading || (isFetching && !isFetchingNextPage)) { if (isLoading || (isFetching && !isFetchingNextPage)) {
return loadingSkeleton; return loadingSkeleton;
} }

View file

@ -67,23 +67,25 @@ export const ErrorDisplay: React.FC<ErrorDisplayProps> = ({ error, onRetry, cont
errorData = error; errorData = error;
} }
// Use user-friendly message from backend if available // Handle network errors first
if (errorData && typeof errorData === 'object' && (errorData as any)?.userMessage) { 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 { return {
title: getContextualTitle(), title: localize('com_agents_error_timeout_title'),
message: (errorData as any).userMessage, message: localize('com_agents_error_timeout_message'),
suggestion: suggestion: localize('com_agents_error_timeout_suggestion'),
(errorData as any).suggestion || localize('com_agents_error_suggestion_generic'),
}; };
} }
// 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')) { if (errorCode === 'NETWORK_ERROR' || errorMessage?.includes('Network Error')) {
return { return {
title: localize('com_agents_error_network_title'), title: localize('com_agents_error_network_title'),
@ -92,7 +94,7 @@ export const ErrorDisplay: React.FC<ErrorDisplayProps> = ({ 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; const status = isErrorObject(error) ? (error as any)?.response?.status : null;
if (status) { if (status) {
if (status === 404) { if (status === 404) {
@ -108,7 +110,8 @@ export const ErrorDisplay: React.FC<ErrorDisplayProps> = ({ error, onRetry, cont
title: localize('com_agents_error_invalid_request'), title: localize('com_agents_error_invalid_request'),
message: message:
(errorData as any)?.userMessage || localize('com_agents_error_bad_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<ErrorDisplayProps> = ({ 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 { return {
title: localize('com_agents_error_title'), title: getContextualTitle(),
message: localize('com_agents_error_generic'), message: localize('com_agents_error_generic'),
suggestion: localize('com_agents_error_suggestion_generic'), suggestion: localize('com_agents_error_suggestion_generic'),
}; };
@ -193,9 +206,9 @@ export const ErrorDisplay: React.FC<ErrorDisplayProps> = ({ error, onRetry, cont
{/* Error content with proper headings and structure */} {/* Error content with proper headings and structure */}
<div className="space-y-3"> <div className="space-y-3">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white" id="error-title"> <h3 className="text-lg font-semibold text-gray-900 dark:text-white" id="error-title">
{title} {title}
</h2> </h3>
<p <p
className="text-gray-600 dark:text-gray-400" className="text-gray-600 dark:text-gray-400"
id="error-message" id="error-message"

View file

@ -6,6 +6,7 @@ import AgentGrid from '../AgentGrid';
import AgentCard from '../AgentCard'; import AgentCard from '../AgentCard';
import SearchBar from '../SearchBar'; import SearchBar from '../SearchBar';
import ErrorDisplay from '../ErrorDisplay'; import ErrorDisplay from '../ErrorDisplay';
import * as t from 'librechat-data-provider';
// Mock matchMedia // Mock matchMedia
Object.defineProperty(window, 'matchMedia', { Object.defineProperty(window, 'matchMedia', {
@ -22,31 +23,110 @@ Object.defineProperty(window, 'matchMedia', {
})), })),
}); });
// Mock Recoil
jest.mock('recoil', () => ({
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<string, string> = {
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 // Mock hooks
jest.mock( jest.mock('~/hooks', () => ({
'~/hooks/useLocalize', useLocalize: () => mockLocalize,
() => () => useDebounce: jest.fn(),
jest.fn((key: string, options?: any) => { }));
const translations: Record<string, string> = {
com_agents_category_tabs_label: 'Agent Categories', jest.mock('~/data-provider/Agents', () => ({
com_agents_category_tab_label: `${options?.category} category, ${options?.position} of ${options?.total}`, useMarketplaceAgentsInfiniteQuery: jest.fn(),
com_agents_search_instructions: 'Type to search agents by name or description', }));
com_agents_search_aria: 'Search agents',
com_agents_search_placeholder: 'Search agents...', jest.mock('~/hooks/Agents', () => ({
com_agents_clear_search: 'Clear search', useAgentCategories: jest.fn(),
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`, // Mock utility functions
com_agents_load_more_label: `Load more agents from ${options?.category} category`, jest.mock('~/utils/agents', () => ({
com_agents_error_retry: 'Try Again', renderAgentAvatar: jest.fn(() => <div data-testid="agent-avatar" />),
com_agents_loading: 'Loading...', getContactDisplayName: jest.fn((agent) => agent.authorName),
com_agents_empty_state_heading: 'No agents found', }));
com_agents_search_empty_heading: 'No search results',
}; // Mock SmartLoader
return translations[key] || key; jest.mock('../SmartLoader', () => ({
}), SmartLoader: ({ children, isLoading }: any) => (isLoading ? <div>Loading...</div> : children),
); useHasData: jest.fn(() => true),
const useDynamicAgentQuery = jest.fn(); }));
// 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 // Create wrapper with QueryClient
const createWrapper = () => { const createWrapper = () => {
@ -61,14 +141,29 @@ const createWrapper = () => {
describe('Accessibility Improvements', () => { describe('Accessibility Improvements', () => {
beforeEach(() => { 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', () => { describe('CategoryTabs Accessibility', () => {
const categories = [ const categories = [
{ name: 'promoted', count: 5 }, { value: 'promoted', label: 'Top Picks', count: 5 },
{ name: 'all', count: 20 }, { value: 'all', label: 'All', count: 20 },
{ name: 'productivity', count: 8 }, { value: 'productivity', label: 'Productivity', count: 8 },
]; ];
it('implements proper tablist role and ARIA attributes', () => { it('implements proper tablist role and ARIA attributes', () => {
@ -91,7 +186,7 @@ describe('Accessibility Improvements', () => {
const tabs = screen.getAllByRole('tab'); const tabs = screen.getAllByRole('tab');
expect(tabs).toHaveLength(3); expect(tabs).toHaveLength(3);
tabs.forEach((tab, index) => { tabs.forEach((tab) => {
expect(tab).toHaveAttribute('aria-selected'); expect(tab).toHaveAttribute('aria-selected');
expect(tab).toHaveAttribute('aria-controls'); expect(tab).toHaveAttribute('aria-controls');
expect(tab).toHaveAttribute('id'); 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 // Test arrow key navigation
fireEvent.keyDown(promotedTab, { key: 'ArrowRight' }); fireEvent.keyDown(promotedTab, { key: 'ArrowRight' });
@ -136,8 +231,8 @@ describe('Accessibility Improvements', () => {
/>, />,
); );
const promotedTab = screen.getByRole('tab', { name: /promoted category/ }); const promotedTab = screen.getByRole('tab', { name: /Top Picks tab/ });
const allTab = screen.getByRole('tab', { name: /all category/ }); const allTab = screen.getByRole('tab', { name: /All tab/ });
// Active tab should be focusable // Active tab should be focusable
expect(promotedTab).toHaveAttribute('tabIndex', '0'); expect(promotedTab).toHaveAttribute('tabIndex', '0');
@ -154,7 +249,7 @@ describe('Accessibility Improvements', () => {
expect(searchRegion).toBeInTheDocument(); expect(searchRegion).toBeInTheDocument();
// Check input accessibility // Check input accessibility
const searchInput = screen.getByRole('searchbox'); const searchInput = screen.getByRole('textbox');
expect(searchInput).toHaveAttribute('id', 'agent-search'); expect(searchInput).toHaveAttribute('id', 'agent-search');
expect(searchInput).toHaveAttribute('aria-label', 'Search agents'); expect(searchInput).toHaveAttribute('aria-label', 'Search agents');
expect(searchInput).toHaveAttribute( expect(searchInput).toHaveAttribute(
@ -162,10 +257,9 @@ describe('Accessibility Improvements', () => {
'search-instructions search-results-count', 'search-instructions search-results-count',
); );
// Check hidden label // Check hidden label exists
expect(screen.getByText('Type to search agents by name or description')).toHaveClass( const hiddenLabel = screen.getByLabelText('Search agents');
'sr-only', expect(hiddenLabel).toBeInTheDocument();
);
}); });
it('provides accessible clear button', () => { it('provides accessible clear button', () => {
@ -192,10 +286,24 @@ describe('Accessibility Improvements', () => {
name: 'Test Agent', name: 'Test Agent',
description: 'A test agent for testing', description: 'A test agent for testing',
authorName: 'Test Author', 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', () => { it('provides comprehensive ARIA labels', () => {
render(<AgentCard agent={mockAgent} onClick={jest.fn()} />); render(<AgentCard agent={mockAgent as t.Agent} onClick={jest.fn()} />);
const card = screen.getByRole('button'); const card = screen.getByRole('button');
expect(card).toHaveAttribute('aria-label', 'Test Agent agent. A test agent for testing'); 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', () => { it('handles agents without descriptions', () => {
const agentWithoutDesc = { ...mockAgent, description: undefined }; const agentWithoutDesc = { ...mockAgent, description: undefined };
render(<AgentCard agent={agentWithoutDesc} onClick={jest.fn()} />); render(<AgentCard agent={agentWithoutDesc as any as t.Agent} onClick={jest.fn()} />);
expect(screen.getByText('No description available')).toBeInTheDocument(); expect(screen.getByText('No description available')).toBeInTheDocument();
}); });
it('supports keyboard interaction', () => { it('supports keyboard interaction', () => {
const onClick = jest.fn(); const onClick = jest.fn();
render(<AgentCard agent={mockAgent} onClick={onClick} />); render(<AgentCard agent={mockAgent as t.Agent} onClick={onClick} />);
const card = screen.getByRole('button'); const card = screen.getByRole('button');
@ -226,19 +334,20 @@ describe('Accessibility Improvements', () => {
describe('AgentGrid Accessibility', () => { describe('AgentGrid Accessibility', () => {
beforeEach(() => { beforeEach(() => {
useDynamicAgentQuery.mockReturnValue({ mockUseMarketplaceAgentsInfiniteQuery.mockReturnValue({
data: { data: {
agents: [ pages: [
{ id: '1', name: 'Agent 1', description: 'First agent' }, {
{ id: '2', name: 'Agent 2', description: 'Second agent' }, 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, isLoading: false,
isFetching: false,
error: null, error: null,
refetch: jest.fn(), } as any);
});
}); });
it('implements proper tabpanel structure', () => { it('implements proper tabpanel structure', () => {
@ -267,7 +376,7 @@ describe('Accessibility Improvements', () => {
// Check grid role // Check grid role
const grid = screen.getByRole('grid'); const grid = screen.getByRole('grid');
expect(grid).toBeInTheDocument(); 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 // Check gridcells
const gridcells = screen.getAllByRole('gridcell'); const gridcells = screen.getAllByRole('gridcell');
@ -275,13 +384,16 @@ describe('Accessibility Improvements', () => {
}); });
it('announces loading states to screen readers', () => { it('announces loading states to screen readers', () => {
useDynamicAgentQuery.mockReturnValue({ mockUseMarketplaceAgentsInfiniteQuery.mockReturnValue({
data: { agents: [{ id: '1', name: 'Agent 1' }] }, data: {
isLoading: false, pages: [{ data: [{ id: '1', name: 'Agent 1' }] }],
},
isFetching: true, isFetching: true,
hasNextPage: true,
isFetchingNextPage: true,
isLoading: false,
error: null, error: null,
refetch: jest.fn(), } as any);
});
const Wrapper = createWrapper(); const Wrapper = createWrapper();
render( render(
@ -290,20 +402,26 @@ describe('Accessibility Improvements', () => {
</Wrapper>, </Wrapper>,
); );
// Check for loading announcement // Check for loading announcement when fetching more data
const loadingStatus = screen.getByRole('status', { name: 'Loading...' }); const loadingStatus = screen.getByRole('status');
expect(loadingStatus).toBeInTheDocument(); expect(loadingStatus).toBeInTheDocument();
expect(loadingStatus).toHaveAttribute('aria-live', 'polite'); 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', () => { it('provides accessible empty states', () => {
useDynamicAgentQuery.mockReturnValue({ mockUseMarketplaceAgentsInfiniteQuery.mockReturnValue({
data: { agents: [], pagination: { hasMore: false, total: 0, current: 1 } }, data: {
pages: [{ data: [] }],
},
isLoading: false, isLoading: false,
isFetching: false, isFetching: false,
error: null, error: null,
refetch: jest.fn(), } as any);
});
const Wrapper = createWrapper(); const Wrapper = createWrapper();
render( render(
@ -377,7 +495,7 @@ describe('Accessibility Improvements', () => {
it('provides visible focus indicators on interactive elements', () => { it('provides visible focus indicators on interactive elements', () => {
render( render(
<CategoryTabs <CategoryTabs
categories={[{ name: 'test', count: 1 }]} categories={[{ value: 'test', label: 'Test', count: 1 }]}
activeTab="test" activeTab="test"
isLoading={false} isLoading={false}
onChange={jest.fn()} onChange={jest.fn()}
@ -386,7 +504,7 @@ describe('Accessibility Improvements', () => {
const tab = screen.getByRole('tab'); const tab = screen.getByRole('tab');
expect(tab.className).toContain('focus:outline-none'); expect(tab.className).toContain('focus:outline-none');
expect(tab.className).toContain('focus:ring-2'); expect(tab.className).toContain('focus:bg-gray-100');
}); });
}); });

View file

@ -21,8 +21,21 @@ describe('AgentCard', () => {
name: 'Test Support', name: 'Test Support',
email: 'test@example.com', email: 'test@example.com',
}, },
avatar: '/test-avatar.png', avatar: { filepath: '/test-avatar.png', source: 'local' },
} as t.Agent; 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(); const mockOnClick = jest.fn();
@ -39,7 +52,7 @@ describe('AgentCard', () => {
expect(screen.getByText('Test Support')).toBeInTheDocument(); expect(screen.getByText('Test Support')).toBeInTheDocument();
}); });
it('displays avatar when provided as string', () => { it('displays avatar when provided as object', () => {
render(<AgentCard agent={mockAgent} onClick={mockOnClick} />); render(<AgentCard agent={mockAgent} onClick={mockOnClick} />);
const avatarImg = screen.getByAltText('Test Agent avatar'); const avatarImg = screen.getByAltText('Test Agent avatar');
@ -47,17 +60,17 @@ describe('AgentCard', () => {
expect(avatarImg).toHaveAttribute('src', '/test-avatar.png'); expect(avatarImg).toHaveAttribute('src', '/test-avatar.png');
}); });
it('displays avatar when provided as object with filepath', () => { it('displays avatar when provided as string', () => {
const agentWithObjectAvatar = { const agentWithStringAvatar = {
...mockAgent, ...mockAgent,
avatar: { filepath: '/object-avatar.png' }, avatar: '/string-avatar.png' as any, // Legacy support for string avatars
}; };
render(<AgentCard agent={agentWithObjectAvatar} onClick={mockOnClick} />); render(<AgentCard agent={agentWithStringAvatar} onClick={mockOnClick} />);
const avatarImg = screen.getByAltText('Test Agent avatar'); const avatarImg = screen.getByAltText('Test Agent avatar');
expect(avatarImg).toBeInTheDocument(); 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', () => { it('displays Bot icon fallback when no avatar is provided', () => {
@ -66,7 +79,7 @@ describe('AgentCard', () => {
avatar: undefined, avatar: undefined,
}; };
render(<AgentCard agent={agentWithoutAvatar} onClick={mockOnClick} />); render(<AgentCard agent={agentWithoutAvatar as any as t.Agent} onClick={mockOnClick} />);
// Check for Bot icon presence by looking for the svg with lucide-bot class // Check for Bot icon presence by looking for the svg with lucide-bot class
const botIcon = document.querySelector('.lucide-bot'); const botIcon = document.querySelector('.lucide-bot');

View file

@ -1,13 +1,16 @@
import React from 'react'; 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 '@testing-library/jest-dom';
import AgentGrid from '../AgentGrid'; import AgentGrid from '../AgentGrid';
import { useGetMarketplaceAgentsQuery } from 'librechat-data-provider/react-query';
import type t from 'librechat-data-provider'; import type t from 'librechat-data-provider';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
// Mock the marketplace agent query hook // Mock the marketplace agent query hook
jest.mock('~/data-provider/Agents', () => ({
useMarketplaceAgentsInfiniteQuery: jest.fn(),
}));
jest.mock('~/hooks/Agents', () => ({ jest.mock('~/hooks/Agents', () => ({
useGetMarketplaceAgentsQuery: jest.fn(),
useAgentCategories: jest.fn(() => ({ useAgentCategories: jest.fn(() => ({
categories: [], categories: [],
isLoading: false, isLoading: false,
@ -15,8 +18,13 @@ jest.mock('~/hooks/Agents', () => ({
})), })),
})); }));
// Mock SmartLoader
jest.mock('../SmartLoader', () => ({
useHasData: jest.fn(() => true),
}));
// Mock useLocalize hook // Mock useLocalize hook
jest.mock('~/hooks/useLocalize', () => () => (key: string) => { jest.mock('~/hooks/useLocalize', () => () => (key: string, options?: any) => {
const mockTranslations: Record<string, string> = { const mockTranslations: Record<string, string> = {
com_agents_top_picks: 'Top Picks', com_agents_top_picks: 'Top Picks',
com_agents_all: 'All Agents', 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_grid_announcement: '{{count}} agents in {{category}}',
com_agents_load_more_label: 'Load more agents from {{category}}', com_agents_load_more_label: 'Load more agents from {{category}}',
}; };
return mockTranslations[key] || key.replace(/{{(\w+)}}/g, (match, key) => `[${key}]`);
});
// Mock SmartLoader components let translation = mockTranslations[key] || key;
jest.mock('../SmartLoader', () => ({
SmartLoader: ({ children, isLoading }: { children: React.ReactNode; isLoading: boolean }) => if (options) {
isLoading ? <div>Loading...</div> : <div>{children}</div>, Object.keys(options).forEach((optionKey) => {
useHasData: (data: any) => !!data?.agents?.length, translation = translation.replace(new RegExp(`{{${optionKey}}}`, 'g'), options[optionKey]);
})); });
}
return translation;
});
// Mock ErrorDisplay component // Mock ErrorDisplay component
jest.mock('../ErrorDisplay', () => ({ jest.mock('../ErrorDisplay', () => ({
__esModule: true, __esModule: true,
default: ({ error, onRetry }: { error: string; onRetry: () => void }) => ( default: ({ error, onRetry }: { error: any; onRetry: () => void }) => (
<div> <div>
<div>Error: {error}</div> <div>
<button onClick={onRetry}>Retry</button> {`Error: `}
{typeof error === 'string' ? error : error?.message || 'Unknown error'}
</div>
<button onClick={onRetry}>{`Retry`}</button>
</div> </div>
), ),
})); }));
@ -65,9 +78,10 @@ jest.mock('../AgentCard', () => ({
), ),
})); }));
const mockUseGetMarketplaceAgentsQuery = useGetMarketplaceAgentsQuery as jest.MockedFunction< // Import the actual modules to get the mocked functions
typeof useGetMarketplaceAgentsQuery import { useMarketplaceAgentsInfiniteQuery } from '~/data-provider/Agents';
>;
const mockUseMarketplaceAgentsInfiniteQuery = jest.mocked(useMarketplaceAgentsInfiniteQuery);
describe('AgentGrid Integration with useGetMarketplaceAgentsQuery', () => { describe('AgentGrid Integration with useGetMarketplaceAgentsQuery', () => {
const mockOnSelectAgent = jest.fn(); const mockOnSelectAgent = jest.fn();
@ -84,7 +98,15 @@ describe('AgentGrid Integration with useGetMarketplaceAgentsQuery', () => {
instructions: null, instructions: null,
provider: 'custom', provider: 'custom',
model: 'gpt-4', 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', id: '2',
@ -97,31 +119,37 @@ describe('AgentGrid Integration with useGetMarketplaceAgentsQuery', () => {
instructions: null, instructions: null,
provider: 'custom', provider: 'custom',
model: 'gpt-4', 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 = { const defaultMockQueryResult = {
data: { data: {
data: mockAgents, pages: [
pagination: { {
current: 1, data: mockAgents,
hasMore: true, },
total: 10, ],
},
}, },
isLoading: false, isLoading: false,
error: null, error: null,
isFetching: false, isFetching: false,
isFetchingNextPage: false,
hasNextPage: true,
fetchNextPage: jest.fn(),
refetch: jest.fn(), refetch: jest.fn(),
isSuccess: true, } as any;
isError: false,
status: 'success' as const,
};
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
mockUseGetMarketplaceAgentsQuery.mockReturnValue(defaultMockQueryResult); mockUseMarketplaceAgentsInfiniteQuery.mockReturnValue(defaultMockQueryResult);
}); });
describe('Query Integration', () => { describe('Query Integration', () => {
@ -130,7 +158,7 @@ describe('AgentGrid Integration with useGetMarketplaceAgentsQuery', () => {
<AgentGrid category="finance" searchQuery="test query" onSelectAgent={mockOnSelectAgent} />, <AgentGrid category="finance" searchQuery="test query" onSelectAgent={mockOnSelectAgent} />,
); );
expect(mockUseGetMarketplaceAgentsQuery).toHaveBeenCalledWith({ expect(mockUseMarketplaceAgentsInfiniteQuery).toHaveBeenCalledWith({
requiredPermission: 1, requiredPermission: 1,
category: 'finance', category: 'finance',
search: 'test query', search: 'test query',
@ -141,7 +169,7 @@ describe('AgentGrid Integration with useGetMarketplaceAgentsQuery', () => {
it('should call useGetMarketplaceAgentsQuery with promoted=1 for promoted category', () => { it('should call useGetMarketplaceAgentsQuery with promoted=1 for promoted category', () => {
render(<AgentGrid category="promoted" searchQuery="" onSelectAgent={mockOnSelectAgent} />); render(<AgentGrid category="promoted" searchQuery="" onSelectAgent={mockOnSelectAgent} />);
expect(mockUseGetMarketplaceAgentsQuery).toHaveBeenCalledWith({ expect(mockUseMarketplaceAgentsInfiniteQuery).toHaveBeenCalledWith({
requiredPermission: 1, requiredPermission: 1,
promoted: 1, promoted: 1,
limit: 6, limit: 6,
@ -151,7 +179,7 @@ describe('AgentGrid Integration with useGetMarketplaceAgentsQuery', () => {
it('should call useGetMarketplaceAgentsQuery without category filter for "all" category', () => { it('should call useGetMarketplaceAgentsQuery without category filter for "all" category', () => {
render(<AgentGrid category="all" searchQuery="" onSelectAgent={mockOnSelectAgent} />); render(<AgentGrid category="all" searchQuery="" onSelectAgent={mockOnSelectAgent} />);
expect(mockUseGetMarketplaceAgentsQuery).toHaveBeenCalledWith({ expect(mockUseMarketplaceAgentsInfiniteQuery).toHaveBeenCalledWith({
requiredPermission: 1, requiredPermission: 1,
limit: 6, limit: 6,
}); });
@ -160,7 +188,7 @@ describe('AgentGrid Integration with useGetMarketplaceAgentsQuery', () => {
it('should not include category in search when category is "all" or "promoted"', () => { it('should not include category in search when category is "all" or "promoted"', () => {
render(<AgentGrid category="all" searchQuery="test" onSelectAgent={mockOnSelectAgent} />); render(<AgentGrid category="all" searchQuery="test" onSelectAgent={mockOnSelectAgent} />);
expect(mockUseGetMarketplaceAgentsQuery).toHaveBeenCalledWith({ expect(mockUseMarketplaceAgentsInfiniteQuery).toHaveBeenCalledWith({
requiredPermission: 1, requiredPermission: 1,
search: 'test', search: 'test',
limit: 6, 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 }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
};
describe('Agent Display', () => { describe('Agent Display', () => {
it('should render agent cards when data is available', () => { it('should render agent cards when data is available', () => {
render(<AgentGrid category="finance" searchQuery="" onSelectAgent={mockOnSelectAgent} />); const Wrapper = createWrapper();
render(
<Wrapper>
<AgentGrid category="finance" searchQuery="" onSelectAgent={mockOnSelectAgent} />
</Wrapper>,
);
expect(screen.getByTestId('agent-card-1')).toBeInTheDocument(); expect(screen.getByTestId('agent-card-1')).toBeInTheDocument();
expect(screen.getByTestId('agent-card-2')).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', () => { it('should call onSelectAgent when agent card is clicked', () => {
render(<AgentGrid category="finance" searchQuery="" onSelectAgent={mockOnSelectAgent} />); const Wrapper = createWrapper();
render(
<Wrapper>
<AgentGrid category="finance" searchQuery="" onSelectAgent={mockOnSelectAgent} />
</Wrapper>,
);
fireEvent.click(screen.getByTestId('agent-card-1')); fireEvent.click(screen.getByTestId('agent-card-1'));
expect(mockOnSelectAgent).toHaveBeenCalledWith(mockAgents[0]); expect(mockOnSelectAgent).toHaveBeenCalledWith(mockAgents[0]);
@ -188,24 +237,41 @@ describe('AgentGrid Integration with useGetMarketplaceAgentsQuery', () => {
describe('Loading States', () => { describe('Loading States', () => {
it('should show loading state when isLoading is true', () => { it('should show loading state when isLoading is true', () => {
mockUseGetMarketplaceAgentsQuery.mockReturnValue({ mockUseMarketplaceAgentsInfiniteQuery.mockReturnValue({
...defaultMockQueryResult, ...defaultMockQueryResult,
isLoading: true, isLoading: true,
data: undefined, data: undefined,
}); });
render(<AgentGrid category="finance" searchQuery="" onSelectAgent={mockOnSelectAgent} />); const Wrapper = createWrapper();
render(
<Wrapper>
<AgentGrid category="finance" searchQuery="" onSelectAgent={mockOnSelectAgent} />
</Wrapper>,
);
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', () => { it('should show empty state when no agents are available', () => {
mockUseGetMarketplaceAgentsQuery.mockReturnValue({ mockUseMarketplaceAgentsInfiniteQuery.mockReturnValue({
...defaultMockQueryResult, ...defaultMockQueryResult,
data: { data: [], pagination: { current: 1, hasMore: false, total: 0 } }, data: {
pages: [
{
data: [],
},
],
},
}); });
render(<AgentGrid category="finance" searchQuery="" onSelectAgent={mockOnSelectAgent} />); const Wrapper = createWrapper();
render(
<Wrapper>
<AgentGrid category="finance" searchQuery="" onSelectAgent={mockOnSelectAgent} />
</Wrapper>,
);
expect(screen.getByText('No agents available')).toBeInTheDocument(); expect(screen.getByText('No agents available')).toBeInTheDocument();
}); });
@ -214,14 +280,19 @@ describe('AgentGrid Integration with useGetMarketplaceAgentsQuery', () => {
describe('Error Handling', () => { describe('Error Handling', () => {
it('should show error display when query has error', () => { it('should show error display when query has error', () => {
const mockError = new Error('Failed to fetch agents'); const mockError = new Error('Failed to fetch agents');
mockUseGetMarketplaceAgentsQuery.mockReturnValue({ mockUseMarketplaceAgentsInfiniteQuery.mockReturnValue({
...defaultMockQueryResult, ...defaultMockQueryResult,
error: mockError, error: mockError,
isError: true, isError: true,
data: undefined, data: undefined,
}); });
render(<AgentGrid category="finance" searchQuery="" onSelectAgent={mockOnSelectAgent} />); const Wrapper = createWrapper();
render(
<Wrapper>
<AgentGrid category="finance" searchQuery="" onSelectAgent={mockOnSelectAgent} />
</Wrapper>,
);
expect(screen.getByText('Error: Failed to fetch agents')).toBeInTheDocument(); expect(screen.getByText('Error: Failed to fetch agents')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Retry' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Retry' })).toBeInTheDocument();
@ -230,25 +301,41 @@ describe('AgentGrid Integration with useGetMarketplaceAgentsQuery', () => {
describe('Search Results', () => { describe('Search Results', () => {
it('should show search results title when searching', () => { it('should show search results title when searching', () => {
const Wrapper = createWrapper();
render( render(
<AgentGrid category="finance" searchQuery="automation" onSelectAgent={mockOnSelectAgent} />, <Wrapper>
<AgentGrid
category="finance"
searchQuery="automation"
onSelectAgent={mockOnSelectAgent}
/>
</Wrapper>,
); );
expect(screen.getByText('Results for "automation"')).toBeInTheDocument(); expect(screen.getByText('Results for "automation"')).toBeInTheDocument();
}); });
it('should show empty search results message', () => { it('should show empty search results message', () => {
mockUseGetMarketplaceAgentsQuery.mockReturnValue({ mockUseMarketplaceAgentsInfiniteQuery.mockReturnValue({
...defaultMockQueryResult, ...defaultMockQueryResult,
data: { data: [], pagination: { current: 1, hasMore: false, total: 0 } }, data: {
pages: [
{
data: [],
},
],
},
}); });
const Wrapper = createWrapper();
render( render(
<AgentGrid <Wrapper>
category="finance" <AgentGrid
searchQuery="nonexistent" category="finance"
onSelectAgent={mockOnSelectAgent} searchQuery="nonexistent"
/>, onSelectAgent={mockOnSelectAgent}
/>
</Wrapper>,
); );
expect(screen.getByText('No results found')).toBeInTheDocument(); expect(screen.getByText('No results found')).toBeInTheDocument();
@ -257,24 +344,33 @@ describe('AgentGrid Integration with useGetMarketplaceAgentsQuery', () => {
}); });
describe('Load More Functionality', () => { describe('Load More Functionality', () => {
it('should show "See more" button when hasMore is true', () => { it('should show "See more" button when hasNextPage is true', () => {
render(<AgentGrid category="finance" searchQuery="" onSelectAgent={mockOnSelectAgent} />); const Wrapper = createWrapper();
render(
<Wrapper>
<AgentGrid category="finance" searchQuery="" onSelectAgent={mockOnSelectAgent} />
</Wrapper>,
);
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', () => { it('should not show "See more" button when hasNextPage is false', () => {
mockUseGetMarketplaceAgentsQuery.mockReturnValue({ mockUseMarketplaceAgentsInfiniteQuery.mockReturnValue({
...defaultMockQueryResult, ...defaultMockQueryResult,
data: { hasNextPage: false,
...defaultMockQueryResult.data,
pagination: { current: 1, hasMore: false, total: 2 },
},
}); });
render(<AgentGrid category="finance" searchQuery="" onSelectAgent={mockOnSelectAgent} />); const Wrapper = createWrapper();
render(
<Wrapper>
<AgentGrid category="finance" searchQuery="" onSelectAgent={mockOnSelectAgent} />
</Wrapper>,
);
expect(screen.queryByRole('button', { name: 'See more' })).not.toBeInTheDocument(); expect(screen.queryByRole('button', { name: /Load more agents/ })).not.toBeInTheDocument();
}); });
}); });
}); });

View file

@ -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_server_suggestion: 'Please try again in a few moments.',
com_agents_error_search_title: 'Search Error', com_agents_error_search_title: 'Search Error',
com_agents_error_category_title: 'Category 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_search_no_results: `No agents found for "${options?.query}"`,
com_agents_category_empty: `No agents found in the ${options?.category} category`, com_agents_category_empty: `No agents found in the ${options?.category} category`,
com_agents_error_retry: 'Try Again', com_agents_error_retry: 'Try Again',

View file

@ -1154,6 +1154,9 @@
"com_agents_error_server_suggestion": "Please try again in a few moments.", "com_agents_error_server_suggestion": "Please try again in a few moments.",
"com_agents_error_search_title": "Search Error", "com_agents_error_search_title": "Search Error",
"com_agents_error_category_title": "Category 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_search_no_results": "No agents found for \"{{query}}\"",
"com_agents_category_empty": "No agents found in the {{category}} category", "com_agents_category_empty": "No agents found in the {{category}} category",
"com_agents_error_retry": "Try Again", "com_agents_error_retry": "Try Again",