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:
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user