Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions src/components/search/IntelligentAutoComplete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,26 @@
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
import { Search, X, Clock, ChevronRight, Sparkles, User, Tag, Type } from 'lucide-react';
import { getSearchSuggestions, highlightMatch } from '../../utils/searchUtils';
import { useDebounce } from '../../hooks/useDebounce';

interface IntelligentAutoCompleteProps {
value: string;
onChange: (value: string) => void;
onSearch: (value: string) => void;
history?: string[];
debounceMs?: number;
}

export const IntelligentAutoComplete = React.memo<IntelligentAutoCompleteProps>(
({ value, onChange, onSearch, history = [] }) => {
({ value, onChange, onSearch, history = [], debounceMs = 300 }) => {
const [isOpen, setIsOpen] = useState(false);
const [activeIndex, setActiveIndex] = useState(-1);
const dropdownRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);

const debouncedValue = useDebounce(value, debounceMs);
const suggestions = useMemo(() => getSearchSuggestions(debouncedValue), [debouncedValue]);

Check failure on line 24 in src/components/search/IntelligentAutoComplete.tsx

View workflow job for this annotation

GitHub Actions / type-check

Cannot redeclare block-scoped variable 'suggestions'.
const suggestions = useMemo(() => getSearchSuggestions(value), [value]);

Check failure on line 25 in src/components/search/IntelligentAutoComplete.tsx

View workflow job for this annotation

GitHub Actions / type-check

Cannot redeclare block-scoped variable 'suggestions'.

useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
Expand Down Expand Up @@ -154,11 +159,11 @@
{getSuggestionIcon(suggestion)}
</div>
<span className="flex-1 truncate">
{highlightMatch(suggestion, value).map((part, index) => (
{highlightMatch(suggestion, debouncedValue).map((part, index) => (
<span
key={index}
className={
part.toLowerCase() === value.toLowerCase()
part.toLowerCase() === debouncedValue.toLowerCase()
? 'font-bold text-primary'
: ''
}
Expand Down
69 changes: 69 additions & 0 deletions src/components/search/__tests__/IntelligentAutoComplete.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import React from 'react';
import { render, screen, fireEvent, act } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { IntelligentAutoComplete } from '../IntelligentAutoComplete';
import * as searchUtils from '../../../utils/searchUtils';

describe('IntelligentAutoComplete Component - Debounce', () => {
beforeEach(() => {
vi.useFakeTimers();
});

afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks();
});

it('renders input with given initial value', () => {
const onChange = vi.fn();
const onSearch = vi.fn();

render(<IntelligentAutoComplete value="react" onChange={onChange} onSearch={onSearch} />);

const input = screen.getByPlaceholderText('Search for knowledge, authors, or topics...');
expect(input).toHaveValue('react');
});

it('debounces calls to getSearchSuggestions when typing', () => {
const getSearchSuggestionsSpy = vi.spyOn(searchUtils, 'getSearchSuggestions');
const onChange = vi.fn();
const onSearch = vi.fn();

const { rerender } = render(
<IntelligentAutoComplete value="" onChange={onChange} onSearch={onSearch} debounceMs={300} />,
);

const initialCalls = getSearchSuggestionsSpy.mock.calls.length;

// Simulate typing multiple characters sequentially
rerender(<IntelligentAutoComplete value="c" onChange={onChange} onSearch={onSearch} debounceMs={300} />);
rerender(<IntelligentAutoComplete value="ca" onChange={onChange} onSearch={onSearch} debounceMs={300} />);
rerender(<IntelligentAutoComplete value="cai" onChange={onChange} onSearch={onSearch} debounceMs={300} />);
rerender(<IntelligentAutoComplete value="cair" onChange={onChange} onSearch={onSearch} debounceMs={300} />);
rerender(<IntelligentAutoComplete value="cairo" onChange={onChange} onSearch={onSearch} debounceMs={300} />);

// Before timer advances, getSearchSuggestions should not have been called for intermediate keystrokes
expect(getSearchSuggestionsSpy.mock.calls.length).toBe(initialCalls);

// Advance time by 300ms
act(() => {
vi.advanceTimersByTime(300);
});

// Now getSearchSuggestions should be called once with the final debounced value
expect(getSearchSuggestionsSpy.mock.calls.length).toBe(initialCalls + 1);
expect(getSearchSuggestionsSpy).toHaveBeenLastCalledWith('cairo');
});

it('triggers onSearch when Enter is pressed without active dropdown selection', () => {
const onChange = vi.fn();
const onSearch = vi.fn();

render(<IntelligentAutoComplete value="starknet" onChange={onChange} onSearch={onSearch} />);

const input = screen.getByPlaceholderText('Search for knowledge, authors, or topics...');
fireEvent.keyDown(input, { key: 'Enter' });

expect(onSearch).toHaveBeenCalledWith('starknet');
});
});
Loading