Skip to content

Commit 020bb92

Browse files
authored
test(react): improve code coverage (TanStack#2826)
1 parent 51fb925 commit 020bb92

File tree

8 files changed

+315
-6
lines changed

8 files changed

+315
-6
lines changed

src/react/setLogger.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
import { setLogger } from '../core'
22
import { logger } from './logger'
33

4-
if (logger) {
5-
setLogger(logger)
6-
}
4+
setLogger(logger)

src/react/tests/Hydrate.test.tsx

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
Hydrate,
1212
} from '../..'
1313
import { sleep } from './utils'
14+
import * as coreModule from '../../core/index'
1415

1516
describe('React hydration', () => {
1617
const fetchData: (value: string) => Promise<string> = value =>
@@ -158,4 +159,50 @@ describe('React hydration', () => {
158159
newClientQueryClient.clear()
159160
})
160161
})
162+
163+
test('should not hydrate queries if state is null', async () => {
164+
const queryCache = new QueryCache()
165+
const queryClient = new QueryClient({ queryCache })
166+
167+
const hydrateSpy = jest.spyOn(coreModule, 'hydrate')
168+
169+
function Page() {
170+
useHydrate(null)
171+
return null
172+
}
173+
174+
render(
175+
<QueryClientProvider client={queryClient}>
176+
<Page />
177+
</QueryClientProvider>
178+
)
179+
180+
expect(hydrateSpy).toHaveBeenCalledTimes(0)
181+
182+
hydrateSpy.mockRestore()
183+
queryClient.clear()
184+
})
185+
186+
test('should not hydrate queries if state is undefined', async () => {
187+
const queryCache = new QueryCache()
188+
const queryClient = new QueryClient({ queryCache })
189+
190+
const hydrateSpy = jest.spyOn(coreModule, 'hydrate')
191+
192+
function Page() {
193+
useHydrate(undefined)
194+
return null
195+
}
196+
197+
render(
198+
<QueryClientProvider client={queryClient}>
199+
<Page />
200+
</QueryClientProvider>
201+
)
202+
203+
expect(hydrateSpy).toHaveBeenCalledTimes(0)
204+
205+
hydrateSpy.mockRestore()
206+
queryClient.clear()
207+
})
161208
})

src/react/tests/QueryClientProvider.test.tsx

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
import React from 'react'
22
import { render, waitFor } from '@testing-library/react'
3+
import { renderToString } from 'react-dom/server'
34

45
import { sleep, queryKey } from './utils'
5-
import { QueryClient, QueryClientProvider, QueryCache, useQuery } from '../..'
6+
import {
7+
QueryClient,
8+
QueryClientProvider,
9+
QueryCache,
10+
useQuery,
11+
useQueryClient,
12+
} from '../..'
613

