import { useState } from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; import { vi, test, expect, beforeEach, afterEach } from 'vitest'; import SearchBar from '../../src/components/SearchBar'; import { search, type SearchResult } from '../../src/api/client'; function ControlledSearchBar({ onResults, debounceMs, }: { onResults: (results: SearchResult[]) => void; debounceMs?: number; }) { const [value, setValue] = useState(''); return ; } vi.mock('../../src/api/client'); beforeEach(() => { vi.mocked(search).mockReset(); vi.useFakeTimers(); }); afterEach(() => { vi.useRealTimers(); }); test('debounces input for 300ms before calling search', async () => { vi.mocked(search).mockResolvedValue({ results: [] }); const onQueryChange = vi.fn(); const onResults = vi.fn(); render(); const input = screen.getByRole('textbox') as HTMLInputElement; fireEvent.change(input, { target: { value: '检索' } }); expect(onQueryChange).toHaveBeenLastCalledWith('检索'); expect(search).not.toHaveBeenCalled(); vi.advanceTimersByTime(300); expect(search).toHaveBeenCalledWith('检索'); }); test('clearing the input immediately reports empty results without calling search', async () => { vi.mocked(search).mockResolvedValue({ results: [] }); const onResults = vi.fn(); render(); const input = screen.getByRole('textbox') as HTMLInputElement; fireEvent.change(input, { target: { value: 'x' } }); vi.advanceTimersByTime(300); fireEvent.change(input, { target: { value: '' } }); expect(onResults).toHaveBeenCalledWith([]); }); test('reflects the value prop, allowing a parent to control/reset the input', () => { const onQueryChange = vi.fn(); const onResults = vi.fn(); const { rerender } = render(); const input = screen.getByRole('textbox') as HTMLInputElement; expect(input).toHaveValue('检索'); rerender(); expect(input).toHaveValue(''); });