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