diff --git a/client/src/components/SidePanel/Agents/__tests__/Accessibility.spec.tsx b/client/src/components/SidePanel/Agents/__tests__/Accessibility.spec.tsx index 21d6cc4b83..2121226f22 100644 --- a/client/src/components/SidePanel/Agents/__tests__/Accessibility.spec.tsx +++ b/client/src/components/SidePanel/Agents/__tests__/Accessibility.spec.tsx @@ -456,7 +456,7 @@ describe('Accessibility Improvements', () => { expect(alert).toHaveAttribute('aria-atomic', 'true'); // Check heading structure - const heading = screen.getByRole('heading', { level: 2 }); + const heading = screen.getByRole('heading', { level: 3 }); expect(heading).toHaveAttribute('id', 'error-title'); }); diff --git a/client/src/components/SidePanel/Agents/__tests__/AgentDetail.spec.tsx b/client/src/components/SidePanel/Agents/__tests__/AgentDetail.spec.tsx index 00df97a062..8efbc5b15f 100644 --- a/client/src/components/SidePanel/Agents/__tests__/AgentDetail.spec.tsx +++ b/client/src/components/SidePanel/Agents/__tests__/AgentDetail.spec.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { MemoryRouter, useNavigate } from 'react-router-dom'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; @@ -19,6 +19,7 @@ jest.mock('react-router-dom', () => ({ jest.mock('~/hooks', () => ({ useToast: jest.fn(), + useMediaQuery: jest.fn(() => false), // Mock as desktop by default })); jest.mock('~/hooks/useLocalize', () => ({ @@ -33,11 +34,7 @@ jest.mock('~/utils/agents', () => ({ })); // Mock clipboard API -Object.assign(navigator, { - clipboard: { - writeText: jest.fn(), - }, -}); +const mockWriteText = jest.fn(); const mockNavigate = jest.fn(); const mockShowToast = jest.fn(); @@ -55,17 +52,23 @@ const mockAgent: t.Agent = { provider: 'openai', instructions: 'You are a helpful test agent', tools: [], - code_interpreter: false, - file_search: false, author: 'test-user-id', - author_name: 'Test User', - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), + created_at: new Date().getTime(), version: 1, support_contact: { name: 'Support Team', email: 'support@test.com', }, + model_parameters: { + model: undefined, + temperature: null, + maxContextTokens: null, + max_context_tokens: null, + max_output_tokens: null, + top_p: null, + frequency_penalty: null, + presence_penalty: null, + }, }; // Helper function to render with providers @@ -95,8 +98,19 @@ describe('AgentDetail', () => { (useToast as jest.Mock).mockReturnValue({ showToast: mockShowToast }); (useLocalize as jest.Mock).mockReturnValue(mockLocalize); - // Reset clipboard mock - (navigator.clipboard.writeText as jest.Mock).mockResolvedValue(undefined); + // Setup clipboard mock if it doesn't exist + if (!navigator.clipboard) { + Object.defineProperty(navigator, 'clipboard', { + value: { + writeText: mockWriteText, + }, + configurable: true, + }); + } else { + // If clipboard exists, spy on it + jest.spyOn(navigator.clipboard, 'writeText').mockImplementation(mockWriteText); + } + mockWriteText.mockResolvedValue(undefined); }); const defaultProps = { @@ -224,11 +238,17 @@ describe('AgentDetail', () => { const copyLinkButton = screen.getByRole('button', { name: 'com_agents_copy_link' }); await user.click(copyLinkButton); - expect(navigator.clipboard.writeText).toHaveBeenCalledWith( - `${window.location.origin}/c/new?agent_id=test-agent-id`, - ); - expect(mockShowToast).toHaveBeenCalledWith({ - message: 'Link copied', + // Wait for async clipboard operation to complete + await waitFor(() => { + expect(mockWriteText).toHaveBeenCalledWith( + `${window.location.origin}/c/new?agent_id=test-agent-id`, + ); + }); + + await waitFor(() => { + expect(mockShowToast).toHaveBeenCalledWith({ + message: 'Link copied', + }); }); // Dropdown should close @@ -241,7 +261,7 @@ describe('AgentDetail', () => { it('should show error toast when clipboard write fails', async () => { const user = userEvent.setup(); - (navigator.clipboard.writeText as jest.Mock).mockRejectedValue(new Error('Clipboard error')); + mockWriteText.mockRejectedValue(new Error('Clipboard error')); renderWithProviders(); @@ -252,6 +272,11 @@ describe('AgentDetail', () => { const copyLinkButton = screen.getByRole('button', { name: 'com_agents_copy_link' }); await user.click(copyLinkButton); + // Wait for clipboard operation to fail and error toast to show + await waitFor(() => { + expect(mockWriteText).toHaveBeenCalled(); + }); + await waitFor(() => { expect(mockShowToast).toHaveBeenCalledWith({ message: 'com_agents_link_copy_failed', @@ -261,7 +286,7 @@ describe('AgentDetail', () => { it('should call onClose when dialog is closed', () => { const mockOnClose = jest.fn(); - render(); + renderWithProviders(); // Since we're testing the onOpenChange callback, we need to trigger it // This would normally be done by the Dialog component when ESC is pressed or overlay is clicked diff --git a/client/src/components/SidePanel/Agents/__tests__/CategoryTabs.spec.tsx b/client/src/components/SidePanel/Agents/__tests__/CategoryTabs.spec.tsx index a5bb1108d6..cbcf9c601d 100644 --- a/client/src/components/SidePanel/Agents/__tests__/CategoryTabs.spec.tsx +++ b/client/src/components/SidePanel/Agents/__tests__/CategoryTabs.spec.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { render, screen, fireEvent } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import '@testing-library/jest-dom'; import CategoryTabs from '../CategoryTabs'; diff --git a/client/src/components/SidePanel/Agents/__tests__/ErrorDisplay.spec.tsx b/client/src/components/SidePanel/Agents/__tests__/ErrorDisplay.spec.tsx index b8ab6bb1c0..303308cefc 100644 --- a/client/src/components/SidePanel/Agents/__tests__/ErrorDisplay.spec.tsx +++ b/client/src/components/SidePanel/Agents/__tests__/ErrorDisplay.spec.tsx @@ -301,5 +301,3 @@ describe('ErrorDisplay', () => { }); }); }); - -export default {}; diff --git a/client/src/components/SidePanel/Agents/__tests__/SearchBar.spec.tsx b/client/src/components/SidePanel/Agents/__tests__/SearchBar.spec.tsx index 10e20b6353..5977680baf 100644 --- a/client/src/components/SidePanel/Agents/__tests__/SearchBar.spec.tsx +++ b/client/src/components/SidePanel/Agents/__tests__/SearchBar.spec.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import '@testing-library/jest-dom'; import SearchBar from '../SearchBar'; @@ -9,7 +9,7 @@ jest.mock('~/hooks/useLocalize', () => () => (key: string) => key); // Mock useDebounce hook jest.mock('~/hooks', () => ({ - useDebounce: (value: string, delay: number) => value, // Return value immediately for testing + useDebounce: (value: string) => value, // Return value immediately for testing })); describe('SearchBar', () => { diff --git a/client/src/data-provider/Agents/mutations.ts b/client/src/data-provider/Agents/mutations.ts index b1c3cb191c..8ce6611cc4 100644 --- a/client/src/data-provider/Agents/mutations.ts +++ b/client/src/data-provider/Agents/mutations.ts @@ -193,8 +193,6 @@ export const useUploadAgentAvatarMutation = ( t.AgentAvatarVariables, // request unknown // context > => { - const queryClient = useQueryClient(); - return useMutation([MutationKeys.agentAvatarUpload], { mutationFn: ({ postCreation, ...variables }: t.AgentAvatarVariables) => dataService.uploadAgentAvatar(variables), diff --git a/client/src/hooks/Agents/__tests__/useAgentCategories.spec.tsx b/client/src/hooks/Agents/__tests__/useAgentCategories.spec.tsx index af44405e10..b689f29e79 100644 --- a/client/src/hooks/Agents/__tests__/useAgentCategories.spec.tsx +++ b/client/src/hooks/Agents/__tests__/useAgentCategories.spec.tsx @@ -1,6 +1,8 @@ -import { renderHook } from '@testing-library/react'; +import React from 'react'; +import { renderHook, waitFor } from '@testing-library/react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import useAgentCategories from '../useAgentCategories'; -import { AGENT_CATEGORIES, EMPTY_AGENT_CATEGORY } from '~/constants/agentCategories'; +import { EMPTY_AGENT_CATEGORY } from '~/constants/agentCategories'; // Mock the useLocalize hook jest.mock('~/hooks/useLocalize', () => ({ @@ -11,25 +13,68 @@ jest.mock('~/hooks/useLocalize', () => ({ }, })); -describe('useAgentCategories', () => { - it('should return processed categories with correct structure', () => { - const { result } = renderHook(() => useAgentCategories()); +// Mock the data provider +jest.mock('~/data-provider/Agents', () => ({ + useGetAgentCategoriesQuery: jest.fn(() => ({ + data: [ + { value: 'general', label: 'com_ui_agent_category_general' }, + { value: 'hr', label: 'com_ui_agent_category_hr' }, + { value: 'rd', label: 'com_ui_agent_category_rd' }, + { value: 'finance', label: 'com_ui_agent_category_finance' }, + { value: 'it', label: 'com_ui_agent_category_it' }, + { value: 'sales', label: 'com_ui_agent_category_sales' }, + { value: 'aftersales', label: 'com_ui_agent_category_aftersales' }, + { value: 'promoted', label: 'Promoted' }, // Should be filtered out + { value: 'all', label: 'All' }, // Should be filtered out + ], + isLoading: false, + error: null, + })), +})); - // Check that we have the expected number of categories - expect(result.current.categories.length).toBe(AGENT_CATEGORIES.length); +const createWrapper = () => { + const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }); + + return ({ children }: { children: React.ReactNode }) => ( + {children} + ); +}; + +describe('useAgentCategories', () => { + it('should return processed categories with correct structure', async () => { + const { result } = renderHook(() => useAgentCategories(), { + wrapper: createWrapper(), + }); + + await waitFor(() => { + // Check that we have the expected number of categories (excluding 'promoted' and 'all') + expect(result.current.categories.length).toBe(7); + }); // Check that the first category has the expected structure const firstCategory = result.current.categories[0]; - const firstOriginalCategory = AGENT_CATEGORIES[0]; - - expect(firstCategory.value).toBe(firstOriginalCategory.value); - - // Check that labels are properly translated - expect(firstCategory.label).toBe('General (Translated)'); + expect(firstCategory.value).toBe('general'); + expect(firstCategory.label).toBe('com_ui_agent_category_general'); expect(firstCategory.className).toBe('w-full'); + // Verify special categories are filtered out + const categoryValues = result.current.categories.map((cat) => cat.value); + expect(categoryValues).not.toContain('promoted'); + expect(categoryValues).not.toContain('all'); + // Check the empty category expect(result.current.emptyCategory.value).toBe(EMPTY_AGENT_CATEGORY.value); - expect(result.current.emptyCategory.label).toBeTruthy(); + expect(result.current.emptyCategory.label).toBe('General (Translated)'); + expect(result.current.emptyCategory.className).toBe('w-full'); + + // Check loading state + expect(result.current.isLoading).toBe(false); + expect(result.current.error).toBeNull(); }); }); diff --git a/client/src/hooks/Nav/useSideNavLinks.ts b/client/src/hooks/Nav/useSideNavLinks.ts index 0d708c68f3..728b856735 100644 --- a/client/src/hooks/Nav/useSideNavLinks.ts +++ b/client/src/hooks/Nav/useSideNavLinks.ts @@ -1,11 +1,5 @@ import { useMemo } from 'react'; -import { - MessageSquareQuote, - ArrowRightToLine, - Settings2, Database, - Bookmark, - LayoutGrid, -} from 'lucide-react'; +import { MessageSquareQuote, ArrowRightToLine, Settings2, Database, Bookmark } from 'lucide-react'; import { isAssistantsEndpoint, isAgentsEndpoint, @@ -27,7 +21,6 @@ import FilesPanel from '~/components/SidePanel/Files/Panel'; import MCPPanel from '~/components/SidePanel/MCP/MCPPanel'; import { useGetStartupConfig } from '~/data-provider'; import { useHasAccess } from '~/hooks'; -import { useNavigate } from 'react-router-dom'; export default function useSideNavLinks({ hidePanel, @@ -44,7 +37,6 @@ export default function useSideNavLinks({ interfaceConfig: Partial; endpointsConfig: TEndpointsConfig; }) { - const navigate = useNavigate(); const hasAccessToPrompts = useHasAccess({ permissionType: PermissionTypes.PROMPTS, permission: Permissions.USE, diff --git a/client/src/utils/__tests__/agents.spec.tsx b/client/src/utils/__tests__/agents.spec.tsx index 3d960a07bf..6bbd2dbc38 100644 --- a/client/src/utils/__tests__/agents.spec.tsx +++ b/client/src/utils/__tests__/agents.spec.tsx @@ -33,7 +33,7 @@ describe('Agent Utilities', () => { id: '1', name: 'Test Agent', avatar: '/path/to/avatar.png', - } as t.Agent; + } as unknown as t.Agent; expect(getAgentAvatarUrl(agent)).toBe('/path/to/avatar.png'); }); @@ -62,7 +62,7 @@ describe('Agent Utilities', () => { id: '1', name: 'Test Agent', avatar: '/test-avatar.png', - } as t.Agent; + } as unknown as t.Agent; render(
{renderAgentAvatar(agent)}
); @@ -90,7 +90,7 @@ describe('Agent Utilities', () => { id: '1', name: 'Test Agent', avatar: '/test-avatar.png', - } as t.Agent; + } as unknown as t.Agent; const { rerender } = render(
{renderAgentAvatar(agent, { size: 'sm' })}
); expect(screen.getByAltText('Test Agent avatar')).toHaveClass('h-12', 'w-12'); @@ -107,7 +107,7 @@ describe('Agent Utilities', () => { id: '1', name: 'Test Agent', avatar: '/test-avatar.png', - } as t.Agent; + } as unknown as t.Agent; render(
{renderAgentAvatar(agent, { className: 'custom-class' })}
); @@ -120,7 +120,7 @@ describe('Agent Utilities', () => { id: '1', name: 'Test Agent', avatar: '/test-avatar.png', - } as t.Agent; + } as unknown as t.Agent; const { rerender } = render(
{renderAgentAvatar(agent, { showBorder: true })}
); expect(screen.getByAltText('Test Agent avatar')).toHaveClass('border-2');