diff --git a/frontend/src/routes/persons/+page.svelte b/frontend/src/routes/persons/+page.svelte
index 919440c6..4a389e23 100644
--- a/frontend/src/routes/persons/+page.svelte
+++ b/frontend/src/routes/persons/+page.svelte
@@ -1,16 +1,24 @@
@@ -49,8 +57,10 @@ function handleSearch(e: Event) {
id="search"
type="text"
placeholder={m.persons_search_placeholder()}
- value={data.q || ''}
+ bind:value={q}
oninput={handleSearch}
+ onfocus={() => (qFocused = true)}
+ onblur={() => (qFocused = false)}
class="block w-full rounded-sm border border-gray-300 bg-white py-2.5 pr-10 pl-4 font-sans text-sm text-brand-navy placeholder-gray-400 shadow-sm focus:border-brand-navy focus:ring-1 focus:ring-brand-navy focus:outline-none"
/>
new Promise((r) => setTimeout(r, 0));
+
+vi.mock('$app/navigation', () => ({ goto: vi.fn() }));
+
+const makePerson = (overrides = {}) => ({
+ id: '1',
+ firstName: 'Max',
+ lastName: 'Mustermann',
+ ...overrides
+});
+
+const emptyData = { user: undefined, canWrite: true, q: '', persons: [] };
+const dataWithPersons = { ...emptyData, persons: [makePerson()] };
+
+// ─── Rendering ────────────────────────────────────────────────────────────────
+
+describe('Persons page – rendering', () => {
+ it('renders the search input', async () => {
+ render(Page, { data: emptyData });
+ await expect.element(page.getByRole('textbox')).toBeInTheDocument();
+ });
+
+ it('pre-fills the search input from data.q', async () => {
+ render(Page, { data: { ...emptyData, q: 'Müller' } });
+ await expect.element(page.getByRole('textbox')).toHaveValue('Müller');
+ });
+
+ it('shows empty state when no persons', async () => {
+ render(Page, { data: emptyData });
+ await expect.element(page.getByText('Keine Personen gefunden')).toBeInTheDocument();
+ });
+
+ it('renders person cards', async () => {
+ render(Page, { data: dataWithPersons });
+ await expect.element(page.getByText('Max Mustermann')).toBeInTheDocument();
+ });
+
+ it('links person card to detail page', async () => {
+ render(Page, { data: dataWithPersons });
+ await expect
+ .element(page.getByRole('link', { name: /Max Mustermann/ }))
+ .toHaveAttribute('href', '/persons/1');
+ });
+});
+
+// ─── Keystroke preservation (issue #34) ──────────────────────────────────────
+
+describe('Persons page – search input keystroke preservation', () => {
+ it('does not overwrite the search input while the user is focused and stale data arrives', async () => {
+ const { rerender } = render(Page, { data: emptyData });
+
+ const input = page.getByRole('textbox');
+
+ // User types "abc" — input is focused
+ await input.click();
+ await input.fill('abc');
+
+ // Simulate a navigation completing with stale data (q='a') while the user is still typing
+ await rerender({ data: { ...emptyData, q: 'a' } });
+ await tick();
+
+ // Input must still show what the user typed, not the stale URL value
+ await expect.element(input).toHaveValue('abc');
+ });
+});