LibreChat/client/src/hooks/Messages/useCopyToClipboard.spec.ts
Danny Avila b2f44fc90f
🧩 feat: Web Search Config Validations & Clipboard Citation Processing (#7530)
* 🔧 chore: Add missing optional `scraperTimeout` to webSearchSchema

* chore: Add missing optional `scraperTimeout` to web search authentication result

* chore: linting

* feat: Integrate attachment handling and citation processing in message components

- Added `useAttachments` hook to manage message attachments and search results.
- Updated `MessageParts`, `ContentParts`, and `ContentRender` components to utilize the new hook for improved attachment handling.
- Enhanced `useCopyToClipboard` to format citations correctly, including support for composite citations and deduplication.
- Introduced utility functions for citation processing and cleanup.
- Added tests for the new `useCopyToClipboard` functionality to ensure proper citation formatting and handling.

* feat: Add configuration for LibreChat Code Interpreter API and Web Search variables

* fix: Update searchResults type to use SearchResultData for better type safety

* feat: Add web search configuration validation and logging

- Introduced `checkWebSearchConfig` function to validate web search configuration values, ensuring they are environment variable references.
- Added logging for proper configuration and warnings for incorrect values.
- Created unit tests for `checkWebSearchConfig` to cover various scenarios, including valid and invalid configurations.

* docs: Update README to include Web Search feature details

- Added a section for the Web Search feature, highlighting its capabilities to search the internet and enhance AI context.
- Included links for further information on the Web Search functionality.

* ci: Add mock for checkWebSearchConfig in AppService tests

* chore: linting

* feat: Enhance Shared Messages with Web Search UI by adding searchResults prop to SearchContent and MinimalHoverButtons components

* chore: linting

* refactor: remove Meilisearch index sync from importConversations function

* feat: update safeSearch implementation to use SafeSearchTypes enum

* refactor: remove commented-out code in loadTools function

* fix: ensure responseMessageId handles latestMessage ID correctly

* feat: enhance Vite configuration for improved chunking and caching

- Added additional globIgnores for map files in Workbox configuration.
- Implemented high-impact chunking for various large libraries to optimize performance.
- Increased chunkSizeWarningLimit from 1200 to 1500 for better handling of larger chunks.

* refactor: move health check hook to Root, fix bad setState for Temporary state

- Enhanced the `useHealthCheck` hook to initiate health checks only when the user is authenticated.
- Added logic for managing health check intervals and handling window focus events.
- Introduced a new test suite for `useHealthCheck` to cover various scenarios including authentication state changes and error handling.
- Removed the health check invocation from `ChatRoute` and added it to `Root` for global health monitoring.

* fix: update font alias in Vite configuration for correct path resolution
2025-05-24 10:23:17 -04:00

494 lines
13 KiB
TypeScript

import { renderHook, act } from '@testing-library/react';
import copy from 'copy-to-clipboard';
import { ContentTypes } from 'librechat-data-provider';
import type {
SearchResultData,
ProcessedOrganic,
TMessageContentParts,
} from 'librechat-data-provider';
import useCopyToClipboard from '~/hooks/Messages/useCopyToClipboard';
// Mock the copy-to-clipboard module
jest.mock('copy-to-clipboard');
describe('useCopyToClipboard', () => {
const mockSetIsCopied = jest.fn();
const mockCopy = copy as jest.MockedFunction<typeof copy>;
beforeEach(() => {
jest.clearAllMocks();
jest.useFakeTimers();
});
afterEach(() => {
jest.runOnlyPendingTimers();
jest.useRealTimers();
});
describe('Basic functionality', () => {
it('should copy plain text without citations', () => {
const { result } = renderHook(() =>
useCopyToClipboard({
text: 'Simple text without citations',
}),
);
act(() => {
result.current(mockSetIsCopied);
});
expect(mockCopy).toHaveBeenCalledWith('Simple text without citations', {
format: 'text/plain',
});
expect(mockSetIsCopied).toHaveBeenCalledWith(true);
});
it('should handle content array with text types', () => {
const content = [
{ type: ContentTypes.TEXT, text: 'First line' },
{ type: ContentTypes.TEXT, text: 'Second line' },
];
const { result } = renderHook(() =>
useCopyToClipboard({
content: content as TMessageContentParts[],
}),
);
act(() => {
result.current(mockSetIsCopied);
});
expect(mockCopy).toHaveBeenCalledWith('First line\nSecond line', {
format: 'text/plain',
});
});
it('should reset isCopied after timeout', () => {
const { result } = renderHook(() =>
useCopyToClipboard({
text: 'Test text',
}),
);
act(() => {
result.current(mockSetIsCopied);
});
expect(mockSetIsCopied).toHaveBeenCalledWith(true);
act(() => {
jest.advanceTimersByTime(3000);
});
expect(mockSetIsCopied).toHaveBeenCalledWith(false);
});
});
describe('Citation formatting', () => {
const mockSearchResults: { [key: string]: SearchResultData } = {
'0': {
organic: [
{
link: 'https://example.com/search1',
title: 'Search Result 1',
snippet: 'This is a search result',
},
],
topStories: [
{
link: 'https://example.com/news1',
title: 'News Story 1',
},
{
link: 'https://example.com/news2',
title: 'News Story 2',
},
],
images: [
{
link: 'https://example.com/image1',
title: 'Image 1',
},
],
videos: [
{
link: 'https://example.com/video1',
title: 'Video 1',
},
],
},
};
it('should format standalone search citations', () => {
const text = 'This is a fact \\ue202turn0search0 from search.';
const { result } = renderHook(() =>
useCopyToClipboard({
text,
searchResults: mockSearchResults,
}),
);
act(() => {
result.current(mockSetIsCopied);
});
const expectedText = `This is a fact [1] from search.
Citations:
[1] https://example.com/search1
`;
expect(mockCopy).toHaveBeenCalledWith(expectedText, { format: 'text/plain' });
});
it('should format news citations with correct mapping', () => {
const text = 'Breaking news \\ue202turn0news0 and more news \\ue202turn0news1.';
const { result } = renderHook(() =>
useCopyToClipboard({
text,
searchResults: mockSearchResults,
}),
);
act(() => {
result.current(mockSetIsCopied);
});
const expectedText = `Breaking news [1] and more news [2].
Citations:
[1] https://example.com/news1
[2] https://example.com/news2
`;
expect(mockCopy).toHaveBeenCalledWith(expectedText, { format: 'text/plain' });
});
it('should handle highlighted text with citations', () => {
const text = '\\ue203This is highlighted text\\ue204 \\ue202turn0search0 with citation.';
const { result } = renderHook(() =>
useCopyToClipboard({
text,
searchResults: mockSearchResults,
}),
);
act(() => {
result.current(mockSetIsCopied);
});
const expectedText = `**This is highlighted text** [1] with citation.
Citations:
[1] https://example.com/search1
`;
expect(mockCopy).toHaveBeenCalledWith(expectedText, { format: 'text/plain' });
});
it('should handle composite citations', () => {
const text =
'Multiple sources \\ue200\\ue202turn0search0\\ue202turn0news0\\ue202turn0news1\\ue201.';
const { result } = renderHook(() =>
useCopyToClipboard({
text,
searchResults: mockSearchResults,
}),
);
act(() => {
result.current(mockSetIsCopied);
});
const expectedText = `Multiple sources [1][2][3].
Citations:
[1] https://example.com/search1
[2] https://example.com/news1
[3] https://example.com/news2
`;
expect(mockCopy).toHaveBeenCalledWith(expectedText, { format: 'text/plain' });
});
});
describe('Citation deduplication', () => {
it('should use same number for duplicate URLs', () => {
const mockSearchResultsWithDupes: { [key: string]: SearchResultData } = {
'0': {
organic: [
{
link: 'https://example.com/article',
title: 'Article from search',
},
],
topStories: [
{
link: 'https://example.com/article', // Same URL
title: 'Article from news',
},
],
},
};
const text = 'First citation \\ue202turn0search0 and second \\ue202turn0news0.';
const { result } = renderHook(() =>
useCopyToClipboard({
text,
searchResults: mockSearchResultsWithDupes,
}),
);
act(() => {
result.current(mockSetIsCopied);
});
const expectedText = `First citation [1] and second [1].
Citations:
[1] https://example.com/article
`;
expect(mockCopy).toHaveBeenCalledWith(expectedText, { format: 'text/plain' });
});
it('should handle multiple citations of the same source', () => {
const mockSearchResults: { [key: string]: SearchResultData } = {
'0': {
organic: [
{
link: 'https://example.com/source1',
title: 'Source 1',
},
],
},
};
const text =
'First mention \\ue202turn0search0. Second mention \\ue202turn0search0. Third \\ue202turn0search0.';
const { result } = renderHook(() =>
useCopyToClipboard({
text,
searchResults: mockSearchResults,
}),
);
act(() => {
result.current(mockSetIsCopied);
});
const expectedText = `First mention [1]. Second mention [1]. Third [1].
Citations:
[1] https://example.com/source1
`;
expect(mockCopy).toHaveBeenCalledWith(expectedText, { format: 'text/plain' });
});
});
describe('Edge cases', () => {
it('should handle missing search results gracefully', () => {
const text = 'Text with citation \\ue202turn0search0 but no data.';
const { result } = renderHook(() =>
useCopyToClipboard({
text,
searchResults: {},
}),
);
act(() => {
result.current(mockSetIsCopied);
});
// Updated expectation: Citation marker should be removed
expect(mockCopy).toHaveBeenCalledWith('Text with citation but no data.', {
format: 'text/plain',
});
});
it('should handle invalid citation indices', () => {
const mockSearchResults: { [key: string]: SearchResultData } = {
'0': {
organic: [
{
link: 'https://example.com/search1',
title: 'Search Result 1',
},
],
},
};
const text = 'Valid citation \\ue202turn0search0 and invalid \\ue202turn0search5.';
const { result } = renderHook(() =>
useCopyToClipboard({
text,
searchResults: mockSearchResults,
}),
);
act(() => {
result.current(mockSetIsCopied);
});
// Updated expectation: Invalid citation marker should be removed
const expectedText = `Valid citation [1] and invalid.
Citations:
[1] https://example.com/search1
`;
expect(mockCopy).toHaveBeenCalledWith(expectedText, { format: 'text/plain' });
});
it('should handle citations without links', () => {
const mockSearchResults: { [key: string]: SearchResultData } = {
'0': {
organic: [
{
title: 'No link source',
// No link property
} as ProcessedOrganic,
],
},
};
const text = 'Citation without link \\ue202turn0search0.';
const { result } = renderHook(() =>
useCopyToClipboard({
text,
searchResults: mockSearchResults,
}),
);
act(() => {
result.current(mockSetIsCopied);
});
// Updated expectation: Citation marker without link should be removed
expect(mockCopy).toHaveBeenCalledWith('Citation without link.', {
format: 'text/plain',
});
});
it('should clean up orphaned citation lists at the end', () => {
const mockSearchResults: { [key: string]: SearchResultData } = {
'0': {
organic: [
{ link: 'https://example.com/1', title: 'Source 1' },
{ link: 'https://example.com/2', title: 'Source 2' },
],
},
};
const text = 'Text with citations \\ue202turn0search0.\n\n[1][2]';
const { result } = renderHook(() =>
useCopyToClipboard({
text,
searchResults: mockSearchResults,
}),
);
act(() => {
result.current(mockSetIsCopied);
});
const expectedText = `Text with citations [1].
Citations:
[1] https://example.com/1
`;
expect(mockCopy).toHaveBeenCalledWith(expectedText, { format: 'text/plain' });
});
});
describe('All citation types', () => {
const mockSearchResults: { [key: string]: SearchResultData } = {
'0': {
organic: [{ link: 'https://example.com/search', title: 'Search' }],
topStories: [{ link: 'https://example.com/news', title: 'News' }],
images: [{ link: 'https://example.com/image', title: 'Image' }],
videos: [{ link: 'https://example.com/video', title: 'Video' }],
references: [{ link: 'https://example.com/ref', title: 'Reference', type: 'link' }],
},
};
it('should handle all citation types correctly', () => {
const text =
'Search \\ue202turn0search0, news \\ue202turn0news0, image \\ue202turn0image0, video \\ue202turn0video0, ref \\ue202turn0ref0.';
const { result } = renderHook(() =>
useCopyToClipboard({
text,
searchResults: mockSearchResults,
}),
);
act(() => {
result.current(mockSetIsCopied);
});
const expectedText = `Search [1], news [2], image [3], video [4], ref [5].
Citations:
[1] https://example.com/search
[2] https://example.com/news
[3] https://example.com/image
[4] https://example.com/video
[5] https://example.com/ref
`;
expect(mockCopy).toHaveBeenCalledWith(expectedText, { format: 'text/plain' });
});
});
describe('Complex scenarios', () => {
it('should handle mixed highlighted text and composite citations', () => {
const mockSearchResults: { [key: string]: SearchResultData } = {
'0': {
organic: [
{ link: 'https://example.com/1', title: 'Source 1' },
{ link: 'https://example.com/2', title: 'Source 2' },
],
topStories: [{ link: 'https://example.com/3', title: 'News 1' }],
},
};
const text =
'\\ue203Highlighted text with citation\\ue204 \\ue202turn0search0 and composite \\ue200\\ue202turn0search1\\ue202turn0news0\\ue201.';
const { result } = renderHook(() =>
useCopyToClipboard({
text,
searchResults: mockSearchResults,
}),
);
act(() => {
result.current(mockSetIsCopied);
});
const expectedText = `**Highlighted text with citation** [1] and composite [2][3].
Citations:
[1] https://example.com/1
[2] https://example.com/2
[3] https://example.com/3
`;
expect(mockCopy).toHaveBeenCalledWith(expectedText, { format: 'text/plain' });
});
});
});