714
describe('QueryClientProvider', () => {
815
test('sets a specific cache for all queries to use', async () => {
@@ -127,4 +134,77 @@ describe('QueryClientProvider', () => {
127134
expect(queryCache.find(key)).toBeDefined()
128135
expect(queryCache.find(key)?.options.cacheTime).toBe(Infinity)
129136
})
137+
138+
describe('useQueryClient', () => {
139+
test('should throw an error if no query client has been set', () => {
140+
const consoleMock = jest
141+
.spyOn(console, 'error')
142+
.mockImplementation(() => undefined)
143+
144+
function Page() {
145+
useQueryClient()
146+
return null
147+
}
148+
149+
expect(() => render(<Page />)).toThrow(
150+
'No QueryClient set, use QueryClientProvider to set one'
151+
)
152+
153+
consoleMock.mockRestore()
154+
})
155+
156+
test('should use window to get the context when contextSharing is true', () => {
157+
const queryCache = new QueryCache()
158+
const queryClient = new QueryClient({ queryCache })
159+
160+
let queryClientFromHook: QueryClient | undefined
161+
let queryClientFromWindow: QueryClient | undefined
162+
163+
function Page() {
164+
queryClientFromHook = useQueryClient()
165+
queryClientFromWindow = React.useContext(
166+
window.ReactQueryClientContext as React.Context<
167+
QueryClient | undefined
168+
>
169+
)
170+
return null
171+
}
172+
173+
render(
174+
<QueryClientProvider client={queryClient} contextSharing={true}>
175+
<Page />
176+
</QueryClientProvider>
177+
)
178+
179+
expect(queryClientFromHook).toEqual(queryClient)
180+
expect(queryClientFromWindow).toEqual(queryClient)
181+
})
182+
183+
test('should not use window to get the context when contextSharing is true and window does not exist', () => {
184+
const queryCache = new QueryCache()
185+
const queryClient = new QueryClient({ queryCache })
186+
187+
// Mock a non web browser environment
188+
const windowSpy = jest
189+
.spyOn(window, 'window', 'get')
190+
.mockImplementation(undefined)
191+
192+
let queryClientFromHook: QueryClient | undefined
193+
194+
function Page() {
195+
queryClientFromHook = useQueryClient()
196+
return null
197+
}
198+
199+
renderToString(
200+
<QueryClientProvider client={queryClient} contextSharing={true}>
201+
<Page />
202+
</QueryClientProvider>
203+
)
204+
205+
expect(queryClientFromHook).toEqual(queryClient)
206+
207+
windowSpy.mockRestore()
208+
})
209+
})
130210
})

src/react/tests/QueryResetErrorBoundary.test.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,4 +383,23 @@ describe('QueryErrorResetBoundary', () => {
383383

384384
consoleMock.mockRestore()
385385
})
386+
387+
it('should render children', async () => {
388+
function Page() {
389+
return (
390+
<div>
391+
<span>page</span>
392+
</div>
393+
)
394+
}
395+
396+
const rendered = renderWithClient(
397+
queryClient,
398+
<QueryErrorResetBoundary>
399+
<Page />
400+
</QueryErrorResetBoundary>
401+
)
402+
403+
expect(rendered.queryByText('page')).not.toBeNull()
404+
})
386405
})
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { logger } from '../logger.native'
2+
3+
describe('logger native', () => {
4+
it('should expose logger properties', () => {
5+
expect(logger).toHaveProperty('log')
6+
expect(logger).toHaveProperty('error')
7+
expect(logger).toHaveProperty('warn')
8+
})
9+
})

src/react/tests/useIsMutating.test.tsx

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import { waitFor } from '@testing-library/react'
1+
import { waitFor, fireEvent } from '@testing-library/react'
22
import React from 'react'
33
import { QueryClient } from '../../core'
44
import { useIsMutating } from '../useIsMutating'
55
import { useMutation } from '../useMutation'
66
import { renderWithClient, setActTimeout, sleep } from './utils'
7+
import * as MutationCacheModule from '../../core/mutationCache'
78

