feat(client/server): add swagger-ui, improve search bar
This commit is contained in:
parent
0abc8f272a
commit
1f50004d66
|
|
@ -1295,6 +1295,7 @@ checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
|
|||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.15.4",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1975,6 +1976,30 @@ dependencies = [
|
|||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
|
||||
dependencies = [
|
||||
"proc-macro-error-attr",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error-attr"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.95"
|
||||
|
|
@ -2066,6 +2091,8 @@ dependencies = [
|
|||
"tower-http",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"utoipa",
|
||||
"utoipa-swagger-ui",
|
||||
"uuid",
|
||||
"walkdir",
|
||||
]
|
||||
|
|
@ -2183,6 +2210,40 @@ dependencies = [
|
|||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed"
|
||||
version = "8.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "025908b8682a26ba8d12f6f2d66b987584a4a87bc024abc5bbc12553a8cd178a"
|
||||
dependencies = [
|
||||
"rust-embed-impl",
|
||||
"rust-embed-utils",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed-impl"
|
||||
version = "8.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6065f1a4392b71819ec1ea1df1120673418bf386f50de1d6f54204d836d4349c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rust-embed-utils",
|
||||
"syn 2.0.102",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed-utils"
|
||||
version = "8.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6cc0c81648b20b70c491ff8cce00c1c3b223bb8ed2b5d41f0e54c6c4c0a3594"
|
||||
dependencies = [
|
||||
"sha2",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.25"
|
||||
|
|
@ -3293,6 +3354,48 @@ version = "0.2.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "utoipa"
|
||||
version = "4.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c5afb1a60e207dca502682537fefcfd9921e71d0b83e9576060f09abc6efab23"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"utoipa-gen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utoipa-gen"
|
||||
version = "4.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20c24e8ab68ff9ee746aad22d39b5535601e6416d1b0feeabf78be986a5c4392"
|
||||
dependencies = [
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"syn 2.0.102",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utoipa-swagger-ui"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b39868d43c011961e04b41623e050aedf2cc93652562ff7935ce0f819aaf2da"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"mime_guess",
|
||||
"regex",
|
||||
"rust-embed",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"utoipa",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.17.0"
|
||||
|
|
@ -3908,3 +4011,15 @@ dependencies = [
|
|||
"quote",
|
||||
"syn 2.0.102",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "0.6.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"crc32fast",
|
||||
"crossbeam-utils",
|
||||
"flate2",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ describe('GlobalSearchBar', () => {
|
|||
expect(screen.getByRole('textbox')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('shows search suggestions when input is focused', async () => {
|
||||
test('shows popular searches when input is focused', async () => {
|
||||
renderWithRouter(<GlobalSearchBar />);
|
||||
|
||||
const searchInput = screen.getByPlaceholderText('Search documents...');
|
||||
|
|
@ -97,6 +97,7 @@ describe('GlobalSearchBar', () => {
|
|||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Start typing to search documents')).toBeInTheDocument();
|
||||
expect(screen.getByText('Popular searches:')).toBeInTheDocument();
|
||||
expect(screen.getByText('invoice')).toBeInTheDocument();
|
||||
expect(screen.getByText('contract')).toBeInTheDocument();
|
||||
expect(screen.getByText('report')).toBeInTheDocument();
|
||||
|
|
@ -135,6 +136,7 @@ describe('GlobalSearchBar', () => {
|
|||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Quick Results')).toBeInTheDocument();
|
||||
expect(screen.getByText('2 found')).toBeInTheDocument(); // Enhanced result count display
|
||||
expect(screen.getByText('test.pdf')).toBeInTheDocument();
|
||||
expect(screen.getByText('image.png')).toBeInTheDocument();
|
||||
});
|
||||
|
|
@ -409,4 +411,55 @@ describe('GlobalSearchBar', () => {
|
|||
expect(screen.queryByText('Quick Results')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// New tests for enhanced functionality
|
||||
test('shows typing indicator while user is typing', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithRouter(<GlobalSearchBar />);
|
||||
|
||||
const searchInput = screen.getByPlaceholderText('Search documents...');
|
||||
|
||||
await act(async () => {
|
||||
await user.type(searchInput, 't', { delay: 50 });
|
||||
});
|
||||
|
||||
// Should show typing indicator
|
||||
expect(screen.getAllByRole('progressbar').length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('shows smart suggestions while typing', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithRouter(<GlobalSearchBar />);
|
||||
|
||||
const searchInput = screen.getByPlaceholderText('Search documents...');
|
||||
|
||||
await act(async () => {
|
||||
await user.type(searchInput, 'inv');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Try these suggestions:')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('popular search chips are clickable', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithRouter(<GlobalSearchBar />);
|
||||
|
||||
const searchInput = screen.getByPlaceholderText('Search documents...');
|
||||
|
||||
await act(async () => {
|
||||
searchInput.focus();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('invoice')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const invoiceChip = screen.getByText('invoice');
|
||||
await user.click(invoiceChip);
|
||||
|
||||
expect(searchInput.value).toBe('invoice');
|
||||
expect(mockNavigate).toHaveBeenCalledWith('/search?q=invoice');
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,279 @@
|
|||
import React from 'react';
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import SearchGuidance from '../SearchGuidance';
|
||||
|
||||
describe('SearchGuidance', () => {
|
||||
const mockOnExampleClick = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('renders search guidance with examples in expanded mode', () => {
|
||||
render(<SearchGuidance onExampleClick={mockOnExampleClick} />);
|
||||
|
||||
expect(screen.getByText('Search Help & Examples')).toBeInTheDocument();
|
||||
|
||||
// Click to expand accordion
|
||||
const accordionButton = screen.getByRole('button', { expanded: false });
|
||||
fireEvent.click(accordionButton);
|
||||
|
||||
expect(screen.getByText('Example Searches')).toBeInTheDocument();
|
||||
expect(screen.getByText('Search Tips')).toBeInTheDocument();
|
||||
expect(screen.getByText('Quick Start')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders compact mode correctly', () => {
|
||||
render(<SearchGuidance compact onExampleClick={mockOnExampleClick} />);
|
||||
|
||||
const helpButton = screen.getByRole('button');
|
||||
expect(helpButton).toBeInTheDocument();
|
||||
|
||||
// Initially collapsed in compact mode
|
||||
expect(screen.queryByText('Quick Search Tips')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('toggles compact help visibility', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<SearchGuidance compact onExampleClick={mockOnExampleClick} />);
|
||||
|
||||
const helpButton = screen.getByRole('button');
|
||||
|
||||
// Expand help
|
||||
await user.click(helpButton);
|
||||
|
||||
expect(screen.getByText('Quick Search Tips')).toBeInTheDocument();
|
||||
expect(screen.getByText('• Use quotes for exact phrases: "annual report"')).toBeInTheDocument();
|
||||
|
||||
// Collapse help
|
||||
await user.click(helpButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Quick Search Tips')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('displays search examples with clickable items', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<SearchGuidance onExampleClick={mockOnExampleClick} />);
|
||||
|
||||
// Expand accordion
|
||||
const accordionButton = screen.getByRole('button', { expanded: false });
|
||||
await user.click(accordionButton);
|
||||
|
||||
// Check for example queries
|
||||
expect(screen.getByText('invoice 2024')).toBeInTheDocument();
|
||||
expect(screen.getByText('"project proposal"')).toBeInTheDocument();
|
||||
expect(screen.getByText('tag:important')).toBeInTheDocument();
|
||||
expect(screen.getByText('contract AND payment')).toBeInTheDocument();
|
||||
expect(screen.getByText('proj*')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('calls onExampleClick when example is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<SearchGuidance onExampleClick={mockOnExampleClick} />);
|
||||
|
||||
// Expand accordion
|
||||
const accordionButton = screen.getByRole('button', { expanded: false });
|
||||
await user.click(accordionButton);
|
||||
|
||||
// Click on an example
|
||||
const exampleItem = screen.getByText('invoice 2024').closest('li');
|
||||
await user.click(exampleItem);
|
||||
|
||||
expect(mockOnExampleClick).toHaveBeenCalledWith('invoice 2024');
|
||||
});
|
||||
|
||||
test('displays search tips', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<SearchGuidance onExampleClick={mockOnExampleClick} />);
|
||||
|
||||
// Expand accordion
|
||||
const accordionButton = screen.getByRole('button', { expanded: false });
|
||||
await user.click(accordionButton);
|
||||
|
||||
// Check for search tips
|
||||
expect(screen.getByText('• Use quotes for exact phrases: "annual report"')).toBeInTheDocument();
|
||||
expect(screen.getByText('• Search by tags: tag:urgent or tag:personal')).toBeInTheDocument();
|
||||
expect(screen.getByText('• Use AND/OR for complex queries: (invoice OR receipt) AND 2024')).toBeInTheDocument();
|
||||
expect(screen.getByText('• Wildcards work great: proj* finds project, projects, projection')).toBeInTheDocument();
|
||||
expect(screen.getByText('• Search OCR text in images and PDFs automatically')).toBeInTheDocument();
|
||||
expect(screen.getByText('• File types are searchable: PDF, Word, Excel, images')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('displays quick start chips that are clickable', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<SearchGuidance onExampleClick={mockOnExampleClick} />);
|
||||
|
||||
// Expand accordion
|
||||
const accordionButton = screen.getByRole('button', { expanded: false });
|
||||
await user.click(accordionButton);
|
||||
|
||||
// Click on a quick start chip
|
||||
const chipElement = screen.getByText('invoice 2024');
|
||||
await user.click(chipElement);
|
||||
|
||||
expect(mockOnExampleClick).toHaveBeenCalledWith('invoice 2024');
|
||||
});
|
||||
|
||||
test('compact mode shows limited examples', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<SearchGuidance compact onExampleClick={mockOnExampleClick} />);
|
||||
|
||||
const helpButton = screen.getByRole('button');
|
||||
await user.click(helpButton);
|
||||
|
||||
// Should show only first 3 examples in compact mode
|
||||
expect(screen.getByText('invoice 2024')).toBeInTheDocument();
|
||||
expect(screen.getByText('"project proposal"')).toBeInTheDocument();
|
||||
expect(screen.getByText('tag:important')).toBeInTheDocument();
|
||||
|
||||
// Should not show all examples in compact mode
|
||||
expect(screen.queryByText('contract AND payment')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('compact mode shows limited tips', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<SearchGuidance compact onExampleClick={mockOnExampleClick} />);
|
||||
|
||||
const helpButton = screen.getByRole('button');
|
||||
await user.click(helpButton);
|
||||
|
||||
// Should show only first 3 tips in compact mode
|
||||
const tips = screen.getAllByText(/^•/);
|
||||
expect(tips).toHaveLength(3);
|
||||
});
|
||||
|
||||
test('handles missing onExampleClick gracefully', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<SearchGuidance />);
|
||||
|
||||
// Expand accordion
|
||||
const accordionButton = screen.getByRole('button', { expanded: false });
|
||||
await user.click(accordionButton);
|
||||
|
||||
// Click on an example - should not crash
|
||||
const exampleItem = screen.getByText('invoice 2024').closest('li');
|
||||
await user.click(exampleItem);
|
||||
|
||||
// Should not crash when onExampleClick is not provided
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
|
||||
test('displays correct icons for different example types', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<SearchGuidance onExampleClick={mockOnExampleClick} />);
|
||||
|
||||
// Expand accordion
|
||||
const accordionButton = screen.getByRole('button', { expanded: false });
|
||||
await user.click(accordionButton);
|
||||
|
||||
// Check for different icons (by test id)
|
||||
expect(screen.getByTestId('SearchIcon')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('FormatQuoteIcon')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('TagIcon')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('ExtensionIcon')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('TrendingUpIcon')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('compact mode toggle button changes icon', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<SearchGuidance compact onExampleClick={mockOnExampleClick} />);
|
||||
|
||||
const helpButton = screen.getByRole('button');
|
||||
|
||||
// Initially shows help icon
|
||||
expect(screen.getByTestId('HelpIcon')).toBeInTheDocument();
|
||||
|
||||
// Click to expand
|
||||
await user.click(helpButton);
|
||||
|
||||
// Should show close icon when expanded
|
||||
expect(screen.getByTestId('CloseIcon')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('applies custom styling props', () => {
|
||||
const customSx = { backgroundColor: 'red' };
|
||||
render(<SearchGuidance sx={customSx} data-testid="search-guidance" />);
|
||||
|
||||
const component = screen.getByTestId('search-guidance');
|
||||
expect(component).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('provides helpful descriptions for each search example', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<SearchGuidance onExampleClick={mockOnExampleClick} />);
|
||||
|
||||
// Expand accordion
|
||||
const accordionButton = screen.getByRole('button', { expanded: false });
|
||||
await user.click(accordionButton);
|
||||
|
||||
// Check for example descriptions
|
||||
expect(screen.getByText('Find documents containing both "invoice" and "2024"')).toBeInTheDocument();
|
||||
expect(screen.getByText('Search for exact phrase "project proposal"')).toBeInTheDocument();
|
||||
expect(screen.getByText('Find all documents tagged as "important"')).toBeInTheDocument();
|
||||
expect(screen.getByText('Advanced search using AND operator')).toBeInTheDocument();
|
||||
expect(screen.getByText('Wildcard search for project, projects, etc.')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('keyboard navigation works for examples', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<SearchGuidance onExampleClick={mockOnExampleClick} />);
|
||||
|
||||
// Expand accordion
|
||||
const accordionButton = screen.getByRole('button', { expanded: false });
|
||||
await user.click(accordionButton);
|
||||
|
||||
// Tab to first example and press Enter
|
||||
const firstExample = screen.getByText('invoice 2024').closest('li');
|
||||
firstExample.focus();
|
||||
await user.keyboard('{Enter}');
|
||||
|
||||
expect(mockOnExampleClick).toHaveBeenCalledWith('invoice 2024');
|
||||
});
|
||||
});
|
||||
|
||||
describe('SearchGuidance Accessibility', () => {
|
||||
test('has proper ARIA labels and roles', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<SearchGuidance />);
|
||||
|
||||
// Accordion should have proper role
|
||||
const accordion = screen.getByRole('button', { expanded: false });
|
||||
expect(accordion).toBeInTheDocument();
|
||||
|
||||
// Expand to check list accessibility
|
||||
await user.click(accordion);
|
||||
|
||||
const list = screen.getByRole('list');
|
||||
expect(list).toBeInTheDocument();
|
||||
|
||||
const listItems = screen.getAllByRole('listitem');
|
||||
expect(listItems.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('compact mode has accessible toggle button', () => {
|
||||
render(<SearchGuidance compact />);
|
||||
|
||||
const toggleButton = screen.getByRole('button');
|
||||
expect(toggleButton).toBeInTheDocument();
|
||||
expect(toggleButton).toHaveAttribute('type', 'button');
|
||||
});
|
||||
|
||||
test('examples are keyboard accessible', async () => {
|
||||
const user = userEvent.setup();
|
||||
const mockOnExampleClick = jest.fn();
|
||||
render(<SearchGuidance onExampleClick={mockOnExampleClick} />);
|
||||
|
||||
// Expand accordion
|
||||
const accordionButton = screen.getByRole('button', { expanded: false });
|
||||
await user.click(accordionButton);
|
||||
|
||||
// All examples should be focusable
|
||||
const examples = screen.getAllByRole('listitem');
|
||||
examples.forEach(example => {
|
||||
expect(example).toHaveAttribute('tabindex', '0');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -14,6 +14,20 @@ jest.mock('../../services/api', () => ({
|
|||
}
|
||||
}));
|
||||
|
||||
// Mock SearchGuidance component
|
||||
jest.mock('../../components/SearchGuidance', () => {
|
||||
return function MockSearchGuidance({ onExampleClick, compact }) {
|
||||
return (
|
||||
<div data-testid="search-guidance">
|
||||
<button onClick={() => onExampleClick?.('test query')}>
|
||||
Mock Guidance Example
|
||||
</button>
|
||||
{compact && <span>Compact Mode</span>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
// Mock useNavigate
|
||||
const mockNavigate = jest.fn();
|
||||
jest.mock('react-router-dom', () => ({
|
||||
|
|
@ -49,7 +63,7 @@ const mockSearchResponse = {
|
|||
],
|
||||
total: 1,
|
||||
query_time_ms: 45,
|
||||
suggestions: ['\"test\"', 'test*']
|
||||
suggestions: ['\"test\"', 'test*', 'tag:test']
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -77,9 +91,10 @@ describe('SearchPage', () => {
|
|||
expect(screen.getByText('Start searching your documents')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('displays search suggestions when no query is entered', () => {
|
||||
test('displays search tips and examples when no query is entered', () => {
|
||||
renderWithRouter(<SearchPage />);
|
||||
|
||||
expect(screen.getByText('Search Tips:')).toBeInTheDocument();
|
||||
expect(screen.getByText('Try: invoice')).toBeInTheDocument();
|
||||
expect(screen.getByText('Try: contract')).toBeInTheDocument();
|
||||
expect(screen.getByText('Try: tag:important')).toBeInTheDocument();
|
||||
|
|
@ -126,7 +141,7 @@ describe('SearchPage', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('shows search suggestions when available', async () => {
|
||||
test('shows quick suggestions while typing', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithRouter(<SearchPage />);
|
||||
|
||||
|
|
@ -137,13 +152,29 @@ describe('SearchPage', () => {
|
|||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Suggestions:')).toBeInTheDocument();
|
||||
expect(screen.getByText('Quick suggestions:')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('shows server suggestions from search results', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithRouter(<SearchPage />);
|
||||
|
||||
const searchInput = screen.getByPlaceholderText(/Search documents by content, filename, or tags/);
|
||||
|
||||
await act(async () => {
|
||||
await user.type(searchInput, 'test');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Related searches:')).toBeInTheDocument();
|
||||
expect(screen.getByText('\"test\"')).toBeInTheDocument();
|
||||
expect(screen.getByText('test*')).toBeInTheDocument();
|
||||
expect(screen.getByText('tag:test')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('toggles advanced search options', async () => {
|
||||
test('toggles advanced search options with guidance', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithRouter(<SearchPage />);
|
||||
|
||||
|
|
@ -154,9 +185,11 @@ describe('SearchPage', () => {
|
|||
expect(screen.getByText('Search Options')).toBeInTheDocument();
|
||||
expect(screen.getByText('Enhanced Search')).toBeInTheDocument();
|
||||
expect(screen.getByText('Show Snippets')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('search-guidance')).toBeInTheDocument();
|
||||
expect(screen.getByText('Compact Mode')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('changes search mode', async () => {
|
||||
test('changes search mode with simplified labels', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithRouter(<SearchPage />);
|
||||
|
||||
|
|
@ -167,11 +200,11 @@ describe('SearchPage', () => {
|
|||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const phraseButton = screen.getByRole('button', { name: 'Phrase' });
|
||||
const phraseButton = screen.getByRole('button', { name: 'Exact phrase' });
|
||||
expect(phraseButton).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const phraseButton = screen.getByRole('button', { name: 'Phrase' });
|
||||
const phraseButton = screen.getByRole('button', { name: 'Exact phrase' });
|
||||
await user.click(phraseButton);
|
||||
|
||||
// Wait for search to be called with new mode
|
||||
|
|
@ -183,6 +216,23 @@ describe('SearchPage', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('displays simplified search mode labels', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithRouter(<SearchPage />);
|
||||
|
||||
const searchInput = screen.getByPlaceholderText(/Search documents by content, filename, or tags/);
|
||||
await act(async () => {
|
||||
await user.type(searchInput, 'test');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: 'Smart' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Exact phrase' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Similar words' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Advanced' })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('handles search suggestions click', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
|
@ -195,7 +245,7 @@ describe('SearchPage', () => {
|
|||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('\"test\"')).toBeInTheDocument();
|
||||
expect(screen.getByText('Related searches:')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const suggestionChip = screen.getByText('\"test\"');
|
||||
|
|
@ -272,12 +322,12 @@ describe('SearchPage', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('displays loading state during search', async () => {
|
||||
test('displays enhanced loading state with progress during search', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
// Mock a delayed response
|
||||
documentService.enhancedSearch.mockImplementation(() =>
|
||||
new Promise(resolve => setTimeout(() => resolve(mockSearchResponse), 100))
|
||||
new Promise(resolve => setTimeout(() => resolve(mockSearchResponse), 200))
|
||||
);
|
||||
|
||||
renderWithRouter(<SearchPage />);
|
||||
|
|
@ -285,15 +335,15 @@ describe('SearchPage', () => {
|
|||
const searchInput = screen.getByPlaceholderText(/Search documents by content, filename, or tags/);
|
||||
|
||||
await act(async () => {
|
||||
await user.type(searchInput, 'test');
|
||||
await user.type(searchInput, 't');
|
||||
});
|
||||
|
||||
// Should show loading indicator
|
||||
expect(screen.getByRole('progressbar')).toBeInTheDocument();
|
||||
// Should show loading indicators
|
||||
expect(screen.getAllByRole('progressbar').length).toBeGreaterThan(0);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('test.pdf')).toBeInTheDocument();
|
||||
});
|
||||
}, { timeout: 3000 });
|
||||
});
|
||||
|
||||
test('handles search error gracefully', async () => {
|
||||
|
|
@ -445,16 +495,106 @@ describe('SearchPage', () => {
|
|||
});
|
||||
});
|
||||
|
||||
// Test helper functions
|
||||
describe('Search Helper Functions', () => {
|
||||
test('formats file sizes correctly', () => {
|
||||
// These would test utility functions if they were exported
|
||||
// For now, we test the component behavior
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
// New functionality tests
|
||||
describe('Enhanced Search Features', () => {
|
||||
test('shows typing indicator while user is typing', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithRouter(<SearchPage />);
|
||||
|
||||
const searchInput = screen.getByPlaceholderText(/Search documents by content, filename, or tags/);
|
||||
|
||||
// Start typing without completing
|
||||
await act(async () => {
|
||||
await user.type(searchInput, 't', { delay: 50 });
|
||||
});
|
||||
|
||||
test('formats dates correctly', () => {
|
||||
// These would test utility functions if they were exported
|
||||
expect(true).toBe(true);
|
||||
// Should show typing indicator
|
||||
expect(screen.getAllByRole('progressbar').length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('shows improved no results state with suggestions', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
// Mock empty response
|
||||
documentService.enhancedSearch.mockResolvedValue({
|
||||
data: {
|
||||
documents: [],
|
||||
total: 0,
|
||||
query_time_ms: 10,
|
||||
suggestions: []
|
||||
}
|
||||
});
|
||||
|
||||
renderWithRouter(<SearchPage />);
|
||||
|
||||
const searchInput = screen.getByPlaceholderText(/Search documents by content, filename, or tags/);
|
||||
|
||||
await act(async () => {
|
||||
await user.type(searchInput, 'nonexistent');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/No results found for "nonexistent"/)).toBeInTheDocument();
|
||||
expect(screen.getByText('Suggestions:')).toBeInTheDocument();
|
||||
expect(screen.getByText('• Try simpler or more general terms')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('clickable example chips in empty state work correctly', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithRouter(<SearchPage />);
|
||||
|
||||
const invoiceChip = screen.getByText('Try: invoice');
|
||||
await user.click(invoiceChip);
|
||||
|
||||
const searchInput = screen.getByPlaceholderText(/Search documents by content, filename, or tags/);
|
||||
expect(searchInput.value).toBe('invoice');
|
||||
});
|
||||
|
||||
test('search guidance example click works', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithRouter(<SearchPage />);
|
||||
|
||||
const settingsButton = screen.getByRole('button', { name: /settings/i });
|
||||
await user.click(settingsButton);
|
||||
|
||||
const guidanceExample = screen.getByText('Mock Guidance Example');
|
||||
await user.click(guidanceExample);
|
||||
|
||||
const searchInput = screen.getByPlaceholderText(/Search documents by content, filename, or tags/);
|
||||
expect(searchInput.value).toBe('test query');
|
||||
});
|
||||
|
||||
test('mobile filter toggle works', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
// Mock mobile viewport
|
||||
Object.defineProperty(window, 'innerWidth', {
|
||||
writable: true,
|
||||
configurable: true,
|
||||
value: 500,
|
||||
});
|
||||
|
||||
renderWithRouter(<SearchPage />);
|
||||
|
||||
// Mobile filter button should be visible
|
||||
const mobileFilterButton = screen.getByTestId('FilterIcon');
|
||||
expect(mobileFilterButton).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('search results have enhanced CSS classes for styling', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithRouter(<SearchPage />);
|
||||
|
||||
const searchInput = screen.getByPlaceholderText(/Search documents by content, filename, or tags/);
|
||||
|
||||
await act(async () => {
|
||||
await user.type(searchInput, 'test');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const resultCard = screen.getByText('test.pdf').closest('[class*="search-result-card"]');
|
||||
expect(resultCard).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -2,7 +2,7 @@ use chrono::{DateTime, Utc};
|
|||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::FromRow;
|
||||
use uuid::Uuid;
|
||||
use utoipa::ToSchema;
|
||||
use utoipa::{ToSchema, IntoParams};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, FromRow, ToSchema)]
|
||||
pub struct User {
|
||||
|
|
@ -68,7 +68,7 @@ pub struct DocumentResponse {
|
|||
pub has_ocr_text: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema, IntoParams)]
|
||||
pub struct SearchRequest {
|
||||
pub query: String,
|
||||
pub tags: Option<Vec<String>>,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ use axum::{
|
|||
Router,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use utoipa::path;
|
||||
|
||||
use crate::{
|
||||
auth::{create_jwt, AuthUser},
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use axum::{
|
|||
};
|
||||
use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
use utoipa::{path, ToSchema};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::{
|
||||
auth::AuthUser,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use axum::{
|
|||
extract::State,
|
||||
http::StatusCode,
|
||||
response::Json,
|
||||
routing::get,
|
||||
routing::{get, post},
|
||||
Router,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
|
@ -15,6 +15,18 @@ pub fn router() -> Router<Arc<AppState>> {
|
|||
.route("/requeue-failed", post(requeue_failed))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/queue/stats",
|
||||
tag = "queue",
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "OCR queue statistics"),
|
||||
(status = 401, description = "Unauthorized")
|
||||
)
|
||||
)]
|
||||
async fn get_queue_stats(
|
||||
State(state): State<Arc<AppState>>,
|
||||
_auth_user: AuthUser, // Require authentication
|
||||
|
|
@ -40,8 +52,18 @@ async fn get_queue_stats(
|
|||
})))
|
||||
}
|
||||
|
||||
use axum::routing::post;
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/queue/requeue-failed",
|
||||
tag = "queue",
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Failed items requeued successfully"),
|
||||
(status = 401, description = "Unauthorized")
|
||||
)
|
||||
)]
|
||||
async fn requeue_failed(
|
||||
State(state): State<Arc<AppState>>,
|
||||
_auth_user: AuthUser, // Require authentication
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ use axum::{
|
|||
Router,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use utoipa::path;
|
||||
|
||||
use crate::{
|
||||
auth::AuthUser,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ use axum::{
|
|||
Router,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use utoipa::path;
|
||||
|
||||
use crate::{
|
||||
auth::AuthUser,
|
||||
|
|
@ -19,6 +18,18 @@ pub fn router() -> Router<Arc<AppState>> {
|
|||
.route("/", get(get_settings).put(update_settings))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/settings",
|
||||
tag = "settings",
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "User settings", body = SettingsResponse),
|
||||
(status = 401, description = "Unauthorized")
|
||||
)
|
||||
)]
|
||||
async fn get_settings(
|
||||
auth_user: AuthUser,
|
||||
State(state): State<Arc<AppState>>,
|
||||
|
|
@ -57,6 +68,20 @@ async fn get_settings(
|
|||
Ok(Json(response))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/api/settings",
|
||||
tag = "settings",
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
request_body = UpdateSettings,
|
||||
responses(
|
||||
(status = 200, description = "Settings updated successfully", body = SettingsResponse),
|
||||
(status = 400, description = "Bad request - invalid settings data"),
|
||||
(status = 401, description = "Unauthorized")
|
||||
)
|
||||
)]
|
||||
async fn update_settings(
|
||||
auth_user: AuthUser,
|
||||
State(state): State<Arc<AppState>>,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use axum::{
|
|||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
response::Json,
|
||||
routing::get,
|
||||
routing::{get, post, put, delete},
|
||||
Router,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
|
@ -20,6 +20,18 @@ pub fn router() -> Router<Arc<AppState>> {
|
|||
.route("/:id", get(get_user).put(update_user).delete(delete_user))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/users",
|
||||
tag = "users",
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "List of all users", body = Vec<UserResponse>),
|
||||
(status = 401, description = "Unauthorized")
|
||||
)
|
||||
)]
|
||||
async fn list_users(
|
||||
_auth_user: AuthUser,
|
||||
State(state): State<Arc<AppState>>,
|
||||
|
|
@ -34,6 +46,22 @@ async fn list_users(
|
|||
Ok(Json(user_responses))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/users/{id}",
|
||||
tag = "users",
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
params(
|
||||
("id" = Uuid, Path, description = "User ID")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "User information", body = UserResponse),
|
||||
(status = 404, description = "User not found"),
|
||||
(status = 401, description = "Unauthorized")
|
||||
)
|
||||
)]
|
||||
async fn get_user(
|
||||
_auth_user: AuthUser,
|
||||
State(state): State<Arc<AppState>>,
|
||||
|
|
@ -49,6 +77,20 @@ async fn get_user(
|
|||
Ok(Json(user.into()))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/users",
|
||||
tag = "users",
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
request_body = CreateUser,
|
||||
responses(
|
||||
(status = 200, description = "User created successfully", body = UserResponse),
|
||||
(status = 400, description = "Bad request - invalid user data"),
|
||||
(status = 401, description = "Unauthorized")
|
||||
)
|
||||
)]
|
||||
async fn create_user(
|
||||
_auth_user: AuthUser,
|
||||
State(state): State<Arc<AppState>>,
|
||||
|
|
@ -63,6 +105,23 @@ async fn create_user(
|
|||
Ok(Json(user.into()))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/api/users/{id}",
|
||||
tag = "users",
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
params(
|
||||
("id" = Uuid, Path, description = "User ID")
|
||||
),
|
||||
request_body = UpdateUser,
|
||||
responses(
|
||||
(status = 200, description = "User updated successfully", body = UserResponse),
|
||||
(status = 400, description = "Bad request - invalid user data"),
|
||||
(status = 401, description = "Unauthorized")
|
||||
)
|
||||
)]
|
||||
async fn update_user(
|
||||
_auth_user: AuthUser,
|
||||
State(state): State<Arc<AppState>>,
|
||||
|
|
@ -78,6 +137,22 @@ async fn update_user(
|
|||
Ok(Json(user.into()))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/users/{id}",
|
||||
tag = "users",
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
params(
|
||||
("id" = Uuid, Path, description = "User ID")
|
||||
),
|
||||
responses(
|
||||
(status = 204, description = "User deleted successfully"),
|
||||
(status = 403, description = "Forbidden - cannot delete yourself"),
|
||||
(status = 401, description = "Unauthorized")
|
||||
)
|
||||
)]
|
||||
async fn delete_user(
|
||||
auth_user: AuthUser,
|
||||
State(state): State<Arc<AppState>>,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
use utoipa::OpenApi;
|
||||
use utoipa::{OpenApi, Modify};
|
||||
use utoipa::openapi::security::{SecurityScheme, HttpAuthScheme, Http};
|
||||
use utoipa_swagger_ui::SwaggerUi;
|
||||
use axum::Router;
|
||||
use std::sync::Arc;
|
||||
|
|
@ -25,16 +26,19 @@ use crate::{
|
|||
crate::routes::documents::download_document,
|
||||
// Search endpoints
|
||||
crate::routes::search::search_documents,
|
||||
crate::routes::search::enhanced_search_documents,
|
||||
// Settings endpoints
|
||||
crate::routes::settings::get_settings,
|
||||
crate::routes::settings::update_settings,
|
||||
// User endpoints
|
||||
crate::routes::users::list_users,
|
||||
crate::routes::users::create_user,
|
||||
crate::routes::users::get_user,
|
||||
crate::routes::users::update_user,
|
||||
crate::routes::users::delete_user,
|
||||
// Queue endpoints
|
||||
crate::routes::queue::get_queue_status,
|
||||
crate::routes::queue::get_queue_stats,
|
||||
crate::routes::queue::requeue_failed,
|
||||
),
|
||||
components(
|
||||
schemas(
|
||||
|
|
@ -51,6 +55,7 @@ use crate::{
|
|||
(name = "users", description = "User management endpoints"),
|
||||
(name = "queue", description = "OCR queue management endpoints"),
|
||||
),
|
||||
modifiers(&SecurityAddon),
|
||||
info(
|
||||
title = "Readur API",
|
||||
version = "0.1.0",
|
||||
|
|
@ -66,7 +71,21 @@ use crate::{
|
|||
)]
|
||||
pub struct ApiDoc;
|
||||
|
||||
struct SecurityAddon;
|
||||
|
||||
impl Modify for SecurityAddon {
|
||||
fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
|
||||
if let Some(components) = openapi.components.as_mut() {
|
||||
components.add_security_scheme(
|
||||
"bearer_auth",
|
||||
SecurityScheme::Http(Http::new(HttpAuthScheme::Bearer))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_swagger_router() -> Router<Arc<AppState>> {
|
||||
SwaggerUi::new("/swagger-ui")
|
||||
.url("/api-docs/openapi.json", ApiDoc::openapi())
|
||||
.into()
|
||||
}
|
||||
|
|
@ -407,12 +407,12 @@ mod tests {
|
|||
assert!(total_highlights >= 2);
|
||||
}
|
||||
|
||||
// Test search suggestions functionality
|
||||
// Test search suggestions functionality - enhanced version
|
||||
fn generate_search_suggestions(query: &str) -> Vec<String> {
|
||||
// Copy of the function from search.rs for testing
|
||||
// Enhanced copy of the function from search.rs for testing
|
||||
let mut suggestions = Vec::new();
|
||||
|
||||
if query.len() > 3 {
|
||||
if query.len() > 2 { // Reduced minimum length for faster suggestions
|
||||
// Common search variations
|
||||
suggestions.push(format!("\"{}\"", query)); // Exact phrase
|
||||
|
||||
|
|
@ -421,16 +421,39 @@ mod tests {
|
|||
suggestions.push(format!("{}*", query));
|
||||
}
|
||||
|
||||
// Add tag search suggestion
|
||||
if !query.starts_with("tag:") {
|
||||
suggestions.push(format!("tag:{}", query));
|
||||
}
|
||||
|
||||
// Add similar terms (this would typically come from a thesaurus or ML model)
|
||||
// Use case-insensitive matching for replacements
|
||||
let query_lower = query.to_lowercase();
|
||||
if query_lower.contains("document") {
|
||||
suggestions.push(query.replace("document", "file").replace("Document", "file"));
|
||||
suggestions.push(query.replace("document", "paper").replace("Document", "paper"));
|
||||
}
|
||||
|
||||
// Add Boolean operator suggestions for longer queries
|
||||
if query.len() > 5 && !query.contains(" AND ") && !query.contains(" OR ") {
|
||||
let words: Vec<&str> = query.split_whitespace().collect();
|
||||
if words.len() >= 2 {
|
||||
suggestions.push(format!("{} AND {}", words[0], words[1]));
|
||||
suggestions.push(format!("{} OR {}", words[0], words[1]));
|
||||
}
|
||||
}
|
||||
|
||||
// Add content type suggestions
|
||||
if query_lower.contains("invoice") {
|
||||
suggestions.push("receipt".to_string());
|
||||
suggestions.push("billing".to_string());
|
||||
}
|
||||
if query_lower.contains("contract") {
|
||||
suggestions.push("agreement".to_string());
|
||||
suggestions.push("legal".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
suggestions.into_iter().take(5).collect() // Increase limit to accommodate replacements
|
||||
suggestions.into_iter().take(6).collect() // Increased limit for enhanced suggestions
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -449,6 +472,46 @@ mod tests {
|
|||
// Should not generate suggestions for very short queries
|
||||
assert!(suggestions.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_suggestions_enhanced_features() {
|
||||
let suggestions = generate_search_suggestions("invoice payment");
|
||||
|
||||
assert!(!suggestions.is_empty());
|
||||
assert!(suggestions.contains(&"\"invoice payment\"".to_string()));
|
||||
assert!(suggestions.contains(&"invoice payment*".to_string()));
|
||||
assert!(suggestions.contains(&"tag:invoice payment".to_string()));
|
||||
assert!(suggestions.contains(&"invoice AND payment".to_string()));
|
||||
assert!(suggestions.contains(&"invoice OR payment".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_suggestions_content_specific() {
|
||||
let invoice_suggestions = generate_search_suggestions("invoice");
|
||||
assert!(invoice_suggestions.contains(&"receipt".to_string()));
|
||||
assert!(invoice_suggestions.contains(&"billing".to_string()));
|
||||
|
||||
let contract_suggestions = generate_search_suggestions("contract");
|
||||
assert!(contract_suggestions.contains(&"agreement".to_string()));
|
||||
assert!(contract_suggestions.contains(&"legal".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_suggestions_tag_prefix() {
|
||||
let suggestions = generate_search_suggestions("tag:important");
|
||||
|
||||
// Should not add tag: prefix if already present
|
||||
assert!(!suggestions.iter().any(|s| s.starts_with("tag:tag:")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_suggestions_boolean_operators() {
|
||||
let suggestions = generate_search_suggestions("document AND file");
|
||||
|
||||
// Should not add Boolean operators if already present
|
||||
// Fixed: Check for suggestions that contain multiple AND operators
|
||||
assert!(!suggestions.iter().any(|s| s.matches(" AND ").count() > 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_suggestions_document_replacement() {
|
||||
|
|
@ -472,8 +535,8 @@ mod tests {
|
|||
fn test_search_suggestions_limit() {
|
||||
let suggestions = generate_search_suggestions("document test example");
|
||||
|
||||
// Should limit to 5 suggestions
|
||||
assert!(suggestions.len() <= 5);
|
||||
// Should limit to 6 suggestions (updated limit)
|
||||
assert!(suggestions.len() <= 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
Loading…
Reference in New Issue