471 lines
14 KiB
TypeScript
471 lines
14 KiB
TypeScript
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(
|
|
<BrowserRouter>
|
|
<SearchPage />
|
|
</BrowserRouter>
|
|
);
|
|
};
|
|
|
|
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();
|
|
});
|
|
}); |