mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-03-01 13:50:19 +01:00
🖼️ style: Improve Marketplace & Sharing Dialog UI
feat: Enhance CategoryTabs and Marketplace components for better responsiveness and navigation feat: Refactor AgentCard and AgentGrid components for improved layout and accessibility feat: Implement animated category transitions in AgentMarketplace and update NewChat component layout feat: Refactor UI components for improved styling and accessibility in sharing dialogs refactor: remove GenericManagePermissionsDialog and GrantAccessDialog components - Deleted GenericManagePermissionsDialog and GrantAccessDialog components to streamline sharing functionality. - Updated ManagePermissionsDialog to utilize AccessRolesPicker directly. - Introduced UnifiedPeopleSearch for improved people selection experience. - Enhanced PublicSharingToggle with InfoHoverCard for better user guidance. - Adjusted AgentPanel to change error status to warning for duplicate agent versions. - Updated translations to include new keys for search and access management. feat: Add responsive design for SelectedPrincipalsList and improve layout in GenericGrantAccessDialog feat: Enhance styling in SelectedPrincipalsList and SearchPicker components for improved UI consistency feat: Improve PublicSharingToggle component with enhanced styling and accessibility features feat: Introduce InfoHoverCard component and refactor enums for better organization feat: Implement infinite scroll for agent grids and enhance performance - Added `useInfiniteScroll` hook to manage infinite scrolling behavior in agent grids. - Integrated infinite scroll functionality into `AgentGrid` and `VirtualizedAgentGrid` components. - Updated `AgentMarketplace` to pass the scroll container to the agent grid components. - Refactored loading indicators to show a spinner instead of a "Load More" button. - Created `VirtualizedAgentGrid` component for optimized rendering of agent cards using virtualization. - Added performance tests for `VirtualizedAgentGrid` to ensure efficient handling of large datasets. - Updated translations to include new messages for end-of-results scenarios. chore: Remove unused permission-related UI localization keys ci: Update Agent model tests to handle duplicate support_contact updates - Modified tests to ensure that updating an agent with the same support_contact does not create a new version and returns successfully. - Enhanced verification for partial changes in support_contact, confirming no new version is created when content remains the same. chore: Address ESLint, clean up unused imports and improve prop definitions in various components ci: fix tests ci: update tests chore: remove unused search localization keys
This commit is contained in:
parent
9585db14ba
commit
d82a63642d
51 changed files with 2074 additions and 1311 deletions
|
|
@ -48,7 +48,7 @@ describe('AgentCard', () => {
|
|||
|
||||
expect(screen.getByText('Test Agent')).toBeInTheDocument();
|
||||
expect(screen.getByText('A test agent for testing purposes')).toBeInTheDocument();
|
||||
expect(screen.getByText('Created by')).toBeInTheDocument();
|
||||
expect(screen.getByText('🔹')).toBeInTheDocument();
|
||||
expect(screen.getByText('Test Support')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
|
@ -152,7 +152,7 @@ describe('AgentCard', () => {
|
|||
|
||||
render(<AgentCard agent={agentWithAuthorName} onClick={mockOnClick} />);
|
||||
|
||||
expect(screen.getByText('Created by')).toBeInTheDocument();
|
||||
expect(screen.getByText('🔹')).toBeInTheDocument();
|
||||
expect(screen.getByText('John Doe')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
|
@ -165,7 +165,7 @@ describe('AgentCard', () => {
|
|||
|
||||
render(<AgentCard agent={agentWithEmailOnly} onClick={mockOnClick} />);
|
||||
|
||||
expect(screen.getByText('Created by')).toBeInTheDocument();
|
||||
expect(screen.getByText('🔹')).toBeInTheDocument();
|
||||
expect(screen.getByText('contact@example.com')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
|
@ -178,7 +178,7 @@ describe('AgentCard', () => {
|
|||
|
||||
render(<AgentCard agent={agentWithBoth} onClick={mockOnClick} />);
|
||||
|
||||
expect(screen.getByText('Created by')).toBeInTheDocument();
|
||||
expect(screen.getByText('🔹')).toBeInTheDocument();
|
||||
expect(screen.getByText('Support Team')).toBeInTheDocument();
|
||||
expect(screen.queryByText('John Doe')).not.toBeInTheDocument();
|
||||
});
|
||||
|
|
@ -195,7 +195,7 @@ describe('AgentCard', () => {
|
|||
|
||||
render(<AgentCard agent={agentWithNameAndEmail} onClick={mockOnClick} />);
|
||||
|
||||
expect(screen.getByText('Created by')).toBeInTheDocument();
|
||||
expect(screen.getByText('🔹')).toBeInTheDocument();
|
||||
expect(screen.getByText('Support Team')).toBeInTheDocument();
|
||||
expect(screen.queryByText('support@example.com')).not.toBeInTheDocument();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -33,13 +33,11 @@ jest.mock('~/hooks/useLocalize', () => () => (key: string, options?: any) => {
|
|||
com_agents_see_more: 'See more',
|
||||
com_agents_error_loading: 'Error loading agents',
|
||||
com_agents_error_searching: 'Error searching agents',
|
||||
com_agents_no_results: 'No agents found. Try another search term.',
|
||||
com_agents_none_in_category: 'No agents found in this category',
|
||||
com_agents_search_empty_heading: 'No results found',
|
||||
com_agents_empty_state_heading: 'No agents available',
|
||||
com_agents_loading: 'Loading...',
|
||||
com_agents_grid_announcement: '{{count}} agents in {{category}}',
|
||||
com_agents_load_more_label: 'Load more agents from {{category}}',
|
||||
com_agents_no_more_results: "You've reached the end of the results",
|
||||
};
|
||||
|
||||
let translation = mockTranslations[key] || key;
|
||||
|
|
@ -250,8 +248,9 @@ describe('AgentGrid Integration with useGetMarketplaceAgentsQuery', () => {
|
|||
</Wrapper>,
|
||||
);
|
||||
|
||||
// Should show skeleton loading state
|
||||
expect(document.querySelector('.animate-pulse')).toBeInTheDocument();
|
||||
// Should show loading spinner
|
||||
const spinner = document.querySelector('.text-primary');
|
||||
expect(spinner).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show empty state when no agents are available', () => {
|
||||
|
|
@ -312,7 +311,8 @@ describe('AgentGrid Integration with useGetMarketplaceAgentsQuery', () => {
|
|||
</Wrapper>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('Results for "automation"')).toBeInTheDocument();
|
||||
// The component doesn't show search result titles, just displays the filtered agents
|
||||
expect(screen.getByTestId('agent-card-1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show empty search results message', () => {
|
||||
|
|
@ -338,29 +338,16 @@ describe('AgentGrid Integration with useGetMarketplaceAgentsQuery', () => {
|
|||
</Wrapper>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('No results found')).toBeInTheDocument();
|
||||
expect(screen.getByText('No agents found. Try another search term.')).toBeInTheDocument();
|
||||
expect(screen.getByText('No agents available')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Load More Functionality', () => {
|
||||
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: 'Load more agents from Finance' }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not show "See more" button when hasNextPage is false', () => {
|
||||
describe('Infinite Scroll Functionality', () => {
|
||||
it('should show loading indicator when fetching next page', () => {
|
||||
mockUseMarketplaceAgentsInfiniteQuery.mockReturnValue({
|
||||
...defaultMockQueryResult,
|
||||
hasNextPage: false,
|
||||
isFetchingNextPage: true,
|
||||
hasNextPage: true,
|
||||
});
|
||||
|
||||
const Wrapper = createWrapper();
|
||||
|
|
@ -370,7 +357,44 @@ describe('AgentGrid Integration with useGetMarketplaceAgentsQuery', () => {
|
|||
</Wrapper>,
|
||||
);
|
||||
|
||||
expect(screen.queryByRole('button', { name: /Load more agents/ })).not.toBeInTheDocument();
|
||||
expect(screen.getByRole('status', { name: 'Loading...' })).toBeInTheDocument();
|
||||
expect(screen.getByText('Loading...')).toHaveClass('sr-only');
|
||||
});
|
||||
|
||||
it('should show end of results message when hasNextPage is false and agents exist', () => {
|
||||
mockUseMarketplaceAgentsInfiniteQuery.mockReturnValue({
|
||||
...defaultMockQueryResult,
|
||||
hasNextPage: false,
|
||||
isFetchingNextPage: false,
|
||||
});
|
||||
|
||||
const Wrapper = createWrapper();
|
||||
render(
|
||||
<Wrapper>
|
||||
<AgentGrid category="finance" searchQuery="" onSelectAgent={mockOnSelectAgent} />
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
expect(screen.getByText("You've reached the end of the results")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not show end of results message when no agents exist', () => {
|
||||
mockUseMarketplaceAgentsInfiniteQuery.mockReturnValue({
|
||||
...defaultMockQueryResult,
|
||||
hasNextPage: false,
|
||||
data: {
|
||||
pages: [{ data: [] }],
|
||||
},
|
||||
});
|
||||
|
||||
const Wrapper = createWrapper();
|
||||
render(
|
||||
<Wrapper>
|
||||
<AgentGrid category="finance" searchQuery="" onSelectAgent={mockOnSelectAgent} />
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
expect(screen.queryByText("You've reached the end of the results")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ describe('CategoryTabs', () => {
|
|||
);
|
||||
|
||||
const generalTab = screen.getByText('General').closest('button');
|
||||
expect(generalTab).toHaveClass('bg-surface-tertiary');
|
||||
expect(generalTab).toHaveClass('bg-surface-hover');
|
||||
|
||||
// Should have active underline
|
||||
const underline = generalTab?.querySelector('.absolute.bottom-0');
|
||||
|
|
|
|||
|
|
@ -0,0 +1,275 @@
|
|||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { jest } from '@jest/globals';
|
||||
import VirtualizedAgentGrid from '../VirtualizedAgentGrid';
|
||||
import type * as t from 'librechat-data-provider';
|
||||
|
||||
// Mock react-virtualized for performance testing
|
||||
const mockRowRenderer = jest.fn();
|
||||
|
||||
jest.mock('react-virtualized', () => {
|
||||
const mockRowRendererRef = { current: jest.fn() };
|
||||
|
||||
return {
|
||||
AutoSizer: ({
|
||||
children,
|
||||
disableHeight,
|
||||
}: {
|
||||
children: (props: { width: number; height?: number }) => React.ReactNode;
|
||||
disableHeight?: boolean;
|
||||
}) => {
|
||||
if (disableHeight) {
|
||||
return children({ width: 1200 });
|
||||
}
|
||||
return children({ width: 1200, height: 800 });
|
||||
},
|
||||
List: ({
|
||||
rowRenderer,
|
||||
rowCount,
|
||||
autoHeight,
|
||||
height,
|
||||
width,
|
||||
rowHeight,
|
||||
overscanRowCount,
|
||||
scrollTop,
|
||||
isScrolling,
|
||||
onScroll,
|
||||
style,
|
||||
'aria-rowcount': ariaRowCount,
|
||||
'data-testid': dataTestId,
|
||||
'data-total-rows': dataTotalRows,
|
||||
}: {
|
||||
rowRenderer: any;
|
||||
rowCount: number;
|
||||
[key: string]: any;
|
||||
}) => {
|
||||
// Store the row renderer for testing
|
||||
if (typeof rowRenderer === 'function') {
|
||||
mockRowRendererRef.current = rowRenderer;
|
||||
mockRowRenderer.mockImplementation(rowRenderer);
|
||||
}
|
||||
// Only render visible rows to simulate virtualization
|
||||
const visibleRows = Math.min(10, rowCount); // Simulate 10 visible rows
|
||||
return (
|
||||
<div
|
||||
data-testid={dataTestId || 'virtual-list'}
|
||||
data-total-rows={dataTotalRows || rowCount}
|
||||
aria-rowcount={ariaRowCount}
|
||||
style={style}
|
||||
>
|
||||
{Array.from({ length: visibleRows }, (_, index) =>
|
||||
rowRenderer({
|
||||
index,
|
||||
key: `row-${index}`,
|
||||
style: { height: 184 },
|
||||
parent: { props: { width: width || 1200 } },
|
||||
}),
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
WindowScroller: ({
|
||||
children,
|
||||
scrollElement,
|
||||
}: {
|
||||
children: (props: any) => React.ReactNode;
|
||||
scrollElement?: HTMLElement | null;
|
||||
}) => {
|
||||
return children({
|
||||
height: 800,
|
||||
isScrolling: false,
|
||||
registerChild: (ref: any) => {},
|
||||
onChildScroll: () => {},
|
||||
scrollTop: 0,
|
||||
});
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// Generate large dataset for performance testing
|
||||
const generateLargeDataset = (count: number) => {
|
||||
const agents: Partial<t.Agent>[] = [];
|
||||
for (let i = 1; i <= count; i++) {
|
||||
agents.push({
|
||||
id: `agent-${i}`,
|
||||
name: `Performance Test Agent ${i}`,
|
||||
description: `This is agent ${i} for performance testing virtual scrolling with large datasets`,
|
||||
category: i % 2 === 0 ? 'productivity' : 'development',
|
||||
});
|
||||
}
|
||||
return agents;
|
||||
};
|
||||
|
||||
// Mock the data provider with large dataset
|
||||
const createMockInfiniteQuery = (agentCount: number) => ({
|
||||
data: {
|
||||
pages: [{ data: generateLargeDataset(agentCount) }],
|
||||
},
|
||||
isLoading: false,
|
||||
error: null,
|
||||
isFetching: false,
|
||||
fetchNextPage: jest.fn(),
|
||||
hasNextPage: false,
|
||||
refetch: jest.fn(),
|
||||
isFetchingNextPage: false,
|
||||
});
|
||||
|
||||
// Mock must be hoisted before imports
|
||||
jest.mock('~/data-provider/Agents', () => ({
|
||||
useMarketplaceAgentsInfiniteQuery: jest.fn(),
|
||||
}));
|
||||
jest.mock('~/hooks', () => ({
|
||||
useAgentCategories: () => ({
|
||||
categories: [
|
||||
{ value: 'productivity', label: 'Productivity' },
|
||||
{ value: 'development', label: 'Development' },
|
||||
],
|
||||
}),
|
||||
useLocalize: () => (key: string, params?: any) => {
|
||||
if (key === 'com_agents_grid_announcement') {
|
||||
return `Found ${params?.count || 0} agents in ${params?.category || 'category'}`;
|
||||
}
|
||||
return key;
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../SmartLoader', () => ({
|
||||
useHasData: () => true,
|
||||
}));
|
||||
|
||||
jest.mock('../AgentCard', () => {
|
||||
return function MockAgentCard({ agent }: { agent: any }) {
|
||||
return (
|
||||
<div data-testid={`agent-card-${agent.id}`} style={{ height: '160px' }}>
|
||||
<h3>{agent.name}</h3>
|
||||
<p>{agent.description}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
describe('Virtual Scrolling Performance', () => {
|
||||
let queryClient: QueryClient;
|
||||
|
||||
beforeEach(() => {
|
||||
queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
mockRowRenderer.mockClear();
|
||||
});
|
||||
|
||||
const renderComponent = (agentCount: number) => {
|
||||
const mockQuery = createMockInfiniteQuery(agentCount);
|
||||
const useMarketplaceAgentsInfiniteQuery =
|
||||
jest.requireMock('~/data-provider/Agents').useMarketplaceAgentsInfiniteQuery;
|
||||
useMarketplaceAgentsInfiniteQuery.mockReturnValue(mockQuery);
|
||||
|
||||
// Clear previous mock calls
|
||||
mockRowRenderer.mockClear();
|
||||
|
||||
return render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<VirtualizedAgentGrid category="all" searchQuery="" onSelectAgent={jest.fn()} />
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
};
|
||||
|
||||
it('efficiently handles 1000 agents without rendering all DOM nodes', () => {
|
||||
const startTime = performance.now();
|
||||
renderComponent(1000);
|
||||
const endTime = performance.now();
|
||||
|
||||
const virtualList = screen.getByTestId('virtual-list');
|
||||
expect(virtualList).toBeInTheDocument();
|
||||
expect(virtualList).toHaveAttribute('data-total-rows', '500'); // 1000 agents / 2 per row
|
||||
|
||||
// Should only render visible cards, not all 1000
|
||||
const renderedCards = screen.getAllByTestId(/agent-card-/);
|
||||
expect(renderedCards.length).toBeLessThan(50); // Much less than 1000
|
||||
expect(renderedCards.length).toBeGreaterThan(0);
|
||||
|
||||
// Performance check: rendering should be fast
|
||||
const renderTime = endTime - startTime;
|
||||
expect(renderTime).toBeLessThan(600); // Should render in less than 600ms
|
||||
|
||||
console.log(`Rendered 1000 agents in ${renderTime.toFixed(2)}ms`);
|
||||
console.log(`Only ${renderedCards.length} DOM nodes created for 1000 agents`);
|
||||
});
|
||||
|
||||
it('efficiently handles 5000 agents (stress test)', () => {
|
||||
const startTime = performance.now();
|
||||
renderComponent(5000);
|
||||
const endTime = performance.now();
|
||||
|
||||
const virtualList = screen.getByTestId('virtual-list');
|
||||
expect(virtualList).toBeInTheDocument();
|
||||
expect(virtualList).toHaveAttribute('data-total-rows', '2500'); // 5000 agents / 2 per row
|
||||
|
||||
// Should still only render visible cards
|
||||
const renderedCards = screen.getAllByTestId(/agent-card-/);
|
||||
expect(renderedCards.length).toBeLessThan(50);
|
||||
expect(renderedCards.length).toBeGreaterThan(0);
|
||||
|
||||
// Performance should still be reasonable
|
||||
const renderTime = endTime - startTime;
|
||||
expect(renderTime).toBeLessThan(200); // Should render in less than 200ms
|
||||
|
||||
console.log(`Rendered 5000 agents in ${renderTime.toFixed(2)}ms`);
|
||||
console.log(`Only ${renderedCards.length} DOM nodes created for 5000 agents`);
|
||||
});
|
||||
|
||||
it('calculates correct number of virtual rows for different screen sizes', () => {
|
||||
// Test desktop layout (2 cards per row)
|
||||
renderComponent(100);
|
||||
|
||||
const virtualList = screen.getByTestId('virtual-list');
|
||||
expect(virtualList).toHaveAttribute('data-total-rows', '50'); // 100 agents / 2 per row
|
||||
});
|
||||
|
||||
it('row renderer is called efficiently', () => {
|
||||
// Reset the mock before testing
|
||||
mockRowRenderer.mockClear();
|
||||
|
||||
renderComponent(1000);
|
||||
|
||||
// Check that virtual list was rendered
|
||||
const virtualList = screen.getByTestId('virtual-list');
|
||||
expect(virtualList).toBeInTheDocument();
|
||||
|
||||
// With virtualization, we should only render visible rows
|
||||
// Our mock renders 10 visible rows max
|
||||
const renderedCards = screen.getAllByTestId(/agent-card-/);
|
||||
expect(renderedCards.length).toBeLessThanOrEqual(20); // At most 10 rows * 2 cards per row
|
||||
expect(renderedCards.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('memory usage remains stable with large datasets', () => {
|
||||
// Test that memory doesn't grow linearly with data size
|
||||
const measureMemory = () => {
|
||||
const cards = screen.queryAllByTestId(/agent-card-/);
|
||||
return cards.length;
|
||||
};
|
||||
|
||||
renderComponent(100);
|
||||
const memory100 = measureMemory();
|
||||
|
||||
renderComponent(1000);
|
||||
const memory1000 = measureMemory();
|
||||
|
||||
renderComponent(5000);
|
||||
const memory5000 = measureMemory();
|
||||
|
||||
// Memory usage should not scale linearly with data size
|
||||
// All should render roughly the same number of DOM nodes
|
||||
expect(Math.abs(memory100 - memory1000)).toBeLessThan(30);
|
||||
expect(Math.abs(memory1000 - memory5000)).toBeLessThan(30);
|
||||
|
||||
console.log(
|
||||
`Memory usage: 100 agents=${memory100}, 1000 agents=${memory1000}, 5000 agents=${memory5000}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
248
client/src/components/Agents/tests/VirtualizedAgentGrid.test.tsx
Normal file
248
client/src/components/Agents/tests/VirtualizedAgentGrid.test.tsx
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
import React from 'react';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { jest } from '@jest/globals';
|
||||
import VirtualizedAgentGrid from '../VirtualizedAgentGrid';
|
||||
import type t from 'librechat-data-provider';
|
||||
|
||||
// Mock react-virtualized
|
||||
jest.mock('react-virtualized', () => ({
|
||||
AutoSizer: ({
|
||||
children,
|
||||
disableHeight,
|
||||
}: {
|
||||
children: (props: { width: number; height?: number }) => React.ReactNode;
|
||||
disableHeight?: boolean;
|
||||
}) => {
|
||||
if (disableHeight) {
|
||||
return children({ width: 800 });
|
||||
}
|
||||
return children({ width: 800, height: 600 });
|
||||
},
|
||||
List: ({
|
||||
rowRenderer,
|
||||
rowCount,
|
||||
width,
|
||||
style,
|
||||
'aria-rowcount': ariaRowCount,
|
||||
'data-testid': dataTestId,
|
||||
'data-total-rows': dataTotalRows,
|
||||
}: {
|
||||
rowRenderer: any;
|
||||
rowCount: number;
|
||||
autoHeight?: boolean;
|
||||
height?: number;
|
||||
width?: number;
|
||||
rowHeight?: number;
|
||||
overscanRowCount?: number;
|
||||
scrollTop?: number;
|
||||
isScrolling?: boolean;
|
||||
onScroll?: any;
|
||||
style?: any;
|
||||
'aria-rowcount'?: number;
|
||||
'data-testid'?: string;
|
||||
'data-total-rows'?: number;
|
||||
}) => (
|
||||
<div
|
||||
data-testid={dataTestId || 'virtual-list'}
|
||||
aria-rowcount={ariaRowCount}
|
||||
data-total-rows={dataTotalRows}
|
||||
style={style}
|
||||
>
|
||||
{Array.from({ length: Math.min(rowCount, 5) }, (_, index) =>
|
||||
rowRenderer({
|
||||
index,
|
||||
key: `row-${index}`,
|
||||
style: {},
|
||||
parent: { props: { width: width || 800 } },
|
||||
}),
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
WindowScroller: ({
|
||||
children,
|
||||
}: {
|
||||
children: (props: any) => React.ReactNode;
|
||||
scrollElement?: HTMLElement | null;
|
||||
}) => {
|
||||
return children({
|
||||
height: 600,
|
||||
isScrolling: false,
|
||||
registerChild: (_ref: any) => {},
|
||||
onChildScroll: () => {},
|
||||
scrollTop: 0,
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock the data provider
|
||||
const mockInfiniteQuery = {
|
||||
data: {
|
||||
pages: [
|
||||
{
|
||||
data: [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Test Agent 1',
|
||||
description: 'A test agent for virtual scrolling',
|
||||
category: 'productivity',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Test Agent 2',
|
||||
description: 'Another test agent',
|
||||
category: 'development',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
isLoading: false,
|
||||
error: null,
|
||||
isFetching: false,
|
||||
fetchNextPage: jest.fn(),
|
||||
hasNextPage: true,
|
||||
refetch: jest.fn(),
|
||||
isFetchingNextPage: false,
|
||||
};
|
||||
|
||||
jest.mock('~/data-provider/Agents', () => ({
|
||||
useMarketplaceAgentsInfiniteQuery: jest.fn(() => mockInfiniteQuery),
|
||||
}));
|
||||
|
||||
// Mock other hooks
|
||||
jest.mock('~/hooks', () => ({
|
||||
useAgentCategories: () => ({
|
||||
categories: [
|
||||
{ value: 'productivity', label: 'Productivity' },
|
||||
{ value: 'development', label: 'Development' },
|
||||
],
|
||||
}),
|
||||
useLocalize: () => (key: string, params?: any) => {
|
||||
if (key === 'com_agents_grid_announcement') {
|
||||
return `Found ${params?.count || 0} agents in ${params?.category || 'category'}`;
|
||||
}
|
||||
return key;
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../SmartLoader', () => ({
|
||||
useHasData: () => true,
|
||||
}));
|
||||
|
||||
jest.mock('../AgentCard', () => {
|
||||
return function MockAgentCard({ agent, onClick }: { agent: t.Agent; onClick: () => void }) {
|
||||
return (
|
||||
<div data-testid={`agent-card-${agent.id}`} onClick={onClick}>
|
||||
<h3>{agent.name}</h3>
|
||||
<p>{agent.description}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
describe('VirtualizedAgentGrid', () => {
|
||||
let queryClient: QueryClient;
|
||||
|
||||
beforeEach(() => {
|
||||
queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const renderComponent = (props = {}) => {
|
||||
const defaultProps = {
|
||||
category: 'all',
|
||||
searchQuery: '',
|
||||
onSelectAgent: jest.fn(),
|
||||
};
|
||||
|
||||
return render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<VirtualizedAgentGrid {...defaultProps} {...props} />
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
};
|
||||
|
||||
it('renders virtual list container', async () => {
|
||||
renderComponent();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('virtual-list')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('displays agent cards in virtual rows', async () => {
|
||||
renderComponent();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('agent-card-1')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('agent-card-2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.getByText('Test Agent 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Test Agent 2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onSelectAgent when agent card is clicked', async () => {
|
||||
const onSelectAgent = jest.fn();
|
||||
renderComponent({ onSelectAgent });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('agent-card-1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
screen.getByTestId('agent-card-1').click();
|
||||
|
||||
expect(onSelectAgent).toHaveBeenCalledWith({
|
||||
id: '1',
|
||||
name: 'Test Agent 1',
|
||||
description: 'A test agent for virtual scrolling',
|
||||
category: 'productivity',
|
||||
});
|
||||
});
|
||||
|
||||
it('shows loading spinner when loading', async () => {
|
||||
const mockQuery = jest.fn(() => ({
|
||||
...mockInfiniteQuery,
|
||||
isLoading: true,
|
||||
data: undefined,
|
||||
}));
|
||||
|
||||
const useMarketplaceAgentsInfiniteQuery =
|
||||
jest.requireMock('~/data-provider/Agents').useMarketplaceAgentsInfiniteQuery;
|
||||
useMarketplaceAgentsInfiniteQuery.mockImplementation(mockQuery);
|
||||
|
||||
renderComponent();
|
||||
|
||||
// Should show loading spinner
|
||||
const spinner = document.querySelector('.spinner');
|
||||
expect(spinner).toBeInTheDocument();
|
||||
expect(spinner).toHaveClass('h-8 w-8 text-primary');
|
||||
});
|
||||
|
||||
it('has proper accessibility attributes', async () => {
|
||||
// Reset the mock to ensure we have data
|
||||
const useMarketplaceAgentsInfiniteQuery =
|
||||
jest.requireMock('~/data-provider/Agents').useMarketplaceAgentsInfiniteQuery;
|
||||
useMarketplaceAgentsInfiniteQuery.mockImplementation(() => mockInfiniteQuery);
|
||||
|
||||
renderComponent({ category: 'productivity' });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('virtual-list')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const gridContainer = screen.getByRole('grid');
|
||||
expect(gridContainer).toHaveAttribute('aria-label');
|
||||
expect(gridContainer.getAttribute('aria-label')).toContain('2');
|
||||
expect(gridContainer.getAttribute('aria-label')).toContain('Productivity');
|
||||
|
||||
const tabpanel = screen.getByRole('tabpanel');
|
||||
expect(tabpanel).toHaveAttribute('id', 'category-panel-productivity');
|
||||
expect(tabpanel).toHaveAttribute('aria-labelledby', 'category-tab-productivity');
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue