@@ -5,12 +5,12 @@ import {
55 sleep ,
66} from '@tanstack/query-test-utils'
77import {
8+ CancelledError ,
89 Query ,
910 QueryClient ,
1011 QueryObserver ,
1112 dehydrate ,
1213 hydrate ,
13- isCancelledError ,
1414} from '..'
1515import { hashQueryKeyByOptions } from '../utils'
1616import { mockOnlineManagerIsOnline , setIsServer } from './utils'
@@ -190,14 +190,52 @@ describe('query', () => {
190190 await promise
191191 expect . unreachable ( )
192192 } catch {
193- expect ( isCancelledError ( result ) ) . toBe ( true )
194- expect ( result instanceof Error ) . toBe ( true )
193+ expect ( result ) . toBeInstanceOf ( CancelledError )
195194 } finally {
196195 // Reset visibilityState to original value
197196 visibilityMock . mockRestore ( )
198197 }
199198 } )
200199
200+ test ( 'should not throw a CancelledError when fetchQuery is in progress and the last observer unsubscribes when AbortSignal is consumed' , async ( ) => {
201+ const key = queryKey ( )
202+
203+ const observer = new QueryObserver ( queryClient , {
204+ queryKey : key ,
205+ queryFn : async ( ) => {
206+ await sleep ( 100 )
207+ return 'data'
208+ } ,
209+ } )
210+
211+ const unsubscribe = observer . subscribe ( ( ) => undefined )
212+ await vi . advanceTimersByTimeAsync ( 100 )
213+
214+ expect ( queryCache . find ( { queryKey : key } ) ?. state . data ) . toBe ( 'data' )
215+
216+ const promise = queryClient . fetchQuery ( {
217+ queryKey : key ,
218+ queryFn : async ( { signal } ) => {
219+ await sleep ( 100 )
220+ return 'data2' + String ( signal )
221+ } ,
222+ } )
223+
224+ // Ensure the fetch is in progress
225+ await vi . advanceTimersByTimeAsync ( 10 )
226+
227+ // Unsubscribe while fetch is in progress
228+ unsubscribe ( )
229+ // await queryClient.cancelQueries()
230+
231+ await vi . advanceTimersByTimeAsync ( 90 )
232+
233+ // Fetch should complete successfully without throwing a CancelledError
234+ await expect ( promise ) . resolves . toBe ( 'data' )
235+
236+ expect ( queryCache . find ( { queryKey : key } ) ?. state . data ) . toBe ( 'data' )
237+ } )
238+
201239 test ( 'should provide context to queryFn' , ( ) => {
202240 const key = queryKey ( )
203241
@@ -330,7 +368,7 @@ describe('query', () => {
330368 expect ( signal . aborted ) . toBe ( true )
331369 expect ( onAbort ) . toHaveBeenCalledTimes ( 1 )
332370 expect ( abortListener ) . toHaveBeenCalledTimes ( 1 )
333- expect ( isCancelledError ( error ) ) . toBe ( true )
371+ expect ( error ) . toBeInstanceOf ( CancelledError )
334372 } )
335373
336374 test ( 'should not continue if explicitly cancelled' , async ( ) => {
@@ -362,7 +400,7 @@ describe('query', () => {
362400 await vi . advanceTimersByTimeAsync ( 100 )
363401
364402 expect ( queryFn ) . toHaveBeenCalledTimes ( 1 )
365- expect ( isCancelledError ( error ) ) . toBe ( true )
403+ expect ( error ) . toBeInstanceOf ( CancelledError )
366404 } )
367405
368406 test ( 'should not error if reset while pending' , async ( ) => {
@@ -375,7 +413,18 @@ describe('query', () => {
375413 throw new Error ( )
376414 } )
377415
378- queryClient . fetchQuery ( { queryKey : key , queryFn, retry : 3 , retryDelay : 10 } )
416+ let error
417+
418+ queryClient
419+ . fetchQuery ( {
420+ queryKey : key ,
421+ queryFn,
422+ retry : 3 ,
423+ retryDelay : 10 ,
424+ } )
425+ . catch ( ( e ) => {
426+ error = e
427+ } )
379428
380429 // Ensure the query is pending
381430 const query = queryCache . find ( { queryKey : key } ) !
@@ -390,6 +439,12 @@ describe('query', () => {
390439 expect ( queryFn ) . toHaveBeenCalledTimes ( 1 ) // have been called,
391440 expect ( query . state . error ) . toBe ( null ) // not have an error, and
392441 expect ( query . state . fetchStatus ) . toBe ( 'idle' ) // not be loading any longer
442+ expect ( query . state . data ) . toBe ( undefined ) // have no data
443+
444+ // the call to fetchQuery must reject
445+ // because it was reset and not reverted
446+ // so it would resolve with undefined otherwise
447+ expect ( error ) . toBeInstanceOf ( CancelledError )
393448 } )
394449
395450 test ( 'should reset to default state when created from hydration' , async ( ) => {
@@ -426,7 +481,7 @@ describe('query', () => {
426481 await vi . advanceTimersByTimeAsync ( 100 )
427482
428483 expect ( queryFn ) . toHaveBeenCalledTimes ( 1 )
429- expect ( isCancelledError ( query . state . error ) ) . toBe ( true )
484+ expect ( query . state . error ) . toBeInstanceOf ( CancelledError )
430485 const result = query . fetch ( )
431486 await vi . advanceTimersByTimeAsync ( 50 )
432487 await expect ( result ) . resolves . toBe ( 'data' )
@@ -459,7 +514,7 @@ describe('query', () => {
459514 await vi . advanceTimersByTimeAsync ( 10 )
460515
461516 expect ( query . state . error ) . toBe ( error )
462- expect ( isCancelledError ( query . state . error ) ) . toBe ( false )
517+ expect ( query . state . error ) . not . toBeInstanceOf ( CancelledError )
463518 } )
464519
465520 test ( 'the previous query status should be kept when refetching' , async ( ) => {
@@ -897,7 +952,7 @@ describe('query', () => {
897952 unsubscribe ( )
898953 } )
899954
900- test ( 'should always revert to idle state (#5958 )' , async ( ) => {
955+ test ( 'should always revert to idle state (#5968 )' , async ( ) => {
901956 let mockedData = [ 1 ]
902957
903958 const key = queryKey ( )
@@ -931,24 +986,69 @@ describe('query', () => {
931986 const unsubscribe = observer . subscribe ( ( ) => undefined )
932987 await vi . advanceTimersByTimeAsync ( 50 ) // let it resolve
933988
989+ expect ( observer . getCurrentResult ( ) . data ) . toBe ( '1' )
990+ expect ( observer . getCurrentResult ( ) . fetchStatus ) . toBe ( 'idle' )
991+
934992 mockedData = [ 1 , 2 ] // update "server" state in the background
935993
936- queryClient . invalidateQueries ( { queryKey : key } )
937- queryClient . invalidateQueries ( { queryKey : key } )
994+ void queryClient . invalidateQueries ( { queryKey : key } )
995+ await vi . advanceTimersByTimeAsync ( 5 )
996+ void queryClient . invalidateQueries ( { queryKey : key } )
997+ await vi . advanceTimersByTimeAsync ( 5 )
938998 unsubscribe ( ) // unsubscribe to simulate unmount
999+ await vi . advanceTimersByTimeAsync ( 5 )
1000+
1001+ // reverted to previous data and idle fetchStatus
1002+ expect ( queryCache . find ( { queryKey : key } ) ?. state ) . toMatchObject ( {
1003+ status : 'success' ,
1004+ data : '1' ,
1005+ fetchStatus : 'idle' ,
1006+ } )
9391007
9401008 // set up a new observer to simulate a mount of new component
9411009 const newObserver = new QueryObserver ( queryClient , {
9421010 queryKey : key ,
9431011 queryFn,
9441012 } )
945-
9461013 const spy = vi . fn ( )
9471014 newObserver . subscribe ( ( { data } ) => spy ( data ) )
9481015 await vi . advanceTimersByTimeAsync ( 60 ) // let it resolve
9491016 expect ( spy ) . toHaveBeenCalledWith ( '1 - 2' )
9501017 } )
9511018
1019+ test ( 'should not reject a promise when silently cancelled in the background' , async ( ) => {
1020+ const key = queryKey ( )
1021+
1022+ let x = 0
1023+
1024+ queryClient . setQueryData ( key , 'initial' )
1025+ const queryFn = vi . fn ( ) . mockImplementation ( async ( ) => {
1026+ await sleep ( 100 )
1027+ return 'data' + x
1028+ } )
1029+
1030+ const promise = queryClient . fetchQuery ( {
1031+ queryKey : key ,
1032+ queryFn,
1033+ } )
1034+
1035+ await vi . advanceTimersByTimeAsync ( 10 )
1036+
1037+ expect ( queryFn ) . toHaveBeenCalledTimes ( 1 )
1038+
1039+ x = 1
1040+
1041+ // cancel ongoing re-fetches
1042+ void queryClient . refetchQueries ( { queryKey : key } , { cancelRefetch : true } )
1043+
1044+ await vi . advanceTimersByTimeAsync ( 10 )
1045+
1046+ // The promise should not reject
1047+ await vi . waitFor ( ( ) => expect ( promise ) . resolves . toBe ( 'data1' ) )
1048+
1049+ expect ( queryFn ) . toHaveBeenCalledTimes ( 2 )
1050+ } )
1051+
9521052 it ( 'should have an error log when queryFn data is not serializable' , async ( ) => {
9531053 const consoleMock = vi . spyOn ( console , 'error' )
9541054
0 commit comments