🧪 ci: Stabilize Virtualized Agent Grid Tests (#13214)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions

This commit is contained in:
Danny Avila 2026-05-20 14:41:36 -04:00 committed by GitHub
parent 9cb650d1d8
commit 8310e9a840
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 219 additions and 152 deletions

View file

@ -2,13 +2,52 @@ 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';
import VirtualizedAgentGrid from '../VirtualizedAgentGrid';
type RowRendererProps = {
index: number;
key: string;
style: React.CSSProperties;
parent: { props: { width: number } };
};
type VirtualListMockProps = {
rowRenderer: (props: RowRendererProps) => React.ReactNode;
rowCount: number;
width?: number;
style?: React.CSSProperties;
'aria-rowcount'?: number;
'data-testid'?: string;
'data-total-rows'?: number;
};
type WindowScrollerChildProps = {
height: number;
isScrolling: boolean;
registerChild: (ref: HTMLElement | null) => void;
onChildScroll: () => void;
scrollTop: number;
};
type LocalizeParams = {
count?: number;
category?: string;
};
type MockAgentCardProps = {
agent: {
id: string;
name?: string;
description?: string;
};
};
// Mock react-virtualized for performance testing
const mockRowRenderer = jest.fn();
jest.mock('react-virtualized', () => {
const ReactActual = jest.requireActual<typeof import('react')>('react');
const mockRowRendererRef = { current: jest.fn() };
return {
@ -24,62 +63,60 @@ jest.mock('react-virtualized', () => {
}
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>
);
},
List: ReactActual.forwardRef(
(
{
rowRenderer,
rowCount,
width,
style,
'aria-rowcount': ariaRowCount,
'data-testid': dataTestId,
'data-total-rows': dataTotalRows,
}: VirtualListMockProps,
ref: React.ForwardedRef<{ forceUpdateGrid: () => void }>,
) => {
ReactActual.useImperativeHandle(ref, () => ({
forceUpdateGrid: () => {},
}));
// 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,
scrollElement: _scrollElement,
}: {
children: (props: any) => React.ReactNode;
children: (props: WindowScrollerChildProps) => React.ReactNode;
scrollElement?: HTMLElement | null;
}) => {
return children({
height: 800,
isScrolling: false,
registerChild: (ref: any) => {},
registerChild: (_ref: HTMLElement | null) => {},
onChildScroll: () => {},
scrollTop: 0,
});
@ -126,7 +163,7 @@ jest.mock('~/hooks', () => ({
{ value: 'development', label: 'Development' },
],
}),
useLocalize: () => (key: string, params?: any) => {
useLocalize: () => (key: string, params?: LocalizeParams) => {
if (key === 'com_agents_grid_announcement') {
return `Found ${params?.count || 0} agents in ${params?.category || 'category'}`;
}
@ -139,7 +176,7 @@ jest.mock('../SmartLoader', () => ({
}));
jest.mock('../AgentCard', () => {
return function MockAgentCard({ agent }: { agent: any }) {
return function MockAgentCard({ agent }: MockAgentCardProps) {
return (
<div data-testid={`agent-card-${agent.id}`} style={{ height: '160px' }}>
<h3>{agent.name}</h3>

View file

@ -1,82 +1,116 @@
import React from 'react';
import { render, screen, waitFor } from '@testing-library/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 t from 'librechat-data-provider';
import VirtualizedAgentGrid from '../VirtualizedAgentGrid';
type RowRendererProps = {
index: number;
key: string;
style: React.CSSProperties;
parent: { props: { width: number } };
};
type VirtualListMockProps = {
rowRenderer: (props: RowRendererProps) => React.ReactNode;
rowCount: number;
width?: number;
style?: React.CSSProperties;
'aria-rowcount'?: number;
'data-testid'?: string;
'data-total-rows'?: number;
};
type WindowScrollerChildProps = {
height: number;
isScrolling: boolean;
registerChild: (ref: HTMLElement | null) => void;
onChildScroll: () => void;
scrollTop: number;
};
type MarketplaceAgentsMock = {
useMarketplaceAgentsInfiniteQuery: jest.Mock;
};
type LocalizeParams = {
count?: number;
category?: string;
};
// 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,
});
},
}));
jest.mock('react-virtualized', () => {
const ReactActual = jest.requireActual<typeof import('react')>('react');
return {
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: ReactActual.forwardRef(
(
{
rowRenderer,
rowCount,
width,
style,
'aria-rowcount': ariaRowCount,
'data-testid': dataTestId,
'data-total-rows': dataTotalRows,
}: VirtualListMockProps,
ref: React.ForwardedRef<{ forceUpdateGrid: () => void }>,
) => {
ReactActual.useImperativeHandle(ref, () => ({
forceUpdateGrid: () => {},
}));
return (
<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: WindowScrollerChildProps) => React.ReactNode;
scrollElement?: HTMLElement | null;
}) => {
return children({
height: 600,
isScrolling: false,
registerChild: (_ref: HTMLElement | null) => {},
onChildScroll: () => {},
scrollTop: 0,
});
},
};
});
// Mock the data provider
const mockInfiniteQuery = {
const createMockInfiniteQuery = (overrides = {}) => ({
data: {
pages: [
{
@ -104,10 +138,11 @@ const mockInfiniteQuery = {
hasNextPage: true,
refetch: jest.fn(),
isFetchingNextPage: false,
};
...overrides,
});
jest.mock('~/data-provider/Agents', () => ({
useMarketplaceAgentsInfiniteQuery: jest.fn(() => mockInfiniteQuery),
useMarketplaceAgentsInfiniteQuery: jest.fn(),
}));
// Mock other hooks
@ -118,7 +153,7 @@ jest.mock('~/hooks', () => ({
{ value: 'development', label: 'Development' },
],
}),
useLocalize: () => (key: string, params?: any) => {
useLocalize: () => (key: string, params?: LocalizeParams) => {
if (key === 'com_agents_grid_announcement') {
return `Found ${params?.count || 0} agents in ${params?.category || 'category'}`;
}
@ -151,9 +186,16 @@ describe('VirtualizedAgentGrid', () => {
mutations: { retry: false },
},
});
const useMarketplaceAgentsInfiniteQuery = (
jest.requireMock('~/data-provider/Agents') as MarketplaceAgentsMock
).useMarketplaceAgentsInfiniteQuery;
useMarketplaceAgentsInfiniteQuery.mockImplementation(() => createMockInfiniteQuery());
});
const renderComponent = (props = {}) => {
const renderComponent = (
props: Partial<React.ComponentProps<typeof VirtualizedAgentGrid>> = {},
) => {
const defaultProps = {
category: 'all',
searchQuery: '',
@ -167,33 +209,27 @@ describe('VirtualizedAgentGrid', () => {
);
};
it('renders virtual list container', async () => {
it('renders virtual list container', () => {
renderComponent();
await waitFor(() => {
expect(screen.getByTestId('virtual-list')).toBeInTheDocument();
});
expect(screen.getByTestId('virtual-list')).toBeInTheDocument();
});
it('displays agent cards in virtual rows', async () => {
it('displays agent cards in virtual rows', () => {
renderComponent();
await waitFor(() => {
expect(screen.getByTestId('agent-card-1')).toBeInTheDocument();
expect(screen.getByTestId('agent-card-2')).toBeInTheDocument();
});
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 () => {
it('calls onSelectAgent when agent card is clicked', () => {
const onSelectAgent = jest.fn();
renderComponent({ onSelectAgent });
await waitFor(() => {
expect(screen.getByTestId('agent-card-1')).toBeInTheDocument();
});
expect(screen.getByTestId('agent-card-1')).toBeInTheDocument();
screen.getByTestId('agent-card-1').click();
@ -205,15 +241,16 @@ describe('VirtualizedAgentGrid', () => {
});
});
it('shows loading spinner when loading', async () => {
it('shows loading spinner when loading', () => {
const mockQuery = jest.fn(() => ({
...mockInfiniteQuery,
...createMockInfiniteQuery(),
isLoading: true,
data: undefined,
}));
const useMarketplaceAgentsInfiniteQuery =
jest.requireMock('~/data-provider/Agents').useMarketplaceAgentsInfiniteQuery;
const useMarketplaceAgentsInfiniteQuery = (
jest.requireMock('~/data-provider/Agents') as MarketplaceAgentsMock
).useMarketplaceAgentsInfiniteQuery;
useMarketplaceAgentsInfiniteQuery.mockImplementation(mockQuery);
renderComponent();
@ -224,17 +261,10 @@ describe('VirtualizedAgentGrid', () => {
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);
it('has proper accessibility attributes', () => {
renderComponent({ category: 'productivity' });
await waitFor(() => {
expect(screen.getByTestId('virtual-list')).toBeInTheDocument();
});
expect(screen.getByTestId('virtual-list')).toBeInTheDocument();
const gridContainer = screen.getByRole('grid');
expect(gridContainer).toHaveAttribute('aria-label');