@tanstack/solid-query 5.59.16 → 5.59.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,1238 +0,0 @@
1
- import { describe, expect, it, vi } from 'vitest'
2
- import {
3
- ErrorBoundary,
4
- createEffect,
5
- createRenderEffect,
6
- createSignal,
7
- } from 'solid-js'
8
- import { fireEvent, render, waitFor } from '@solidjs/testing-library'
9
- import {
10
- MutationCache,
11
- QueryCache,
12
- QueryClientProvider,
13
- createMutation,
14
- } from '..'
15
- import {
16
- createQueryClient,
17
- mockOnlineManagerIsOnline,
18
- queryKey,
19
- setActTimeout,
20
- sleep,
21
- } from './utils'
22
- import type { CreateMutationResult } from '../types'
23
-
24
- describe('createMutation', () => {
25
- const queryCache = new QueryCache()
26
- const mutationCache = new MutationCache()
27
- const queryClient = createQueryClient({ queryCache, mutationCache })
28
-
29
- it('should be able to reset `data`', async () => {
30
- function Page() {
31
- const mutation = createMutation(() => ({
32
- mutationFn: () => Promise.resolve('mutation'),
33
- }))
34
-
35
- return (
36
- <div>
37
- <h1>{mutation.data ?? 'empty'}</h1>
38
- <button onClick={() => mutation.reset()}>reset</button>
39
- <button onClick={() => mutation.mutate()}>mutate</button>
40
- </div>
41
- )
42
- }
43
-
44
- const rendered = render(() => (
45
- <QueryClientProvider client={queryClient}>
46
- <Page />
47
- </QueryClientProvider>
48
- ))
49
-
50
- expect(rendered.getByRole('heading').textContent).toBe('empty')
51
-
52
- fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
53
-
54
- await waitFor(() => {
55
- expect(rendered.getByRole('heading').textContent).toBe('mutation')
56
- })
57
-
58
- fireEvent.click(rendered.getByRole('button', { name: /reset/i }))
59
-
60
- await waitFor(() => {
61
- expect(rendered.getByRole('heading').textContent).toBe('empty')
62
- })
63
- })
64
-
65
- it('should be able to reset `error`', async () => {
66
- const consoleMock = vi
67
- .spyOn(console, 'error')
68
- .mockImplementation(() => undefined)
69
-
70
- function Page() {
71
- const mutation = createMutation<string, Error>(() => ({
72
- mutationFn: () => {
73
- const err = new Error('Expected mock error. All is well!')
74
- err.stack = ''
75
- return Promise.reject(err)
76
- },
77
- }))
78
-
79
- return (
80
- <div>
81
- {mutation.error && <h1>{mutation.error.message}</h1>}
82
- <button onClick={() => mutation.reset()}>reset</button>
83
- <button onClick={() => mutation.mutate()}>mutate</button>
84
- </div>
85
- )
86
- }
87
-
88
- const rendered = render(() => (
89
- <QueryClientProvider client={queryClient}>
90
- <Page />
91
- </QueryClientProvider>
92
- ))
93
-
94
- await waitFor(() => {
95
- expect(rendered.queryByRole('heading')).toBeNull()
96
- })
97
-
98
- fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
99
-
100
- await waitFor(() => {
101
- expect(rendered.getByRole('heading').textContent).toBe(
102
- 'Expected mock error. All is well!',
103
- )
104
- })
105
-
106
- fireEvent.click(rendered.getByRole('button', { name: /reset/i }))
107
-
108
- await waitFor(() => {
109
- expect(rendered.queryByRole('heading')).toBeNull()
110
- })
111
-
112
- consoleMock.mockRestore()
113
- })
114
-
115
- it('should be able to call `onSuccess` and `onSettled` after each successful mutate', async () => {
116
- const [count, setCount] = createSignal(0)
117
- const onSuccessMock = vi.fn()
118
- const onSettledMock = vi.fn()
119
-
120
- function Page() {
121
- const mutation = createMutation(() => ({
122
- mutationFn: (vars: { count: number }) => Promise.resolve(vars.count),
123
- onSuccess: (data) => {
124
- onSuccessMock(data)
125
- },
126
- onSettled: (data) => {
127
- onSettledMock(data)
128
- },
129
- }))
130
-
131
- return (
132
- <div>
133
- <h1>{count()}</h1>
134
- <button
135
- onClick={() => {
136
- setCount((c) => c + 1)
137
- return mutation.mutate({ count: count() })
138
- }}
139
- >
140
- mutate
141
- </button>
142
- </div>
143
- )
144
- }
145
-
146
- const rendered = render(() => (
147
- <QueryClientProvider client={queryClient}>
148
- <Page />
149
- </QueryClientProvider>
150
- ))
151
-
152
- expect(rendered.getByRole('heading').textContent).toBe('0')
153
-
154
- fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
155
- fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
156
- fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
157
-
158
- await waitFor(() => {
159
- expect(rendered.getByRole('heading').textContent).toBe('3')
160
- })
161
-
162
- await waitFor(() => {
163
- expect(onSuccessMock).toHaveBeenCalledTimes(3)
164
- })
165
-
166
- expect(onSuccessMock).toHaveBeenCalledWith(1)
167
- expect(onSuccessMock).toHaveBeenCalledWith(2)
168
- expect(onSuccessMock).toHaveBeenCalledWith(3)
169
-
170
- await waitFor(() => {
171
- expect(onSettledMock).toHaveBeenCalledTimes(3)
172
- })
173
-
174
- expect(onSettledMock).toHaveBeenCalledWith(1)
175
- expect(onSettledMock).toHaveBeenCalledWith(2)
176
- expect(onSettledMock).toHaveBeenCalledWith(3)
177
- })
178
-
179
- it('should set correct values for `failureReason` and `failureCount` on multiple mutate calls', async () => {
180
- const [count, setCount] = createSignal(0)
181
- type Value = { count: number }
182
-
183
- const mutateFn = vi.fn<(value: Value) => Promise<Value>>()
184
-
185
- mutateFn.mockImplementationOnce(() => {
186
- return Promise.reject(new Error('Error test Jonas'))
187
- })
188
-
189
- mutateFn.mockImplementation(async (value) => {
190
- await sleep(10)
191
- return Promise.resolve(value)
192
- })
193
-
194
- function Page() {
195
- const mutation = createMutation(() => ({
196
- mutationFn: mutateFn,
197
- }))
198
-
199
- return (
200
- <div>
201
- <h1>Data {mutation.data?.count}</h1>
202
- <h2>Status {mutation.status}</h2>
203
- <h2>Failed {mutation.failureCount} times</h2>
204
- <h2>Failed because {mutation.failureReason?.message ?? 'null'}</h2>
205
- <button
206
- onClick={() => {
207
- setCount((c) => c + 1)
208
- return mutation.mutate({ count: count() })
209
- }}
210
- >
211
- mutate
212
- </button>
213
- </div>
214
- )
215
- }
216
-
217
- const rendered = render(() => (
218
- <QueryClientProvider client={queryClient}>
219
- <Page />
220
- </QueryClientProvider>
221
- ))
222
-
223
- await waitFor(() => rendered.getByText('Data'))
224
-
225
- fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
226
- await waitFor(() => rendered.getByText('Data'))
227
- await waitFor(() => rendered.getByText('Status error'))
228
- await waitFor(() => rendered.getByText('Failed 1 times'))
229
- await waitFor(() => rendered.getByText('Failed because Error test Jonas'))
230
-
231
- fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
232
- await waitFor(() => rendered.getByText('Status pending'))
233
- await waitFor(() => rendered.getByText('Status success'))
234
- await waitFor(() => rendered.getByText('Data 2'))
235
- await waitFor(() => rendered.getByText('Failed 0 times'))
236
- await waitFor(() => rendered.getByText('Failed because null'))
237
- })
238
-
239
- it('should be able to call `onError` and `onSettled` after each failed mutate', async () => {
240
- const onErrorMock = vi.fn()
241
- const onSettledMock = vi.fn()
242
- const [count, setCount] = createSignal(0)
243
-
244
- function Page() {
245
- const mutation = createMutation(() => ({
246
- mutationFn: (vars: { count: number }) => {
247
- const error = new Error(
248
- `Expected mock error. All is well! ${vars.count}`,
249
- )
250
- error.stack = ''
251
- return Promise.reject(error)
252
- },
253
- onError: (error: Error) => {
254
- onErrorMock(error.message)
255
- },
256
- onSettled: (_data, error) => {
257
- onSettledMock(error?.message)
258
- },
259
- }))
260
-
261
- return (
262
- <div>
263
- <h1>{count()}</h1>
264
- <button
265
- onClick={() => {
266
- setCount((c) => c + 1)
267
- return mutation.mutate({ count: count() })
268
- }}
269
- >
270
- mutate
271
- </button>
272
- </div>
273
- )
274
- }
275
-
276
- const rendered = render(() => (
277
- <QueryClientProvider client={queryClient}>
278
- <Page />
279
- </QueryClientProvider>
280
- ))
281
-
282
- expect(rendered.getByRole('heading').textContent).toBe('0')
283
-
284
- fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
285
- fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
286
- fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
287
-
288
- await waitFor(() => {
289
- expect(rendered.getByRole('heading').textContent).toBe('3')
290
- })
291
-
292
- await waitFor(() => {
293
- expect(onErrorMock).toHaveBeenCalledTimes(3)
294
- })
295
- expect(onErrorMock).toHaveBeenCalledWith(
296
- 'Expected mock error. All is well! 1',
297
- )
298
- expect(onErrorMock).toHaveBeenCalledWith(
299
- 'Expected mock error. All is well! 2',
300
- )
301
- expect(onErrorMock).toHaveBeenCalledWith(
302
- 'Expected mock error. All is well! 3',
303
- )
304
-
305
- await waitFor(() => {
306
- expect(onSettledMock).toHaveBeenCalledTimes(3)
307
- })
308
- expect(onSettledMock).toHaveBeenCalledWith(
309
- 'Expected mock error. All is well! 1',
310
- )
311
- expect(onSettledMock).toHaveBeenCalledWith(
312
- 'Expected mock error. All is well! 2',
313
- )
314
- expect(onSettledMock).toHaveBeenCalledWith(
315
- 'Expected mock error. All is well! 3',
316
- )
317
- })
318
-
319
- it('should be able to override the useMutation success callbacks', async () => {
320
- const callbacks: Array<string> = []
321
-
322
- function Page() {
323
- const mutation = createMutation(() => ({
324
- mutationFn: async (text: string) => text,
325
- onSuccess: async () => {
326
- callbacks.push('useMutation.onSuccess')
327
- },
328
- onSettled: async () => {
329
- callbacks.push('useMutation.onSettled')
330
- },
331
- }))
332
-
333
- createEffect(() => {
334
- const { mutateAsync } = mutation
335
- setActTimeout(async () => {
336
- try {
337
- const result = await mutateAsync('todo', {
338
- onSuccess: async () => {
339
- callbacks.push('mutateAsync.onSuccess')
340
- },
341
- onSettled: async () => {
342
- callbacks.push('mutateAsync.onSettled')
343
- },
344
- })
345
- callbacks.push(`mutateAsync.result:${result}`)
346
- } catch {}
347
- }, 10)
348
- })
349
-
350
- return null
351
- }
352
-
353
- render(() => (
354
- <QueryClientProvider client={queryClient}>
355
- <Page />
356
- </QueryClientProvider>
357
- ))
358
-
359
- await sleep(100)
360
-
361
- expect(callbacks).toEqual([
362
- 'useMutation.onSuccess',
363
- 'useMutation.onSettled',
364
- 'mutateAsync.onSuccess',
365
- 'mutateAsync.onSettled',
366
- 'mutateAsync.result:todo',
367
- ])
368
- })
369
-
370
- it('should be able to override the error callbacks when using mutateAsync', async () => {
371
- const callbacks: Array<string> = []
372
-
373
- function Page() {
374
- const mutation = createMutation(() => ({
375
- mutationFn: async (_text: string) => Promise.reject(new Error('oops')),
376
-
377
- onError: async () => {
378
- callbacks.push('useMutation.onError')
379
- },
380
- onSettled: async () => {
381
- callbacks.push('useMutation.onSettled')
382
- },
383
- }))
384
-
385
- createEffect(() => {
386
- const { mutateAsync } = mutation
387
- setActTimeout(async () => {
388
- try {
389
- await mutateAsync('todo', {
390
- onError: async () => {
391
- callbacks.push('mutateAsync.onError')
392
- },
393
- onSettled: async () => {
394
- callbacks.push('mutateAsync.onSettled')
395
- },
396
- })
397
- } catch (error) {
398
- callbacks.push(`mutateAsync.error:${(error as Error).message}`)
399
- }
400
- }, 10)
401
- })
402
-
403
- return null
404
- }
405
-
406
- render(() => (
407
- <QueryClientProvider client={queryClient}>
408
- <Page />
409
- </QueryClientProvider>
410
- ))
411
-
412
- await sleep(100)
413
-
414
- expect(callbacks).toEqual([
415
- 'useMutation.onError',
416
- 'useMutation.onSettled',
417
- 'mutateAsync.onError',
418
- 'mutateAsync.onSettled',
419
- 'mutateAsync.error:oops',
420
- ])
421
- })
422
-
423
- it('should be able to use mutation defaults', async () => {
424
- const key = queryKey()
425
-
426
- queryClient.setMutationDefaults(key, {
427
- mutationFn: async (text: string) => {
428
- await sleep(10)
429
- return text
430
- },
431
- })
432
-
433
- const states: Array<CreateMutationResult<any, any, any, any>> = []
434
-
435
- function Page() {
436
- const mutation = createMutation<string, unknown, string>(() => ({
437
- mutationKey: key,
438
- }))
439
-
440
- createRenderEffect(() => {
441
- states.push({ ...mutation })
442
- })
443
-
444
- createEffect(() => {
445
- const { mutate } = mutation
446
- setActTimeout(() => {
447
- mutate('todo')
448
- }, 10)
449
- })
450
-
451
- return null
452
- }
453
-
454
- render(() => (
455
- <QueryClientProvider client={queryClient}>
456
- <Page />
457
- </QueryClientProvider>
458
- ))
459
-
460
- await sleep(100)
461
-
462
- expect(states.length).toBe(3)
463
- expect(states[0]).toMatchObject({ data: undefined, isPending: false })
464
- expect(states[1]).toMatchObject({ data: undefined, isPending: true })
465
- expect(states[2]).toMatchObject({ data: 'todo', isPending: false })
466
- })
467
-
468
- it('should be able to retry a failed mutation', async () => {
469
- let count = 0
470
-
471
- function Page() {
472
- const mutation = createMutation(() => ({
473
- mutationFn: (_text: string) => {
474
- count++
475
- return Promise.reject(new Error('oops'))
476
- },
477
- retry: 1,
478
- retryDelay: 5,
479
- }))
480
-
481
- createEffect(() => {
482
- const { mutate } = mutation
483
- setActTimeout(() => {
484
- mutate('todo')
485
- }, 10)
486
- })
487
-
488
- return null
489
- }
490
-
491
- render(() => (
492
- <QueryClientProvider client={queryClient}>
493
- <Page />
494
- </QueryClientProvider>
495
- ))
496
-
497
- await sleep(100)
498
-
499
- expect(count).toBe(2)
500
- })
501
-
502
- it('should not retry mutations while offline', async () => {
503
- const onlineMock = mockOnlineManagerIsOnline(false)
504
-
505
- let count = 0
506
-
507
- function Page() {
508
- const mutation = createMutation(() => ({
509
- mutationFn: (_text: string) => {
510
- count++
511
- return Promise.reject(new Error('oops'))
512
- },
513
- retry: 1,
514
- retryDelay: 5,
515
- }))
516
-
517
- return (
518
- <div>
519
- <button onClick={() => mutation.mutate('todo')}>mutate</button>
520
- <div>
521
- {`error: ${
522
- mutation.error instanceof Error ? mutation.error.message : 'null'
523
- }, status: ${mutation.status}, isPaused: ${String(
524
- mutation.isPaused,
525
- )}`}
526
- </div>
527
- </div>
528
- )
529
- }
530
-
531
- const rendered = render(() => (
532
- <QueryClientProvider client={queryClient}>
533
- <Page />
534
- </QueryClientProvider>
535
- ))
536
-
537
- await waitFor(() => {
538
- expect(
539
- rendered.getByText('error: null, status: idle, isPaused: false'),
540
- ).toBeInTheDocument()
541
- })
542
-
543
- window.dispatchEvent(new Event('offline'))
544
-
545
- fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
546
-
547
- await waitFor(() => {
548
- expect(
549
- rendered.getByText('error: null, status: pending, isPaused: true'),
550
- ).toBeInTheDocument()
551
- })
552
-
553
- expect(count).toBe(0)
554
-
555
- onlineMock.mockRestore()
556
- window.dispatchEvent(new Event('online'))
557
-
558
- await sleep(100)
559
-
560
- await waitFor(() => {
561
- expect(
562
- rendered.getByText('error: oops, status: error, isPaused: false'),
563
- ).toBeInTheDocument()
564
- })
565
-
566
- expect(count).toBe(2)
567
- })
568
-
569
- it('should call onMutate even if paused', async () => {
570
- const onlineMock = mockOnlineManagerIsOnline(false)
571
- const onMutate = vi.fn()
572
- let count = 0
573
-
574
- function Page() {
575
- const mutation = createMutation(() => ({
576
- mutationFn: async (_text: string) => {
577
- count++
578
- await sleep(10)
579
- return count
580
- },
581
- onMutate,
582
- }))
583
-
584
- return (
585
- <div>
586
- <button onClick={() => mutation.mutate('todo')}>mutate</button>
587
- <div>
588
- data: {mutation.data ?? 'null'}, status: {mutation.status},
589
- isPaused: {String(mutation.isPaused)}
590
- </div>
591
- </div>
592
- )
593
- }
594
-
595
- const rendered = render(() => (
596
- <QueryClientProvider client={queryClient}>
597
- <Page />
598
- </QueryClientProvider>
599
- ))
600
-
601
- await rendered.findByText('data: null, status: idle, isPaused: false')
602
-
603
- window.dispatchEvent(new Event('offline'))
604
-
605
- fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
606
-
607
- await rendered.findByText('data: null, status: pending, isPaused: true')
608
-
609
- expect(onMutate).toHaveBeenCalledTimes(1)
610
- expect(onMutate).toHaveBeenCalledWith('todo')
611
-
612
- onlineMock.mockRestore()
613
- window.dispatchEvent(new Event('online'))
614
-
615
- await rendered.findByText('data: 1, status: success, isPaused: false')
616
-
617
- expect(onMutate).toHaveBeenCalledTimes(1)
618
- expect(count).toBe(1)
619
- })
620
-
621
- it('should optimistically go to paused state if offline', async () => {
622
- const onlineMock = mockOnlineManagerIsOnline(false)
623
- let count = 0
624
- const states: Array<string> = []
625
-
626
- function Page() {
627
- const mutation = createMutation(() => ({
628
- mutationFn: async (_text: string) => {
629
- count++
630
- await sleep(10)
631
- return count
632
- },
633
- }))
634
-
635
- createRenderEffect(() => {
636
- states.push(`${mutation.status}, ${mutation.isPaused}`)
637
- })
638
-
639
- return (
640
- <div>
641
- <button onClick={() => mutation.mutate('todo')}>mutate</button>
642
- <div>
643
- data: {mutation.data ?? 'null'}, status: {mutation.status},
644
- isPaused: {String(mutation.isPaused)}
645
- </div>
646
- </div>
647
- )
648
- }
649
-
650
- const rendered = render(() => (
651
- <QueryClientProvider client={queryClient}>
652
- <Page />
653
- </QueryClientProvider>
654
- ))
655
-
656
- await rendered.findByText('data: null, status: idle, isPaused: false')
657
-
658
- fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
659
-
660
- await rendered.findByText('data: null, status: pending, isPaused: true')
661
-
662
- // no intermediate 'pending, false' state is expected because we don't start mutating!
663
- expect(states[0]).toBe('idle, false')
664
- expect(states[1]).toBe('pending, true')
665
-
666
- onlineMock.mockReturnValue(true)
667
- window.dispatchEvent(new Event('online'))
668
-
669
- await rendered.findByText('data: 1, status: success, isPaused: false')
670
-
671
- onlineMock.mockRestore()
672
- })
673
-
674
- it('should be able to retry a mutation when online', async () => {
675
- const onlineMock = mockOnlineManagerIsOnline(false)
676
-
677
- let count = 0
678
- const states: Array<CreateMutationResult<any, any, any, any>> = []
679
-
680
- function Page() {
681
- const mutation = createMutation(() => ({
682
- mutationFn: async (_text: string) => {
683
- await sleep(1)
684
- count++
685
- return count > 1
686
- ? Promise.resolve('data')
687
- : Promise.reject(new Error('oops'))
688
- },
689
- retry: 1,
690
- retryDelay: 5,
691
- networkMode: 'offlineFirst',
692
- }))
693
-
694
- createRenderEffect(() => {
695
- states.push({ ...mutation })
696
- })
697
-
698
- createEffect(() => {
699
- const { mutate } = mutation
700
- setActTimeout(() => {
701
- window.dispatchEvent(new Event('offline'))
702
- mutate('todo')
703
- }, 10)
704
- })
705
-
706
- return null
707
- }
708
-
709
- render(() => (
710
- <QueryClientProvider client={queryClient}>
711
- <Page />
712
- </QueryClientProvider>
713
- ))
714
-
715
- await sleep(50)
716
-
717
- expect(states.length).toBe(4)
718
- expect(states[0]).toMatchObject({
719
- isPending: false,
720
- isPaused: false,
721
- failureCount: 0,
722
- failureReason: null,
723
- })
724
- expect(states[1]).toMatchObject({
725
- isPending: true,
726
- isPaused: false,
727
- failureCount: 0,
728
- failureReason: null,
729
- })
730
- expect(states[2]).toMatchObject({
731
- isPending: true,
732
- isPaused: false,
733
- failureCount: 1,
734
- failureReason: new Error('oops'),
735
- })
736
- expect(states[3]).toMatchObject({
737
- isPending: true,
738
- isPaused: true,
739
- failureCount: 1,
740
- failureReason: new Error('oops'),
741
- })
742
-
743
- onlineMock.mockRestore()
744
- window.dispatchEvent(new Event('online'))
745
-
746
- await sleep(50)
747
-
748
- expect(states.length).toBe(6)
749
- expect(states[4]).toMatchObject({
750
- isPending: true,
751
- isPaused: false,
752
- failureCount: 1,
753
- failureReason: new Error('oops'),
754
- })
755
- expect(states[5]).toMatchObject({
756
- isPending: false,
757
- isPaused: false,
758
- failureCount: 0,
759
- failureReason: null,
760
- data: 'data',
761
- })
762
- })
763
-
764
- it('should not change state if unmounted', async () => {
765
- function Mutates() {
766
- const mutation = createMutation(() => ({ mutationFn: () => sleep(10) }))
767
- return <button onClick={() => mutation.mutate()}>mutate</button>
768
- }
769
- function Page() {
770
- const [mounted, setMounted] = createSignal(true)
771
- return (
772
- <div>
773
- <button onClick={() => setMounted(false)}>unmount</button>
774
- {mounted() && <Mutates />}
775
- </div>
776
- )
777
- }
778
-
779
- const rendered = render(() => (
780
- <QueryClientProvider client={queryClient}>
781
- <Page />
782
- </QueryClientProvider>
783
- ))
784
- fireEvent.click(rendered.getByText('mutate'))
785
- fireEvent.click(rendered.getByText('unmount'))
786
- })
787
-
788
- it('should be able to throw an error when throwOnError is set to true', async () => {
789
- const consoleMock = vi
790
- .spyOn(console, 'error')
791
- .mockImplementation(() => undefined)
792
-
793
- function Page() {
794
- const mutation = createMutation<string, Error>(() => ({
795
- mutationFn: () => {
796
- const err = new Error('Expected mock error. All is well!')
797
- err.stack = ''
798
- return Promise.reject(err)
799
- },
800
- throwOnError: true,
801
- }))
802
-
803
- return (
804
- <div>
805
- <button onClick={() => mutation.mutate()}>mutate</button>
806
- </div>
807
- )
808
- }
809
-
810
- const rendered = render(() => (
811
- <QueryClientProvider client={queryClient}>
812
- <ErrorBoundary
813
- fallback={() => (
814
- <div>
815
- <span>error</span>
816
- </div>
817
- )}
818
- >
819
- <Page />
820
- </ErrorBoundary>
821
- </QueryClientProvider>
822
- ))
823
-
824
- fireEvent.click(rendered.getByText('mutate'))
825
-
826
- await waitFor(() => {
827
- expect(rendered.queryByText('error')).not.toBeNull()
828
- })
829
-
830
- consoleMock.mockRestore()
831
- })
832
-
833
- it('should be able to throw an error when throwOnError is a function that returns true', async () => {
834
- const consoleMock = vi
835
- .spyOn(console, 'error')
836
- .mockImplementation(() => undefined)
837
-
838
- let boundary = false
839
- function Page() {
840
- const mutation = createMutation<string, Error>(() => ({
841
- mutationFn: () => {
842
- const err = new Error('mock error')
843
- err.stack = ''
844
- return Promise.reject(err)
845
- },
846
- throwOnError: () => {
847
- boundary = !boundary
848
- return !boundary
849
- },
850
- }))
851
-
852
- return (
853
- <div>
854
- <button onClick={() => mutation.mutate()}>mutate</button>
855
- {mutation.error && mutation.error.message}
856
- </div>
857
- )
858
- }
859
-
860
- const rendered = render(() => (
861
- <QueryClientProvider client={queryClient}>
862
- <ErrorBoundary
863
- fallback={() => (
864
- <div>
865
- <span>error boundary</span>
866
- </div>
867
- )}
868
- >
869
- <Page />
870
- </ErrorBoundary>
871
- </QueryClientProvider>
872
- ))
873
-
874
- // first error goes to component
875
- fireEvent.click(rendered.getByText('mutate'))
876
- await waitFor(() => {
877
- expect(rendered.queryByText('mock error')).not.toBeNull()
878
- })
879
-
880
- // second error goes to boundary
881
- fireEvent.click(rendered.getByText('mutate'))
882
- await waitFor(() => {
883
- expect(rendered.queryByText('error boundary')).not.toBeNull()
884
- })
885
-
886
- consoleMock.mockRestore()
887
- })
888
-
889
- it('should pass meta to mutation', async () => {
890
- const errorMock = vi.fn()
891
- const successMock = vi.fn()
892
-
893
- const queryClientMutationMeta = createQueryClient({
894
- mutationCache: new MutationCache({
895
- onSuccess: (_, __, ___, mutation) => {
896
- successMock(mutation.meta?.metaSuccessMessage)
897
- },
898
- onError: (_, __, ___, mutation) => {
899
- errorMock(mutation.meta?.metaErrorMessage)
900
- },
901
- }),
902
- })
903
-
904
- const metaSuccessMessage = 'mutation succeeded'
905
- const metaErrorMessage = 'mutation failed'
906
-
907
- function Page() {
908
- const mutationSucceed = createMutation(() => ({
909
- mutationFn: async () => '',
910
- meta: { metaSuccessMessage },
911
- }))
912
- const mutationError = createMutation(() => ({
913
- mutationFn: async () => {
914
- throw new Error('')
915
- },
916
- meta: { metaErrorMessage },
917
- }))
918
-
919
- return (
920
- <div>
921
- <button onClick={() => mutationSucceed.mutate()}>succeed</button>
922
- <button onClick={() => mutationError.mutate()}>error</button>
923
- {mutationSucceed.isSuccess && <div>successTest</div>}
924
- {mutationError.isError && <div>errorTest</div>}
925
- </div>
926
- )
927
- }
928
-
929
- const rendered = render(() => (
930
- <QueryClientProvider client={queryClientMutationMeta}>
931
- <Page />
932
- </QueryClientProvider>
933
- ))
934
-
935
- fireEvent.click(rendered.getByText('succeed'))
936
- fireEvent.click(rendered.getByText('error'))
937
-
938
- await waitFor(() => {
939
- expect(rendered.queryByText('successTest')).not.toBeNull()
940
- expect(rendered.queryByText('errorTest')).not.toBeNull()
941
- })
942
-
943
- expect(successMock).toHaveBeenCalledTimes(1)
944
- expect(successMock).toHaveBeenCalledWith(metaSuccessMessage)
945
- expect(errorMock).toHaveBeenCalledTimes(1)
946
- expect(errorMock).toHaveBeenCalledWith(metaErrorMessage)
947
- })
948
-
949
- it('should call cache callbacks when unmounted', async () => {
950
- const onSuccess = vi.fn()
951
- const onSuccessMutate = vi.fn()
952
- const onSettled = vi.fn()
953
- const onSettledMutate = vi.fn()
954
- const mutationKey = queryKey()
955
- let count = 0
956
-
957
- function Page() {
958
- const [show, setShow] = createSignal(true)
959
- return (
960
- <div>
961
- <button onClick={() => setShow(false)}>hide</button>
962
- {show() && <Component />}
963
- </div>
964
- )
965
- }
966
-
967
- function Component() {
968
- const mutation = createMutation(() => ({
969
- mutationFn: async (_text: string) => {
970
- count++
971
- await sleep(10)
972
- return count
973
- },
974
- mutationKey: mutationKey,
975
- gcTime: 0,
976
- onSuccess,
977
- onSettled,
978
- }))
979
-
980
- return (
981
- <div>
982
- <button
983
- onClick={() =>
984
- mutation.mutate('todo', {
985
- onSuccess: onSuccessMutate,
986
- onSettled: onSettledMutate,
987
- })
988
- }
989
- >
990
- mutate
991
- </button>
992
- <div>
993
- data: {mutation.data ?? 'null'}, status: {mutation.status},
994
- isPaused: {String(mutation.isPaused)}
995
- </div>
996
- </div>
997
- )
998
- }
999
-
1000
- const rendered = render(() => (
1001
- <QueryClientProvider client={queryClient}>
1002
- <Page />
1003
- </QueryClientProvider>
1004
- ))
1005
-
1006
- await rendered.findByText('data: null, status: idle, isPaused: false')
1007
-
1008
- fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
1009
- fireEvent.click(rendered.getByRole('button', { name: /hide/i }))
1010
-
1011
- await waitFor(() => {
1012
- expect(
1013
- queryClient.getMutationCache().findAll({ mutationKey: mutationKey }),
1014
- ).toHaveLength(0)
1015
- })
1016
-
1017
- expect(count).toBe(1)
1018
-
1019
- expect(onSuccess).toHaveBeenCalledTimes(1)
1020
- expect(onSettled).toHaveBeenCalledTimes(1)
1021
- expect(onSuccessMutate).toHaveBeenCalledTimes(0)
1022
- expect(onSettledMutate).toHaveBeenCalledTimes(0)
1023
- })
1024
-
1025
- it('should call mutate callbacks only for the last observer', async () => {
1026
- const onSuccess = vi.fn()
1027
- const onSuccessMutate = vi.fn()
1028
- const onSettled = vi.fn()
1029
- const onSettledMutate = vi.fn()
1030
- let count = 0
1031
-
1032
- function Page() {
1033
- const mutation = createMutation(() => ({
1034
- mutationFn: async (_text: string) => {
1035
- count++
1036
- await sleep(10)
1037
- return `result${count}`
1038
- },
1039
- onSuccess,
1040
- onSettled,
1041
- }))
1042
-
1043
- return (
1044
- <div>
1045
- <button
1046
- onClick={() =>
1047
- mutation.mutate('todo', {
1048
- onSuccess: onSuccessMutate,
1049
- onSettled: onSettledMutate,
1050
- })
1051
- }
1052
- >
1053
- mutate
1054
- </button>
1055
- <div>
1056
- data: {mutation.data ?? 'null'}, status: {mutation.status}
1057
- </div>
1058
- </div>
1059
- )
1060
- }
1061
-
1062
- const rendered = render(() => (
1063
- <QueryClientProvider client={queryClient}>
1064
- <Page />
1065
- </QueryClientProvider>
1066
- ))
1067
-
1068
- await rendered.findByText('data: null, status: idle')
1069
-
1070
- fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
1071
- fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
1072
-
1073
- await rendered.findByText('data: result2, status: success')
1074
-
1075
- expect(count).toBe(2)
1076
-
1077
- expect(onSuccess).toHaveBeenCalledTimes(2)
1078
- expect(onSettled).toHaveBeenCalledTimes(2)
1079
- expect(onSuccessMutate).toHaveBeenCalledTimes(1)
1080
- expect(onSuccessMutate).toHaveBeenCalledWith('result2', 'todo', undefined)
1081
- expect(onSettledMutate).toHaveBeenCalledTimes(1)
1082
- expect(onSettledMutate).toHaveBeenCalledWith(
1083
- 'result2',
1084
- null,
1085
- 'todo',
1086
- undefined,
1087
- )
1088
- })
1089
-
1090
- it('should go to error state if onSuccess callback errors', async () => {
1091
- const error = new Error('error from onSuccess')
1092
- const onError = vi.fn()
1093
-
1094
- function Page() {
1095
- const mutation = createMutation(() => ({
1096
- mutationFn: async (_text: string) => {
1097
- await sleep(10)
1098
- return 'result'
1099
- },
1100
- onSuccess: () => Promise.reject(error),
1101
- onError,
1102
- }))
1103
-
1104
- return (
1105
- <div>
1106
- <button onClick={() => mutation.mutate('todo')}>mutate</button>
1107
- <div>status: {mutation.status}</div>
1108
- </div>
1109
- )
1110
- }
1111
-
1112
- const rendered = render(() => (
1113
- <QueryClientProvider client={queryClient}>
1114
- <Page />
1115
- </QueryClientProvider>
1116
- ))
1117
-
1118
- await rendered.findByText('status: idle')
1119
-
1120
- rendered.getByRole('button', { name: /mutate/i }).click()
1121
-
1122
- await rendered.findByText('status: error')
1123
-
1124
- expect(onError).toHaveBeenCalledWith(error, 'todo', undefined)
1125
- })
1126
-
1127
- it('should go to error state if onError callback errors', async () => {
1128
- const error = new Error('error from onError')
1129
- const mutateFnError = new Error('mutateFnError')
1130
-
1131
- function Page() {
1132
- const mutation = createMutation(() => ({
1133
- mutationFn: async (_text: string) => {
1134
- await sleep(10)
1135
- throw mutateFnError
1136
- },
1137
- onError: () => Promise.reject(error),
1138
- }))
1139
-
1140
- return (
1141
- <div>
1142
- <button onClick={() => mutation.mutate('todo')}>mutate</button>
1143
- <div>
1144
- error:{' '}
1145
- {mutation.error instanceof Error ? mutation.error.message : 'null'},
1146
- status: {mutation.status}
1147
- </div>
1148
- </div>
1149
- )
1150
- }
1151
-
1152
- const rendered = render(() => (
1153
- <QueryClientProvider client={queryClient}>
1154
- <Page />
1155
- </QueryClientProvider>
1156
- ))
1157
-
1158
- await rendered.findByText('error: null, status: idle')
1159
-
1160
- rendered.getByRole('button', { name: /mutate/i }).click()
1161
-
1162
- await rendered.findByText('error: mutateFnError, status: error')
1163
- })
1164
-
1165
- it('should go to error state if onSettled callback errors', async () => {
1166
- const error = new Error('error from onSettled')
1167
- const mutateFnError = new Error('mutateFnError')
1168
- const onError = vi.fn()
1169
-
1170
- function Page() {
1171
- const mutation = createMutation(() => ({
1172
- mutationFn: async (_text: string) => {
1173
- await sleep(10)
1174
- throw mutateFnError
1175
- },
1176
- onSettled: () => Promise.reject(error),
1177
- onError,
1178
- }))
1179
-
1180
- return (
1181
- <div>
1182
- <button onClick={() => mutation.mutate('todo')}>mutate</button>
1183
- <div>
1184
- error:{' '}
1185
- {mutation.error instanceof Error ? mutation.error.message : 'null'},
1186
- status: {mutation.status}
1187
- </div>
1188
- </div>
1189
- )
1190
- }
1191
-
1192
- const rendered = render(() => (
1193
- <QueryClientProvider client={queryClient}>
1194
- <Page />
1195
- </QueryClientProvider>
1196
- ))
1197
-
1198
- await rendered.findByText('error: null, status: idle')
1199
-
1200
- rendered.getByRole('button', { name: /mutate/i }).click()
1201
-
1202
- await rendered.findByText('error: mutateFnError, status: error')
1203
-
1204
- expect(onError).toHaveBeenCalledWith(mutateFnError, 'todo', undefined)
1205
- })
1206
-
1207
- it('should use provided custom queryClient', async () => {
1208
- function Page() {
1209
- const mutation = createMutation(
1210
- () => ({
1211
- mutationFn: async (text: string) => {
1212
- return Promise.resolve(text)
1213
- },
1214
- }),
1215
- () => queryClient,
1216
- )
1217
-
1218
- return (
1219
- <div>
1220
- <button onClick={() => mutation.mutate('custom client')}>
1221
- mutate
1222
- </button>
1223
- <div>
1224
- data: {mutation.data ?? 'null'}, status: {mutation.status}
1225
- </div>
1226
- </div>
1227
- )
1228
- }
1229
-
1230
- const rendered = render(() => <Page></Page>)
1231
-
1232
- await rendered.findByText('data: null, status: idle')
1233
-
1234
- fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
1235
-
1236
- await rendered.findByText('data: custom client, status: success')
1237
- })
1238
- })