// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= // Comprehensive unit tests for SearchInput component import { fireEvent, render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { useState } from 'react'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import SearchInput from '../../../src/components/SearchInput/index'; // Mock the Input component from ui (matching relative import in component) vi.mock('../../../src/components/ui/input', () => ({ Input: vi.fn().mockImplementation((props) => { const { leadingIcon, ...restProps } = props; return (
{leadingIcon && (
{leadingIcon}
)}
); }), })); // Mock lucide-react vi.mock('lucide-react', () => ({ Search: vi .fn() .mockImplementation((props) => (
)), })); // Mock i18next vi.mock('react-i18next', () => ({ useTranslation: () => ({ t: (key: string) => { if (key === 'setting.search-mcp') return 'Search MCPs'; return key; }, }), })); describe('SearchInput Component', () => { const defaultProps = { value: '', onChange: vi.fn(), }; beforeEach(() => { vi.clearAllMocks(); }); afterEach(() => { vi.clearAllMocks(); }); describe('Initial Render', () => { it('should render input field', () => { render(); const input = screen.getByRole('textbox'); expect(input).toBeInTheDocument(); }); it('should render with empty value initially', () => { render(); const input = screen.getByRole('textbox'); expect(input).toHaveValue(''); }); it('should render with provided value', () => { render(); const input = screen.getByRole('textbox'); expect(input).toHaveValue('test search'); }); it('should render search icon', () => { render(); const searchIcons = screen.getAllByTestId('search-icon'); expect(searchIcons.length).toBeGreaterThan(0); }); }); describe('Placeholder Behavior', () => { it('should have placeholder attribute', () => { render(); const input = screen.getByPlaceholderText('Search MCPs'); expect(input).toBeInTheDocument(); }); it('should show placeholder when value is empty', () => { render(); const input = screen.getByPlaceholderText('Search MCPs'); expect(input).toHaveValue(''); }); it('should not show placeholder text when input has value', () => { render(); const input = screen.getByRole('textbox') as HTMLInputElement; expect(input.value).toBe('search term'); }); it('should maintain placeholder after focus and blur when empty', async () => { const user = userEvent.setup(); render(); const input = screen.getByRole('textbox'); // Focus the input await user.click(input); // Blur the input await user.tab(); // Placeholder should still be present expect(screen.getByPlaceholderText('Search MCPs')).toBeInTheDocument(); }); }); describe('Focus States', () => { it('should handle focus event', async () => { const user = userEvent.setup(); render(); const input = screen.getByRole('textbox'); await user.click(input); expect(input).toHaveFocus(); }); it('should handle blur event', async () => { const user = userEvent.setup(); render(); const input = screen.getByRole('textbox'); await user.click(input); await user.tab(); expect(input).not.toHaveFocus(); }); it('should accept text input when focused', async () => { const user = userEvent.setup(); const mockOnChange = vi.fn(); render(); const input = screen.getByRole('textbox'); await user.click(input); await user.keyboard('test'); expect(mockOnChange).toHaveBeenCalled(); }); }); describe('Input Handling', () => { it('should call onChange when input value changes', async () => { const user = userEvent.setup(); // Use a controlled wrapper so typing updates the input's value reliably in tests const Controlled = () => { const [val, setVal] = useState(''); return ( setVal(e.target.value)} /> ); }; render(); const input = screen.getByRole('textbox') as HTMLInputElement; await user.type(input, 'test'); // The DOM input should now contain 'test' expect(input.value).toBe('test'); }); it('should handle backspace correctly', async () => { const user = userEvent.setup(); // Controlled instance to reflect backspace in DOM const Controlled = () => { const [val, setVal] = useState('test'); return ( setVal(e.target.value)} /> ); }; render(); const input = screen.getByRole('textbox') as HTMLInputElement; await user.click(input); await user.keyboard('{Backspace}'); // The DOM input should have one less character expect(input.value).toBe('tes'); }); it('should handle clear input', async () => { const user = userEvent.setup(); const Controlled = () => { const [val, setVal] = useState('test'); return ( setVal(e.target.value)} /> ); }; render(); const input = screen.getByRole('textbox') as HTMLInputElement; await user.clear(input); expect(input.value).toBe(''); }); }); describe('Icon Positioning', () => { it('should render search icon in component', () => { render(); const searchIcon = screen.getByTestId('search-icon'); expect(searchIcon).toBeInTheDocument(); }); it('should include leading icon when value is empty', () => { render(); // The component should render with a leading icon const iconWrapper = document.querySelector('.leading-icon-wrapper'); expect(iconWrapper).toBeInTheDocument(); }); it('should include leading icon when input has value', () => { render(); // The component should render with a leading icon const iconWrapper = document.querySelector('.leading-icon-wrapper'); expect(iconWrapper).toBeInTheDocument(); }); }); describe('Styling and Classes', () => { it('should render within a container with relative positioning', () => { render(); const container = screen.getByRole('textbox').parentElement; expect(container).toHaveClass('relative', 'w-full'); }); it('should apply placeholder to input', () => { render(); const input = screen.getByRole('textbox'); expect(input).toHaveAttribute('placeholder', 'Search MCPs'); }); it('should render search icon component', () => { render(); const searchIcon = screen.getByTestId('search-icon'); expect(searchIcon).toBeInTheDocument(); }); }); describe('Keyboard Navigation', () => { it('should handle Tab key for navigation', async () => { const user = userEvent.setup(); render(
); const input = screen.getByRole('textbox'); const button = screen.getByRole('button'); await user.click(input); expect(input).toHaveFocus(); await user.tab(); expect(button).toHaveFocus(); }); it('should handle Shift+Tab for reverse navigation', async () => { const user = userEvent.setup(); render(
); const input = screen.getByRole('textbox'); const button = screen.getByRole('button'); await user.click(input); expect(input).toHaveFocus(); await user.keyboard('{Shift>}{Tab}{/Shift}'); expect(button).toHaveFocus(); }); it('should handle Enter key', async () => { const user = userEvent.setup(); const mockOnChange = vi.fn(); render(); const input = screen.getByRole('textbox'); await user.click(input); await user.keyboard('{Enter}'); // Enter key should not change the value expect(mockOnChange).not.toHaveBeenCalledWith( expect.objectContaining({ target: expect.objectContaining({ value: expect.stringContaining('\n'), }), }) ); }); it('should handle Escape key', async () => { const user = userEvent.setup(); render(); const input = screen.getByRole('textbox'); await user.click(input); expect(input).toHaveFocus(); await user.keyboard('{Escape}'); // Component doesn't implement Escape key handling, so focus remains // This is expected behavior for a simple search input expect(input).toHaveFocus(); }); }); describe('Accessibility', () => { it('should have proper role attribute', () => { render(); const input = screen.getByRole('textbox'); expect(input).toBeInTheDocument(); }); it('should be focusable', async () => { const user = userEvent.setup(); render(); const input = screen.getByRole('textbox'); await user.tab(); expect(input).toHaveFocus(); }); it('should handle screen reader accessibility', () => { render(); const input = screen.getByRole('textbox'); // Should be accessible to screen readers expect(input).toBeVisible(); expect(input).not.toHaveAttribute('aria-hidden', 'true'); }); }); describe('Edge Cases', () => { it('should handle very long input values', async () => { const user = userEvent.setup(); const longValue = 'a'.repeat(1000); const mockOnChange = vi.fn(); render(); const input = screen.getByRole('textbox'); await user.type(input, longValue); expect(mockOnChange).toHaveBeenCalledTimes(1000); }); it('should handle special characters', async () => { const _user = userEvent.setup(); const specialChars = '!@#$%^&*()_+-=[]{}|;:,.<>?'; const mockOnChange = vi.fn(); render(); const input = screen.getByRole('textbox'); // Send each character as an input change to avoid user-event parsing of bracket sequences for (const ch of specialChars) { const newVal = (input as HTMLInputElement).value + ch; fireEvent.change(input, { target: { value: newVal } }); } expect(mockOnChange).toHaveBeenCalledTimes(specialChars.length); }); it('should handle unicode characters', async () => { const user = userEvent.setup(); const unicodeText = '测试 🚀 العربية'; const mockOnChange = vi.fn(); render(); const input = screen.getByRole('textbox'); await user.type(input, unicodeText); expect(mockOnChange).toHaveBeenCalled(); }); it('should handle rapid typing', async () => { const user = userEvent.setup(); const mockOnChange = vi.fn(); render(); const input = screen.getByRole('textbox'); // Type multiple characters quickly await user.type(input, 'quick', { delay: 1 }); expect(mockOnChange).toHaveBeenCalledTimes(5); // 'q', 'u', 'i', 'c', 'k' }); }); describe('Component State Management', () => { it('should handle value changes correctly', async () => { const user = userEvent.setup(); const Controlled = () => { const [val, setVal] = useState(''); return ( setVal(e.target.value)} /> ); }; render(); const input = screen.getByRole('textbox'); // Type text await user.type(input, 'test'); expect((input as HTMLInputElement).value).toBe('test'); }); it('should handle rapid value changes', async () => { const user = userEvent.setup(); const Controlled = () => { const [val, setVal] = useState(''); return ( setVal(e.target.value)} /> ); }; render(); const input = screen.getByRole('textbox') as HTMLInputElement; // Rapid focus and type await user.click(input); await user.keyboard('quick'); expect(input.value).toBe('quick'); }); }); describe('Props Validation', () => { it('should handle missing onChange prop gracefully', () => { const consoleErrorSpy = vi .spyOn(console, 'error') .mockImplementation(() => {}); expect(() => { render(); }).not.toThrow(); consoleErrorSpy.mockRestore(); }); it('should handle null value prop', () => { render(); const input = screen.getByRole('textbox'); expect(input).toHaveValue(''); }); it('should handle undefined value prop', () => { render(); const input = screen.getByRole('textbox'); expect(input).toHaveValue(''); }); }); });