import { queryKey, sleep } from '@tanstack/query-test-utils' import { act, fireEvent } from '@testing-library/preact' import type { VNode } from 'preact' import { Suspense } from 'preact/compat' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { QueryCache, QueryClient, noop, usePrefetchQuery, useQueryErrorResetBoundary, useSuspenseQuery, } from '..' import { ErrorBoundary } from './ErrorBoundary' import { renderWithClient } from './utils' describe('usePrefetchQuery', () => { let queryCache: QueryCache let queryClient: QueryClient beforeEach(() => { vi.useFakeTimers() queryCache = new QueryCache() queryClient = new QueryClient({ queryCache }) }) afterEach(() => { queryClient.clear() vi.useRealTimers() }) it('should prefetch query if query state does not exist', async () => { const queryOpts = { queryKey: queryKey(), queryFn: vi.fn(() => sleep(10).then(() => 'prefetchQuery')), } const componentQueryOpts = { ...queryOpts, queryFn: () => sleep(10).then(() => 'useSuspenseQuery'), } function Page() { const state = useSuspenseQuery(componentQueryOpts) return
data: {String(state.data)}
} function App() { usePrefetchQuery(queryOpts) return ( ) } const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('Loading...')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(10) expect(rendered.getByText('data: prefetchQuery')).toBeInTheDocument() expect(queryOpts.queryFn).toHaveBeenCalledTimes(1) }) it('should not prefetch query if query state exists', async () => { const queryOpts = { queryKey: queryKey(), queryFn: vi.fn(() => sleep(10).then(() => 'The usePrefetchQuery hook is smart!'), ), } function Page() { const state = useSuspenseQuery(queryOpts) return
data: {String(state.data)}
} function App() { usePrefetchQuery(queryOpts) return ( ) } void queryClient.query(queryOpts).catch(noop) await vi.advanceTimersByTimeAsync(10) queryOpts.queryFn.mockClear() const rendered = renderWithClient(queryClient, ) expect(rendered.queryByText('fetching: true')).not.toBeInTheDocument() expect( rendered.getByText('data: The usePrefetchQuery hook is smart!'), ).toBeInTheDocument() expect(queryOpts.queryFn).not.toHaveBeenCalled() }) it('should let errors fall through and not refetch failed queries', async () => { const consoleMock = vi.spyOn(console, 'error') consoleMock.mockImplementation(() => undefined) const queryFn = vi.fn(() => sleep(10).then(() => 'Not an error')) const queryOpts = { queryKey: queryKey(), queryFn, } queryFn.mockImplementationOnce(() => sleep(10).then(() => { throw new Error('Oops! Server error!') }), ) function Page() { const state = useSuspenseQuery(queryOpts) return
data: {String(state.data)}
} function App() { usePrefetchQuery(queryOpts) return (
Oops!
}>
) } void queryClient.query(queryOpts).catch(noop) await vi.advanceTimersByTimeAsync(10) queryFn.mockClear() const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('Oops!')).toBeInTheDocument() expect(rendered.queryByText('data: Not an error')).not.toBeInTheDocument() expect(queryOpts.queryFn).not.toHaveBeenCalled() consoleMock.mockRestore() }) it('should not create an endless loop when using inside a suspense boundary', async () => { const queryFn = vi.fn(() => sleep(10).then(() => 'prefetchedQuery')) const queryOpts = { queryKey: queryKey(), queryFn, } function Prefetch({ children }: { children: VNode }) { usePrefetchQuery(queryOpts) return <>{children} } function Page() { const state = useSuspenseQuery(queryOpts) return
data: {String(state.data)}
} function App() { return ( }> ) } const rendered = renderWithClient(queryClient, ) await vi.advanceTimersByTimeAsync(10) expect(rendered.getByText('data: prefetchedQuery')).toBeInTheDocument() expect(queryOpts.queryFn).toHaveBeenCalledTimes(1) }) it('should be able to recover from errors and try fetching again', async () => { const consoleMock = vi.spyOn(console, 'error') consoleMock.mockImplementation(() => undefined) const queryFn = vi.fn(() => sleep(10).then(() => 'This is fine :dog: :fire:'), ) const queryOpts = { queryKey: queryKey(), queryFn, } queryFn.mockImplementationOnce(() => sleep(10).then(() => { throw new Error('Oops! Server error!') }), ) function Page() { const state = useSuspenseQuery(queryOpts) return
data: {String(state.data)}
} function App() { const { reset } = useQueryErrorResetBoundary() usePrefetchQuery(queryOpts) return ( (
Oops!
)} >
) } void queryClient.query(queryOpts).catch(noop) await vi.advanceTimersByTimeAsync(10) queryFn.mockClear() const rendered = renderWithClient(queryClient, ) expect(rendered.getByText('Oops!')).toBeInTheDocument() fireEvent.click(rendered.getByText('Try again')) await vi.advanceTimersByTimeAsync(10) expect( rendered.getByText('data: This is fine :dog: :fire:'), ).toBeInTheDocument() expect(queryOpts.queryFn).toHaveBeenCalledTimes(1) consoleMock.mockRestore() }) it('should not create a suspense waterfall if prefetch is fired', async () => { const firstQueryOpts = { queryKey: queryKey(), queryFn: vi.fn(() => sleep(10).then(() => 'Prefetch is nice!')), } const secondQueryOpts = { queryKey: queryKey(), queryFn: vi.fn(() => sleep(10).then(() => 'Prefetch is really nice!!')), } const thirdQueryOpts = { queryKey: queryKey(), queryFn: vi.fn(() => sleep(10).then(() => 'Prefetch does not create waterfalls!!'), ), } const Fallback = vi.fn().mockImplementation(() =>
Loading...
) function FirstQuery({ children }: { children?: VNode }) { const state = useSuspenseQuery(firstQueryOpts) return (
data: {String(state.data)}
{children}
) } function SecondQuery({ children }: { children?: VNode }) { const state = useSuspenseQuery(secondQueryOpts) return (
data: {String(state.data)}
{children}
) } function ThirdQuery() { const state = useSuspenseQuery(thirdQueryOpts) return
data: {String(state.data)}
} function App() { usePrefetchQuery(firstQueryOpts) usePrefetchQuery(secondQueryOpts) usePrefetchQuery(thirdQueryOpts) return ( }> ) } const rendered = renderWithClient(queryClient, ) expect( queryClient.getQueryState(firstQueryOpts.queryKey)?.fetchStatus, ).toBe('fetching') expect( queryClient.getQueryState(secondQueryOpts.queryKey)?.fetchStatus, ).toBe('fetching') expect( queryClient.getQueryState(thirdQueryOpts.queryKey)?.fetchStatus, ).toBe('fetching') expect(rendered.getByText('Loading...')).toBeInTheDocument() await act(async () => { await vi.advanceTimersByTimeAsync(10) }) expect(rendered.getByText('data: Prefetch is nice!')).toBeInTheDocument() expect( rendered.getByText('data: Prefetch is really nice!!'), ).toBeInTheDocument() expect( rendered.getByText('data: Prefetch does not create waterfalls!!'), ).toBeInTheDocument() expect(Fallback).toHaveBeenCalledTimes(1) expect(firstQueryOpts.queryFn).toHaveBeenCalledTimes(1) expect(secondQueryOpts.queryFn).toHaveBeenCalledTimes(1) expect(thirdQueryOpts.queryFn).toHaveBeenCalledTimes(1) }) })