diff --git a/src/components/search/IntelligentAutoComplete.tsx b/src/components/search/IntelligentAutoComplete.tsx index f5cb6463..7cdbcfd5 100644 --- a/src/components/search/IntelligentAutoComplete.tsx +++ b/src/components/search/IntelligentAutoComplete.tsx @@ -3,20 +3,25 @@ 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( - ({ value, onChange, onSearch, history = [] }) => { + ({ value, onChange, onSearch, history = [], debounceMs = 300 }) => { const [isOpen, setIsOpen] = useState(false); const [activeIndex, setActiveIndex] = useState(-1); const dropdownRef = useRef(null); const inputRef = useRef(null); + + const debouncedValue = useDebounce(value, debounceMs); + const suggestions = useMemo(() => getSearchSuggestions(debouncedValue), [debouncedValue]); const suggestions = useMemo(() => getSearchSuggestions(value), [value]); useEffect(() => { @@ -154,11 +159,11 @@ export const IntelligentAutoComplete = React.memo( {getSuggestionIcon(suggestion)} - {highlightMatch(suggestion, value).map((part, index) => ( + {highlightMatch(suggestion, debouncedValue).map((part, index) => ( { + beforeEach(() => { + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.useRealTimers(); + vi.restoreAllMocks(); + }); + + it('renders input with given initial value', () => { + const onChange = vi.fn(); + const onSearch = vi.fn(); + + render(); + + 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( + , + ); + + const initialCalls = getSearchSuggestionsSpy.mock.calls.length; + + // Simulate typing multiple characters sequentially + rerender(); + rerender(); + rerender(); + rerender(); + rerender(); + + // 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(); + + const input = screen.getByPlaceholderText('Search for knowledge, authors, or topics...'); + fireEvent.keyDown(input, { key: 'Enter' }); + + expect(onSearch).toHaveBeenCalledWith('starknet'); + }); +});