import { QueryClient, QueryClientProvider, dehydrate, infiniteQueryOptions } from '@tanstack/react-query' import { render, screen } from '@testing-library/react' import type { ComponentProps, ReactNode } from 'react' import { describe, expect, it, vi } from 'vitest' import { QueriesHydration } from './QueriesHydration' // Mock the ClientOnly component vi.mock('./components/ClientOnly', () => ({ ClientOnly: ({ children, fallback }: { children: ReactNode; fallback?: ReactNode }) => (
{fallback || children}
), })) describe('', () => { it('should fetch queries and hydrate them successfully', async () => { const queryClient = new QueryClient() const mockQueryFn = vi.fn().mockResolvedValue({ data: 'test-data' }) const queries = [ { queryKey: ['test-query'], queryFn: mockQueryFn, }, ] const result = await QueriesHydration({ queries, queryClient, children:
Test Children
, }) expect(mockQueryFn).toHaveBeenCalledTimes(1) expect(result).toBeDefined() expect(result.type).toBeDefined() }) it('should handle multiple queries', async () => { const queryClient = new QueryClient() const mockQueryFn1 = vi.fn().mockResolvedValue({ data: 'data-1' }) const mockQueryFn2 = vi.fn().mockResolvedValue({ data: 'data-2' }) const queries = [ { queryKey: ['query-1'], queryFn: mockQueryFn1, }, { queryKey: ['query-2'], queryFn: mockQueryFn2, }, ] const result = await QueriesHydration({ queries, queryClient, children:
Test Children
, }) expect(mockQueryFn1).toHaveBeenCalledTimes(1) expect(mockQueryFn2).toHaveBeenCalledTimes(1) expect(result).toBeDefined() }) it('should skip SSR and render ClientOnly when query fails and skipSsrOnError is true (default)', async () => { const queryClient = new QueryClient() const mockQueryFn = vi.fn().mockRejectedValue(new Error('Query failed')) const queries = [ { queryKey: ['failing-query'], queryFn: mockQueryFn, }, ] const result = await QueriesHydration({ queries, queryClient, children:
Test Children
, }) expect(mockQueryFn).toHaveBeenCalledTimes(1) // Render the result to verify it's ClientOnly render(result as React.ReactElement) expect(screen.getByTestId('client-only')).toBeInTheDocument() }) it('should skip SSR and render ClientOnly when query fails and skipSsrOnError is explicitly true', async () => { const queryClient = new QueryClient() const mockQueryFn = vi.fn().mockRejectedValue(new Error('Query failed')) const queries = [ { queryKey: ['failing-query'], queryFn: mockQueryFn, }, ] const result = await QueriesHydration({ queries, queryClient, skipSsrOnError: true, children:
Test Children
, }) expect(mockQueryFn).toHaveBeenCalledTimes(1) render(result as React.ReactElement) expect(screen.getByTestId('client-only')).toBeInTheDocument() }) it('should show custom fallback when query fails and skipSsrOnError has fallback', async () => { const queryClient = new QueryClient() const mockQueryFn = vi.fn().mockRejectedValue(new Error('Query failed')) const queries = [ { queryKey: ['failing-query'], queryFn: mockQueryFn, }, ] const customFallback =
Custom Fallback
const result = await QueriesHydration({ queries, queryClient, skipSsrOnError: { fallback: customFallback }, children:
Test Children
, }) expect(mockQueryFn).toHaveBeenCalledTimes(1) render(result as React.ReactElement) expect(screen.getByTestId('client-only')).toBeInTheDocument() }) it('should proceed with SSR without hydration when query fails and skipSsrOnError is false', async () => { const queryClient = new QueryClient() const mockQueryFn = vi.fn().mockRejectedValue(new Error('Query failed')) const queries = [ { queryKey: ['failing-query'], queryFn: mockQueryFn, }, ] const result = await QueriesHydration({ queries, queryClient, skipSsrOnError: false, children:
Test Children
, }) expect(mockQueryFn).toHaveBeenCalledTimes(1) // When skipSsrOnError is false, it should still render Hydrate component // even though the query failed expect(result).toBeDefined() expect(result.type).toBeDefined() }) it('should handle partial query failures when skipSsrOnError is false', async () => { const queryClient = new QueryClient() const mockSuccessQueryFn = vi.fn().mockResolvedValue({ data: 'success' }) const mockFailQueryFn = vi.fn().mockRejectedValue(new Error('Query failed')) const queries = [ { queryKey: ['success-query'], queryFn: mockSuccessQueryFn, }, { queryKey: ['fail-query'], queryFn: mockFailQueryFn, }, ] const result = await QueriesHydration({ queries, queryClient, skipSsrOnError: false, children:
Test Children
, }) // Promise.all rejects if any promise rejects expect(mockSuccessQueryFn).toHaveBeenCalledTimes(1) expect(mockFailQueryFn).toHaveBeenCalledTimes(1) expect(result).toBeDefined() }) it('should use default QueryClient when queryClient prop is not provided', async () => { const mockQueryFn = vi.fn().mockResolvedValue({ data: 'test-data' }) const queries = [ { queryKey: ['test-query'], queryFn: mockQueryFn, }, ] const result = await QueriesHydration({ queries, children:
Test Children
, }) expect(mockQueryFn).toHaveBeenCalledTimes(1) expect(result).toBeDefined() }) it('should pass additional Hydrate props through', async () => { const queryClient = new QueryClient() const mockQueryFn = vi.fn().mockResolvedValue({ data: 'test-data' }) const queries = [ { queryKey: ['test-query'], queryFn: mockQueryFn, }, ] const result = await QueriesHydration({ queries, queryClient, children:
Test Children
, // Additional props that should be passed to Hydrate } as ComponentProps) expect(mockQueryFn).toHaveBeenCalledTimes(1) expect(result).toBeDefined() }) it('should dehydrate query state correctly', async () => { const queryClient = new QueryClient() const mockData = { data: 'test-data' } const mockQueryFn = vi.fn().mockResolvedValue(mockData) const queries = [ { queryKey: ['test-query'], queryFn: mockQueryFn, }, ] await QueriesHydration({ queries, queryClient, children:
Test Children
, }) const dehydratedState = dehydrate(queryClient) expect(dehydratedState.queries).toHaveLength(1) expect(dehydratedState.queries[0].queryKey).toEqual(['test-query']) expect(dehydratedState.queries[0].state.data).toEqual(mockData) }) it('should fetch infiniteQueryOptions and hydrate them successfully', async () => { const queryClient = new QueryClient() const mockInfiniteQueryFn = vi.fn().mockResolvedValue({ data: 'page-1' }) const infiniteOptions = infiniteQueryOptions({ queryKey: ['infinite-query'], queryFn: mockInfiniteQueryFn, getNextPageParam: () => null, }) const result = await QueriesHydration({ queries: [infiniteOptions], queryClient, children:
Test Children
, }) expect(mockInfiniteQueryFn).toHaveBeenCalledTimes(1) expect(result).toBeDefined() expect(result.type).toBeDefined() }) it('should handle mixed queries and infiniteQueryOptions', async () => { const queryClient = new QueryClient() const mockQueryFn = vi.fn().mockResolvedValue({ data: 'regular-data' }) const mockInfiniteQueryFn = vi.fn().mockResolvedValue({ data: 'infinite-data' }) const queries = [ { queryKey: ['regular-query'], queryFn: mockQueryFn, }, infiniteQueryOptions({ queryKey: ['infinite-query'], queryFn: mockInfiniteQueryFn, getNextPageParam: () => null, }), ] const result = await QueriesHydration({ queries, queryClient, children:
Test Children
, }) expect(mockQueryFn).toHaveBeenCalledTimes(1) expect(mockInfiniteQueryFn).toHaveBeenCalledTimes(1) expect(result).toBeDefined() }) it('should skip SSR when infiniteQueryOptions fails and skipSsrOnError is true', async () => { const queryClient = new QueryClient() const mockInfiniteQueryFn = vi.fn().mockRejectedValue(new Error('Infinite query failed')) const infiniteOptions = infiniteQueryOptions({ queryKey: ['failing-infinite-query'], queryFn: mockInfiniteQueryFn, getNextPageParam: () => null, }) const result = await QueriesHydration({ queries: [infiniteOptions], queryClient, children:
Test Children
, }) expect(mockInfiniteQueryFn).toHaveBeenCalledTimes(1) render(result as React.ReactElement) expect(screen.getByTestId('client-only')).toBeInTheDocument() }) it('should dehydrate infiniteQueryOptions state correctly', async () => { const queryClient = new QueryClient() const mockInfiniteQueryFn = vi.fn().mockResolvedValue({ data: 'page-1' }) const infiniteOptions = infiniteQueryOptions({ queryKey: ['infinite-query'], queryFn: mockInfiniteQueryFn, getNextPageParam: () => null, }) await QueriesHydration({ queries: [infiniteOptions], queryClient, children:
Test Children
, }) const dehydratedState = dehydrate(queryClient) expect(dehydratedState.queries).toHaveLength(1) expect(dehydratedState.queries[0].queryKey).toEqual(['infinite-query']) expect(dehydratedState.queries[0].state.data).toEqual({ pages: [{ data: 'page-1' }], pageParams: [undefined], }) }) it('should timeout when query takes longer than the timeout', async () => { const serverQueryClient = new QueryClient() const timeoutMs = 100 const queryDelayMs = 200 const mockQueryFn = vi .fn() .mockImplementation( () => new Promise((resolve) => setTimeout(() => resolve({ data: 'test-data' }), queryDelayMs)) ) const queries = [ { queryKey: ['test-query'], queryFn: mockQueryFn, }, ] const ClientChild = () => { return
Client Child
} const result = await QueriesHydration({ queries, queryClient: serverQueryClient, timeout: timeoutMs, children: , }) expect(mockQueryFn).toHaveBeenCalledTimes(1) expect(screen.queryByText('Client Child')).not.toBeInTheDocument() const clientQueryClient = new QueryClient() render({result}) expect(screen.getByTestId('client-only')).toBeInTheDocument() expect(screen.getByText('Client Child')).toBeInTheDocument() }) it('should cancel queries when timeout occurs', async () => { const queryClient = new QueryClient() const cancelQueriesSpy = vi.spyOn(queryClient, 'cancelQueries') const timeoutMs = 100 const queryDelayMs = 200 const queries = [ { queryKey: ['slow-query-1'], queryFn: () => new Promise((resolve) => setTimeout(() => resolve({ data: '1' }), queryDelayMs)), }, { queryKey: ['slow-query-2'], queryFn: () => new Promise((resolve) => setTimeout(() => resolve({ data: '2' }), queryDelayMs)), }, ] await QueriesHydration({ queries, queryClient, timeout: timeoutMs, children:
children
, }) expect(cancelQueriesSpy).toHaveBeenCalledTimes(2) expect(cancelQueriesSpy).toHaveBeenCalledWith(queries[0]) expect(cancelQueriesSpy).toHaveBeenCalledWith(queries[1]) }) it('should cancel queries when query fails without timeout', async () => { const queryClient = new QueryClient() const cancelQueriesSpy = vi.spyOn(queryClient, 'cancelQueries') const queries = [ { queryKey: ['failing-query'], queryFn: () => Promise.reject(new Error('network error')), }, { queryKey: ['slow-query'], queryFn: () => new Promise((resolve) => setTimeout(() => resolve({ data: 'slow' }), 200)), }, ] await QueriesHydration({ queries, queryClient, children:
children
, }) expect(cancelQueriesSpy).toHaveBeenCalledTimes(2) expect(cancelQueriesSpy).toHaveBeenCalledWith(queries[0]) expect(cancelQueriesSpy).toHaveBeenCalledWith(queries[1]) }) it('should not cancel queries on successful fetch', async () => { const queryClient = new QueryClient() const cancelQueriesSpy = vi.spyOn(queryClient, 'cancelQueries') const queries = [ { queryKey: ['success-query'], queryFn: () => Promise.resolve({ data: 'ok' }), }, ] await QueriesHydration({ queries, queryClient, timeout: 5000, children:
children
, }) expect(cancelQueriesSpy).not.toHaveBeenCalled() }) it('should cancel all queries even when only some are slow enough to timeout', async () => { const queryClient = new QueryClient() const cancelQueriesSpy = vi.spyOn(queryClient, 'cancelQueries') const timeoutMs = 100 const queries = [ { queryKey: ['fast-query'], queryFn: () => Promise.resolve({ data: 'fast' }), }, { queryKey: ['slow-query'], queryFn: () => new Promise((resolve) => setTimeout(() => resolve({ data: 'slow' }), 200)), }, ] await QueriesHydration({ queries, queryClient, timeout: timeoutMs, children:
children
, }) // Promise.all waits for all, so timeout cancels all queries in the list expect(cancelQueriesSpy).toHaveBeenCalledTimes(2) expect(cancelQueriesSpy).toHaveBeenCalledWith(queries[0]) expect(cancelQueriesSpy).toHaveBeenCalledWith(queries[1]) }) it('should still skip SSR on timeout when skipSsrOnError is true', async () => { const queryClient = new QueryClient() const queries = [ { queryKey: ['slow-query'], queryFn: () => new Promise((resolve) => setTimeout(() => resolve({ data: 'slow' }), 200)), }, ] const result = await QueriesHydration({ queries, queryClient, timeout: 100, skipSsrOnError: true, children:
children
, }) render(result as React.ReactElement) expect(screen.getByTestId('client-only')).toBeInTheDocument() }) it('should cancel queries on timeout but proceed with SSR when skipSsrOnError is false', async () => { const queryClient = new QueryClient() const cancelQueriesSpy = vi.spyOn(queryClient, 'cancelQueries') const queries = [ { queryKey: ['slow-query'], queryFn: () => new Promise((resolve) => setTimeout(() => resolve({ data: 'slow' }), 200)), }, ] const result = await QueriesHydration({ queries, queryClient, timeout: 100, skipSsrOnError: false, children:
children
, }) expect(cancelQueriesSpy).toHaveBeenCalledTimes(1) expect(cancelQueriesSpy).toHaveBeenCalledWith(queries[0]) // skipSsrOnError is false, so it should render Hydrate, not ClientOnly expect(result.type).not.toEqual(expect.objectContaining({ name: 'ClientOnly' })) }) })