diff --git a/frontend/src/lib/person/PersonTypeahead.svelte.test.ts b/frontend/src/lib/person/PersonTypeahead.svelte.test.ts index 7e582589..b23be043 100644 --- a/frontend/src/lib/person/PersonTypeahead.svelte.test.ts +++ b/frontend/src/lib/person/PersonTypeahead.svelte.test.ts @@ -106,9 +106,9 @@ describe('PersonTypeahead', () => { const input = document.querySelector('input#s-search') as HTMLInputElement; input.dispatchEvent(new Event('focus', { bubbles: true })); - await new Promise((r) => setTimeout(r, 100)); - const listbox = document.querySelector('[role="listbox"]'); - expect(listbox).not.toBeNull(); + await vi.waitFor(() => { + expect(document.querySelector('[role="listbox"]')).not.toBeNull(); + }); }); it('updates aria-expanded when the dropdown opens', async () => { @@ -120,30 +120,35 @@ describe('PersonTypeahead', () => { expect(input.getAttribute('aria-expanded')).toBe('false'); input.dispatchEvent(new Event('focus', { bubbles: true })); - await new Promise((r) => setTimeout(r, 100)); - expect(input.getAttribute('aria-expanded')).toBe('true'); + await vi.waitFor(() => { + expect(input.getAttribute('aria-expanded')).toBe('true'); + }); }); - it('opens the dropdown via Escape key without throwing', async () => { + it('Escape key on a closed dropdown is a no-op (no listbox appears)', async () => { render(PersonTypeahead, { props: { name: 's', label: 'Absender' } }); const input = document.querySelector('input#s-search') as HTMLInputElement; - expect(() => - input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true })) - ).not.toThrow(); + input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true })); + + expect(document.querySelector('[role="listbox"]')).toBeNull(); }); - it('handles fetch failure gracefully on focus', async () => { + it('keeps the input usable when fetch rejects on focus (no error UI, no crash)', async () => { fetchSpy.mockRejectedValueOnce(new Error('boom')); render(PersonTypeahead, { props: { name: 's', label: 'Absender', restrictToCorrespondentsOf: 'parent-id' } }); const input = document.querySelector('input#s-search') as HTMLInputElement; - expect(() => input.dispatchEvent(new Event('focus', { bubbles: true }))).not.toThrow(); + input.dispatchEvent(new Event('focus', { bubbles: true })); + + // Graceful failure: no listbox surfaces but the input stays mounted and interactive. + await vi.waitFor(() => expect(fetchSpy).toHaveBeenCalled()); + expect(document.querySelector('input#s-search')).not.toBeNull(); }); - it('handles fetch returning non-ok response on focus', async () => { + it('keeps the input usable when fetch returns a non-OK response on focus', async () => { fetchSpy.mockResolvedValueOnce(new Response('error', { status: 500 })); render(PersonTypeahead, { props: { name: 's', label: 'Absender', restrictToCorrespondentsOf: 'parent-id' } @@ -151,10 +156,9 @@ describe('PersonTypeahead', () => { const input = document.querySelector('input#s-search') as HTMLInputElement; input.dispatchEvent(new Event('focus', { bubbles: true })); - await new Promise((r) => setTimeout(r, 50)); - // Should still open the listbox (with no results) - // no throw is the assertion here - expect(true).toBe(true); + + await vi.waitFor(() => expect(fetchSpy).toHaveBeenCalled()); + expect(document.querySelector('input#s-search')).not.toBeNull(); }); it('renders the FieldLabelBadge when badge is provided', async () => { @@ -162,9 +166,7 @@ describe('PersonTypeahead', () => { props: { name: 's', label: 'Absender', badge: 'replace' } }); - // FieldLabelBadge renders a small badge element const label = document.querySelector('label[for="s-search"]'); - // The badge variant is passed; check the label has the additional badge child expect(label?.children.length).toBeGreaterThan(0); }); @@ -174,7 +176,6 @@ describe('PersonTypeahead', () => { }); const label = document.querySelector('label[for="s-search"]'); - // No badge child expect(label?.querySelector('[class*="badge"]')).toBeNull(); });