diff --git a/src/components/research/SearchFilters.test.tsx b/src/components/research/SearchFilters.test.tsx index 7324c6723..3da8d69d9 100644 --- a/src/components/research/SearchFilters.test.tsx +++ b/src/components/research/SearchFilters.test.tsx @@ -1,6 +1,8 @@ import { render, screen, fireEvent } from '@testing-library/react' import React from 'react' -// @vitest-environment jsdom +/** + * @vitest-environment jsdom + */ import { describe, it, expect, vi } from 'vitest' import SearchFilters, { type SearchFiltersState } from './SearchFilters' @@ -8,6 +10,14 @@ import SearchFilters, { type SearchFiltersState } from './SearchFilters' // Setup Mock for onChange const mockOnChange = vi.fn() +import { afterEach } from 'vitest' +import { cleanup } from '@testing-library/react' + +afterEach(() => { + cleanup() + vi.clearAllMocks() +}) + const defaultFilters: SearchFiltersState = { topics: [], minRelevance: 0, @@ -26,11 +36,11 @@ describe('SearchFilters', () => { it('renders all filter sections', () => { render() - expect(screen.getByText('Advanced Filters')).toBeInTheDocument() - expect(screen.getByLabelText('Year From')).toBeInTheDocument() - expect(screen.getByLabelText('Year To')).toBeInTheDocument() - expect(screen.getByText('Therapeutic Topics')).toBeInTheDocument() - expect(screen.getByText('Min Relevance Score')).toBeInTheDocument() + expect(screen.getByText('Advanced Filters')).not.toBeNull() + expect(screen.getByLabelText('Year From')).not.toBeNull() + expect(screen.getByLabelText('Year To')).not.toBeNull() + expect(screen.getByText('Therapeutic Topics')).not.toBeNull() + expect(screen.getByText('Min Relevance Score')).not.toBeNull() }) it('toggles topics correctly', () => { @@ -43,7 +53,7 @@ describe('SearchFilters', () => { // AND handleApply calls onChange. But wait, toggleTopic updates local state. // We verify the button indicates it is pressed or selected visually (class check or aria-pressed). // After clicking, it should be pressed (true) - expect(topicButton).toHaveAttribute('aria-pressed', 'true') + expect(topicButton.getAttribute('aria-pressed')).toBe('true') }) it('calls onChange with new filters when Apply is clicked', () => { diff --git a/src/components/research/SearchInterface.tsx b/src/components/research/SearchInterface.tsx index 386be3835..3fd3be090 100644 --- a/src/components/research/SearchInterface.tsx +++ b/src/components/research/SearchInterface.tsx @@ -1,5 +1,5 @@ import { motion } from 'framer-motion' -import React, { useState } from 'react' +import React, { useState, useCallback } from 'react' import { researchAPI, type BookMetadata } from '@/lib/api/research' @@ -30,62 +30,72 @@ export default function SearchInterface() { // Export State const [showExport, setShowExport] = useState(false) - const executeSearch = async ( - currentQuery: string, - currentSources: string[], - currentFilters: SearchFiltersState, - ) => { - if (!currentQuery.trim()) return - - setLoading(true) - setHasSearched(true) - setError(null) - setResults([]) - - try { - // Track search event - void researchAPI.trackEvent('search_literature', { - query: currentQuery, - sources: currentSources, - filter_count: - (currentFilters.topics.length || 0) + - (currentFilters.yearFrom ? 1 : 0) + - (currentFilters.yearTo ? 1 : 0), - }) - - const data = await researchAPI.searchLiterature({ - q: currentQuery, - limit: 12, - sources: currentSources.includes('all') ? undefined : currentSources, - year_from: currentFilters.yearFrom, - year_to: currentFilters.yearTo, - min_relevance: currentFilters.minRelevance, - topics: currentFilters.topics, - sort_by: currentFilters.sortBy, - }) - - setResults(data.results) - } catch (err: any) { - console.error('Search error:', err) - setError(err.message || 'Failed to fetch results.') - } finally { - setLoading(false) - } - } - - const handleSearch = (e: React.FormEvent) => { - e.preventDefault() - void executeSearch(query, selectedSources, filters) - } - - const handleFilterChange = (newFilters: SearchFiltersState) => { - setFilters(newFilters) - setShowFilters(false) - // Auto-refresh search if we already have a query - if (query.trim()) { - void executeSearch(query, selectedSources, newFilters) - } - } + // ⚡ Bolt: wrap executeSearch and related event handlers in useCallback to prevent unnecessary re-creations and re-renders of child components + const executeSearch = useCallback( + async ( + currentQuery: string, + currentSources: string[], + currentFilters: SearchFiltersState, + ) => { + if (!currentQuery.trim()) return + + setLoading(true) + setHasSearched(true) + setError(null) + setResults([]) + + try { + // Track search event + void researchAPI.trackEvent('search_literature', { + query: currentQuery, + sources: currentSources, + filter_count: + (currentFilters.topics.length || 0) + + (currentFilters.yearFrom ? 1 : 0) + + (currentFilters.yearTo ? 1 : 0), + }) + + const data = await researchAPI.searchLiterature({ + q: currentQuery, + limit: 12, + sources: currentSources.includes('all') ? undefined : currentSources, + year_from: currentFilters.yearFrom, + year_to: currentFilters.yearTo, + min_relevance: currentFilters.minRelevance, + topics: currentFilters.topics, + sort_by: currentFilters.sortBy, + }) + + setResults(data.results) + } catch (err: any) { + console.error('Search error:', err) + setError(err.message || 'Failed to fetch results.') + } finally { + setLoading(false) + } + }, + [], + ) + + const handleSearch = useCallback( + (e: React.FormEvent) => { + e.preventDefault() + void executeSearch(query, selectedSources, filters) + }, + [executeSearch, query, selectedSources, filters], + ) + + const handleFilterChange = useCallback( + (newFilters: SearchFiltersState) => { + setFilters(newFilters) + setShowFilters(false) + // Auto-refresh search if we already have a query + if (query.trim()) { + void executeSearch(query, selectedSources, newFilters) + } + }, + [executeSearch, query, selectedSources], + ) return (