mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-07-05 05:41:59 +00:00
* 🎨 feat: Enhance Import Conversations UI with loading state and new localization key * fix: Correct pluralization in selected items message in translation.json * Refactor Chat Input File Table Headers to Use SortFilterHeader Component - Replaced button-based sorting headers in the Chat Input Files Table with a new SortFilterHeader component for better code organization and consistency. - Updated the header for filename, updatedAt, and bytes columns to utilize the new component. Enhance Navigation Component with Skeleton Loading States - Added Skeleton loading states to the Nav component for better user experience during data fetching. - Updated Suspense fallbacks for AgentMarketplaceButton and BookmarkNav components to display Skeletons. Refactor Avatar Component for Improved UI - Enhanced the Avatar component by adding a Label for drag-and-drop functionality. - Improved styling and structure for the file upload area. Update Shared Links Component for Better Error Handling and Sorting - Improved error handling in the Shared Links component for fetching next pages and deleting shared links. - Simplified the header rendering for sorting columns and added sorting functionality to the title and createdAt columns. Refactor Archived Chats Component - Merged ArchivedChats and ArchivedChatsTable components into a single ArchivedChats component for better maintainability. - Implemented sorting and searching functionality with debouncing for improved performance. - Enhanced the UI with better loading states and error handling. Update DataTable Component for Sorting Icons - Added sorting icons (ChevronUp, ChevronDown, ChevronsUpDown) to the DataTable headers for better visual feedback on sorting state. Localization Updates - Updated translation.json to fix missing translations and improve existing ones for better user experience. * ✨ feat: Update DataTable component to streamline props and enhance sorting icons * fix: TS issues * feat: polish and redefine DataTable + shared links and archived chats * feat: enhance DataTable with column pinning and improve sorting functionality * feat: enhance deepEqual function for array support and improve column style stability * refactor: DataTable and ArchivedChats; fix: sorting ArchivedChats API * feat(DataTable): Implement new DataTable component with hooks and optimized features - Added DataTable component with support for virtual scrolling, row selection, and customizable columns. - Introduced hooks for debouncing search input, managing row selection, and calculating column styles. - Enhanced accessibility with keyboard navigation and selection checkboxes. - Implemented skeleton loading state for better user experience during data fetching. - Added DataTableSearch component for filtering data with debounced input. - Created utility logger for improved debugging in development. - Updated translations to support new UI elements and actions. * refactor: update SharedLinks and ArchivedChats to use desktopOnly instead of hideOnMobile; remove unused DataTableColumnHeader component * fix: ensure desktopOnly columns are hidden on mobile in DataTable * refactor: reorganize imports in DataTable components and update index exports * refactor: improve styling and animations in Artifacts, ArtifactsSubMenu, and MCPSubMenu components; update border-radius in style.css * refactor(Artifacts): enhance button toggle functionality and manage expanded state with useEffect * refactor: comment out desktopOnly property in SharedLinks and ArchivedChats components; update translation.json with new keys for link actions * refactor(DataTable): streamline column visibility logic and enhance type definitions; improve cleanup timers and optimize rendering * refactor(DataTable): enhance type definitions for processed data rows and update custom actions renderer type * refactor(DataTable): optimize processed data handling and improve warning for missing IDs; streamline DataTableComponents imports * refactor(DataTable): enhance accessibility features and improve localization for selection and loading states * refactor: improve padding in dialog content and enhance row selection functionality in ArchivedChats and DataTable components * refactor(DataTable): remove unnecessary role and tabindex attributes from select all button for improved accessibility * refactor(translation): remove outdated error messages and unused UI strings for cleaner localization * refactor(DataTable): enhance virtualization and scrolling performance with dynamic overscan adjustments * refactor(DataTableErrorBoundary): enhance error handling and localization support * refactor(DataTable): improve column sizing and visibility handling; remove deprecated features * refactor: enhance UI components with improved class handling and state management * refactor(DataTable): improve column width handling and responsiveness; disable row selection * refactor(DataTable): enhance accessibility with row header support and improve column visibility handling * chore(DataTable): comments update * refactor(Table): add unwrapped prop for direct table rendering; adjust minWidth calculation for responsiveness * refactor(DataTable): simplify search handling by removing unnecessary trimming; adjust column width handling for better responsiveness * refactor(translation): remove redundant drag and drop UI text for clarity * refactor(parsers): change uiResources to a constant and streamline artifacts handling * chore: remove unused file, bump @librechat/client to 0.3.2; fix(SharedLinks): missing import; * refactor: change button variant from destructive to ghost for delete actions in SharedLinks and ArchivedChats components * refactor(DataTable): simplify aria-sort assignment for better readability * refactor(DataTable): update aria-label and ariaLabel to use indexed placeholder for localization * refactor(translation): update no data messages for consistency * Refactor code structure for improved readability and maintainability * chore: restore linting fixes * chore: restore linting fixes 2; refactor: remove unused translation keys * feat(tests): add unit tests for DataTable components and error handling - Implement tests for SelectionCheckbox and SkeletonRows components in DataTable. - Add tests for DataTableErrorBoundary to ensure proper error handling and UI rendering. - Create tests for DataTableSearch to validate search functionality and accessibility. - Update DialogTemplate tests to reflect hardcoded cancel text. - Remove redundant IntersectionObserver mock in SplitText tests. - Unmock react-i18next in Translation tests to validate actual i18n functionality. * refactor: Remove jest-environment-jsdom dependency from package.json; fix: reset package-lock * chore: revert lint fixes * chore: clean up package.json by removing unused devDependencies and redundant test scripts * chore: update package dependencies in package.json and package-lock.json - Added new devDependencies: @babel/core, @babel/preset-env, @babel/preset-react, @babel/preset-typescript, @tanstack/react-table, @tanstack/react-virtual, @testing-library/jest-dom, identity-obj-proxy, jest, jest-environment-jsdom, and lucide-react. - Updated existing devDependencies to their latest versions. - Added new module @asamuzakjp/css-color to package-lock.json with its dependencies. - Updated version of @babel/plugin-transform-destructuring and added @babel/plugin-transform-explicit-resource-management in package-lock.json. --------- Co-authored-by: Danny Avila <danny@librechat.ai>
311 lines
8.5 KiB
TypeScript
311 lines
8.5 KiB
TypeScript
import React from 'react';
|
|
import { render, screen, fireEvent } from '@testing-library/react';
|
|
import { DataTableErrorBoundary } from './DataTableErrorBoundary';
|
|
|
|
// Mock the logger
|
|
jest.mock('~/utils', () => ({
|
|
logger: {
|
|
error: jest.fn(),
|
|
log: jest.fn(),
|
|
warn: jest.fn(),
|
|
},
|
|
}));
|
|
|
|
// Mock lucide-react
|
|
jest.mock('lucide-react', () => ({
|
|
RefreshCw: ({ className }: { className?: string }) => (
|
|
<svg data-testid="refresh-icon" className={className} />
|
|
),
|
|
}));
|
|
|
|
// Mock the Button component
|
|
jest.mock('../Button', () => ({
|
|
Button: ({
|
|
children,
|
|
onClick,
|
|
variant,
|
|
className,
|
|
'aria-label': ariaLabel,
|
|
}: {
|
|
children: React.ReactNode;
|
|
onClick?: () => void;
|
|
variant?: string;
|
|
className?: string;
|
|
'aria-label'?: string;
|
|
}) => (
|
|
<button
|
|
onClick={onClick}
|
|
data-variant={variant}
|
|
className={className}
|
|
aria-label={ariaLabel}
|
|
data-testid="retry-button"
|
|
>
|
|
{children}
|
|
</button>
|
|
),
|
|
}));
|
|
|
|
// Component that throws an error
|
|
const ThrowingComponent = ({ shouldThrow }: { shouldThrow: boolean }) => {
|
|
if (shouldThrow) {
|
|
throw new Error('Test error message');
|
|
}
|
|
return <div data-testid="child-content">Child content</div>;
|
|
};
|
|
|
|
// Suppress console.error for expected errors during tests
|
|
const originalConsoleError = console.error;
|
|
beforeAll(() => {
|
|
console.error = jest.fn();
|
|
});
|
|
|
|
afterAll(() => {
|
|
console.error = originalConsoleError;
|
|
});
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
describe('DataTableErrorBoundary', () => {
|
|
it('should render children when no error', () => {
|
|
render(
|
|
<DataTableErrorBoundary>
|
|
<ThrowingComponent shouldThrow={false} />
|
|
</DataTableErrorBoundary>,
|
|
);
|
|
|
|
expect(screen.getByTestId('child-content')).toBeInTheDocument();
|
|
expect(screen.getByText('Child content')).toBeInTheDocument();
|
|
});
|
|
|
|
it('should catch errors and display fallback UI', () => {
|
|
render(
|
|
<DataTableErrorBoundary>
|
|
<ThrowingComponent shouldThrow={true} />
|
|
</DataTableErrorBoundary>,
|
|
);
|
|
|
|
expect(screen.queryByTestId('child-content')).not.toBeInTheDocument();
|
|
expect(screen.getByRole('alert')).toBeInTheDocument();
|
|
});
|
|
|
|
it('should show error title', () => {
|
|
render(
|
|
<DataTableErrorBoundary>
|
|
<ThrowingComponent shouldThrow={true} />
|
|
</DataTableErrorBoundary>,
|
|
);
|
|
|
|
// The title comes from localize which returns the key
|
|
expect(screen.getByText('com_ui_table_error')).toBeInTheDocument();
|
|
});
|
|
|
|
it('should show error description', () => {
|
|
render(
|
|
<DataTableErrorBoundary>
|
|
<ThrowingComponent shouldThrow={true} />
|
|
</DataTableErrorBoundary>,
|
|
);
|
|
|
|
expect(screen.getByText('com_ui_table_error_description')).toBeInTheDocument();
|
|
});
|
|
|
|
it('should render retry button', () => {
|
|
render(
|
|
<DataTableErrorBoundary>
|
|
<ThrowingComponent shouldThrow={true} />
|
|
</DataTableErrorBoundary>,
|
|
);
|
|
|
|
expect(screen.getByTestId('retry-button')).toBeInTheDocument();
|
|
expect(screen.getByText('com_ui_retry')).toBeInTheDocument();
|
|
});
|
|
|
|
it('should reset error state when retry button is clicked', () => {
|
|
const { rerender } = render(
|
|
<DataTableErrorBoundary>
|
|
<ThrowingComponent shouldThrow={true} />
|
|
</DataTableErrorBoundary>,
|
|
);
|
|
|
|
// Error state should be shown
|
|
expect(screen.getByRole('alert')).toBeInTheDocument();
|
|
|
|
// First rerender with non-throwing component (error boundary still shows fallback
|
|
// because it hasn't reset yet)
|
|
rerender(
|
|
<DataTableErrorBoundary>
|
|
<ThrowingComponent shouldThrow={false} />
|
|
</DataTableErrorBoundary>,
|
|
);
|
|
|
|
// Still showing error (because error boundary hasn't reset)
|
|
expect(screen.getByRole('alert')).toBeInTheDocument();
|
|
|
|
// Click retry to reset error boundary state
|
|
fireEvent.click(screen.getByTestId('retry-button'));
|
|
|
|
// Now children should render without throwing
|
|
expect(screen.getByTestId('child-content')).toBeInTheDocument();
|
|
});
|
|
|
|
it('should call onError callback with error', () => {
|
|
const mockOnError = jest.fn();
|
|
|
|
render(
|
|
<DataTableErrorBoundary onError={mockOnError}>
|
|
<ThrowingComponent shouldThrow={true} />
|
|
</DataTableErrorBoundary>,
|
|
);
|
|
|
|
expect(mockOnError).toHaveBeenCalledTimes(1);
|
|
expect(mockOnError).toHaveBeenCalledWith(expect.any(Error));
|
|
expect(mockOnError.mock.calls[0][0].message).toBe('Test error message');
|
|
});
|
|
|
|
it('should call onReset callback on retry', () => {
|
|
const mockOnReset = jest.fn();
|
|
|
|
render(
|
|
<DataTableErrorBoundary onReset={mockOnReset}>
|
|
<ThrowingComponent shouldThrow={true} />
|
|
</DataTableErrorBoundary>,
|
|
);
|
|
|
|
fireEvent.click(screen.getByTestId('retry-button'));
|
|
|
|
expect(mockOnReset).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('should have proper ARIA attributes on error card', () => {
|
|
render(
|
|
<DataTableErrorBoundary>
|
|
<ThrowingComponent shouldThrow={true} />
|
|
</DataTableErrorBoundary>,
|
|
);
|
|
|
|
const alertElement = screen.getByRole('alert');
|
|
expect(alertElement).toHaveAttribute('aria-live', 'assertive');
|
|
expect(alertElement).toHaveAttribute('aria-labelledby', 'datatable-error-title');
|
|
expect(alertElement).toHaveAttribute('aria-describedby', 'datatable-error-desc');
|
|
});
|
|
|
|
it('should have proper id on title element', () => {
|
|
render(
|
|
<DataTableErrorBoundary>
|
|
<ThrowingComponent shouldThrow={true} />
|
|
</DataTableErrorBoundary>,
|
|
);
|
|
|
|
const title = screen.getByText('com_ui_table_error');
|
|
expect(title).toHaveAttribute('id', 'datatable-error-title');
|
|
});
|
|
|
|
it('should have proper id on description element', () => {
|
|
render(
|
|
<DataTableErrorBoundary>
|
|
<ThrowingComponent shouldThrow={true} />
|
|
</DataTableErrorBoundary>,
|
|
);
|
|
|
|
const description = screen.getByText('com_ui_table_error_description');
|
|
expect(description).toHaveAttribute('id', 'datatable-error-desc');
|
|
});
|
|
|
|
it('should render refresh icon in error state', () => {
|
|
render(
|
|
<DataTableErrorBoundary>
|
|
<ThrowingComponent shouldThrow={true} />
|
|
</DataTableErrorBoundary>,
|
|
);
|
|
|
|
const refreshIcons = screen.getAllByTestId('refresh-icon');
|
|
expect(refreshIcons.length).toBeGreaterThanOrEqual(1);
|
|
});
|
|
|
|
it('should have aria-label on retry button', () => {
|
|
render(
|
|
<DataTableErrorBoundary>
|
|
<ThrowingComponent shouldThrow={true} />
|
|
</DataTableErrorBoundary>,
|
|
);
|
|
|
|
const retryButton = screen.getByTestId('retry-button');
|
|
expect(retryButton).toHaveAttribute('aria-label', 'Retry loading table');
|
|
});
|
|
|
|
it('should render with outline variant button', () => {
|
|
render(
|
|
<DataTableErrorBoundary>
|
|
<ThrowingComponent shouldThrow={true} />
|
|
</DataTableErrorBoundary>,
|
|
);
|
|
|
|
const retryButton = screen.getByTestId('retry-button');
|
|
expect(retryButton).toHaveAttribute('data-variant', 'outline');
|
|
});
|
|
|
|
it('should handle multiple consecutive errors', () => {
|
|
const mockOnError = jest.fn();
|
|
|
|
const { rerender } = render(
|
|
<DataTableErrorBoundary onError={mockOnError}>
|
|
<ThrowingComponent shouldThrow={true} />
|
|
</DataTableErrorBoundary>,
|
|
);
|
|
|
|
expect(mockOnError).toHaveBeenCalledTimes(1);
|
|
|
|
// Reset and throw again
|
|
fireEvent.click(screen.getByTestId('retry-button'));
|
|
|
|
rerender(
|
|
<DataTableErrorBoundary onError={mockOnError}>
|
|
<ThrowingComponent shouldThrow={true} />
|
|
</DataTableErrorBoundary>,
|
|
);
|
|
|
|
// Should have caught the error again
|
|
expect(screen.getByRole('alert')).toBeInTheDocument();
|
|
});
|
|
|
|
it('should not call onError when no error occurs', () => {
|
|
const mockOnError = jest.fn();
|
|
|
|
render(
|
|
<DataTableErrorBoundary onError={mockOnError}>
|
|
<ThrowingComponent shouldThrow={false} />
|
|
</DataTableErrorBoundary>,
|
|
);
|
|
|
|
expect(mockOnError).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should handle children that return null', () => {
|
|
const NullComponent = () => null;
|
|
|
|
render(
|
|
<DataTableErrorBoundary>
|
|
<NullComponent />
|
|
</DataTableErrorBoundary>,
|
|
);
|
|
|
|
// Should not show error state
|
|
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('should handle nested children', () => {
|
|
render(
|
|
<DataTableErrorBoundary>
|
|
<div>
|
|
<span>Nested content</span>
|
|
<ThrowingComponent shouldThrow={false} />
|
|
</div>
|
|
</DataTableErrorBoundary>,
|
|
);
|
|
|
|
expect(screen.getByText('Nested content')).toBeInTheDocument();
|
|
expect(screen.getByTestId('child-content')).toBeInTheDocument();
|
|
});
|
|
});
|