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:
@@ -22,7 +22,7 @@ export type SearchStoreOptions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export class SearchStore {
|
export class SearchStore {
|
||||||
#query = $state('');
|
query = $state('');
|
||||||
hits = $state<SearchHit[]>([]);
|
hits = $state<SearchHit[]>([]);
|
||||||
webHits = $state<WebHit[]>([]);
|
webHits = $state<WebHit[]>([]);
|
||||||
searching = $state(false);
|
searching = $state(false);
|
||||||
@@ -34,21 +34,6 @@ export class SearchStore {
|
|||||||
webExhausted = $state(false);
|
webExhausted = $state(false);
|
||||||
loadingMore = $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 pageSize: number;
|
||||||
private readonly debounceMs: number;
|
private readonly debounceMs: number;
|
||||||
private readonly filterDebounceMs: number;
|
private readonly filterDebounceMs: number;
|
||||||
|
|||||||
@@ -66,18 +66,46 @@ describe('SearchStore', () => {
|
|||||||
expect(store.webPageno).toBe(1);
|
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();
|
vi.useFakeTimers();
|
||||||
const fetchImpl = mockFetch([
|
let resolveFetch!: (v: Response) => void;
|
||||||
{ body: { hits: [{ id: 99, title: 'Stale', description: null, image_path: null, source_domain: null, avg_stars: null, last_cooked_at: null }] } }
|
const fetchImpl = vi.fn(
|
||||||
]);
|
() =>
|
||||||
|
new Promise<Response>((resolve) => {
|
||||||
|
resolveFetch = resolve;
|
||||||
|
})
|
||||||
|
);
|
||||||
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(store.query);
|
||||||
await vi.advanceTimersByTimeAsync(15);
|
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';
|
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.hits).toEqual([]);
|
||||||
|
expect(store.searchedFor).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('loadMore: drains local first (offset pagination)', async () => {
|
it('loadMore: drains local first (offset pagination)', async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user