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' }))
})
})