89
describe('useIsMutating', () => {
910
it('should return the number of fetching mutations', async () => {
@@ -105,4 +106,56 @@ describe('useIsMutating', () => {
105106
renderWithClient(queryClient, <Page />)
106107
await waitFor(() => expect(isMutatings).toEqual([0, 1, 1, 1, 0, 0]))
107108
})
109+
110+
it('should not change state if unmounted', async () => {
111+
// We have to mock the MutationCache to not unsubscribe
112+
// the listener when the component is unmounted
113+
class MutationCacheMock extends MutationCacheModule.MutationCache {
114+
subscribe(listener: any) {
115+
super.subscribe(listener)
116+
return () => void 0
117+
}
118+
}
119+
120+
const MutationCacheSpy = jest
121+
.spyOn(MutationCacheModule, 'MutationCache')
122+
.mockImplementation(fn => {
123+
return new MutationCacheMock(fn)
124+
})
125+
126+
const queryClient = new QueryClient()
127+
128+
function IsMutating() {
129+
useIsMutating()
130+
return null
131+
}
132+
133+
function Page() {
134+
const [mounted, setMounted] = React.useState(true)
135+
const { mutate: mutate1 } = useMutation('mutation1', async () => {
136+
await sleep(10)
137+
return 'data'
138+
})
139+
140+
React.useEffect(() => {
141+
mutate1()
142+
}, [mutate1])
143+
144+
return (
145+
<div>
146+
<button onClick={() => setMounted(false)}>unmount</button>
147+
{mounted && <IsMutating />}
148+
</div>
149+
)
150+
}
151+
152+
const { getByText } = renderWithClient(queryClient, <Page />)
153+
fireEvent.click(getByText('unmount'))
154+
155+
// Should not display the console error
156+
// "Warning: Can't perform a React state update on an unmounted component"
157+
158+
await sleep(20)
159+
MutationCacheSpy.mockRestore()
160+
})
108161
})

src/react/tests/useMutation.test.tsx

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { fireEvent, waitFor } from '@testing-library/react'
22
import React from 'react'
3+
import { ErrorBoundary } from 'react-error-boundary'
34

45
import { useMutation, QueryClient, QueryCache, MutationCache } from '../..'
56
import { UseMutationResult } from '../types'
@@ -476,4 +477,46 @@ describe('useMutation', () => {
476477
fireEvent.click(getByText('mutate'))
477478
fireEvent.click(getByText('unmount'))
478479
})
480+
481+
it('should be able to throw an error when useErrorBoundary is set to true', async () => {
482+
const consoleMock = mockConsoleError()
483+
484+
function Page() {
485+
const { mutate } = useMutation<string, Error>(
486+
() => {
487+
const err = new Error('Expected mock error. All is well!')
488+
err.stack = ''
489+
return Promise.reject(err)
490+
},
491+
{ useErrorBoundary: true }
492+
)
493+
494+
return (
495+
<div>
496+
<button onClick={() => mutate()}>mutate</button>
497+
</div>
498+
)
499+
}
500+
501+
const { getByText, queryByText } = renderWithClient(
502+
queryClient,
503+
<ErrorBoundary
504+
fallbackRender={() => (
505+
<div>
506+
<span>error</span>
507+
</div>
508+
)}
509+
>
510+
<Page />
511+
</ErrorBoundary>
512+
)
513+
514+
fireEvent.click(getByText('mutate'))
515+
516+
await waitFor(() => {
517+
expect(queryByText('error')).not.toBeNull()
518+
})
519+
520+
consoleMock.mockRestore()
521+
})
479522
})

src/react/tests/useQueries.test.tsx

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
import { waitFor } from '@testing-library/react'
1+
import { waitFor, fireEvent } from '@testing-library/react'
22
import React from 'react'
33

4+
import * as QueriesObserverModule from '../../core/queriesObserver'
5+
46
import {
57
expectType,
68
expectTypeNotAny,
@@ -15,6 +17,7 @@ import {
1517
UseQueryResult,
1618
QueryCache,
1719
QueryObserverResult,
20+
QueriesObserver,
1821
} from '../..'
1922

2023
describe('useQueries', () => {
@@ -712,4 +715,61 @@ describe('useQueries', () => {
712715
)
713716
}
714717
})
718+
719+
it('should not change state if unmounted', async () => {
720+
const key1 = queryKey()
721+
722+
// We have to mock the QueriesObserver to not unsubscribe
723+
// the listener when the component is unmounted
724+
class QueriesObserverMock extends QueriesObserver {
725+
subscribe(listener: any) {
726+
super.subscribe(listener)
727+
return () => void 0
728+
}
729+
}
730+
731+
const QueriesObserverSpy = jest
732+
.spyOn(QueriesObserverModule, 'QueriesObserver')
733+
.mockImplementation(fn => {
734+
return new QueriesObserverMock(fn)
735+
})
736+
737+
function Queries() {
738+
useQueries([
739+
{
740+
queryKey: key1,
741+
queryFn: async () => {
742+
await sleep(10)
743+
return 1
744+
},
745+
},
746+
])
747+
748+
return (
749+
<div>
750+
<span>queries</span>
751+
</div>
752+
)
753+
}
754+
755+
function Page() {
756+
const [mounted, setMounted] = React.useState(true)
757+
758+
return (
759+
<div>
760+
<button onClick={() => setMounted(false)}>unmount</button>
761+
{mounted && <Queries />}
762+
</div>
763+
)
764+
}
765+
766+
const { getByText } = renderWithClient(queryClient, <Page />)
767+
fireEvent.click(getByText('unmount'))
768+
769+
// Should not display the console error
770+
// "Warning: Can't perform a React state update on an unmounted component"
771+
772+
await sleep(20)
773+
QueriesObserverSpy.mockRestore()
774+
})
715775
})

0 commit comments

Comments
 (0)