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('');
});