renderHook() renders a throwaway component that calls a hook and exposes its latest return value as result.current. BookNest's data hooks are TanStack Query 79,069 calls (Query and Zustand), which need a QueryClient and a network; the test supplies a fresh client in a wrapper and a jest.spyOn() stand-in for fetch:
// A fresh client per test: no retries, and no garbage-collection timer left running
function freshClient() {
const client = new QueryClient({
defaultOptions: { queries: { retry: false, gcTime: Infinity } } });
return ({ children }: { children: ReactNode }) =>
<QueryClientProvider client={client}>{children}</QueryClientProvider>;
}
const reply = (body: unknown, status = 200) => jest.spyOn(globalThis, 'fetch')
.mockResolvedValue(new Response(JSON.stringify(body), { status }));
const useBook = (id: number) => useQuery({ queryKey: keys.book(id), queryFn: () => getBook(id) });
afterEach(() => jest.restoreAllMocks());
test('useQuery goes from pending to the book', async () => {
const fetchMock = reply({ ...data.books[2], favorite: false });
const { result } = await renderHook(() => useBook(3), { wrapper: freshClient() });
expect(result.current.isPending).toBe(true);
await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(result.current.data?.title).toBe('Salt and Saffron');
expect(fetchMock.mock.calls[0][0]).toMatch(/\/books\/3$/);
});waitFor() re-runs its callback every 50 ms until it passes or a second is up, and findByText() is getByText() inside waitFor(); the file's third test uses it to wait for The Quiet Harbor in the whole catalog tab. A second test replies with status 500 and expects GET /books/3 failed: 500, which needs retry: false: TanStack Query otherwise retries three times, and the error arrives after waitFor() gives up. Without gcTime: Infinity, runs ended with Jest 62,362 did not exit one second after the test run has completed, because each client left a five-minute garbage-collection timer running. For code built on timers, use jest.useFakeTimers() and jest.advanceTimersByTime() instead of waiting.