| 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'; |
|
|
| |
| 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; |
| }) => { |
| |
| if (typeof rowRenderer === 'function') { |
| mockRowRendererRef.current = rowRenderer; |
| mockRowRenderer.mockImplementation(rowRenderer); |
| } |
| |
| const visibleRows = Math.min(10, rowCount); |
| 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, |
| }); |
| }, |
| }; |
| }); |
|
|
| |
| 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; |
| }; |
|
|
| |
| 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, |
| }); |
|
|
| |
| 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); |
|
|
| |
| 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'); |
|
|
| |
| const renderedCards = screen.getAllByTestId(/agent-card-/); |
| expect(renderedCards.length).toBeLessThan(50); |
| expect(renderedCards.length).toBeGreaterThan(0); |
|
|
| |
| const renderTime = endTime - startTime; |
| expect(renderTime).toBeLessThan(740); |
|
|
| 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'); |
|
|
| |
| const renderedCards = screen.getAllByTestId(/agent-card-/); |
| expect(renderedCards.length).toBeLessThan(50); |
| expect(renderedCards.length).toBeGreaterThan(0); |
|
|
| |
| const renderTime = endTime - startTime; |
| expect(renderTime).toBeLessThan(200); |
|
|
| 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', () => { |
| |
| renderComponent(100); |
|
|
| const virtualList = screen.getByTestId('virtual-list'); |
| expect(virtualList).toHaveAttribute('data-total-rows', '50'); |
| }); |
|
|
| it('row renderer is called efficiently', () => { |
| |
| mockRowRenderer.mockClear(); |
|
|
| renderComponent(1000); |
|
|
| |
| const virtualList = screen.getByTestId('virtual-list'); |
| expect(virtualList).toBeInTheDocument(); |
|
|
| |
| |
| const renderedCards = screen.getAllByTestId(/agent-card-/); |
| expect(renderedCards.length).toBeLessThanOrEqual(20); |
| expect(renderedCards.length).toBeGreaterThan(0); |
| }); |
|
|
| it('memory usage remains stable with large datasets', () => { |
| |
| 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(); |
|
|
| |
| |
| 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}`, |
| ); |
| }); |
| }); |
|
|