dd67f28745
Centralizes the inline { page: { get url() { return state.pageUrl } } }
mock shape via test-utils/mocks/appState.ts. The vi.hoisted state
declaration remains per-file (vitest mock-hoisting requires module-
level declaration). Adds a $test-utils alias to svelte.config.js so
the helper can be imported without ../../../ chains.
SKIPPED (mock exposes more than url):
- Shell.test.ts, MobileNavDrawer.test.ts, admin.test.ts: static
page: { url: ... } shape, no hoisted state to centralize.
- album.test.ts, artist.test.ts: page exposes both params and url.
- playlist.test.ts, reset-password.test.ts: page exposes params only.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
94 lines
3.5 KiB
TypeScript
94 lines
3.5 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
|
|
import { render, screen, fireEvent } from '@testing-library/svelte';
|
|
import { pageUrlModule } from '$test-utils/mocks/appState';
|
|
|
|
const state = vi.hoisted(() => ({
|
|
pageUrl: new URL('http://localhost/')
|
|
}));
|
|
|
|
vi.mock('$app/state', () => pageUrlModule(state));
|
|
|
|
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, keepFocus: true });
|
|
});
|
|
|
|
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, keepFocus: true });
|
|
});
|
|
|
|
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, keepFocus: 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, keepFocus: 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, keepFocus: true });
|
|
});
|
|
});
|