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 (