test(person): rewrite PersonTypeahead test with behavioral assertions

Replaces 3 setTimeout sleeps with vi.waitFor on listbox / aria-expanded
state and converts 2 .not.toThrow smoke tests + 1 vacuous expect(true)
into assertions about the input remaining usable after fetch errors
and Escape on a closed dropdown being a no-op.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-05-11 17:19:04 +02:00
committed by marcel
parent f3915c4878
commit ca6342363a

View File

@@ -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();
});