refactor(search): runDebounced ohne missweisenden Parameter

Der _q-Parameter wurde nie benutzt — Consumer sollen stattdessen
store.query im \$effect lesen, dann runDebounced() callen. Weniger
Footgun, explizitere Call-Site.

Tests-Rename: "mid-flight" → "cleared/changed", beschreibt was der
Test tatsaechlich absichert.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-19 12:47:40 +02:00
parent fc47c78397
commit 4edddc38e3
2 changed files with 13 additions and 13 deletions

View File

@@ -52,7 +52,7 @@ export class SearchStore {
this.fetchImpl = opts.fetchImpl ?? ((...a) => fetch(...a)); this.fetchImpl = opts.fetchImpl ?? ((...a) => fetch(...a));
} }
runDebounced(_q: string): void { runDebounced(): void {
if (this.debounceTimer) clearTimeout(this.debounceTimer); if (this.debounceTimer) clearTimeout(this.debounceTimer);
if (this.skipNextDebounce) { if (this.skipNextDebounce) {
this.skipNextDebounce = false; this.skipNextDebounce = false;

View File

@@ -27,7 +27,7 @@ describe('SearchStore', () => {
const fetchImpl = mockFetch([]); const fetchImpl = mockFetch([]);
const store = new SearchStore({ fetchImpl, debounceMs: 50 }); const store = new SearchStore({ fetchImpl, debounceMs: 50 });
store.query = 'abc'; store.query = 'abc';
store.runDebounced(store.query); store.runDebounced();
await vi.advanceTimersByTimeAsync(100); await vi.advanceTimersByTimeAsync(100);
expect(store.searching).toBe(false); expect(store.searching).toBe(false);
expect(fetchImpl).not.toHaveBeenCalled(); expect(fetchImpl).not.toHaveBeenCalled();
@@ -40,7 +40,7 @@ describe('SearchStore', () => {
]); ]);
const store = new SearchStore({ fetchImpl, debounceMs: 50, pageSize: 30 }); const store = new SearchStore({ fetchImpl, debounceMs: 50, pageSize: 30 });
store.query = 'pasta'; store.query = 'pasta';
store.runDebounced(store.query); store.runDebounced();
expect(store.searching).toBe(true); expect(store.searching).toBe(true);
await vi.advanceTimersByTimeAsync(100); await vi.advanceTimersByTimeAsync(100);
await vi.waitFor(() => expect(fetchImpl).toHaveBeenCalled()); await vi.waitFor(() => expect(fetchImpl).toHaveBeenCalled());
@@ -58,7 +58,7 @@ describe('SearchStore', () => {
]); ]);
const store = new SearchStore({ fetchImpl, debounceMs: 50 }); const store = new SearchStore({ fetchImpl, debounceMs: 50 });
store.query = 'pizza'; store.query = 'pizza';
store.runDebounced(store.query); store.runDebounced();
await vi.advanceTimersByTimeAsync(100); await vi.advanceTimersByTimeAsync(100);
await vi.waitFor(() => expect(store.webHits).toHaveLength(1)); await vi.waitFor(() => expect(store.webHits).toHaveLength(1));
expect(fetchImpl).toHaveBeenCalledTimes(2); expect(fetchImpl).toHaveBeenCalledTimes(2);
@@ -66,7 +66,7 @@ describe('SearchStore', () => {
expect(store.webPageno).toBe(1); expect(store.webPageno).toBe(1);
}); });
it('race-guard: fetch response discarded when query changed mid-flight', async () => { it('race-guard: stale fetch response discarded when query was cleared/changed', async () => {
vi.useFakeTimers(); vi.useFakeTimers();
let resolveFetch!: (v: Response) => void; let resolveFetch!: (v: Response) => void;
const fetchImpl = vi.fn( const fetchImpl = vi.fn(
@@ -77,7 +77,7 @@ describe('SearchStore', () => {
); );
const store = new SearchStore({ fetchImpl, debounceMs: 10 }); const store = new SearchStore({ fetchImpl, debounceMs: 10 });
store.query = 'stale-query'; store.query = 'stale-query';
store.runDebounced(store.query); store.runDebounced();
await vi.advanceTimersByTimeAsync(15); await vi.advanceTimersByTimeAsync(15);
expect(fetchImpl).toHaveBeenCalledTimes(1); expect(fetchImpl).toHaveBeenCalledTimes(1);
// User keeps typing BEFORE the response arrives — race-guard should kick in // User keeps typing BEFORE the response arrives — race-guard should kick in
@@ -118,7 +118,7 @@ describe('SearchStore', () => {
]); ]);
const store = new SearchStore({ fetchImpl, debounceMs: 10, pageSize: 30 }); const store = new SearchStore({ fetchImpl, debounceMs: 10, pageSize: 30 });
store.query = 'meal'; store.query = 'meal';
store.runDebounced(store.query); store.runDebounced();
await vi.advanceTimersByTimeAsync(15); await vi.advanceTimersByTimeAsync(15);
await vi.waitFor(() => expect(store.hits).toHaveLength(30)); await vi.waitFor(() => expect(store.hits).toHaveLength(30));
expect(store.localExhausted).toBe(false); expect(store.localExhausted).toBe(false);
@@ -140,7 +140,7 @@ describe('SearchStore', () => {
]); ]);
const store = new SearchStore({ fetchImpl, debounceMs: 10, pageSize: 30 }); const store = new SearchStore({ fetchImpl, debounceMs: 10, pageSize: 30 });
store.query = 'soup'; store.query = 'soup';
store.runDebounced(store.query); store.runDebounced();
await vi.advanceTimersByTimeAsync(15); await vi.advanceTimersByTimeAsync(15);
await vi.waitFor(() => expect(store.hits).toHaveLength(1)); await vi.waitFor(() => expect(store.hits).toHaveLength(1));
expect(store.localExhausted).toBe(true); expect(store.localExhausted).toBe(true);
@@ -159,7 +159,7 @@ describe('SearchStore', () => {
]); ]);
const store = new SearchStore({ fetchImpl, debounceMs: 10 }); const store = new SearchStore({ fetchImpl, debounceMs: 10 });
store.query = 'anything'; store.query = 'anything';
store.runDebounced(store.query); store.runDebounced();
await vi.advanceTimersByTimeAsync(15); await vi.advanceTimersByTimeAsync(15);
await vi.waitFor(() => expect(store.webError).toBe('SearXNG unreachable')); await vi.waitFor(() => expect(store.webError).toBe('SearXNG unreachable'));
expect(store.webExhausted).toBe(true); expect(store.webExhausted).toBe(true);
@@ -170,7 +170,7 @@ describe('SearchStore', () => {
const fetchImpl = mockFetch([]); const fetchImpl = mockFetch([]);
const store = new SearchStore({ fetchImpl, debounceMs: 100 }); const store = new SearchStore({ fetchImpl, debounceMs: 100 });
store.query = 'foobar'; store.query = 'foobar';
store.runDebounced(store.query); store.runDebounced();
store.reset(); store.reset();
await vi.advanceTimersByTimeAsync(200); await vi.advanceTimersByTimeAsync(200);
expect(store.query).toBe(''); expect(store.query).toBe('');
@@ -195,7 +195,7 @@ describe('SearchStore', () => {
store.restoreSnapshot(snap); store.restoreSnapshot(snap);
expect(store.query).toBe('lasagne'); expect(store.query).toBe('lasagne');
expect(store.hits).toHaveLength(1); expect(store.hits).toHaveLength(1);
store.runDebounced(store.query); store.runDebounced();
await vi.advanceTimersByTimeAsync(100); await vi.advanceTimersByTimeAsync(100);
expect(fetchImpl).not.toHaveBeenCalled(); expect(fetchImpl).not.toHaveBeenCalled();
const round = store.captureSnapshot(); const round = store.captureSnapshot();
@@ -214,7 +214,7 @@ describe('SearchStore', () => {
filterParam: () => '&domains=chefkoch.de' filterParam: () => '&domains=chefkoch.de'
}); });
store.query = 'curry'; store.query = 'curry';
store.runDebounced(store.query); store.runDebounced();
await vi.advanceTimersByTimeAsync(15); await vi.advanceTimersByTimeAsync(15);
await vi.waitFor(() => expect(fetchImpl).toHaveBeenCalledTimes(2)); await vi.waitFor(() => expect(fetchImpl).toHaveBeenCalledTimes(2));
expect(fetchImpl.mock.calls[0][0]).toMatch(/&domains=chefkoch\.de/); expect(fetchImpl.mock.calls[0][0]).toMatch(/&domains=chefkoch\.de/);
@@ -236,7 +236,7 @@ describe('SearchStore', () => {
filterParam: () => filter filterParam: () => filter
}); });
store.query = 'broth'; store.query = 'broth';
store.runDebounced(store.query); store.runDebounced();
await vi.advanceTimersByTimeAsync(15); await vi.advanceTimersByTimeAsync(15);
filter = '&domains=chefkoch.de'; filter = '&domains=chefkoch.de';
store.reSearch(); store.reSearch();