diff --git a/src/lib/client/search.svelte.ts b/src/lib/client/search.svelte.ts index 9272859..3b4ceaf 100644 --- a/src/lib/client/search.svelte.ts +++ b/src/lib/client/search.svelte.ts @@ -73,6 +73,8 @@ export class SearchStore { } async runSearch(q: string): Promise { + if (this.debounceTimer) clearTimeout(this.debounceTimer); + this.debounceTimer = null; this.localExhausted = false; this.webPageno = 0; this.webExhausted = false; diff --git a/tests/unit/search-store.test.ts b/tests/unit/search-store.test.ts index 744c457..c73c5bd 100644 --- a/tests/unit/search-store.test.ts +++ b/tests/unit/search-store.test.ts @@ -221,6 +221,22 @@ describe('SearchStore', () => { expect(fetchImpl.mock.calls[1][0]).toMatch(/&domains=chefkoch\.de/); }); + it('runSearch(q) cancels pending debounce to avoid double-fetch', async () => { + vi.useFakeTimers(); + const fetchImpl = mockFetch([ + { body: { hits: [{ id: 1, title: 'immediate', description: null, image_path: null, source_domain: null, avg_stars: null, last_cooked_at: null }] } } + ]); + const store = new SearchStore({ fetchImpl, debounceMs: 300 }); + store.query = 'meal'; + store.runDebounced(); // schedules the 300ms timer + // Before the timer fires, call runSearch immediately (e.g. form submit). + await store.runSearch('meal'); + expect(fetchImpl).toHaveBeenCalledTimes(1); + // Now advance past the original debounce — timer must not still fire. + await vi.advanceTimersByTimeAsync(400); + expect(fetchImpl).toHaveBeenCalledTimes(1); + }); + it('reSearch: immediate re-run with current query on filter change', async () => { vi.useFakeTimers(); let filter = '';