diff --git a/docs/framework/react/reference/useSuspenseInfiniteQuery.md b/docs/framework/react/reference/useSuspenseInfiniteQuery.md index f4af09b8640..46453f72999 100644 --- a/docs/framework/react/reference/useSuspenseInfiniteQuery.md +++ b/docs/framework/react/reference/useSuspenseInfiniteQuery.md @@ -22,7 +22,7 @@ Same object as [useInfiniteQuery](../reference/useInfiniteQuery.md), except that - `data` is guaranteed to be defined - `isPlaceholderData` is missing -- `status` is always `success` +- `status` is either `success` or `error` - the derived flags are set accordingly. **Caveat** diff --git a/docs/framework/react/reference/useSuspenseQueries.md b/docs/framework/react/reference/useSuspenseQueries.md index f30f5d1d530..8f3e7f8ebda 100644 --- a/docs/framework/react/reference/useSuspenseQueries.md +++ b/docs/framework/react/reference/useSuspenseQueries.md @@ -22,7 +22,7 @@ Same structure as [useQueries](../reference/useQueries.md), except that for each - `data` is guaranteed to be defined - `isPlaceholderData` is missing -- `status` is always `success` +- `status` is either `success` or `error` - the derived flags are set accordingly. **Caveats** diff --git a/docs/framework/react/reference/useSuspenseQuery.md b/docs/framework/react/reference/useSuspenseQuery.md index f192a8c2d32..f51416947ea 100644 --- a/docs/framework/react/reference/useSuspenseQuery.md +++ b/docs/framework/react/reference/useSuspenseQuery.md @@ -21,7 +21,7 @@ Same object as [useQuery](../reference/useQuery.md), except that: - `data` is guaranteed to be defined - `isPlaceholderData` is missing -- `status` is always `success` +- `status` is either `success` or `error` - the derived flags are set accordingly. **Caveat** diff --git a/packages/query-core/src/__tests__/hydration.test.tsx b/packages/query-core/src/__tests__/hydration.test.tsx index a794c2694c1..49eb6eb0192 100644 --- a/packages/query-core/src/__tests__/hydration.test.tsx +++ b/packages/query-core/src/__tests__/hydration.test.tsx @@ -1,5 +1,4 @@ -import { describe, expect, test, vi } from 'vitest' -import { waitFor } from '@testing-library/dom' +import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { QueryCache } from '../queryCache' import { dehydrate, hydrate } from '../hydration' import { MutationCache } from '../mutationCache' @@ -21,33 +20,54 @@ async function fetchDate(value: string, ms?: number): Promise { } describe('dehydration and rehydration', () => { + beforeEach(() => { + vi.useFakeTimers() + }) + + afterEach(() => { + vi.useRealTimers() + }) + test('should work with serializable values', async () => { const queryCache = new QueryCache() const queryClient = createQueryClient({ queryCache }) - await queryClient.prefetchQuery({ - queryKey: ['string'], - queryFn: () => fetchData('string'), - }) - await queryClient.prefetchQuery({ - queryKey: ['number'], - queryFn: () => fetchData(1), - }) - await queryClient.prefetchQuery({ - queryKey: ['boolean'], - queryFn: () => fetchData(true), - }) - await queryClient.prefetchQuery({ - queryKey: ['null'], - queryFn: () => fetchData(null), - }) - await queryClient.prefetchQuery({ - queryKey: ['array'], - queryFn: () => fetchData(['string', 0]), - }) - await queryClient.prefetchQuery({ - queryKey: ['nested'], - queryFn: () => fetchData({ key: [{ nestedKey: 1 }] }), - }) + await vi.waitFor(() => + queryClient.prefetchQuery({ + queryKey: ['string'], + queryFn: () => fetchData('string'), + }), + ) + await vi.waitFor(() => + queryClient.prefetchQuery({ + queryKey: ['number'], + queryFn: () => fetchData(1), + }), + ) + + await vi.waitFor(() => + queryClient.prefetchQuery({ + queryKey: ['boolean'], + queryFn: () => fetchData(true), + }), + ) + await vi.waitFor(() => + queryClient.prefetchQuery({ + queryKey: ['null'], + queryFn: () => fetchData(null), + }), + ) + await vi.waitFor(() => + queryClient.prefetchQuery({ + queryKey: ['array'], + queryFn: () => fetchData(['string', 0]), + }), + ) + await vi.waitFor(() => + queryClient.prefetchQuery({ + queryKey: ['nested'], + queryFn: () => fetchData({ key: [{ nestedKey: 1 }] }), + }), + ) const dehydrated = dehydrate(queryClient) const stringified = JSON.stringify(dehydrated) @@ -116,10 +136,12 @@ describe('dehydration and rehydration', () => { test('should not dehydrate queries if dehydrateQueries is set to false', async () => { const queryCache = new QueryCache() const queryClient = createQueryClient({ queryCache }) - await queryClient.prefetchQuery({ - queryKey: ['string'], - queryFn: () => fetchData('string'), - }) + vi.waitFor(() => + queryClient.prefetchQuery({ + queryKey: ['string'], + queryFn: () => fetchData('string'), + }), + ) const dehydrated = dehydrate(queryClient, { shouldDehydrateQuery: () => false, @@ -133,15 +155,17 @@ describe('dehydration and rehydration', () => { test('should use the garbage collection time from the client', async () => { const queryCache = new QueryCache() const queryClient = createQueryClient({ queryCache }) - await queryClient.prefetchQuery({ - queryKey: ['string'], - queryFn: () => fetchData('string'), - gcTime: 50, - }) + await vi.waitFor(() => + queryClient.prefetchQuery({ + queryKey: ['string'], + queryFn: () => fetchData('string'), + gcTime: 50, + }), + ) const dehydrated = dehydrate(queryClient) const stringified = JSON.stringify(dehydrated) - await sleep(20) + await vi.advanceTimersByTimeAsync(20) // --- @@ -152,7 +176,7 @@ describe('dehydration and rehydration', () => { expect(hydrationCache.find({ queryKey: ['string'] })?.state.data).toBe( 'string', ) - await sleep(100) + await vi.advanceTimersByTimeAsync(100) expect(hydrationCache.find({ queryKey: ['string'] })).toBeTruthy() queryClient.clear() @@ -162,10 +186,12 @@ describe('dehydration and rehydration', () => { test('should be able to provide default options for the hydrated queries', async () => { const queryCache = new QueryCache() const queryClient = createQueryClient({ queryCache }) - await queryClient.prefetchQuery({ - queryKey: ['string'], - queryFn: () => fetchData('string'), - }) + await vi.waitFor(() => + queryClient.prefetchQuery({ + queryKey: ['string'], + queryFn: () => fetchData('string'), + }), + ) const dehydrated = dehydrate(queryClient) const stringified = JSON.stringify(dehydrated) const parsed = JSON.parse(stringified) @@ -189,11 +215,13 @@ describe('dehydration and rehydration', () => { dehydrate: { shouldDehydrateQuery: () => true }, }, }) - await queryClient.prefetchQuery({ - queryKey: ['string'], - retry: 0, - queryFn: () => Promise.reject(new Error('error')), - }) + await vi.waitFor(() => + queryClient.prefetchQuery({ + queryKey: ['string'], + retry: 0, + queryFn: () => Promise.reject(new Error('error')), + }), + ) const dehydrated = dehydrate(queryClient) expect(dehydrated.queries.length).toBe(1) expect(dehydrated.queries[0]?.state.error).toStrictEqual(new Error('error')) @@ -262,10 +290,12 @@ describe('dehydration and rehydration', () => { test('should work with complex keys', async () => { const queryCache = new QueryCache() const queryClient = createQueryClient({ queryCache }) - await queryClient.prefetchQuery({ - queryKey: ['string', { key: ['string'], key2: 0 }], - queryFn: () => fetchData('string'), - }) + await vi.waitFor(() => + queryClient.prefetchQuery({ + queryKey: ['string', { key: ['string'], key2: 0 }], + queryFn: () => fetchData('string'), + }), + ) const dehydrated = dehydrate(queryClient) const stringified = JSON.stringify(dehydrated) @@ -283,11 +313,13 @@ describe('dehydration and rehydration', () => { const fetchDataAfterHydration = vi.fn<(...args: Array) => unknown>() - await hydrationClient.prefetchQuery({ - queryKey: ['string', { key: ['string'], key2: 0 }], - queryFn: fetchDataAfterHydration, - staleTime: 100, - }) + await vi.waitFor(() => + hydrationClient.prefetchQuery({ + queryKey: ['string', { key: ['string'], key2: 0 }], + queryFn: fetchDataAfterHydration, + staleTime: 100, + }), + ) expect(fetchDataAfterHydration).toHaveBeenCalledTimes(0) queryClient.clear() @@ -300,20 +332,24 @@ describe('dehydration and rehydration', () => { const queryCache = new QueryCache() const queryClient = createQueryClient({ queryCache }) - await queryClient.prefetchQuery({ - queryKey: ['success'], - queryFn: () => fetchData('success'), - }) + await vi.waitFor(() => + queryClient.prefetchQuery({ + queryKey: ['success'], + queryFn: () => fetchData('success'), + }), + ) queryClient.prefetchQuery({ queryKey: ['loading'], queryFn: () => fetchData('loading', 10000), }) - await queryClient.prefetchQuery({ - queryKey: ['error'], - queryFn: () => { - throw new Error() - }, - }) + await vi.waitFor(() => + queryClient.prefetchQuery({ + queryKey: ['error'], + queryFn: () => { + throw new Error() + }, + }), + ) const dehydrated = dehydrate(queryClient) const stringified = JSON.stringify(dehydrated) @@ -336,14 +372,18 @@ describe('dehydration and rehydration', () => { test('should filter queries via dehydrateQuery', async () => { const queryCache = new QueryCache() const queryClient = createQueryClient({ queryCache }) - await queryClient.prefetchQuery({ - queryKey: ['string'], - queryFn: () => fetchData('string'), - }) - await queryClient.prefetchQuery({ - queryKey: ['number'], - queryFn: () => fetchData(1), - }) + await vi.waitFor(() => + queryClient.prefetchQuery({ + queryKey: ['string'], + queryFn: () => fetchData('string'), + }), + ) + await vi.waitFor(() => + queryClient.prefetchQuery({ + queryKey: ['number'], + queryFn: () => fetchData(1), + }), + ) const dehydrated = dehydrate(queryClient, { shouldDehydrateQuery: (query) => query.queryKey[0] !== 'string', }) @@ -373,10 +413,12 @@ describe('dehydration and rehydration', () => { test('should not overwrite query in cache if hydrated query is older', async () => { const queryCache = new QueryCache() const queryClient = createQueryClient({ queryCache }) - await queryClient.prefetchQuery({ - queryKey: ['string'], - queryFn: () => fetchData('string-older', 5), - }) + await vi.waitFor(() => + queryClient.prefetchQuery({ + queryKey: ['string'], + queryFn: () => fetchData('string-older', 5), + }), + ) const dehydrated = dehydrate(queryClient) const stringified = JSON.stringify(dehydrated) @@ -385,10 +427,12 @@ describe('dehydration and rehydration', () => { const parsed = JSON.parse(stringified) const hydrationCache = new QueryCache() const hydrationClient = createQueryClient({ queryCache: hydrationCache }) - await hydrationClient.prefetchQuery({ - queryKey: ['string'], - queryFn: () => fetchData('string-newer', 5), - }) + await vi.waitFor(() => + hydrationClient.prefetchQuery({ + queryKey: ['string'], + queryFn: () => fetchData('string-newer', 5), + }), + ) hydrate(hydrationClient, parsed) expect(hydrationCache.find({ queryKey: ['string'] })?.state.data).toBe( @@ -402,19 +446,23 @@ describe('dehydration and rehydration', () => { test('should overwrite query in cache if hydrated query is newer', async () => { const hydrationCache = new QueryCache() const hydrationClient = createQueryClient({ queryCache: hydrationCache }) - await hydrationClient.prefetchQuery({ - queryKey: ['string'], - queryFn: () => fetchData('string-older', 5), - }) + await vi.waitFor(() => + hydrationClient.prefetchQuery({ + queryKey: ['string'], + queryFn: () => fetchData('string-older', 5), + }), + ) // --- const queryCache = new QueryCache() const queryClient = createQueryClient({ queryCache }) - await queryClient.prefetchQuery({ - queryKey: ['string'], - queryFn: () => fetchData('string-newer', 5), - }) + await vi.waitFor(() => + queryClient.prefetchQuery({ + queryKey: ['string'], + queryFn: () => fetchData('string-newer', 5), + }), + ) const dehydrated = dehydrate(queryClient) const stringified = JSON.stringify(dehydrated) @@ -462,7 +510,7 @@ describe('dehydration and rehydration', () => { { text: 'text' }, ).catch(() => undefined) - await sleep(50) + await vi.advanceTimersByTimeAsync(50) const dehydrated = dehydrate(serverClient) const stringified = JSON.stringify(dehydrated) @@ -534,7 +582,7 @@ describe('dehydration and rehydration', () => { { text: 'text' }, ).catch(() => undefined) - await sleep(1) + await vi.advanceTimersByTimeAsync(1) const dehydrated = dehydrate(queryClient, { shouldDehydrateMutation: () => false, }) @@ -570,12 +618,12 @@ describe('dehydration and rehydration', () => { ).catch(() => undefined) // Dehydrate mutation between retries - await sleep(1) + await vi.advanceTimersByTimeAsync(1) const dehydrated = dehydrate(queryClient) expect(dehydrated.mutations.length).toBe(0) - await sleep(30) + await vi.advanceTimersByTimeAsync(30) queryClient.clear() consoleMock.mockRestore() }) @@ -648,17 +696,21 @@ describe('dehydration and rehydration', () => { test('should dehydrate and hydrate meta for queries', async () => { const queryCache = new QueryCache() const queryClient = createQueryClient({ queryCache }) - await queryClient.prefetchQuery({ - queryKey: ['meta'], - queryFn: () => Promise.resolve('meta'), - meta: { - some: 'meta', - }, - }) - await queryClient.prefetchQuery({ - queryKey: ['no-meta'], - queryFn: () => Promise.resolve('no-meta'), - }) + await vi.waitFor(() => + queryClient.prefetchQuery({ + queryKey: ['meta'], + queryFn: () => Promise.resolve('meta'), + meta: { + some: 'meta', + }, + }), + ) + await vi.waitFor(() => + queryClient.prefetchQuery({ + queryKey: ['no-meta'], + queryFn: () => Promise.resolve('no-meta'), + }), + ) const dehydrated = dehydrate(queryClient) @@ -766,7 +818,7 @@ describe('dehydration and rehydration', () => { }, } as const - await queryClient.prefetchQuery(options) + await vi.waitFor(() => queryClient.prefetchQuery(options)) const dehydrated = dehydrate(queryClient) expect( @@ -785,7 +837,7 @@ describe('dehydration and rehydration', () => { expect( hydrationCache.find({ queryKey: ['string'] })?.state.fetchStatus, ).toBe('fetching') - await promise + await vi.waitFor(() => promise) expect( hydrationCache.find({ queryKey: ['string'] })?.state.fetchStatus, ).toBe('idle') @@ -831,10 +883,12 @@ describe('dehydration and rehydration', () => { queryCache, defaultOptions: { dehydrate: { shouldDehydrateQuery: () => true } }, }) - await queryClient.prefetchQuery({ - queryKey: ['success'], - queryFn: () => fetchData('success'), - }) + await vi.waitFor(() => + queryClient.prefetchQuery({ + queryKey: ['success'], + queryFn: () => fetchData('success'), + }), + ) const promise = queryClient.prefetchQuery({ queryKey: ['pending'], @@ -845,7 +899,7 @@ describe('dehydration and rehydration', () => { expect(dehydrated.queries[0]?.promise).toBeUndefined() expect(dehydrated.queries[1]?.promise).toBeInstanceOf(Promise) - await promise + await vi.waitFor(() => promise) queryClient.clear() }) @@ -855,10 +909,12 @@ describe('dehydration and rehydration', () => { queryCache, defaultOptions: { dehydrate: { shouldDehydrateQuery: () => true } }, }) - await queryClient.prefetchQuery({ - queryKey: ['success'], - queryFn: () => fetchData('success'), - }) + await vi.waitFor(() => + queryClient.prefetchQuery({ + queryKey: ['success'], + queryFn: () => fetchData('success'), + }), + ) void queryClient.prefetchQuery({ queryKey: ['pending'], @@ -896,7 +952,7 @@ describe('dehydration and rehydration', () => { }, ) - await waitFor(() => + await vi.waitFor(() => expect( hydrationCache.find({ queryKey: ['pending'] })?.state, ).toMatchObject({ @@ -942,8 +998,8 @@ describe('dehydration and rehydration', () => { }) hydrate(hydrationClient, dehydrated) - await promise - await waitFor(() => + await vi.waitFor(() => promise) + await vi.waitFor(() => expect( hydrationClient.getQueryData(['transformedStringToDate']), ).toBeInstanceOf(Date), @@ -966,7 +1022,7 @@ describe('dehydration and rehydration', () => { queryKey: ['transformedStringToDate'], queryFn: () => fetchDate('2024-01-01T00:00:00.000Z', 0), }) - await sleep(20) + await vi.advanceTimersByTimeAsync(20) const dehydrated = dehydrate(queryClient) const hydrationClient = createQueryClient({ @@ -978,8 +1034,8 @@ describe('dehydration and rehydration', () => { }) hydrate(hydrationClient, dehydrated) - await promise - await waitFor(() => + await vi.waitFor(() => promise) + await vi.waitFor(() => expect( hydrationClient.getQueryData(['transformedStringToDate']), ).toBeInstanceOf(Date), @@ -996,10 +1052,12 @@ describe('dehydration and rehydration', () => { }, }, }) - await hydrationClient.prefetchQuery({ - queryKey: ['date'], - queryFn: () => fetchDate('2024-01-01T00:00:00.000Z', 5), - }) + await vi.waitFor(() => + hydrationClient.prefetchQuery({ + queryKey: ['date'], + queryFn: () => fetchDate('2024-01-01T00:00:00.000Z', 5), + }), + ) // --- @@ -1011,10 +1069,12 @@ describe('dehydration and rehydration', () => { }, }, }) - await queryClient.prefetchQuery({ - queryKey: ['date'], - queryFn: () => fetchDate('2024-01-02T00:00:00.000Z', 10), - }) + await vi.waitFor(() => + queryClient.prefetchQuery({ + queryKey: ['date'], + queryFn: () => fetchDate('2024-01-02T00:00:00.000Z', 10), + }), + ) const dehydrated = dehydrate(queryClient) // --- @@ -1058,8 +1118,8 @@ describe('dehydration and rehydration', () => { hydrate(clientQueryClient, dehydrated) - await promise - await waitFor(() => + await vi.waitFor(() => promise) + await vi.waitFor(() => expect(clientQueryClient.getQueryData(['data'])).toBe('server data'), ) @@ -1106,8 +1166,8 @@ describe('dehydration and rehydration', () => { hydrate(clientQueryClient, dehydrated) - await promise - await waitFor(() => + await vi.waitFor(() => promise) + await vi.waitFor(() => expect(clientQueryClient.getQueryData(query.queryKey)).toBe(0), ) @@ -1128,8 +1188,8 @@ describe('dehydration and rehydration', () => { hydrate(clientQueryClient, dehydrated) - await promise2 - await waitFor(() => + await vi.waitFor(() => promise2) + await vi.waitFor(() => expect(clientQueryClient.getQueryData(query.queryKey)).toBe(1), ) diff --git a/packages/query-core/src/__tests__/infiniteQueryBehavior.test.tsx b/packages/query-core/src/__tests__/infiniteQueryBehavior.test.tsx index 13f57f90647..6bd3eb84f93 100644 --- a/packages/query-core/src/__tests__/infiniteQueryBehavior.test.tsx +++ b/packages/query-core/src/__tests__/infiniteQueryBehavior.test.tsx @@ -1,5 +1,4 @@ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' -import { waitFor } from '@testing-library/dom' import { CancelledError, InfiniteQueryObserver } from '..' import { createQueryClient, queryKey, sleep } from './utils' import type { @@ -14,6 +13,7 @@ describe('InfiniteQueryBehavior', () => { let queryCache: QueryCache beforeEach(() => { + vi.useFakeTimers() queryClient = createQueryClient() queryCache = queryClient.getQueryCache() queryClient.mount() @@ -21,6 +21,7 @@ describe('InfiniteQueryBehavior', () => { afterEach(() => { queryClient.clear() + vi.useRealTimers() }) test('InfiniteQueryBehavior should throw an error if the queryFn is not defined', async () => { @@ -41,7 +42,7 @@ describe('InfiniteQueryBehavior', () => { observerResult = result }) - await waitFor(() => { + await vi.waitFor(() => { const query = queryCache.find({ queryKey: key })! return expect(observerResult).toMatchObject({ isError: true, @@ -79,7 +80,7 @@ describe('InfiniteQueryBehavior', () => { }) // Wait for the first page to be fetched - await waitFor(() => + await vi.waitFor(() => expect(observerResult).toMatchObject({ isFetching: false, data: { pages: [1], pageParams: [1] }, @@ -231,7 +232,7 @@ describe('InfiniteQueryBehavior', () => { query.cancel() // Wait for the first page to be cancelled - await waitFor(() => + await vi.waitFor(() => expect(observerResult).toMatchObject({ isFetching: false, isError: true, @@ -280,7 +281,7 @@ describe('InfiniteQueryBehavior', () => { }) // Wait for the first page to be fetched - await waitFor(() => + await vi.waitFor(() => expect(observerResult).toMatchObject({ isFetching: false, data: { pages: [1], pageParams: [1] }, @@ -385,25 +386,32 @@ describe('InfiniteQueryBehavior', () => { }) // Fetch Page 1 - const page1Data = await observer.fetchNextPage() - expect(page1Data.data?.pageParams).toEqual([1]) + await vi.waitFor(async () => { + const page1Data = await observer.fetchNextPage() + expect(page1Data.data?.pageParams).toEqual([1]) + }) // Fetch Page 2, as per the queryFn, this will reject 2 times then resolves - const page2Data = await observer.fetchNextPage() - expect(page2Data.data?.pageParams).toEqual([1, 2]) + await vi.waitFor(async () => { + const page2Data = await observer.fetchNextPage() + expect(page2Data.data?.pageParams).toEqual([1, 2]) + }) // Fetch Page 3 - const page3Data = await observer.fetchNextPage() - expect(page3Data.data?.pageParams).toEqual([1, 2, 3]) + await vi.waitFor(async () => { + const page3Data = await observer.fetchNextPage() + expect(page3Data.data?.pageParams).toEqual([1, 2, 3]) + }) // Now the real deal; re-fetching this query **should not** stamp into an // infinite loop where the retryer every time restarts from page 1 // once it reaches the page where it errors. // For this to work, we'd need to reset the error count so we actually retry errorCount = 0 - const reFetchedData = await observer.refetch() - - expect(reFetchedData.data?.pageParams).toEqual([1, 2, 3]) + await vi.waitFor(async () => { + const reFetchedData = await observer.refetch() + expect(reFetchedData.data?.pageParams).toEqual([1, 2, 3]) + }) }) test('should fetch even if initialPageParam is null', async () => { @@ -424,7 +432,7 @@ describe('InfiniteQueryBehavior', () => { observerResult = result }) - await waitFor(() => + await vi.waitFor(() => expect(observerResult).toMatchObject({ isFetching: false, data: { pages: ['data'], pageParams: [null] }, diff --git a/packages/query-core/src/__tests__/mutationObserver.test.tsx b/packages/query-core/src/__tests__/mutationObserver.test.tsx index 3d0fe61df52..13347d56ed4 100644 --- a/packages/query-core/src/__tests__/mutationObserver.test.tsx +++ b/packages/query-core/src/__tests__/mutationObserver.test.tsx @@ -307,4 +307,64 @@ describe('mutationObserver', () => { unsubscribe() }) + + test('mutation callbacks should be called in correct order with correct arguments for success case', async () => { + const onSuccess = vi.fn() + const onSettled = vi.fn() + + const mutationObserver = new MutationObserver(queryClient, { + mutationFn: (text: string) => Promise.resolve(text.toUpperCase()), + }) + + const subscriptionHandler = vi.fn() + const unsubscribe = mutationObserver.subscribe(subscriptionHandler) + + mutationObserver.mutate('success', { + onSuccess, + onSettled, + }) + + await vi.advanceTimersByTimeAsync(0) + + expect(onSuccess).toHaveBeenCalledTimes(1) + expect(onSuccess).toHaveBeenCalledWith('SUCCESS', 'success', undefined) + expect(onSettled).toHaveBeenCalledTimes(1) + expect(onSettled).toHaveBeenCalledWith( + 'SUCCESS', + null, + 'success', + undefined, + ) + + unsubscribe() + }) + + test('mutation callbacks should be called in correct order with correct arguments for error case', async () => { + const onError = vi.fn() + const onSettled = vi.fn() + + const error = new Error('error') + const mutationObserver = new MutationObserver(queryClient, { + mutationFn: (_: string) => Promise.reject(error), + }) + + const subscriptionHandler = vi.fn() + const unsubscribe = mutationObserver.subscribe(subscriptionHandler) + + mutationObserver + .mutate('error', { + onError, + onSettled, + }) + .catch(() => {}) + + await vi.advanceTimersByTimeAsync(0) + + expect(onError).toHaveBeenCalledTimes(1) + expect(onError).toHaveBeenCalledWith(error, 'error', undefined) + expect(onSettled).toHaveBeenCalledTimes(1) + expect(onSettled).toHaveBeenCalledWith(undefined, error, 'error', undefined) + + unsubscribe() + }) }) diff --git a/packages/query-core/src/__tests__/notifyManager.test.tsx b/packages/query-core/src/__tests__/notifyManager.test.tsx index 01f1e09c21d..20a3103eca1 100644 --- a/packages/query-core/src/__tests__/notifyManager.test.tsx +++ b/packages/query-core/src/__tests__/notifyManager.test.tsx @@ -86,4 +86,38 @@ describe('notifyManager', () => { // @ts-expect-error someFn('im not happy', false) }) + + it('should use custom batch notify function', async () => { + const notifyManagerTest = createNotifyManager() + const batchNotifySpy = vi.fn((cb) => cb()) + const callbackSpy1 = vi.fn() + const callbackSpy2 = vi.fn() + + notifyManagerTest.setBatchNotifyFunction(batchNotifySpy) + + notifyManagerTest.batch(() => { + notifyManagerTest.schedule(callbackSpy1) + notifyManagerTest.schedule(callbackSpy2) + }) + + await vi.advanceTimersByTimeAsync(0) + + expect(batchNotifySpy).toHaveBeenCalled() + expect(callbackSpy1).toHaveBeenCalled() + expect(callbackSpy2).toHaveBeenCalled() + }) + + it('should batch calls correctly', async () => { + const notifyManagerTest = createNotifyManager() + const callbackSpy = vi.fn() + + const batchedFn = notifyManagerTest.batchCalls((a: number, b: string) => { + callbackSpy(a, b) + }) + + batchedFn(1, 'test') + await vi.advanceTimersByTimeAsync(0) + + expect(callbackSpy).toHaveBeenCalledWith(1, 'test') + }) }) diff --git a/packages/query-core/src/__tests__/onlineManager.test.tsx b/packages/query-core/src/__tests__/onlineManager.test.tsx index 234c7273593..40ede743fed 100644 --- a/packages/query-core/src/__tests__/onlineManager.test.tsx +++ b/packages/query-core/src/__tests__/onlineManager.test.tsx @@ -1,13 +1,19 @@ -import { beforeEach, describe, expect, test, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { OnlineManager } from '../onlineManager' -import { setIsServer, sleep } from './utils' +import { setIsServer } from './utils' describe('onlineManager', () => { let onlineManager: OnlineManager + beforeEach(() => { + vi.useFakeTimers() onlineManager = new OnlineManager() }) + afterEach(() => { + vi.useRealTimers() + }) + test('isOnline should return true if navigator is undefined', () => { const navigatorSpy = vi.spyOn(globalThis, 'navigator', 'get') @@ -41,7 +47,7 @@ describe('onlineManager', () => { onlineManager.setEventListener(setup) - await sleep(30) + await vi.advanceTimersByTimeAsync(30) expect(count).toEqual(1) expect(onlineManager.isOnline()).toBeFalsy() }) diff --git a/packages/query-core/src/__tests__/queriesObserver.test.tsx b/packages/query-core/src/__tests__/queriesObserver.test.tsx index 91317d9fe90..c6d58d8d910 100644 --- a/packages/query-core/src/__tests__/queriesObserver.test.tsx +++ b/packages/query-core/src/__tests__/queriesObserver.test.tsx @@ -1,5 +1,4 @@ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' -import { waitFor } from '@testing-library/dom' import { QueriesObserver } from '..' import { createQueryClient, queryKey, sleep } from './utils' import type { QueryClient, QueryObserverResult } from '..' @@ -8,12 +7,14 @@ describe('queriesObserver', () => { let queryClient: QueryClient beforeEach(() => { + vi.useFakeTimers() queryClient = createQueryClient() queryClient.mount() }) afterEach(() => { queryClient.clear() + vi.useRealTimers() }) test('should return an array with all query results', async () => { @@ -29,7 +30,7 @@ describe('queriesObserver', () => { const unsubscribe = observer.subscribe((result) => { observerResult = result }) - await sleep(1) + await vi.advanceTimersByTimeAsync(1) unsubscribe() expect(observerResult).toMatchObject([{ data: 1 }, { data: 2 }]) }) @@ -48,9 +49,9 @@ describe('queriesObserver', () => { const unsubscribe = observer.subscribe((result) => { results.push(result) }) - await sleep(1) + await vi.advanceTimersByTimeAsync(1) queryClient.setQueryData(key2, 3) - await sleep(1) + await vi.advanceTimersByTimeAsync(1) unsubscribe() expect(results.length).toBe(6) expect(results[0]).toMatchObject([ @@ -93,9 +94,9 @@ describe('queriesObserver', () => { const unsubscribe = observer.subscribe((result) => { results.push(result) }) - await sleep(1) + await vi.advanceTimersByTimeAsync(1) observer.setQueries([{ queryKey: key2, queryFn: queryFn2 }]) - await sleep(1) + await vi.advanceTimersByTimeAsync(1) const queryCache = queryClient.getQueryCache() expect(queryCache.find({ queryKey: key1, type: 'active' })).toBeUndefined() expect(queryCache.find({ queryKey: key2, type: 'active' })).toBeDefined() @@ -140,12 +141,12 @@ describe('queriesObserver', () => { const unsubscribe = observer.subscribe((result) => { results.push(result) }) - await sleep(1) + await vi.advanceTimersByTimeAsync(1) observer.setQueries([ { queryKey: key2, queryFn: queryFn2 }, { queryKey: key1, queryFn: queryFn1 }, ]) - await sleep(1) + await vi.advanceTimersByTimeAsync(1) unsubscribe() expect(results.length).toBe(6) expect(results[0]).toMatchObject([ @@ -188,12 +189,12 @@ describe('queriesObserver', () => { const unsubscribe = observer.subscribe((result) => { results.push(result) }) - await sleep(1) + await vi.advanceTimersByTimeAsync(1) observer.setQueries([ { queryKey: key1, queryFn: queryFn1 }, { queryKey: key2, queryFn: queryFn2 }, ]) - await sleep(1) + await vi.advanceTimersByTimeAsync(1) unsubscribe() expect(results.length).toBe(5) expect(results[0]).toMatchObject([ @@ -228,7 +229,7 @@ describe('queriesObserver', () => { { queryKey: key2, queryFn: queryFn2 }, ]) const unsubscribe = observer.subscribe(() => undefined) - await sleep(1) + await vi.advanceTimersByTimeAsync(1) unsubscribe() expect(queryFn1).toHaveBeenCalledTimes(1) expect(queryFn2).toHaveBeenCalledTimes(1) @@ -254,7 +255,7 @@ describe('queriesObserver', () => { unsubscribe1() - await waitFor(() => { + await vi.waitFor(() => { // 1 call: pending expect(subscription1Handler).toBeCalledTimes(1) // 1 call: success @@ -293,7 +294,7 @@ describe('queriesObserver', () => { results.push(result) }) - await sleep(1) + await vi.advanceTimersByTimeAsync(1) unsubscribe() expect(results.length).toBe(6) diff --git a/packages/react-query/src/__tests__/ssr.test.tsx b/packages/react-query/src/__tests__/ssr.test.tsx index ce7d7bfd072..49d51abcaf6 100644 --- a/packages/react-query/src/__tests__/ssr.test.tsx +++ b/packages/react-query/src/__tests__/ssr.test.tsx @@ -1,12 +1,20 @@ import * as React from 'react' import { renderToString } from 'react-dom/server' -import { describe, expect, it, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { QueryCache, QueryClientProvider, useInfiniteQuery, useQuery } from '..' -import { createQueryClient, queryKey, setIsServer, sleep } from './utils' +import { createQueryClient, queryKey, setIsServer } from './utils' describe('Server Side Rendering', () => { setIsServer(true) + beforeEach(() => { + vi.useFakeTimers() + }) + + afterEach(() => { + vi.useRealTimers() + }) + it('should not trigger fetch', () => { const queryCache = new QueryCache() const queryClient = createQueryClient({ queryCache }) @@ -54,8 +62,8 @@ describe('Server Side Rendering', () => { const queryCache = new QueryCache() const queryClient = createQueryClient({ queryCache }) const key = queryKey() - const queryFn = vi.fn(() => { - sleep(10) + const queryFn = vi.fn(async () => { + await vi.advanceTimersByTimeAsync(10) return 'data' }) @@ -123,7 +131,7 @@ describe('Server Side Rendering', () => { const queryClient = createQueryClient({ queryCache }) const key = queryKey() const queryFn = vi.fn(async () => { - await sleep(5) + await vi.advanceTimersByTimeAsync(5) return 'page 1' }) diff --git a/packages/react-query/src/__tests__/useIsFetching.test.tsx b/packages/react-query/src/__tests__/useIsFetching.test.tsx index 22fa478f4fa..223d586d646 100644 --- a/packages/react-query/src/__tests__/useIsFetching.test.tsx +++ b/packages/react-query/src/__tests__/useIsFetching.test.tsx @@ -1,5 +1,5 @@ -import { describe, expect, it } from 'vitest' -import { fireEvent, render, waitFor } from '@testing-library/react' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { fireEvent, render } from '@testing-library/react' import * as React from 'react' import { QueryCache, useIsFetching, useQuery } from '..' import { @@ -7,10 +7,17 @@ import { queryKey, renderWithClient, setActTimeout, - sleep, } from './utils' describe('useIsFetching', () => { + beforeEach(() => { + vi.useFakeTimers() + }) + + afterEach(() => { + vi.useRealTimers() + }) + // See https://github.com/tannerlinsley/react-query/issues/105 it('should update as queries start and stop fetching', async () => { const queryCache = new QueryCache() @@ -28,7 +35,7 @@ describe('useIsFetching', () => { useQuery({ queryKey: key, queryFn: async () => { - await sleep(50) + await vi.advanceTimersByTimeAsync(50) return 'test' }, enabled: ready, @@ -48,10 +55,19 @@ describe('useIsFetching', () => { const { findByText, getByRole } = renderWithClient(queryClient, ) - await findByText('isFetching: 0') + await vi.waitFor(() => { + findByText('isFetching: 0') + }) + fireEvent.click(getByRole('button', { name: /setReady/i })) - await findByText('isFetching: 1') - await findByText('isFetching: 0') + + await vi.waitFor(() => { + findByText('isFetching: 1') + }) + + await vi.waitFor(() => { + findByText('isFetching: 0') + }) }) it('should not update state while rendering', async () => { @@ -73,7 +89,7 @@ describe('useIsFetching', () => { useQuery({ queryKey: key1, queryFn: async () => { - await sleep(100) + await vi.advanceTimersByTimeAsync(100) return 'data' }, }) @@ -84,7 +100,7 @@ describe('useIsFetching', () => { useQuery({ queryKey: key2, queryFn: async () => { - await sleep(100) + await vi.advanceTimersByTimeAsync(100) return 'data' }, }) @@ -110,7 +126,10 @@ describe('useIsFetching', () => { } renderWithClient(queryClient, ) - await waitFor(() => expect(isFetchingArray).toEqual([0, 1, 1, 2, 1, 0])) + + await vi.waitFor(() => { + expect(isFetchingArray).toEqual([0, 1, 1, 2, 1, 0]) + }) }) it('should be able to filter', async () => { @@ -124,7 +143,7 @@ describe('useIsFetching', () => { useQuery({ queryKey: key1, queryFn: async () => { - await sleep(10) + await vi.advanceTimersByTimeAsync(10) return 'test' }, }) @@ -135,7 +154,7 @@ describe('useIsFetching', () => { useQuery({ queryKey: key2, queryFn: async () => { - await sleep(20) + await vi.advanceTimersByTimeAsync(20) return 'test' }, }) @@ -164,10 +183,20 @@ describe('useIsFetching', () => { const { findByText, getByRole } = renderWithClient(queryClient, ) - await findByText('isFetching: 0') + await vi.waitFor(() => { + findByText('isFetching: 0') + }) + fireEvent.click(getByRole('button', { name: /setStarted/i })) - await findByText('isFetching: 1') - await findByText('isFetching: 0') + + await vi.waitFor(() => { + findByText('isFetching: 1') + }) + + await vi.waitFor(() => { + findByText('isFetching: 0') + }) + // at no point should we have isFetching: 2 expect(isFetchingArray).toEqual(expect.not.arrayContaining([2])) }) @@ -180,7 +209,7 @@ describe('useIsFetching', () => { useQuery({ queryKey: key, queryFn: async () => { - await sleep(10) + await vi.advanceTimersByTimeAsync(10) return 'test' }, }) @@ -196,8 +225,13 @@ describe('useIsFetching', () => { const rendered = renderWithClient(queryClient, ) - await rendered.findByText('isFetching: 1') - await rendered.findByText('isFetching: 0') + await vi.waitFor(() => { + rendered.findByText('isFetching: 1') + }) + + await vi.waitFor(() => { + rendered.findByText('isFetching: 0') + }) }) it('should use provided custom queryClient', async () => { @@ -209,7 +243,7 @@ describe('useIsFetching', () => { { queryKey: key, queryFn: async () => { - await sleep(10) + await vi.advanceTimersByTimeAsync(10) return 'test' }, }, @@ -227,6 +261,8 @@ describe('useIsFetching', () => { const rendered = render() - await waitFor(() => rendered.getByText('isFetching: 1')) + await vi.waitFor(() => { + rendered.getByText('isFetching: 1') + }) }) }) diff --git a/packages/react-query/src/__tests__/usePrefetchQuery.test.tsx b/packages/react-query/src/__tests__/usePrefetchQuery.test.tsx index c63c9ca5886..894d7c2ac20 100644 --- a/packages/react-query/src/__tests__/usePrefetchQuery.test.tsx +++ b/packages/react-query/src/__tests__/usePrefetchQuery.test.tsx @@ -1,6 +1,6 @@ -import { describe, expect, it, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import React from 'react' -import { fireEvent, waitFor } from '@testing-library/react' +import { fireEvent } from '@testing-library/react' import { ErrorBoundary } from 'react-error-boundary' import { QueryCache, @@ -8,7 +8,7 @@ import { useQueryErrorResetBoundary, useSuspenseQuery, } from '..' -import { createQueryClient, queryKey, renderWithClient, sleep } from './utils' +import { createQueryClient, queryKey, renderWithClient } from './utils' import type { UseSuspenseQueryOptions } from '..' @@ -16,7 +16,7 @@ const generateQueryFn = (data: string) => vi .fn<(...args: Array) => Promise>() .mockImplementation(async () => { - await sleep(10) + await vi.advanceTimersByTimeAsync(10) return data }) @@ -25,6 +25,14 @@ describe('usePrefetchQuery', () => { const queryCache = new QueryCache() const queryClient = createQueryClient({ queryCache }) + beforeEach(() => { + vi.useFakeTimers() + }) + + afterEach(() => { + vi.useRealTimers() + }) + function Suspended(props: { queryOpts: UseSuspenseQueryOptions> children?: React.ReactNode @@ -62,7 +70,7 @@ describe('usePrefetchQuery', () => { const rendered = renderWithClient(queryClient, ) - await waitFor(() => rendered.getByText('data: prefetchQuery')) + await vi.waitFor(() => rendered.getByText('data: prefetchQuery')) expect(queryOpts.queryFn).toHaveBeenCalledTimes(1) }) @@ -87,7 +95,7 @@ describe('usePrefetchQuery', () => { const rendered = renderWithClient(queryClient, ) expect(rendered.queryByText('fetching: true')).not.toBeInTheDocument() - await waitFor(() => + await vi.waitFor(() => rendered.getByText('data: The usePrefetchQuery hook is smart!'), ) expect(queryOpts.queryFn).not.toHaveBeenCalled() @@ -104,7 +112,7 @@ describe('usePrefetchQuery', () => { } queryFn.mockImplementationOnce(async () => { - await sleep(10) + await vi.advanceTimersByTimeAsync(10) throw new Error('Oops! Server error!') }) @@ -125,7 +133,7 @@ describe('usePrefetchQuery', () => { queryFn.mockClear() const rendered = renderWithClient(queryClient, ) - await waitFor(() => rendered.getByText('Oops!')) + await vi.waitFor(() => rendered.getByText('Oops!')) expect(rendered.queryByText('data: Not an error')).not.toBeInTheDocument() expect(queryOpts.queryFn).not.toHaveBeenCalled() @@ -156,7 +164,7 @@ describe('usePrefetchQuery', () => { } const rendered = renderWithClient(queryClient, ) - await waitFor(() => rendered.getByText('data: prefetchedQuery')) + await vi.waitFor(() => rendered.getByText('data: prefetchedQuery')) expect(queryOpts.queryFn).toHaveBeenCalledTimes(1) }) @@ -171,7 +179,7 @@ describe('usePrefetchQuery', () => { } queryFn.mockImplementationOnce(async () => { - await sleep(10) + await vi.advanceTimersByTimeAsync(10) throw new Error('Oops! Server error!') }) @@ -202,9 +210,11 @@ describe('usePrefetchQuery', () => { const rendered = renderWithClient(queryClient, ) - await waitFor(() => rendered.getByText('Oops!')) + await vi.waitFor(() => rendered.getByText('Oops!')) fireEvent.click(rendered.getByText('Try again')) - await waitFor(() => rendered.getByText('data: This is fine :dog: :fire:')) + await vi.waitFor(() => + rendered.getByText('data: This is fine :dog: :fire:'), + ) expect(queryOpts.queryFn).toHaveBeenCalledTimes(1) consoleMock.mockRestore() }) @@ -253,10 +263,12 @@ describe('usePrefetchQuery', () => { expect( queryClient.getQueryState(thirdQueryOpts.queryKey)?.fetchStatus, ).toBe('fetching') - await waitFor(() => rendered.getByText('Loading...')) - await waitFor(() => rendered.getByText('data: Prefetch is nice!')) - await waitFor(() => rendered.getByText('data: Prefetch is really nice!!')) - await waitFor(() => + await vi.waitFor(() => rendered.getByText('Loading...')) + await vi.waitFor(() => rendered.getByText('data: Prefetch is nice!')) + await vi.waitFor(() => + rendered.getByText('data: Prefetch is really nice!!'), + ) + await vi.waitFor(() => rendered.getByText('data: Prefetch does not create waterfalls!!'), ) expect(Fallback).toHaveBeenCalledTimes(1)