import { fireEvent, waitFor } from '@testing-library/react'
import { ErrorBoundary } from 'react-error-boundary'
import * as React from 'react'
import { createQueryClient, queryKey, renderWithClient, sleep } from './utils'
import { QueryCache, QueryErrorResetBoundary, useQueries, useQuery } from '..'
// TODO: This should be removed with the types for react-error-boundary get updated.
declare module 'react-error-boundary' {
interface ErrorBoundaryPropsWithFallback {
children: any
}
}
describe('QueryErrorResetBoundary', () => {
const queryCache = new QueryCache()
const queryClient = createQueryClient({ queryCache })
describe('useQuery', () => {
it('should retry fetch if the reset error boundary has been reset', async () => {
const key = queryKey()
let succeed = false
function Page() {
const { data } = useQuery(
key,
async () => {
await sleep(10)
if (!succeed) {
throw new Error('Error')
} else {
return 'data'
}
},
{
retry: false,
useErrorBoundary: true,
},
)
return
{data}
}
const rendered = renderWithClient(
queryClient,
{({ reset }) => (
(
error boundary
)}
>
)}
,
)
await waitFor(() => rendered.getByText('error boundary'))
await waitFor(() => rendered.getByText('retry'))
succeed = true
fireEvent.click(rendered.getByText('retry'))
await waitFor(() => rendered.getByText('data'))
})
it('should not throw error if query is disabled', async () => {
const key = queryKey()
let succeed = false
function Page() {
const { data, status } = useQuery(
key,
async () => {
await sleep(10)
if (!succeed) {
throw new Error('Error')
} else {
return 'data'
}
},
{
retry: false,
enabled: !succeed,
useErrorBoundary: true,
},
)
return (
)
}
const rendered = renderWithClient(
queryClient,
{({ reset }) => (
(
error boundary
)}
>
)}
,
)
await waitFor(() => rendered.getByText('error boundary'))
await waitFor(() => rendered.getByText('retry'))
succeed = true
fireEvent.click(rendered.getByText('retry'))
await waitFor(() => rendered.getByText('status: error'))
})
it('should not throw error if query is disabled, and refetch if query becomes enabled again', async () => {
const key = queryKey()
let succeed = false
function Page() {
const [enabled, setEnabled] = React.useState(false)
const { data } = useQuery(
key,
async () => {
await sleep(10)
if (!succeed) {
throw new Error('Error')
} else {
return 'data'
}
},
{
retry: false,
enabled,
useErrorBoundary: true,
},
)
React.useEffect(() => {
setEnabled(true)
}, [])
return {data}
}
const rendered = renderWithClient(
queryClient,
{({ reset }) => (
(
error boundary
)}
>
)}
,
)
await waitFor(() => rendered.getByText('error boundary'))
await waitFor(() => rendered.getByText('retry'))
succeed = true
fireEvent.click(rendered.getByText('retry'))
await waitFor(() => rendered.getByText('data'))
})
it('should throw error if query is disabled and manually refetched', async () => {
const key = queryKey()
function Page() {
const { data, refetch, status, fetchStatus } = useQuery(
key,
async () => {
throw new Error('Error')
},
{
retry: false,
enabled: false,
useErrorBoundary: true,
},
)
return (
status: {status}, fetchStatus: {fetchStatus}
{data}
)
}
const rendered = renderWithClient(
queryClient,
{({ reset }) => (
(
error boundary
)}
>
)}
,
)
await waitFor(() =>
rendered.getByText('status: loading, fetchStatus: idle'),
)
fireEvent.click(rendered.getByRole('button', { name: /refetch/i }))
await waitFor(() => rendered.getByText('error boundary'))
})
it('should not retry fetch if the reset error boundary has not been reset', async () => {
const key = queryKey()
let succeed = false
function Page() {
const { data } = useQuery(
key,
async () => {
await sleep(10)
if (!succeed) {
throw new Error('Error')
} else {
return 'data'
}
},
{
retry: false,
useErrorBoundary: true,
},
)
return {data}
}
const rendered = renderWithClient(
queryClient,
{() => (
(
error boundary
)}
>
)}
,
)
await waitFor(() => rendered.getByText('error boundary'))
await waitFor(() => rendered.getByText('retry'))
succeed = true
fireEvent.click(rendered.getByText('retry'))
await waitFor(() => rendered.getByText('error boundary'))
})
it('should retry fetch if the reset error boundary has been reset and the query contains data from a previous fetch', async () => {
const key = queryKey()
let succeed = false
function Page() {
const { data } = useQuery(
key,
async () => {
await sleep(10)
if (!succeed) {
throw new Error('Error')
} else {
return 'data'
}
},
{
retry: false,
useErrorBoundary: true,
initialData: 'initial',
},
)
return {data}
}
const rendered = renderWithClient(
queryClient,
{({ reset }) => (
(
error boundary
)}
>
)}
,
)
await waitFor(() => rendered.getByText('error boundary'))
await waitFor(() => rendered.getByText('retry'))
succeed = true
fireEvent.click(rendered.getByText('retry'))
await waitFor(() => rendered.getByText('data'))
})
it('should not retry fetch if the reset error boundary has not been reset after a previous reset', async () => {
const key = queryKey()
let succeed = false
let shouldReset = true
function Page() {
const { data } = useQuery(
key,
async () => {
await sleep(10)
if (!succeed) {
throw new Error('Error')
} else {
return 'data'
}
},
{
retry: false,
useErrorBoundary: true,
},
)
return {data}
}
const rendered = renderWithClient(
queryClient,
{({ reset }) => (
{
if (shouldReset) {
reset()
}
}}
fallbackRender={({ resetErrorBoundary }) => (
error boundary
)}
>
)}
,
)
await waitFor(() => rendered.getByText('error boundary'))
await waitFor(() => rendered.getByText('retry'))
shouldReset = true
fireEvent.click(rendered.getByText('retry'))
await waitFor(() => rendered.getByText('error boundary'))
succeed = true
shouldReset = false
fireEvent.click(rendered.getByText('retry'))
await waitFor(() => rendered.getByText('error boundary'))
})
it('should throw again on error after the reset error boundary has been reset', async () => {
const key = queryKey()
let fetchCount = 0
function Page() {
const { data } = useQuery(
key,
async () => {
fetchCount++
await sleep(10)
throw new Error('Error')
},
{
retry: false,
useErrorBoundary: true,
},
)
return {data}
}
const rendered = renderWithClient(
queryClient,
{({ reset }) => (
(
error boundary
)}
>
)}
,
)
await waitFor(() => rendered.getByText('error boundary'))
await waitFor(() => rendered.getByText('retry'))
fireEvent.click(rendered.getByText('retry'))
await waitFor(() => rendered.getByText('error boundary'))
await waitFor(() => rendered.getByText('retry'))
fireEvent.click(rendered.getByText('retry'))
await waitFor(() => rendered.getByText('error boundary'))
expect(fetchCount).toBe(3)
})
it('should never render the component while the query is in error state', async () => {
const key = queryKey()
let fetchCount = 0
let renders = 0
function Page() {
const { data } = useQuery(
key,
async () => {
fetchCount++
await sleep(10)
if (fetchCount > 2) {
return 'data'
} else {
throw new Error('Error')
}
},
{
retry: false,
suspense: true,
},
)
renders++
return {data}
}
const rendered = renderWithClient(
queryClient,
{({ reset }) => (
(
error boundary
)}
>
loading}>
)}
,
)
await waitFor(() => rendered.getByText('error boundary'))
await waitFor(() => rendered.getByText('retry'))
fireEvent.click(rendered.getByText('retry'))
await waitFor(() => rendered.getByText('error boundary'))
await waitFor(() => rendered.getByText('retry'))
fireEvent.click(rendered.getByText('retry'))
await waitFor(() => rendered.getByText('data'))
expect(fetchCount).toBe(3)
expect(renders).toBe(1)
})
it('should render children', async () => {
function Page() {
return (
page
)
}
const rendered = renderWithClient(
queryClient,
,
)
expect(rendered.queryByText('page')).not.toBeNull()
})
it('should show error boundary when using tracked queries even though we do not track the error field', async () => {
const key = queryKey()
let succeed = false
function Page() {
const { data } = useQuery(
key,
async () => {
await sleep(10)
if (!succeed) {
throw new Error('Error')
} else {
return 'data'
}
},
{
retry: false,
useErrorBoundary: true,
},
)
return {data}
}
const rendered = renderWithClient(
queryClient,
{({ reset }) => (
(
error boundary
)}
>
)}
,
)
await waitFor(() => rendered.getByText('error boundary'))
await waitFor(() => rendered.getByText('retry'))
succeed = true
fireEvent.click(rendered.getByText('retry'))
await waitFor(() => rendered.getByText('data'))
})
})
describe('useQueries', () => {
it('should retry fetch if the reset error boundary has been reset', async () => {
const key = queryKey()
let succeed = false
const queryOptions = {
queryKey: key,
queryFn: async () => {
await sleep(10)
if (!succeed) {
throw new Error('Error')
} else {
return 'data'
}
},
retry: false,
useErrorBoundary: true,
retryOnMount: true,
}
function Page() {
const [{ data }] = useQueries({ queries: [queryOptions] })
return {data}
}
const rendered = renderWithClient(
queryClient,
{({ reset }) => (
(
error boundary
)}
>
)}
,
)
await waitFor(() => rendered.getByText('error boundary'))
await waitFor(() => rendered.getByText('retry'))
succeed = true
fireEvent.click(rendered.getByText('retry'))
await waitFor(() => rendered.getByText('data'))
})
it('with suspense should retry fetch if the reset error boundary has been reset', async () => {
const key = queryKey()
let succeed = false
const queryOptions = {
queryKey: key,
queryFn: async () => {
await sleep(10)
if (!succeed) {
throw new Error('Error')
} else {
return 'data'
}
},
retry: false,
useErrorBoundary: true,
retryOnMount: true,
suspense: true,
}
function Page() {
const [{ data }] = useQueries({ queries: [queryOptions] })
return {data}
}
const rendered = renderWithClient(
queryClient,
{({ reset }) => (
(
error boundary
)}
>
)}
,
)
await waitFor(() => rendered.getByText('error boundary'))
await waitFor(() => rendered.getByText('retry'))
succeed = true
fireEvent.click(rendered.getByText('retry'))
await waitFor(() => rendered.getByText('data'))
})
})
})