source: node_modules/@reduxjs/toolkit/src/query/tests/cacheLifecycle.test.ts@ a762898

Last change on this file since a762898 was a762898, checked in by istevanoska <ilinastevanoska@…>, 5 months ago

Added visualizations

  • Property mode set to 100644
File size: 21.7 KB
Line 
1import {
2 DEFAULT_DELAY_MS,
3 fakeTimerWaitFor,
4 setupApiStore,
5} from '@internal/tests/utils/helpers'
6import type { QueryActionCreatorResult } from '@reduxjs/toolkit/query'
7import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'
8
9beforeAll(() => {
10 vi.useFakeTimers()
11})
12
13const api = createApi({
14 baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }),
15 endpoints: () => ({}),
16})
17const storeRef = setupApiStore(api)
18
19const onNewCacheEntry = vi.fn()
20const gotFirstValue = vi.fn()
21const onCleanup = vi.fn()
22const onCatch = vi.fn()
23
24beforeEach(() => {
25 onNewCacheEntry.mockClear()
26 gotFirstValue.mockClear()
27 onCleanup.mockClear()
28 onCatch.mockClear()
29})
30
31describe.each([['query'], ['mutation']] as const)(
32 'generic cases: %s',
33 (type) => {
34 test(`${type}: new cache entry only`, async () => {
35 const extended = api.injectEndpoints({
36 overrideExisting: true,
37 endpoints: (build) => ({
38 injected: build[type as 'mutation']<unknown, string>({
39 query: () => '/success',
40 onCacheEntryAdded(arg, { dispatch, getState }) {
41 onNewCacheEntry(arg)
42 },
43 }),
44 }),
45 })
46 storeRef.store.dispatch(extended.endpoints.injected.initiate('arg'))
47 expect(onNewCacheEntry).toHaveBeenCalledWith('arg')
48 })
49
50 test(`${type}: await cacheEntryRemoved`, async () => {
51 const extended = api.injectEndpoints({
52 overrideExisting: true,
53 endpoints: (build) => ({
54 // Lying to TS here
55 injected: build[type as 'mutation']<unknown, string>({
56 query: () => '/success',
57 async onCacheEntryAdded(
58 arg,
59 { dispatch, getState, cacheEntryRemoved },
60 ) {
61 onNewCacheEntry(arg)
62 await cacheEntryRemoved
63 onCleanup()
64 },
65 }),
66 }),
67 })
68 const promise = storeRef.store.dispatch(
69 extended.endpoints.injected.initiate('arg'),
70 )
71
72 expect(onNewCacheEntry).toHaveBeenCalledWith('arg')
73 expect(onCleanup).not.toHaveBeenCalled()
74
75 await promise
76 if (type === 'mutation') {
77 promise.reset()
78 } else {
79 ;(promise as unknown as QueryActionCreatorResult<any>).unsubscribe()
80 }
81 await vi.advanceTimersByTimeAsync(DEFAULT_DELAY_MS)
82 if (type === 'query') {
83 await vi.advanceTimersByTimeAsync(59000)
84 expect(onCleanup).not.toHaveBeenCalled()
85 await vi.advanceTimersByTimeAsync(2000)
86 }
87
88 expect(onCleanup).toHaveBeenCalled()
89 })
90
91 test(`${type}: await cacheDataLoaded, await cacheEntryRemoved (success)`, async () => {
92 const extended = api.injectEndpoints({
93 overrideExisting: true,
94 endpoints: (build) => ({
95 injected: build[type as 'mutation']<number, string>({
96 query: () => '/success',
97 async onCacheEntryAdded(
98 arg,
99 { dispatch, getState, cacheEntryRemoved, cacheDataLoaded },
100 ) {
101 onNewCacheEntry(arg)
102 const firstValue = await cacheDataLoaded
103 gotFirstValue(firstValue)
104 await cacheEntryRemoved
105 onCleanup()
106 },
107 }),
108 }),
109 })
110 const promise = storeRef.store.dispatch(
111 extended.endpoints.injected.initiate('arg'),
112 )
113
114 expect(onNewCacheEntry).toHaveBeenCalledWith('arg')
115
116 expect(gotFirstValue).not.toHaveBeenCalled()
117 expect(onCleanup).not.toHaveBeenCalled()
118
119 await fakeTimerWaitFor(() => {
120 expect(gotFirstValue).toHaveBeenCalled()
121 })
122 expect(gotFirstValue).toHaveBeenCalledWith({
123 data: { value: 'success' },
124 meta: {
125 request: expect.any(Request),
126 response: expect.any(Object), // Response is not available in jest env
127 },
128 })
129 expect(onCleanup).not.toHaveBeenCalled()
130
131 if (type === 'mutation') {
132 promise.reset()
133 } else {
134 ;(promise as unknown as QueryActionCreatorResult<any>).unsubscribe()
135 }
136 await vi.advanceTimersByTimeAsync(DEFAULT_DELAY_MS)
137 if (type === 'query') {
138 await vi.advanceTimersByTimeAsync(59000)
139 expect(onCleanup).not.toHaveBeenCalled()
140 await vi.advanceTimersByTimeAsync(2000)
141 }
142
143 expect(onCleanup).toHaveBeenCalled()
144 })
145
146 test(`${type}: await cacheDataLoaded, await cacheEntryRemoved (cacheDataLoaded never resolves)`, async () => {
147 const extended = api.injectEndpoints({
148 overrideExisting: true,
149 endpoints: (build) => ({
150 injected: build[type as 'mutation']<unknown, string>({
151 query: () => '/error', // we will initiate only once and that one time will be an error -> cacheDataLoaded will never resolve
152 async onCacheEntryAdded(
153 arg,
154 { dispatch, getState, cacheEntryRemoved, cacheDataLoaded },
155 ) {
156 onNewCacheEntry(arg)
157 // this will wait until cacheEntryRemoved, then reject => nothing past that line will execute
158 // but since this special "cacheEntryRemoved" rejection is handled outside, there will be no
159 // uncaught rejection error
160 const firstValue = await cacheDataLoaded
161 gotFirstValue(firstValue)
162 await cacheEntryRemoved
163 onCleanup()
164 },
165 }),
166 }),
167 })
168 const promise = storeRef.store.dispatch(
169 extended.endpoints.injected.initiate('arg'),
170 )
171 expect(onNewCacheEntry).toHaveBeenCalledWith('arg')
172
173 if (type === 'mutation') {
174 promise.reset()
175 } else {
176 ;(promise as unknown as QueryActionCreatorResult<any>).unsubscribe()
177 }
178 await vi.advanceTimersByTimeAsync(DEFAULT_DELAY_MS)
179 if (type === 'query') {
180 await vi.advanceTimersByTimeAsync(120000)
181 }
182 expect(gotFirstValue).not.toHaveBeenCalled()
183 expect(onCleanup).not.toHaveBeenCalled()
184 })
185
186 test(`${type}: try { await cacheDataLoaded }, await cacheEntryRemoved (cacheDataLoaded never resolves)`, async () => {
187 const extended = api.injectEndpoints({
188 overrideExisting: true,
189 endpoints: (build) => ({
190 injected: build[type as 'mutation']<unknown, string>({
191 query: () => '/error', // we will initiate only once and that one time will be an error -> cacheDataLoaded will never resolve
192 async onCacheEntryAdded(
193 arg,
194 { dispatch, getState, cacheEntryRemoved, cacheDataLoaded },
195 ) {
196 onNewCacheEntry(arg)
197
198 try {
199 // this will wait until cacheEntryRemoved, then reject => nothing else in this try..catch block will execute
200 const firstValue = await cacheDataLoaded
201 gotFirstValue(firstValue)
202 } catch (e) {
203 onCatch(e)
204 }
205 await cacheEntryRemoved
206 onCleanup()
207 },
208 }),
209 }),
210 })
211 const promise = storeRef.store.dispatch(
212 extended.endpoints.injected.initiate('arg'),
213 )
214
215 expect(onNewCacheEntry).toHaveBeenCalledWith('arg')
216 await promise
217 if (type === 'mutation') {
218 promise.reset()
219 } else {
220 ;(promise as unknown as QueryActionCreatorResult<any>).unsubscribe()
221 }
222 await vi.advanceTimersByTimeAsync(DEFAULT_DELAY_MS)
223
224 if (type === 'query') {
225 await vi.advanceTimersByTimeAsync(59000)
226 expect(onCleanup).not.toHaveBeenCalled()
227 await vi.advanceTimersByTimeAsync(2000)
228 }
229
230 expect(onCleanup).toHaveBeenCalled()
231 expect(gotFirstValue).not.toHaveBeenCalled()
232 expect(onCatch.mock.calls[0][0]).toMatchObject({
233 message: 'Promise never resolved before cacheEntryRemoved.',
234 })
235 })
236
237 test(`${type}: try { await cacheDataLoaded, await cacheEntryRemoved } (cacheDataLoaded never resolves)`, async () => {
238 const extended = api.injectEndpoints({
239 overrideExisting: true,
240 endpoints: (build) => ({
241 injected: build[type as 'mutation']<unknown, string>({
242 query: () => '/error', // we will initiate only once and that one time will be an error -> cacheDataLoaded will never resolve
243 async onCacheEntryAdded(
244 arg,
245 { dispatch, getState, cacheEntryRemoved, cacheDataLoaded },
246 ) {
247 onNewCacheEntry(arg)
248
249 try {
250 // this will wait until cacheEntryRemoved, then reject => nothing else in this try..catch block will execute
251 const firstValue = await cacheDataLoaded
252 gotFirstValue(firstValue)
253 // cleanup in this scenario only needs to be done for stuff within this try..catch block - totally valid scenario
254 await cacheEntryRemoved
255 onCleanup()
256 } catch (e) {
257 onCatch(e)
258 }
259 },
260 }),
261 }),
262 })
263 const promise = storeRef.store.dispatch(
264 extended.endpoints.injected.initiate('arg'),
265 )
266
267 expect(onNewCacheEntry).toHaveBeenCalledWith('arg')
268 await promise
269
270 if (type === 'mutation') {
271 promise.reset()
272 } else {
273 ;(promise as unknown as QueryActionCreatorResult<any>).unsubscribe()
274 }
275 await vi.advanceTimersByTimeAsync(DEFAULT_DELAY_MS)
276 if (type === 'query') {
277 await vi.advanceTimersByTimeAsync(59000)
278 expect(onCleanup).not.toHaveBeenCalled()
279 await vi.advanceTimersByTimeAsync(2000)
280 }
281 expect(onCleanup).not.toHaveBeenCalled()
282 expect(gotFirstValue).not.toHaveBeenCalled()
283 expect(onCatch.mock.calls[0][0]).toMatchObject({
284 message: 'Promise never resolved before cacheEntryRemoved.',
285 })
286 })
287
288 test(`${type}: try { await cacheDataLoaded } finally { await cacheEntryRemoved } (cacheDataLoaded never resolves)`, async () => {
289 const extended = api.injectEndpoints({
290 overrideExisting: true,
291 endpoints: (build) => ({
292 injected: build[type as 'mutation']<unknown, string>({
293 query: () => '/error', // we will initiate only once and that one time will be an error -> cacheDataLoaded will never resolve
294 async onCacheEntryAdded(
295 arg,
296 { dispatch, getState, cacheEntryRemoved, cacheDataLoaded },
297 ) {
298 onNewCacheEntry(arg)
299
300 try {
301 // this will wait until cacheEntryRemoved, then reject => nothing else in this try..catch block will execute
302 const firstValue = await cacheDataLoaded
303 gotFirstValue(firstValue)
304 } catch (e) {
305 onCatch(e)
306 } finally {
307 await cacheEntryRemoved
308 onCleanup()
309 }
310 },
311 }),
312 }),
313 })
314 const promise = storeRef.store.dispatch(
315 extended.endpoints.injected.initiate('arg'),
316 )
317
318 expect(onNewCacheEntry).toHaveBeenCalledWith('arg')
319
320 await promise
321 if (type === 'mutation') {
322 promise.reset()
323 } else {
324 ;(promise as unknown as QueryActionCreatorResult<any>).unsubscribe()
325 }
326 await vi.advanceTimersByTimeAsync(DEFAULT_DELAY_MS)
327 if (type === 'query') {
328 await vi.advanceTimersByTimeAsync(59000)
329 expect(onCleanup).not.toHaveBeenCalled()
330 await vi.advanceTimersByTimeAsync(2000)
331 }
332 expect(onCleanup).toHaveBeenCalled()
333 expect(gotFirstValue).not.toHaveBeenCalled()
334 expect(onCatch.mock.calls[0][0]).toMatchObject({
335 message: 'Promise never resolved before cacheEntryRemoved.',
336 })
337 })
338 },
339)
340
341test(`query: getCacheEntry`, async () => {
342 const snapshot = vi.fn()
343 const extended = api.injectEndpoints({
344 overrideExisting: true,
345 endpoints: (build) => ({
346 injected: build.query<unknown, string>({
347 query: () => '/success',
348 async onCacheEntryAdded(
349 arg,
350 {
351 dispatch,
352 getState,
353 getCacheEntry,
354 cacheEntryRemoved,
355 cacheDataLoaded,
356 },
357 ) {
358 snapshot(getCacheEntry())
359 gotFirstValue(await cacheDataLoaded)
360 snapshot(getCacheEntry())
361 await cacheEntryRemoved
362 snapshot(getCacheEntry())
363 },
364 }),
365 }),
366 })
367 const promise = storeRef.store.dispatch(
368 extended.endpoints.injected.initiate('arg'),
369 )
370 await promise
371 promise.unsubscribe()
372
373 await fakeTimerWaitFor(() => {
374 expect(gotFirstValue).toHaveBeenCalled()
375 })
376
377 await vi.advanceTimersByTimeAsync(120000)
378
379 expect(snapshot).toHaveBeenCalledTimes(3)
380 expect(snapshot.mock.calls[0][0]).toMatchObject({
381 endpointName: 'injected',
382 isError: false,
383 isLoading: true,
384 isSuccess: false,
385 isUninitialized: false,
386 originalArgs: 'arg',
387 requestId: promise.requestId,
388 startedTimeStamp: expect.any(Number),
389 status: 'pending',
390 })
391 expect(snapshot.mock.calls[1][0]).toMatchObject({
392 data: {
393 value: 'success',
394 },
395 endpointName: 'injected',
396 fulfilledTimeStamp: expect.any(Number),
397 isError: false,
398 isLoading: false,
399 isSuccess: true,
400 isUninitialized: false,
401 originalArgs: 'arg',
402 requestId: promise.requestId,
403 startedTimeStamp: expect.any(Number),
404 status: 'fulfilled',
405 })
406 expect(snapshot.mock.calls[2][0]).toMatchObject({
407 isError: false,
408 isLoading: false,
409 isSuccess: false,
410 isUninitialized: true,
411 status: 'uninitialized',
412 })
413})
414
415test(`mutation: getCacheEntry`, async () => {
416 const snapshot = vi.fn()
417 const extended = api.injectEndpoints({
418 overrideExisting: true,
419 endpoints: (build) => ({
420 injected: build.mutation<unknown, string>({
421 query: () => '/success',
422 async onCacheEntryAdded(
423 arg,
424 {
425 dispatch,
426 getState,
427 getCacheEntry,
428 cacheEntryRemoved,
429 cacheDataLoaded,
430 },
431 ) {
432 snapshot(getCacheEntry())
433 gotFirstValue(await cacheDataLoaded)
434 snapshot(getCacheEntry())
435 await cacheEntryRemoved
436 snapshot(getCacheEntry())
437 },
438 }),
439 }),
440 })
441 const promise = storeRef.store.dispatch(
442 extended.endpoints.injected.initiate('arg'),
443 )
444 await fakeTimerWaitFor(() => {
445 expect(gotFirstValue).toHaveBeenCalled()
446 })
447
448 promise.reset()
449 await vi.advanceTimersByTimeAsync(DEFAULT_DELAY_MS)
450
451 expect(snapshot).toHaveBeenCalledTimes(3)
452 expect(snapshot.mock.calls[0][0]).toMatchObject({
453 endpointName: 'injected',
454 isError: false,
455 isLoading: true,
456 isSuccess: false,
457 isUninitialized: false,
458 startedTimeStamp: expect.any(Number),
459 status: 'pending',
460 })
461 expect(snapshot.mock.calls[1][0]).toMatchObject({
462 data: {
463 value: 'success',
464 },
465 endpointName: 'injected',
466 fulfilledTimeStamp: expect.any(Number),
467 isError: false,
468 isLoading: false,
469 isSuccess: true,
470 isUninitialized: false,
471 startedTimeStamp: expect.any(Number),
472 status: 'fulfilled',
473 })
474 expect(snapshot.mock.calls[2][0]).toMatchObject({
475 isError: false,
476 isLoading: false,
477 isSuccess: false,
478 isUninitialized: true,
479 status: 'uninitialized',
480 })
481})
482
483test('query: updateCachedData', async () => {
484 const trackCalls = vi.fn()
485
486 const extended = api.injectEndpoints({
487 overrideExisting: true,
488 endpoints: (build) => ({
489 injected: build.query<{ value: string }, string>({
490 query: () => '/success',
491 async onCacheEntryAdded(
492 arg,
493 {
494 dispatch,
495 getState,
496 getCacheEntry,
497 updateCachedData,
498 cacheEntryRemoved,
499 cacheDataLoaded,
500 },
501 ) {
502 expect(getCacheEntry().data).toEqual(undefined)
503 // calling `updateCachedData` when there is no data yet should not do anything
504 updateCachedData((draft) => {
505 draft.value = 'TEST'
506 trackCalls()
507 })
508 expect(trackCalls).not.toHaveBeenCalled()
509 expect(getCacheEntry().data).toEqual(undefined)
510
511 gotFirstValue(await cacheDataLoaded)
512
513 expect(getCacheEntry().data).toEqual({ value: 'success' })
514 updateCachedData((draft) => {
515 draft.value = 'TEST'
516 trackCalls()
517 })
518 expect(trackCalls).toHaveBeenCalledOnce()
519 expect(getCacheEntry().data).toEqual({ value: 'TEST' })
520
521 await cacheEntryRemoved
522
523 expect(getCacheEntry().data).toEqual(undefined)
524 // calling `updateCachedData` when there is no data any more should not do anything
525 updateCachedData((draft) => {
526 draft.value = 'TEST2'
527 trackCalls()
528 })
529 expect(trackCalls).toHaveBeenCalledOnce()
530 expect(getCacheEntry().data).toEqual(undefined)
531
532 onCleanup()
533 },
534 }),
535 }),
536 })
537 const promise = storeRef.store.dispatch(
538 extended.endpoints.injected.initiate('arg'),
539 )
540 await promise
541 promise.unsubscribe()
542
543 await fakeTimerWaitFor(() => {
544 expect(gotFirstValue).toHaveBeenCalled()
545 })
546
547 await vi.advanceTimersByTimeAsync(61000)
548
549 await fakeTimerWaitFor(() => {
550 expect(onCleanup).toHaveBeenCalled()
551 })
552})
553
554test('updateCachedData - infinite query', async () => {
555 const trackCalls = vi.fn()
556
557 const extended = api.injectEndpoints({
558 overrideExisting: true,
559 endpoints: (build) => ({
560 infiniteInjected: build.infiniteQuery<{ value: string }, string, number>({
561 query: () => '/success',
562 infiniteQueryOptions: {
563 initialPageParam: 1,
564 getNextPageParam: (
565 lastPage,
566 allPages,
567 lastPageParam,
568 allPageParams,
569 ) => lastPageParam + 1,
570 },
571 async onCacheEntryAdded(
572 arg,
573 {
574 dispatch,
575 getState,
576 getCacheEntry,
577 updateCachedData,
578 cacheEntryRemoved,
579 cacheDataLoaded,
580 },
581 ) {
582 expect(getCacheEntry().data).toEqual(undefined)
583 // calling `updateCachedData` when there is no data yet should not do anything
584 updateCachedData((draft) => {
585 draft.pages = [{ value: 'TEST' }]
586 draft.pageParams = [1]
587 trackCalls()
588 })
589 expect(trackCalls).not.toHaveBeenCalled()
590 expect(getCacheEntry().data).toEqual(undefined)
591
592 gotFirstValue(await cacheDataLoaded)
593
594 expect(getCacheEntry().data).toEqual({
595 pages: [{ value: 'success' }],
596 pageParams: [1],
597 })
598 updateCachedData((draft) => {
599 draft.pages = [{ value: 'TEST' }]
600 draft.pageParams = [1]
601 trackCalls()
602 })
603 expect(trackCalls).toHaveBeenCalledOnce()
604 expect(getCacheEntry().data).toEqual({
605 pages: [{ value: 'TEST' }],
606 pageParams: [1],
607 })
608
609 await cacheEntryRemoved
610
611 expect(getCacheEntry().data).toEqual(undefined)
612 // calling `updateCachedData` when there is no data any more should not do anything
613 updateCachedData((draft) => {
614 draft.pages = [{ value: 'TEST' }, { value: 'TEST2' }]
615 draft.pageParams = [1, 2]
616 trackCalls()
617 })
618 expect(trackCalls).toHaveBeenCalledOnce()
619 expect(getCacheEntry().data).toEqual(undefined)
620
621 onCleanup()
622 },
623 }),
624 }),
625 })
626 const promise = storeRef.store.dispatch(
627 extended.endpoints.infiniteInjected.initiate('arg'),
628 )
629 await promise
630 promise.unsubscribe()
631
632 await fakeTimerWaitFor(() => {
633 expect(gotFirstValue).toHaveBeenCalled()
634 })
635
636 await vi.advanceTimersByTimeAsync(61000)
637
638 await fakeTimerWaitFor(() => {
639 expect(onCleanup).toHaveBeenCalled()
640 })
641})
642
643test('dispatching further actions does not trigger another lifecycle', async () => {
644 const extended = api.injectEndpoints({
645 overrideExisting: true,
646 endpoints: (build) => ({
647 injected: build.query<unknown, void>({
648 query: () => '/success',
649 async onCacheEntryAdded() {
650 onNewCacheEntry()
651 },
652 }),
653 }),
654 })
655 await storeRef.store.dispatch(extended.endpoints.injected.initiate())
656 expect(onNewCacheEntry).toHaveBeenCalledOnce()
657
658 await storeRef.store.dispatch(extended.endpoints.injected.initiate())
659 expect(onNewCacheEntry).toHaveBeenCalledOnce()
660
661 await storeRef.store.dispatch(
662 extended.endpoints.injected.initiate(undefined, { forceRefetch: true }),
663 )
664 expect(onNewCacheEntry).toHaveBeenCalledOnce()
665})
666
667test('dispatching a query initializer with `subscribe: false` does also start a lifecycle', async () => {
668 const extended = api.injectEndpoints({
669 overrideExisting: true,
670 endpoints: (build) => ({
671 injected: build.query<unknown, void>({
672 query: () => '/success',
673 async onCacheEntryAdded() {
674 onNewCacheEntry()
675 },
676 }),
677 }),
678 })
679 await storeRef.store.dispatch(
680 extended.endpoints.injected.initiate(undefined, { subscribe: false }),
681 )
682 expect(onNewCacheEntry).toHaveBeenCalledOnce()
683
684 // will not be called a second time though
685 await storeRef.store.dispatch(extended.endpoints.injected.initiate(undefined))
686 expect(onNewCacheEntry).toHaveBeenCalledOnce()
687})
688
689test('dispatching a mutation initializer with `track: false` does not start a lifecycle', async () => {
690 const extended = api.injectEndpoints({
691 overrideExisting: true,
692 endpoints: (build) => ({
693 injected: build.mutation<unknown, void>({
694 query: () => '/success',
695 async onCacheEntryAdded() {
696 onNewCacheEntry()
697 },
698 }),
699 }),
700 })
701 await storeRef.store.dispatch(
702 extended.endpoints.injected.initiate(undefined, { track: false }),
703 )
704 expect(onNewCacheEntry).not.toHaveBeenCalled()
705
706 await storeRef.store.dispatch(extended.endpoints.injected.initiate(undefined))
707 expect(onNewCacheEntry).toHaveBeenCalledOnce()
708})
Note: See TracBrowser for help on using the repository browser.