fix(typeahead): surface search failures instead of an empty dropdown

The document picker parsed the response without checking r.ok, so a 401/500
rendered identically to 'no matches' and the dropdown silently vanished —
which is how the broken relevance path shipped invisibly. The fetch now
throws on non-OK, the useTypeahead hook exposes an error flag, and the
picker renders a visible failure message (de/en/es).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-06-09 23:53:29 +02:00
parent e988c3eae7
commit 6e006bafd0
8 changed files with 68 additions and 2 deletions

View File

@@ -78,6 +78,34 @@ describe('createTypeahead', () => {
errorSpy.mockRestore();
});
it('fetch error sets the error flag so callers can render a failure state', async () => {
const fetchUrl = vi.fn().mockRejectedValue(new Error('500'));
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const ta = createTypeahead({ fetchUrl, debounceMs: 0 });
expect(ta.error).toBe(false);
ta.setQuery('foo');
await vi.advanceTimersByTimeAsync(0);
expect(ta.error).toBe(true);
errorSpy.mockRestore();
});
it('error flag clears on the next successful fetch', async () => {
const fetchUrl = vi
.fn()
.mockRejectedValueOnce(new Error('500'))
.mockResolvedValueOnce([{ id: '1' }]);
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const ta = createTypeahead({ fetchUrl, debounceMs: 0 });
ta.setQuery('foo');
await vi.advanceTimersByTimeAsync(0);
expect(ta.error).toBe(true);
ta.setQuery('foob');
await vi.advanceTimersByTimeAsync(0);
expect(ta.error).toBe(false);
expect(ta.results).toEqual([{ id: '1' }]);
errorSpy.mockRestore();
});
it('setActiveIndex updates activeIndex', () => {
const ta = createTypeahead({ fetchUrl: vi.fn().mockResolvedValue([]) });
expect(ta.activeIndex).toBe(-1);

View File

@@ -11,6 +11,7 @@ export function createTypeahead<T>(options: Options<T>) {
let results: T[] = $state([]);
let isOpen = $state(false);
let loading = $state(false);
let error = $state(false);
let activeIndex = $state(-1);
let debounceTimer: ReturnType<typeof setTimeout> | undefined;
@@ -21,11 +22,13 @@ export function createTypeahead<T>(options: Options<T>) {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(async () => {
loading = true;
error = false;
try {
results = await fetchUrl(q);
} catch (e) {
console.error('typeahead fetch error', e);
results = [];
error = true;
} finally {
loading = false;
}
@@ -65,6 +68,9 @@ export function createTypeahead<T>(options: Options<T>) {
get loading() {
return loading;
},
get error() {
return error;
},
get activeIndex() {
return activeIndex;
},