fix(search): Race-Guard-Test korrekt auf in-flight abzielen

Der vorherige Test setzte query NACH dem Fetch-Abschluss und erzwang
dafuer einen setter-Side-Effect, der bei normalem Tippen die Treffer
waehrend des Debounce-Fensters fuer 300ms leer geblitzt haette.

Jetzt: echter Race-Test mit manuell aufloesbarem fetch. Setter-Nebenwirkung
entfernt, query ist wieder plain \$state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-19 12:41:43 +02:00
parent 58ce19c160
commit fc47c78397
2 changed files with 34 additions and 21 deletions

View File

@@ -22,7 +22,7 @@ export type SearchStoreOptions = {
};
export class SearchStore {
#query = $state('');
query = $state('');
hits = $state<SearchHit[]>([]);
webHits = $state<WebHit[]>([]);
searching = $state(false);
@@ -34,21 +34,6 @@ export class SearchStore {
webExhausted = $state(false);
loadingMore = $state(false);
get query(): string {
return this.#query;
}
set query(v: string) {
this.#query = v;
// Race-guard: when the query diverges from the last committed search,
// any currently shown hits are stale and must be cleared immediately.
if (this.searchedFor !== null && v.trim() !== this.searchedFor) {
this.hits = [];
this.webHits = [];
this.searchedFor = null;
}
}
private readonly pageSize: number;
private readonly debounceMs: number;
private readonly filterDebounceMs: number;

View File

@@ -66,18 +66,46 @@ describe('SearchStore', () => {
expect(store.webPageno).toBe(1);
});
it('races-guards: stale response discarded when query changed mid-flight', async () => {
it('race-guard: fetch response discarded when query changed mid-flight', async () => {
vi.useFakeTimers();
const fetchImpl = mockFetch([
{ body: { hits: [{ id: 99, title: 'Stale', description: null, image_path: null, source_domain: null, avg_stars: null, last_cooked_at: null }] } }
]);
let resolveFetch!: (v: Response) => void;
const fetchImpl = vi.fn(
() =>
new Promise<Response>((resolve) => {
resolveFetch = resolve;
})
);
const store = new SearchStore({ fetchImpl, debounceMs: 10 });
store.query = 'stale-query';
store.runDebounced(store.query);
await vi.advanceTimersByTimeAsync(15);
expect(fetchImpl).toHaveBeenCalledTimes(1);
// User keeps typing BEFORE the response arrives — race-guard should kick in
// when the fetch finally resolves.
store.query = 'different';
await vi.waitFor(() => expect(fetchImpl).toHaveBeenCalled());
resolveFetch({
ok: true,
status: 200,
json: async () => ({
hits: [
{
id: 99,
title: 'Stale',
description: null,
image_path: null,
source_domain: null,
avg_stars: null,
last_cooked_at: null
}
]
})
} as Response);
// Flush microtasks so the awaited response + race-guard run.
await vi.runOnlyPendingTimersAsync();
await Promise.resolve();
await Promise.resolve();
expect(store.hits).toEqual([]);
expect(store.searchedFor).toBeNull();
});
it('loadMore: drains local first (offset pagination)', async () => {