import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; import { render, screen, fireEvent } from '@testing-library/svelte'; const state = vi.hoisted(() => ({ pageUrl: new URL('http://localhost/') })); vi.mock('$app/state', () => ({ page: { get url() { return state.pageUrl; } } })); vi.mock('$app/navigation', () => ({ goto: vi.fn() })); import SearchInput from './SearchInput.svelte'; import { goto } from '$app/navigation'; beforeEach(() => { vi.useFakeTimers(); state.pageUrl = new URL('http://localhost/'); }); afterEach(() => { vi.clearAllMocks(); vi.useRealTimers(); }); describe('SearchInput', () => { test('mount pre-fills value from page.url.searchParams', () => { state.pageUrl = new URL('http://localhost/search?q=hello'); render(SearchInput); const input = screen.getByRole('searchbox') as HTMLInputElement; expect(input.value).toBe('hello'); }); test('typing fires goto once after 250ms debounce', async () => { render(SearchInput); const input = screen.getByRole('searchbox') as HTMLInputElement; await fireEvent.input(input, { target: { value: 'h' } }); expect(goto).not.toHaveBeenCalled(); vi.advanceTimersByTime(250); expect(goto).toHaveBeenCalledTimes(1); expect(goto).toHaveBeenCalledWith('/search?q=h', { replaceState: false }); }); test('rapid keystrokes debounce to a single goto', async () => { render(SearchInput); const input = screen.getByRole('searchbox') as HTMLInputElement; await fireEvent.input(input, { target: { value: 'h' } }); vi.advanceTimersByTime(100); await fireEvent.input(input, { target: { value: 'he' } }); vi.advanceTimersByTime(100); await fireEvent.input(input, { target: { value: 'hel' } }); vi.advanceTimersByTime(250); expect(goto).toHaveBeenCalledTimes(1); expect(goto).toHaveBeenCalledWith('/search?q=hel', { replaceState: false }); }); test('typing while on /search uses replaceState=true', async () => { state.pageUrl = new URL('http://localhost/search?q=h'); render(SearchInput); const input = screen.getByRole('searchbox') as HTMLInputElement; await fireEvent.input(input, { target: { value: 'he' } }); vi.advanceTimersByTime(250); expect(goto).toHaveBeenCalledWith('/search?q=he', { replaceState: true }); }); test('clearing input on /search calls goto("/search") (drops ?q=)', async () => { state.pageUrl = new URL('http://localhost/search?q=h'); render(SearchInput); const input = screen.getByRole('searchbox') as HTMLInputElement; await fireEvent.input(input, { target: { value: '' } }); vi.advanceTimersByTime(250); expect(goto).toHaveBeenCalledWith('/search', { replaceState: true }); }); test('Escape clears the input value', async () => { render(SearchInput); const input = screen.getByRole('searchbox') as HTMLInputElement; await fireEvent.input(input, { target: { value: 'hello' } }); expect(input.value).toBe('hello'); await fireEvent.keyDown(input, { key: 'Escape' }); expect(input.value).toBe(''); }); test('encodeURIComponent applied to special chars', async () => { render(SearchInput); const input = screen.getByRole('searchbox') as HTMLInputElement; await fireEvent.input(input, { target: { value: 'a b&c' } }); vi.advanceTimersByTime(250); expect(goto).toHaveBeenCalledWith('/search?q=a%20b%26c', { replaceState: false }); }); });