mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-27 21:58:51 +01:00
Fix Unit tests WIP
This commit is contained in:
parent
37c423eb00
commit
33c4ef03c3
7 changed files with 402 additions and 158 deletions
|
|
@ -278,9 +278,7 @@ const AgentGrid: React.FC<AgentGridProps> = ({ category, searchQuery, onSelectAg
|
|||
)}
|
||||
</div>
|
||||
);
|
||||
console.log('isLoading', isLoading);
|
||||
console.log('isFetching', isFetching);
|
||||
console.log('isFetchingNextPage', isFetchingNextPage);
|
||||
|
||||
if (isLoading || (isFetching && !isFetchingNextPage)) {
|
||||
return loadingSkeleton;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,23 +67,25 @@ export const ErrorDisplay: React.FC<ErrorDisplayProps> = ({ 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<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;
|
||||
if (status) {
|
||||
if (status === 404) {
|
||||
|
|
@ -108,7 +110,8 @@ export const ErrorDisplay: React.FC<ErrorDisplayProps> = ({ 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<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 {
|
||||
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<ErrorDisplayProps> = ({ error, onRetry, cont
|
|||
|
||||
{/* Error content with proper headings and structure */}
|
||||
<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}
|
||||
</h2>
|
||||
</h3>
|
||||
<p
|
||||
className="text-gray-600 dark:text-gray-400"
|
||||
id="error-message"
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import AgentGrid from '../AgentGrid';
|
|||
import AgentCard from '../AgentCard';
|
||||
import SearchBar from '../SearchBar';
|
||||
import ErrorDisplay from '../ErrorDisplay';
|
||||
import * as t from 'librechat-data-provider';
|
||||
|
||||
// Mock 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
|
||||
jest.mock(
|
||||
'~/hooks/useLocalize',
|
||||
() => () =>
|
||||
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',
|
||||
};
|
||||
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(() => <div data-testid="agent-avatar" />),
|
||||
getContactDisplayName: jest.fn((agent) => agent.authorName),
|
||||
}));
|
||||
|
||||
// Mock SmartLoader
|
||||
jest.mock('../SmartLoader', () => ({
|
||||
SmartLoader: ({ children, isLoading }: any) => (isLoading ? <div>Loading...</div> : 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(<AgentCard agent={mockAgent} onClick={jest.fn()} />);
|
||||
render(<AgentCard agent={mockAgent as t.Agent} onClick={jest.fn()} />);
|
||||
|
||||
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(<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();
|
||||
});
|
||||
|
||||
it('supports keyboard interaction', () => {
|
||||
const onClick = jest.fn();
|
||||
render(<AgentCard agent={mockAgent} onClick={onClick} />);
|
||||
render(<AgentCard agent={mockAgent as t.Agent} onClick={onClick} />);
|
||||
|
||||
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', () => {
|
|||
</Wrapper>,
|
||||
);
|
||||
|
||||
// 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(
|
||||
<CategoryTabs
|
||||
categories={[{ name: 'test', count: 1 }]}
|
||||
categories={[{ value: 'test', label: 'Test', count: 1 }]}
|
||||
activeTab="test"
|
||||
isLoading={false}
|
||||
onChange={jest.fn()}
|
||||
|
|
@ -386,7 +504,7 @@ describe('Accessibility Improvements', () => {
|
|||
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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(<AgentCard agent={mockAgent} onClick={mockOnClick} />);
|
||||
|
||||
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(<AgentCard agent={agentWithObjectAvatar} onClick={mockOnClick} />);
|
||||
render(<AgentCard agent={agentWithStringAvatar} onClick={mockOnClick} />);
|
||||
|
||||
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(<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
|
||||
const botIcon = document.querySelector('.lucide-bot');
|
||||
|
|
|
|||
|
|
@ -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<string, string> = {
|
||||
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 ? <div>Loading...</div> : <div>{children}</div>,
|
||||
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 }) => (
|
||||
<div>
|
||||
<div>Error: {error}</div>
|
||||
<button onClick={onRetry}>Retry</button>
|
||||
<div>
|
||||
{`Error: `}
|
||||
{typeof error === 'string' ? error : error?.message || 'Unknown error'}
|
||||
</div>
|
||||
<button onClick={onRetry}>{`Retry`}</button>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
|
@ -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', () => {
|
|||
<AgentGrid category="finance" searchQuery="test query" onSelectAgent={mockOnSelectAgent} />,
|
||||
);
|
||||
|
||||
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(<AgentGrid category="promoted" searchQuery="" onSelectAgent={mockOnSelectAgent} />);
|
||||
|
||||
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(<AgentGrid category="all" searchQuery="" onSelectAgent={mockOnSelectAgent} />);
|
||||
|
||||
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(<AgentGrid category="all" searchQuery="test" onSelectAgent={mockOnSelectAgent} />);
|
||||
|
||||
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 }) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
|
||||
describe('Agent Display', () => {
|
||||
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-2')).toBeInTheDocument();
|
||||
|
|
@ -179,7 +223,12 @@ describe('AgentGrid Integration with useGetMarketplaceAgentsQuery', () => {
|
|||
});
|
||||
|
||||
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'));
|
||||
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(<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', () => {
|
||||
mockUseGetMarketplaceAgentsQuery.mockReturnValue({
|
||||
mockUseMarketplaceAgentsInfiniteQuery.mockReturnValue({
|
||||
...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();
|
||||
});
|
||||
|
|
@ -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(<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.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(
|
||||
<AgentGrid category="finance" searchQuery="automation" onSelectAgent={mockOnSelectAgent} />,
|
||||
<Wrapper>
|
||||
<AgentGrid
|
||||
category="finance"
|
||||
searchQuery="automation"
|
||||
onSelectAgent={mockOnSelectAgent}
|
||||
/>
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
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(
|
||||
<AgentGrid
|
||||
category="finance"
|
||||
searchQuery="nonexistent"
|
||||
onSelectAgent={mockOnSelectAgent}
|
||||
/>,
|
||||
<Wrapper>
|
||||
<AgentGrid
|
||||
category="finance"
|
||||
searchQuery="nonexistent"
|
||||
onSelectAgent={mockOnSelectAgent}
|
||||
/>
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
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(<AgentGrid category="finance" searchQuery="" onSelectAgent={mockOnSelectAgent} />);
|
||||
it('should show "See more" button when hasNextPage is true', () => {
|
||||
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', () => {
|
||||
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(<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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue