🧪 ci: Resolve DataTable test infinite re-render (#13947)
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

DataTable.spec failed with "Too many re-renders" (35 tests). Root cause: @tanstack/react-virtual is measurement-driven, and jsdom has no real layout, so its re-render loop never converges. This went unnoticed because packages/client had no jest CI job (only the client workspace runs jest in frontend-review.yml).

- DataTable: only read the virtualizer (getVirtualItems/getTotalSize) when virtualization is active; the non-virtualized branch renders rows directly, so engaging it for small tables was wasted render-phase work.
- Spec: mock @tanstack/react-virtual, since jsdom can't exercise real virtualization layout.
- Add a test:ci script to @librechat/client and a Tests: @librechat/client CI job so packages/client specs run on every frontend PR.
This commit is contained in:
Danny Avila 2026-06-24 23:40:18 -04:00 committed by GitHub
parent 5c5ef37e30
commit 03ecac8ac1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 77 additions and 6 deletions

View file

@ -122,6 +122,44 @@ jobs:
run: npm run typecheck
working-directory: client
test-packages-client:
name: 'Tests: @librechat/client'
needs: build
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- name: Use Node.js 24.16.0
uses: actions/setup-node@v4
with:
node-version: '24.16.0'
- name: Restore node_modules cache
id: cache-node-modules
uses: actions/cache@v4
with:
path: |
node_modules
client/node_modules
packages/client/node_modules
packages/data-provider/node_modules
key: node-modules-frontend-${{ runner.os }}-24.16.0-${{ hashFiles('package-lock.json') }}
- name: Install dependencies
if: steps.cache-node-modules.outputs.cache-hit != 'true'
run: npm ci
- name: Download data-provider build
uses: actions/download-artifact@v4
with:
name: build-data-provider
path: packages/data-provider/dist
- name: Run unit tests
run: npm run test:ci
working-directory: packages/client
test-ubuntu:
name: 'Tests: Ubuntu (shard ${{ matrix.shard }}/4)'
needs: build

View file

@ -33,7 +33,9 @@
"build:dev": "npm run clean && NODE_ENV=development tsdown",
"b:build": "bun run b:clean && bun run tsdown",
"build:watch": "tsdown --watch",
"dev": "tsdown --watch"
"dev": "tsdown --watch",
"test": "jest",
"test:ci": "jest --ci"
},
"peerDependencies": {
"@ariakit/react": "^0.4.29",

View file

@ -1,9 +1,9 @@
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { Provider as JotaiProvider } from 'jotai';
import DataTable from './DataTable';
import type { TableColumn } from './DataTable.types';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import type { SortingState } from '@tanstack/react-table';
import type { TableColumn } from './DataTable.types';
import DataTable from './DataTable';
// Mock utilities
jest.mock('~/utils', () => ({
@ -30,6 +30,35 @@ jest.mock('~/hooks', () => ({
useMediaQuery: jest.fn(() => false),
}));
// jsdom can't measure layout, so @tanstack/react-virtual's measurement-driven re-render loop
// never converges (infinite "Too many re-renders"). Stub it to render every row deterministically.
jest.mock('@tanstack/react-virtual', () => ({
useVirtualizer: ({
count,
estimateSize,
}: {
count: number;
estimateSize?: (index: number) => number;
}) => {
const size = estimateSize ? estimateSize(0) : 56;
return {
getVirtualItems: () =>
Array.from({ length: count }, (_, index) => ({
index,
key: index,
start: index * size,
end: (index + 1) * size,
size,
lane: 0,
})),
getTotalSize: () => count * size,
calculateRange: () => {},
measureElement: () => {},
scrollToIndex: () => {},
};
},
}));
// Mock svgs
jest.mock('~/svgs', () => ({
Spinner: ({ className }: { className?: string }) => (

View file

@ -271,8 +271,10 @@ function DataTable<TData extends Record<string, unknown>, TValue>({
overscan: dynamicOverscan,
});
const virtualRows = rowVirtualizer.getVirtualItems();
const totalSize = rowVirtualizer.getTotalSize();
// Only read the virtualizer when active; the non-virtualized branch renders rows directly,
// so engaging it for small tables is wasted render-phase work.
const virtualRows = virtualizationActive ? rowVirtualizer.getVirtualItems() : [];
const totalSize = virtualizationActive ? rowVirtualizer.getTotalSize() : 0;
const paddingTop = virtualRows[0]?.start ?? 0;
const paddingBottom =
virtualRows.length > 0 ? totalSize - (virtualRows[virtualRows.length - 1]?.end ?? 0) : 0;