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>
);
console.log('isLoading', isLoading);
console.log('isFetching', isFetching);
console.log('isFetchingNextPage', isFetchingNextPage);
if (isLoading || (isFetching && !isFetchingNextPage)) {
return loadingSkeleton;
}

View file

@ -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"

View file

@ -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');
});
});

View file

@ -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');

View file

@ -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();
});
});
});

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_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',

View file

@ -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",