import { describe, test, expect, vi, beforeEach } from 'vitest'; import { render, screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { BrowserRouter } from 'react-router-dom'; import SearchPage from '../SearchPage'; import { documentService } from '../../services/api'; // Mock the document service const mockDocumentService = { search: vi.fn(), enhancedSearch: vi.fn(), getFacets: vi.fn(), download: vi.fn(), }; vi.mock('../../services/api', () => ({ documentService: mockDocumentService, })); const mockSearchResponse = { data: { documents: [ { id: '1', original_filename: 'invoice_2024.pdf', filename: 'invoice_2024.pdf', file_size: 1024000, mime_type: 'application/pdf', created_at: '2024-01-01T10:00:00Z', has_ocr_text: true, tags: ['invoice', '2024'], snippets: [ { text: 'This is an invoice for services rendered in January 2024.', highlight_ranges: [{ start: 10, end: 17 }, { start: 50, end: 57 }], }, ], search_rank: 0.95, }, { id: '2', original_filename: 'contract_agreement.docx', filename: 'contract_agreement.docx', file_size: 512000, mime_type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', created_at: '2024-01-15T14:30:00Z', has_ocr_text: false, tags: ['contract', 'legal'], snippets: [ { text: 'Contract agreement between parties for invoice processing.', highlight_ranges: [{ start: 0, end: 8 }, { start: 40, end: 47 }], }, ], search_rank: 0.87, }, ], total: 2, query_time_ms: 45, suggestions: ['invoice processing', 'invoice payment'], }, }; const mockFacetsResponse = { data: { mime_types: [ { value: 'application/pdf', count: 15 }, { value: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', count: 8 }, { value: 'image/jpeg', count: 5 }, { value: 'text/plain', count: 3 }, ], tags: [ { value: 'invoice', count: 12 }, { value: 'contract', count: 6 }, { value: 'legal', count: 4 }, { value: '2024', count: 20 }, ], }, }; const renderSearchPage = () => { return render( ); }; describe('SearchPage Integration Tests', () => { beforeEach(() => { vi.clearAllMocks(); mockDocumentService.enhancedSearch.mockResolvedValue(mockSearchResponse); mockDocumentService.search.mockResolvedValue(mockSearchResponse); mockDocumentService.getFacets.mockResolvedValue(mockFacetsResponse); }); test('performs complete search workflow', async () => { const user = userEvent.setup(); renderSearchPage(); // Wait for facets to load await waitFor(() => { expect(screen.getByText('File Types')).toBeInTheDocument(); }); // Enter search query const searchInput = screen.getByPlaceholderText(/search documents/i); await user.type(searchInput, 'invoice'); // Wait for search results await waitFor(() => { expect(screen.getByText('invoice_2024.pdf')).toBeInTheDocument(); expect(screen.getByText('contract_agreement.docx')).toBeInTheDocument(); }); // Verify search was called expect(documentService.enhancedSearch).toHaveBeenCalledWith( expect.objectContaining({ query: 'invoice', limit: 100, include_snippets: true, snippet_length: 200, search_mode: 'simple', }) ); // Verify results are displayed expect(screen.getByText('2 documents found')).toBeInTheDocument(); expect(screen.getByText('Search completed in 45ms')).toBeInTheDocument(); }); test('filters results using MIME type facets', async () => { const user = userEvent.setup(); renderSearchPage(); // Wait for facets to load await waitFor(() => { expect(screen.getByText('PDFs')).toBeInTheDocument(); }); // Enter search query first const searchInput = screen.getByPlaceholderText(/search documents/i); await user.type(searchInput, 'invoice'); // Wait for initial results await waitFor(() => { expect(screen.getByText('invoice_2024.pdf')).toBeInTheDocument(); }); // Apply PDF filter const pdfCheckbox = screen.getByText('PDF Documents').closest('label')?.querySelector('input'); await user.click(pdfCheckbox!); // Verify search is called again with MIME type filter await waitFor(() => { expect(documentService.enhancedSearch).toHaveBeenCalledWith( expect.objectContaining({ query: 'invoice', mime_types: ['application/pdf'], }) ); }); }); test('uses advanced search options', async () => { const user = userEvent.setup(); renderSearchPage(); // Open advanced search panel const advancedButton = screen.getByText('Advanced Search Options'); await user.click(advancedButton); // Wait for panel to expand await waitFor(() => { expect(screen.getByText('Search Behavior')).toBeInTheDocument(); }); // Change search mode to fuzzy const searchModeSelect = screen.getByDisplayValue('simple'); await user.click(searchModeSelect); await user.click(screen.getByText('Fuzzy Search')); // Go to Results Display section await user.click(screen.getByText('Results Display')); // Change snippet length const snippetLengthSelect = screen.getByDisplayValue('200'); await user.click(snippetLengthSelect); await user.click(screen.getByText('Long (400 chars)')); // Perform search const searchInput = screen.getByPlaceholderText(/search documents/i); await user.type(searchInput, 'invoice'); // Verify advanced settings are applied await waitFor(() => { expect(documentService.enhancedSearch).toHaveBeenCalledWith( expect.objectContaining({ query: 'invoice', search_mode: 'fuzzy', snippet_length: 400, }) ); }); }); test('displays enhanced snippets with customization', async () => { const user = userEvent.setup(); renderSearchPage(); // Perform search const searchInput = screen.getByPlaceholderText(/search documents/i); await user.type(searchInput, 'invoice'); // Wait for results with snippets await waitFor(() => { expect(screen.getByText(/This is an invoice for services/)).toBeInTheDocument(); }); // Find snippet viewer settings const settingsButton = screen.getAllByLabelText('Snippet settings')[0]; await user.click(settingsButton); // Change to compact view const compactOption = screen.getByLabelText('Compact'); await user.click(compactOption); // Verify compact view is applied (content should still be visible but styled differently) expect(screen.getByText(/This is an invoice for services/)).toBeInTheDocument(); }); test('suggests search examples and allows interaction', async () => { const user = userEvent.setup(); renderSearchPage(); // Open search guide const showGuideButton = screen.getByText('Show Guide'); await user.click(showGuideButton); // Wait for guide to expand await waitFor(() => { expect(screen.getByText('Search Guide')).toBeInTheDocument(); }); // Click on an example const exampleButtons = screen.getAllByLabelText('Try this search'); await user.click(exampleButtons[0]); // Verify search input is populated const searchInput = screen.getByPlaceholderText(/search documents/i); expect(searchInput).toHaveValue('invoice'); // Verify search is triggered await waitFor(() => { expect(documentService.enhancedSearch).toHaveBeenCalledWith( expect.objectContaining({ query: 'invoice', }) ); }); }); test('handles search errors gracefully', async () => { const user = userEvent.setup(); (documentService.enhancedSearch as any).mockRejectedValue(new Error('Search failed')); renderSearchPage(); const searchInput = screen.getByPlaceholderText(/search documents/i); await user.type(searchInput, 'invoice'); // Should show error message await waitFor(() => { expect(screen.getByText('Search failed. Please try again.')).toBeInTheDocument(); }); }); test('switches between view modes', async () => { const user = userEvent.setup(); renderSearchPage(); // Perform search first const searchInput = screen.getByPlaceholderText(/search documents/i); await user.type(searchInput, 'invoice'); // Wait for results await waitFor(() => { expect(screen.getByText('invoice_2024.pdf')).toBeInTheDocument(); }); // Switch to list view const listViewButton = screen.getByLabelText('List view'); await user.click(listViewButton); // Results should still be visible but in list format expect(screen.getByText('invoice_2024.pdf')).toBeInTheDocument(); expect(screen.getByText('contract_agreement.docx')).toBeInTheDocument(); }); test('shows search suggestions', async () => { const user = userEvent.setup(); renderSearchPage(); const searchInput = screen.getByPlaceholderText(/search documents/i); await user.type(searchInput, 'invoice'); // Wait for suggestions to appear await waitFor(() => { expect(screen.getByText('Suggestions:')).toBeInTheDocument(); expect(screen.getByText('invoice processing')).toBeInTheDocument(); expect(screen.getByText('invoice payment')).toBeInTheDocument(); }); // Click on a suggestion const suggestionChip = screen.getByText('invoice processing'); await user.click(suggestionChip); // Verify search input is updated expect(searchInput).toHaveValue('invoice processing'); }); test('applies multiple filters simultaneously', async () => { const user = userEvent.setup(); renderSearchPage(); // Wait for facets to load await waitFor(() => { expect(screen.getByText('File Types')).toBeInTheDocument(); }); // Enter search query const searchInput = screen.getByPlaceholderText(/search documents/i); await user.type(searchInput, 'invoice'); // Apply PDF filter const pdfCheckbox = screen.getByText('PDF Documents').closest('label')?.querySelector('input'); await user.click(pdfCheckbox!); // Apply date range filter (if visible) const dateRangeSlider = screen.queryByRole('slider', { name: /date range/i }); if (dateRangeSlider) { await user.click(dateRangeSlider); } // Apply OCR filter const ocrSelect = screen.getByDisplayValue('All Documents'); await user.click(ocrSelect); await user.click(screen.getByText('Has OCR Text')); // Verify search is called with all filters await waitFor(() => { expect(documentService.enhancedSearch).toHaveBeenCalledWith( expect.objectContaining({ query: 'invoice', mime_types: ['application/pdf'], }) ); }); }); test('clears all filters when clear button is clicked', async () => { const user = userEvent.setup(); renderSearchPage(); // Wait for facets to load await waitFor(() => { expect(screen.getByText('File Types')).toBeInTheDocument(); }); // Apply some filters first const pdfCheckbox = screen.getByText('PDF Documents').closest('label')?.querySelector('input'); await user.click(pdfCheckbox!); // Click clear filters button const clearButton = screen.getByText('Clear'); await user.click(clearButton); // Verify filters are cleared expect(pdfCheckbox).not.toBeChecked(); }); test('handles empty search results', async () => { const user = userEvent.setup(); const emptyResponse = { data: { documents: [], total: 0, query_time_ms: 10, suggestions: [], }, }; mockDocumentService.enhancedSearch.mockResolvedValue(emptyResponse); renderSearchPage(); const searchInput = screen.getByPlaceholderText(/search documents/i); await user.type(searchInput, 'nonexistent'); await waitFor(() => { expect(screen.getByText('No documents found')).toBeInTheDocument(); }); }); test('preserves search state in URL', async () => { const user = userEvent.setup(); renderSearchPage(); const searchInput = screen.getByPlaceholderText(/search documents/i); await user.type(searchInput, 'invoice'); // Verify URL is updated (this would require checking window.location or using a memory router) await waitFor(() => { expect(searchInput).toHaveValue('invoice'); }); }); }); describe('SearchPage Performance Tests', () => { beforeEach(() => { vi.clearAllMocks(); mockDocumentService.enhancedSearch.mockResolvedValue(mockSearchResponse); mockDocumentService.getFacets.mockResolvedValue(mockFacetsResponse); }); test('debounces search input to avoid excessive API calls', async () => { const user = userEvent.setup(); renderSearchPage(); const searchInput = screen.getByPlaceholderText(/search documents/i); // Type quickly await user.type(searchInput, 'invoice', { delay: 50 }); // Wait for debounce await waitFor(() => { expect(documentService.enhancedSearch).toHaveBeenCalledTimes(1); }); // Should only be called once due to debouncing expect(documentService.enhancedSearch).toHaveBeenCalledWith( expect.objectContaining({ query: 'invoice', }) ); }); test('shows loading states during search', async () => { const user = userEvent.setup(); // Make the API call take longer to see loading state (documentService.enhancedSearch as any).mockImplementation( () => new Promise(resolve => setTimeout(() => resolve(mockSearchResponse), 1000)) ); renderSearchPage(); const searchInput = screen.getByPlaceholderText(/search documents/i); await user.type(searchInput, 'invoice'); // Should show loading indicator expect(screen.getByRole('progressbar')).toBeInTheDocument(); // Wait for search to complete await waitFor(() => { expect(screen.getByText('invoice_2024.pdf')).toBeInTheDocument(); }, { timeout: 2000 }); // Loading indicator should be gone expect(screen.queryByRole('progressbar')).not.toBeInTheDocument(); }); });