@geekmidas/client 0.3.0 → 0.5.0

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.
Files changed (80) hide show
  1. package/dist/auth-fetcher.cjs +2 -3
  2. package/dist/auth-fetcher.cjs.map +1 -1
  3. package/dist/auth-fetcher.d.cts +2 -1
  4. package/dist/auth-fetcher.d.cts.map +1 -0
  5. package/dist/auth-fetcher.d.mts +2 -1
  6. package/dist/auth-fetcher.d.mts.map +1 -0
  7. package/dist/auth-fetcher.mjs +2 -3
  8. package/dist/auth-fetcher.mjs.map +1 -1
  9. package/dist/endpoint-hooks.cjs +10 -5
  10. package/dist/endpoint-hooks.cjs.map +1 -1
  11. package/dist/endpoint-hooks.d.cts +2 -2
  12. package/dist/endpoint-hooks.d.cts.map +1 -0
  13. package/dist/endpoint-hooks.d.mts +2 -2
  14. package/dist/endpoint-hooks.d.mts.map +1 -0
  15. package/dist/endpoint-hooks.mjs +10 -5
  16. package/dist/endpoint-hooks.mjs.map +1 -1
  17. package/dist/{fetcher-DLDD_7Sa.mjs → fetcher-5fnBE7Dk.mjs} +2 -2
  18. package/dist/fetcher-5fnBE7Dk.mjs.map +1 -0
  19. package/dist/{fetcher-KdwHgdAl.cjs → fetcher-CgLEaIAC.cjs} +2 -2
  20. package/dist/fetcher-CgLEaIAC.cjs.map +1 -0
  21. package/dist/fetcher.cjs +1 -1
  22. package/dist/fetcher.d.cts +3 -1
  23. package/dist/fetcher.d.cts.map +1 -0
  24. package/dist/fetcher.d.mts +3 -1
  25. package/dist/fetcher.d.mts.map +1 -0
  26. package/dist/fetcher.mjs +1 -1
  27. package/dist/infer.d.cts.map +1 -0
  28. package/dist/infer.d.mts.map +1 -0
  29. package/dist/openapi-hooks.cjs +3 -5
  30. package/dist/openapi-hooks.cjs.map +1 -1
  31. package/dist/openapi-hooks.d.cts +3 -3
  32. package/dist/openapi-hooks.d.cts.map +1 -0
  33. package/dist/openapi-hooks.d.mts +6 -6
  34. package/dist/openapi-hooks.d.mts.map +1 -0
  35. package/dist/openapi-hooks.mjs +3 -5
  36. package/dist/openapi-hooks.mjs.map +1 -1
  37. package/dist/openapi-types.d.cts.map +1 -0
  38. package/dist/openapi-types.d.mts.map +1 -0
  39. package/dist/openapi.cjs.map +1 -1
  40. package/dist/openapi.mjs.map +1 -1
  41. package/dist/react-query.cjs +1 -1
  42. package/dist/react-query.cjs.map +1 -1
  43. package/dist/react-query.d.cts +3 -1
  44. package/dist/react-query.d.cts.map +1 -0
  45. package/dist/react-query.d.mts +10 -8
  46. package/dist/react-query.d.mts.map +1 -0
  47. package/dist/react-query.mjs +1 -1
  48. package/dist/react-query.mjs.map +1 -1
  49. package/dist/{types-D4OSWveN.d.cts → types-4K4N-Fl1.d.cts} +8 -6
  50. package/dist/types-4K4N-Fl1.d.cts.map +1 -0
  51. package/dist/{types-Cdv1XAWr.d.mts → types-hhkZWjQn.d.mts} +8 -6
  52. package/dist/types-hhkZWjQn.d.mts.map +1 -0
  53. package/dist/types.d.cts +1 -1
  54. package/dist/types.d.mts +1 -1
  55. package/package.json +6 -5
  56. package/src/__tests__/auth-fetcher.spec.ts +505 -0
  57. package/src/__tests__/endpoint-hooks.spec.tsx +348 -348
  58. package/src/__tests__/fetcher.spec.ts +403 -403
  59. package/src/__tests__/infer.integration.spec.ts +171 -171
  60. package/src/__tests__/infer.spec.ts +182 -182
  61. package/src/__tests__/method-restrictions.spec.tsx +165 -165
  62. package/src/__tests__/openapi-hooks.spec.tsx +536 -537
  63. package/src/__tests__/react-query-infinite.spec.tsx +989 -989
  64. package/src/__tests__/react-query-invalidation.spec.tsx +223 -222
  65. package/src/__tests__/react-query.spec.tsx +567 -567
  66. package/src/__tests__/setup.ts +260 -260
  67. package/src/__tests__/types.spec.ts +127 -127
  68. package/src/__tests__/url-parsing.spec.ts +191 -191
  69. package/src/auth-fetcher.ts +160 -162
  70. package/src/endpoint-hooks.ts +151 -150
  71. package/src/fetcher.ts +163 -163
  72. package/src/infer.ts +64 -63
  73. package/src/openapi-hooks.ts +146 -146
  74. package/src/openapi-types.d.ts +428 -428
  75. package/src/openapi.json +593 -593
  76. package/src/react-query.ts +281 -281
  77. package/src/types.ts +124 -125
  78. package/tsconfig.json +12 -0
  79. package/dist/fetcher-DLDD_7Sa.mjs.map +0 -1
  80. package/dist/fetcher-KdwHgdAl.cjs.map +0 -1
@@ -3,580 +3,580 @@
3
3
  */
4
4
  import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
5
5
  import { act, renderHook, waitFor } from '@testing-library/react';
6
- import { http, HttpResponse } from 'msw';
6
+ import { HttpResponse, http } from 'msw';
7
7
  // biome-ignore lint/style/useImportType: <explanation>
8
8
  import React from 'react';
9
9
  import { describe, expect, it } from 'vitest';
10
10
  import type { paths } from '../openapi-types';
11
11
  import {
12
- createTypedQueryClient,
13
- useTypedMutation,
14
- useTypedQuery,
12
+ createTypedQueryClient,
13
+ useTypedMutation,
14
+ useTypedQuery,
15
15
  } from '../react-query';
16
16
  import './setup';
17
17
 
18
18
  describe('TypedQueryClient', () => {
19
- const queryClient = new QueryClient({
20
- defaultOptions: {
21
- queries: {
22
- retry: false,
23
- },
24
- },
25
- });
26
-
27
- const wrapper = ({ children }: { children: React.ReactNode }) => (
28
- <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
29
- );
30
-
31
- it('should create query client with correct configuration', () => {
32
- const typedClient = createTypedQueryClient<paths>({
33
- baseURL: 'https://api.example.com',
34
- headers: { Authorization: 'Bearer token' },
35
- });
36
-
37
- expect(typedClient).toBeDefined();
38
- });
39
-
40
- describe('useQuery', () => {
41
- it('should fetch data successfully', async () => {
42
- const typedClient = createTypedQueryClient<paths>({
43
- baseURL: 'https://api.example.com',
44
- });
45
-
46
- const { result } = renderHook(
47
- () =>
48
- typedClient.useQuery('GET /users/{id}', {
49
- params: { id: '123' },
50
- }),
51
- { wrapper },
52
- );
53
-
54
- expect(result.current.isLoading).toBe(true);
55
-
56
- await waitFor(() => {
57
- expect(result.current.isSuccess).toBe(true);
58
- });
59
-
60
- expect(result.current.data).toEqual({
61
- id: '123',
62
- name: 'John Doe',
63
- email: 'john@example.com',
64
- });
65
- });
66
-
67
- it('should handle query parameters', async () => {
68
- const typedClient = createTypedQueryClient<paths>({
69
- baseURL: 'https://api.example.com',
70
- });
71
-
72
- const { result } = renderHook(
73
- () =>
74
- typedClient.useQuery('GET /posts', {
75
- query: { page: 1, limit: 10, sort: 'desc' },
76
- }),
77
- { wrapper },
78
- );
79
-
80
- await waitFor(() => {
81
- expect(result.current.isSuccess).toBe(true);
82
- });
83
-
84
- expect(result.current.data).toMatchObject({
85
- posts: expect.any(Array),
86
- pagination: {
87
- page: 1,
88
- limit: 10,
89
- total: 50,
90
- },
91
- sort: 'desc',
92
- });
93
- });
94
-
95
- it('should handle errors', async () => {
96
- const typedClient = createTypedQueryClient<paths>({
97
- baseURL: 'https://api.example.com',
98
- });
99
-
100
- const { result } = renderHook(() => typedClient.useQuery('GET /error'), {
101
- wrapper,
102
- });
103
-
104
- await waitFor(() => {
105
- expect(result.current.isError).toBe(true);
106
- });
107
-
108
- expect(result.current.error).toBeDefined();
109
- });
110
-
111
- it('should pass additional query options', async () => {
112
- const typedClient = createTypedQueryClient<paths>({
113
- baseURL: 'https://api.example.com',
114
- });
115
-
116
- const { result } = renderHook(
117
- () =>
118
- typedClient.useQuery(
119
- 'GET /users/{id}',
120
- { params: { id: '123' } },
121
- {
122
- staleTime: 5 * 60 * 1000,
123
- gcTime: 10 * 60 * 1000,
124
- },
125
- ),
126
- { wrapper },
127
- );
128
-
129
- await waitFor(() => {
130
- expect(result.current.isSuccess).toBe(true);
131
- });
132
-
133
- expect(result.current.data).toBeDefined();
134
- });
135
- });
136
-
137
- describe('useMutation', () => {
138
- it('should perform mutations successfully', async () => {
139
- const typedClient = createTypedQueryClient<paths>({
140
- baseURL: 'https://api.example.com',
141
- });
142
-
143
- const { result } = renderHook(
144
- () => typedClient.useMutation('POST /users'),
145
- { wrapper },
146
- );
147
-
148
- expect(result.current.isPending).toBe(false);
149
-
150
- act(() => {
151
- result.current.mutate({
152
- body: {
153
- name: 'New User',
154
- email: 'new@example.com',
155
- },
156
- });
157
- });
158
-
159
- await waitFor(() => {
160
- expect(result.current.isSuccess).toBe(true);
161
- });
162
-
163
- expect(result.current.data).toEqual({
164
- id: '123',
165
- name: 'New User',
166
- email: 'new@example.com',
167
- });
168
- });
169
-
170
- it('should handle mutation with path parameters', async () => {
171
- const typedClient = createTypedQueryClient<paths>({
172
- baseURL: 'https://api.example.com',
173
- });
174
-
175
- const { result } = renderHook(
176
- () => typedClient.useMutation('PUT /users/{id}'),
177
- { wrapper },
178
- );
179
-
180
- act(() => {
181
- result.current.mutate({
182
- params: { id: '456' },
183
- body: {
184
- name: 'Updated User',
185
- },
186
- });
187
- });
188
-
189
- await waitFor(() => {
190
- expect(result.current.isSuccess).toBe(true);
191
- });
192
-
193
- expect(result.current.data).toMatchObject({
194
- id: '456',
195
- name: 'Updated User',
196
- });
197
- });
198
-
199
- it('should accept mutation options', async () => {
200
- const typedClient = createTypedQueryClient<paths>({
201
- baseURL: 'https://api.example.com',
202
- });
203
-
204
- let successData: any;
205
- const onSuccess = (data: any) => {
206
- successData = data;
207
- };
208
-
209
- const { result } = renderHook(
210
- () => typedClient.useMutation('POST /users', { onSuccess }),
211
- { wrapper },
212
- );
213
-
214
- act(() => {
215
- result.current.mutate({
216
- body: {
217
- name: 'Test User',
218
- email: 'test@example.com',
219
- },
220
- });
221
- });
222
-
223
- await waitFor(() => {
224
- expect(result.current.isSuccess).toBe(true);
225
- });
226
-
227
- expect(successData).toEqual({
228
- id: '123',
229
- name: 'Test User',
230
- email: 'test@example.com',
231
- });
232
- });
233
- });
234
-
235
- describe('helper hooks', () => {
236
- it('should work with useTypedQuery helper', async () => {
237
- const typedClient = createTypedQueryClient<paths>({
238
- baseURL: 'https://api.example.com',
239
- });
240
-
241
- const { result } = renderHook(
242
- () =>
243
- useTypedQuery(typedClient, 'GET /users/{id}', {
244
- params: { id: '789' },
245
- }),
246
- { wrapper },
247
- );
248
-
249
- await waitFor(() => {
250
- expect(result.current.isSuccess).toBe(true);
251
- });
252
-
253
- expect(result.current.data).toMatchObject({
254
- id: '789',
255
- name: 'John Doe',
256
- email: 'john@example.com',
257
- });
258
- });
259
-
260
- it('should work with useTypedMutation helper', async () => {
261
- const typedClient = createTypedQueryClient<paths>({
262
- baseURL: 'https://api.example.com',
263
- });
264
-
265
- const { result } = renderHook(
266
- () => useTypedMutation(typedClient, 'DELETE /users/{id}'),
267
- { wrapper },
268
- );
269
-
270
- act(() => {
271
- result.current.mutate({
272
- params: { id: '999' },
273
- });
274
- });
275
-
276
- await waitFor(() => {
277
- expect(result.current.isSuccess).toBe(true);
278
- });
279
-
280
- expect(result.current.data).toBeUndefined();
281
- });
282
- });
283
-
284
- describe('query key generation', () => {
285
- it('should generate correct query keys', async () => {
286
- const typedClient = createTypedQueryClient<paths>({
287
- baseURL: 'https://api.example.com',
288
- });
289
-
290
- // Test with path params only
291
- const { result: result1 } = renderHook(
292
- () =>
293
- typedClient.useQuery('GET /users/{id}', {
294
- params: { id: '123' },
295
- }),
296
- { wrapper },
297
- );
298
-
299
- await waitFor(() => expect(result1.current.isSuccess).toBe(true));
300
-
301
- // The query key should include the endpoint and params
302
- const queryState = queryClient.getQueryState([
303
- 'GET /users/{id}',
304
- { params: { id: '123' } },
305
- ]);
306
- expect(queryState).toBeDefined();
307
-
308
- // Test with query params
309
- const { result: result2 } = renderHook(
310
- () =>
311
- typedClient.useQuery('GET /posts', {
312
- query: { page: 1, limit: 10 },
313
- }),
314
- { wrapper },
315
- );
316
-
317
- await waitFor(() => expect(result2.current.isSuccess).toBe(true));
318
-
319
- const queryState2 = queryClient.getQueryState([
320
- 'GET /posts',
321
- { query: { page: 1, limit: 10 } },
322
- ]);
323
- expect(queryState2).toBeDefined();
324
- });
325
- });
326
-
327
- describe('error handling', () => {
328
- it('should handle 404 errors', async () => {
329
- const typedClient = createTypedQueryClient<paths>({
330
- baseURL: 'https://api.example.com',
331
- });
332
-
333
- const { result } = renderHook(
334
- () =>
335
- typedClient.useQuery('GET /users/{id}', {
336
- params: { id: '404' },
337
- }),
338
- { wrapper },
339
- );
340
-
341
- await waitFor(() => {
342
- expect(result.current.isError).toBe(true);
343
- });
344
-
345
- expect(result.current.error?.status).toBe(404);
346
- });
347
-
348
- it('should handle authorization errors', async () => {
349
- const typedClient = createTypedQueryClient<paths>({
350
- baseURL: 'https://api.example.com',
351
- });
352
-
353
- const { result } = renderHook(
354
- () => typedClient.useQuery('GET /protected'),
355
- { wrapper },
356
- );
357
-
358
- await waitFor(() => {
359
- expect(result.current.isError).toBe(true);
360
- });
361
-
362
- expect(result.current.error?.status).toBe(401);
363
- });
364
- });
365
-
366
- describe('Memoization', () => {
367
- it('should not cause unnecessary re-renders when options object is recreated with same values', async () => {
368
- const typedClient = createTypedQueryClient<paths>({
369
- baseURL: 'https://api.example.com',
370
- });
371
-
372
- // Track API calls using MSW request interception
373
- let requestCount = 0;
374
- const { server } = await import('./setup');
375
- server.use(
376
- http.get('https://api.example.com/users/:id', () => {
377
- requestCount++;
378
- return HttpResponse.json({ id: '123', name: 'John Doe' });
379
- }),
380
- );
381
-
382
- let renderCount = 0;
383
-
384
- const { rerender } = renderHook(
385
- ({ options }: { options?: { enabled?: boolean } }) => {
386
- renderCount++;
387
- return typedClient.useQuery(
388
- 'GET /users/{id}',
389
- { params: { id: '123' } },
390
- options,
391
- );
392
- },
393
- {
394
- wrapper,
395
- initialProps: { options: { enabled: true } },
396
- },
397
- );
398
-
399
- await waitFor(() => {
400
- expect(requestCount).toBe(1);
401
- });
402
-
403
- const initialRenderCount = renderCount;
404
-
405
- // Rerender with a new options object that has the same values
406
- rerender({ options: { enabled: true } });
407
-
408
- // Give React time to potentially trigger re-renders
409
- await act(async () => {
410
- await new Promise((resolve) => setTimeout(resolve, 10));
411
- });
412
-
413
- // The API should not be called again because options haven't actually changed
414
- expect(requestCount).toBe(1);
415
-
416
- // Component should re-render due to props change, but React Query shouldn't refetch
417
- expect(renderCount).toBeGreaterThan(initialRenderCount);
418
- });
419
-
420
- it('should re-fetch when options actually change', async () => {
421
- const typedClient = createTypedQueryClient<paths>({
422
- baseURL: 'https://api.example.com',
423
- });
424
-
425
- // Track API calls using MSW request interception
426
- let requestCount = 0;
427
- const { server } = await import('./setup');
428
- server.use(
429
- http.get('https://api.example.com/users/:id', () => {
430
- requestCount++;
431
- return HttpResponse.json({ id: '123', name: 'John Doe' });
432
- }),
433
- );
434
-
435
- const { rerender } = renderHook(
436
- ({ enabled }: { enabled: boolean }) =>
437
- typedClient.useQuery(
438
- 'GET /users/{id}',
439
- { params: { id: '123' } },
440
- { enabled },
441
- ),
442
- {
443
- wrapper,
444
- initialProps: { enabled: true },
445
- },
446
- );
447
-
448
- await waitFor(() => {
449
- expect(requestCount).toBe(1);
450
- });
451
-
452
- // Disable the query
453
- rerender({ enabled: false });
454
-
455
- await act(async () => {
456
- await new Promise((resolve) => setTimeout(resolve, 10));
457
- });
458
-
459
- // Should still only be called once since we disabled it
460
- expect(requestCount).toBe(1);
461
-
462
- // Re-enable the query
463
- rerender({ enabled: true });
464
-
465
- await waitFor(() => {
466
- expect(requestCount).toBe(2);
467
- });
468
- });
469
-
470
- it('should memoize mutation options correctly', async () => {
471
- const typedClient = createTypedQueryClient<paths>({
472
- baseURL: 'https://api.example.com',
473
- });
474
-
475
- // Track API calls using MSW request interception
476
- let requestCount = 0;
477
- const { server } = await import('./setup');
478
- server.use(
479
- http.post('https://api.example.com/users', async ({ request }) => {
480
- requestCount++;
481
- const body = await request.json();
482
- return HttpResponse.json({ id: '123', name: (body as any).name });
483
- }),
484
- );
485
-
486
- let renderCount = 0;
487
- let successCallCount = 0;
488
-
489
- const { result, rerender } = renderHook(
490
- ({ options }: { options?: { onSuccess?: () => void } }) => {
491
- renderCount++;
492
- return typedClient.useMutation('POST /users', options);
493
- },
494
- {
495
- wrapper,
496
- initialProps: {
497
- options: {
498
- onSuccess: () => {
499
- successCallCount++;
500
- },
501
- },
502
- },
503
- },
504
- );
505
-
506
- const initialRenderCount = renderCount;
507
-
508
- // Trigger a mutation to test the options
509
- await act(async () => {
510
- result.current.mutate({
511
- body: { name: 'Test User', email: 'test@example.com' },
512
- });
513
- });
514
-
515
- await waitFor(() => {
516
- expect(requestCount).toBe(1);
517
- expect(successCallCount).toBe(1);
518
- });
519
-
520
- // Rerender with a new options object that has the same function reference
521
- const onSuccess = () => {
522
- successCallCount++;
523
- };
524
- rerender({ options: { onSuccess } });
525
- rerender({ options: { onSuccess } });
526
-
527
- // Component should re-render due to props change
528
- expect(renderCount).toBeGreaterThan(initialRenderCount);
529
- });
530
-
531
- it('should handle config changes in useQuery correctly', async () => {
532
- const typedClient = createTypedQueryClient<paths>({
533
- baseURL: 'https://api.example.com',
534
- });
535
-
536
- // Track API calls using MSW request interception
537
- let requestCount = 0;
538
- const requestedIds: string[] = [];
539
- const { server } = await import('./setup');
540
- server.use(
541
- http.get('https://api.example.com/users/:id', ({ params }) => {
542
- requestCount++;
543
- const { id } = params;
544
- requestedIds.push(id as string);
545
- return HttpResponse.json({ id, name: `User ${id}` });
546
- }),
547
- );
548
-
549
- const { rerender } = renderHook(
550
- ({ userId }: { userId: string }) =>
551
- typedClient.useQuery('GET /users/{id}', { params: { id: userId } }),
552
- {
553
- wrapper,
554
- initialProps: { userId: '123' },
555
- },
556
- );
557
-
558
- await waitFor(() => {
559
- expect(requestCount).toBe(1);
560
- expect(requestedIds).toEqual(['123']);
561
- });
562
-
563
- // Change the user ID - should trigger a new fetch
564
- rerender({ userId: '456' });
565
-
566
- await waitFor(() => {
567
- expect(requestCount).toBe(2);
568
- expect(requestedIds).toEqual(['123', '456']);
569
- });
570
-
571
- // Call with same ID again - should not trigger new fetch due to React Query caching
572
- rerender({ userId: '456' });
573
-
574
- await act(async () => {
575
- await new Promise((resolve) => setTimeout(resolve, 10));
576
- });
577
-
578
- expect(requestCount).toBe(2);
579
- expect(requestedIds).toEqual(['123', '456']);
580
- });
581
- });
19
+ const queryClient = new QueryClient({
20
+ defaultOptions: {
21
+ queries: {
22
+ retry: false,
23
+ },
24
+ },
25
+ });
26
+
27
+ const wrapper = ({ children }: { children: React.ReactNode }) => (
28
+ <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
29
+ );
30
+
31
+ it('should create query client with correct configuration', () => {
32
+ const typedClient = createTypedQueryClient<paths>({
33
+ baseURL: 'https://api.example.com',
34
+ headers: { Authorization: 'Bearer token' },
35
+ });
36
+
37
+ expect(typedClient).toBeDefined();
38
+ });
39
+
40
+ describe('useQuery', () => {
41
+ it('should fetch data successfully', async () => {
42
+ const typedClient = createTypedQueryClient<paths>({
43
+ baseURL: 'https://api.example.com',
44
+ });
45
+
46
+ const { result } = renderHook(
47
+ () =>
48
+ typedClient.useQuery('GET /users/{id}', {
49
+ params: { id: '123' },
50
+ }),
51
+ { wrapper },
52
+ );
53
+
54
+ expect(result.current.isLoading).toBe(true);
55
+
56
+ await waitFor(() => {
57
+ expect(result.current.isSuccess).toBe(true);
58
+ });
59
+
60
+ expect(result.current.data).toEqual({
61
+ id: '123',
62
+ name: 'John Doe',
63
+ email: 'john@example.com',
64
+ });
65
+ });
66
+
67
+ it('should handle query parameters', async () => {
68
+ const typedClient = createTypedQueryClient<paths>({
69
+ baseURL: 'https://api.example.com',
70
+ });
71
+
72
+ const { result } = renderHook(
73
+ () =>
74
+ typedClient.useQuery('GET /posts', {
75
+ query: { page: 1, limit: 10, sort: 'desc' },
76
+ }),
77
+ { wrapper },
78
+ );
79
+
80
+ await waitFor(() => {
81
+ expect(result.current.isSuccess).toBe(true);
82
+ });
83
+
84
+ expect(result.current.data).toMatchObject({
85
+ posts: expect.any(Array),
86
+ pagination: {
87
+ page: 1,
88
+ limit: 10,
89
+ total: 50,
90
+ },
91
+ sort: 'desc',
92
+ });
93
+ });
94
+
95
+ it('should handle errors', async () => {
96
+ const typedClient = createTypedQueryClient<paths>({
97
+ baseURL: 'https://api.example.com',
98
+ });
99
+
100
+ const { result } = renderHook(() => typedClient.useQuery('GET /error'), {
101
+ wrapper,
102
+ });
103
+
104
+ await waitFor(() => {
105
+ expect(result.current.isError).toBe(true);
106
+ });
107
+
108
+ expect(result.current.error).toBeDefined();
109
+ });
110
+
111
+ it('should pass additional query options', async () => {
112
+ const typedClient = createTypedQueryClient<paths>({
113
+ baseURL: 'https://api.example.com',
114
+ });
115
+
116
+ const { result } = renderHook(
117
+ () =>
118
+ typedClient.useQuery(
119
+ 'GET /users/{id}',
120
+ { params: { id: '123' } },
121
+ {
122
+ staleTime: 5 * 60 * 1000,
123
+ gcTime: 10 * 60 * 1000,
124
+ },
125
+ ),
126
+ { wrapper },
127
+ );
128
+
129
+ await waitFor(() => {
130
+ expect(result.current.isSuccess).toBe(true);
131
+ });
132
+
133
+ expect(result.current.data).toBeDefined();
134
+ });
135
+ });
136
+
137
+ describe('useMutation', () => {
138
+ it('should perform mutations successfully', async () => {
139
+ const typedClient = createTypedQueryClient<paths>({
140
+ baseURL: 'https://api.example.com',
141
+ });
142
+
143
+ const { result } = renderHook(
144
+ () => typedClient.useMutation('POST /users'),
145
+ { wrapper },
146
+ );
147
+
148
+ expect(result.current.isPending).toBe(false);
149
+
150
+ act(() => {
151
+ result.current.mutate({
152
+ body: {
153
+ name: 'New User',
154
+ email: 'new@example.com',
155
+ },
156
+ });
157
+ });
158
+
159
+ await waitFor(() => {
160
+ expect(result.current.isSuccess).toBe(true);
161
+ });
162
+
163
+ expect(result.current.data).toEqual({
164
+ id: '123',
165
+ name: 'New User',
166
+ email: 'new@example.com',
167
+ });
168
+ });
169
+
170
+ it('should handle mutation with path parameters', async () => {
171
+ const typedClient = createTypedQueryClient<paths>({
172
+ baseURL: 'https://api.example.com',
173
+ });
174
+
175
+ const { result } = renderHook(
176
+ () => typedClient.useMutation('PUT /users/{id}'),
177
+ { wrapper },
178
+ );
179
+
180
+ act(() => {
181
+ result.current.mutate({
182
+ params: { id: '456' },
183
+ body: {
184
+ name: 'Updated User',
185
+ },
186
+ });
187
+ });
188
+
189
+ await waitFor(() => {
190
+ expect(result.current.isSuccess).toBe(true);
191
+ });
192
+
193
+ expect(result.current.data).toMatchObject({
194
+ id: '456',
195
+ name: 'Updated User',
196
+ });
197
+ });
198
+
199
+ it('should accept mutation options', async () => {
200
+ const typedClient = createTypedQueryClient<paths>({
201
+ baseURL: 'https://api.example.com',
202
+ });
203
+
204
+ let successData: any;
205
+ const onSuccess = (data: any) => {
206
+ successData = data;
207
+ };
208
+
209
+ const { result } = renderHook(
210
+ () => typedClient.useMutation('POST /users', { onSuccess }),
211
+ { wrapper },
212
+ );
213
+
214
+ act(() => {
215
+ result.current.mutate({
216
+ body: {
217
+ name: 'Test User',
218
+ email: 'test@example.com',
219
+ },
220
+ });
221
+ });
222
+
223
+ await waitFor(() => {
224
+ expect(result.current.isSuccess).toBe(true);
225
+ });
226
+
227
+ expect(successData).toEqual({
228
+ id: '123',
229
+ name: 'Test User',
230
+ email: 'test@example.com',
231
+ });
232
+ });
233
+ });
234
+
235
+ describe('helper hooks', () => {
236
+ it('should work with useTypedQuery helper', async () => {
237
+ const typedClient = createTypedQueryClient<paths>({
238
+ baseURL: 'https://api.example.com',
239
+ });
240
+
241
+ const { result } = renderHook(
242
+ () =>
243
+ useTypedQuery(typedClient, 'GET /users/{id}', {
244
+ params: { id: '789' },
245
+ }),
246
+ { wrapper },
247
+ );
248
+
249
+ await waitFor(() => {
250
+ expect(result.current.isSuccess).toBe(true);
251
+ });
252
+
253
+ expect(result.current.data).toMatchObject({
254
+ id: '789',
255
+ name: 'John Doe',
256
+ email: 'john@example.com',
257
+ });
258
+ });
259
+
260
+ it('should work with useTypedMutation helper', async () => {
261
+ const typedClient = createTypedQueryClient<paths>({
262
+ baseURL: 'https://api.example.com',
263
+ });
264
+
265
+ const { result } = renderHook(
266
+ () => useTypedMutation(typedClient, 'DELETE /users/{id}'),
267
+ { wrapper },
268
+ );
269
+
270
+ act(() => {
271
+ result.current.mutate({
272
+ params: { id: '999' },
273
+ });
274
+ });
275
+
276
+ await waitFor(() => {
277
+ expect(result.current.isSuccess).toBe(true);
278
+ });
279
+
280
+ expect(result.current.data).toBeUndefined();
281
+ });
282
+ });
283
+
284
+ describe('query key generation', () => {
285
+ it('should generate correct query keys', async () => {
286
+ const typedClient = createTypedQueryClient<paths>({
287
+ baseURL: 'https://api.example.com',
288
+ });
289
+
290
+ // Test with path params only
291
+ const { result: result1 } = renderHook(
292
+ () =>
293
+ typedClient.useQuery('GET /users/{id}', {
294
+ params: { id: '123' },
295
+ }),
296
+ { wrapper },
297
+ );
298
+
299
+ await waitFor(() => expect(result1.current.isSuccess).toBe(true));
300
+
301
+ // The query key should include the endpoint and params
302
+ const queryState = queryClient.getQueryState([
303
+ 'GET /users/{id}',
304
+ { params: { id: '123' } },
305
+ ]);
306
+ expect(queryState).toBeDefined();
307
+
308
+ // Test with query params
309
+ const { result: result2 } = renderHook(
310
+ () =>
311
+ typedClient.useQuery('GET /posts', {
312
+ query: { page: 1, limit: 10 },
313
+ }),
314
+ { wrapper },
315
+ );
316
+
317
+ await waitFor(() => expect(result2.current.isSuccess).toBe(true));
318
+
319
+ const queryState2 = queryClient.getQueryState([
320
+ 'GET /posts',
321
+ { query: { page: 1, limit: 10 } },
322
+ ]);
323
+ expect(queryState2).toBeDefined();
324
+ });
325
+ });
326
+
327
+ describe('error handling', () => {
328
+ it('should handle 404 errors', async () => {
329
+ const typedClient = createTypedQueryClient<paths>({
330
+ baseURL: 'https://api.example.com',
331
+ });
332
+
333
+ const { result } = renderHook(
334
+ () =>
335
+ typedClient.useQuery('GET /users/{id}', {
336
+ params: { id: '404' },
337
+ }),
338
+ { wrapper },
339
+ );
340
+
341
+ await waitFor(() => {
342
+ expect(result.current.isError).toBe(true);
343
+ });
344
+
345
+ expect(result.current.error?.status).toBe(404);
346
+ });
347
+
348
+ it('should handle authorization errors', async () => {
349
+ const typedClient = createTypedQueryClient<paths>({
350
+ baseURL: 'https://api.example.com',
351
+ });
352
+
353
+ const { result } = renderHook(
354
+ () => typedClient.useQuery('GET /protected'),
355
+ { wrapper },
356
+ );
357
+
358
+ await waitFor(() => {
359
+ expect(result.current.isError).toBe(true);
360
+ });
361
+
362
+ expect(result.current.error?.status).toBe(401);
363
+ });
364
+ });
365
+
366
+ describe('Memoization', () => {
367
+ it('should not cause unnecessary re-renders when options object is recreated with same values', async () => {
368
+ const typedClient = createTypedQueryClient<paths>({
369
+ baseURL: 'https://api.example.com',
370
+ });
371
+
372
+ // Track API calls using MSW request interception
373
+ let requestCount = 0;
374
+ const { server } = await import('./setup');
375
+ server.use(
376
+ http.get('https://api.example.com/users/:id', () => {
377
+ requestCount++;
378
+ return HttpResponse.json({ id: '123', name: 'John Doe' });
379
+ }),
380
+ );
381
+
382
+ let renderCount = 0;
383
+
384
+ const { rerender } = renderHook(
385
+ ({ options }: { options?: { enabled?: boolean } }) => {
386
+ renderCount++;
387
+ return typedClient.useQuery(
388
+ 'GET /users/{id}',
389
+ { params: { id: '123' } },
390
+ options,
391
+ );
392
+ },
393
+ {
394
+ wrapper,
395
+ initialProps: { options: { enabled: true } },
396
+ },
397
+ );
398
+
399
+ await waitFor(() => {
400
+ expect(requestCount).toBe(1);
401
+ });
402
+
403
+ const initialRenderCount = renderCount;
404
+
405
+ // Rerender with a new options object that has the same values
406
+ rerender({ options: { enabled: true } });
407
+
408
+ // Give React time to potentially trigger re-renders
409
+ await act(async () => {
410
+ await new Promise((resolve) => setTimeout(resolve, 10));
411
+ });
412
+
413
+ // The API should not be called again because options haven't actually changed
414
+ expect(requestCount).toBe(1);
415
+
416
+ // Component should re-render due to props change, but React Query shouldn't refetch
417
+ expect(renderCount).toBeGreaterThan(initialRenderCount);
418
+ });
419
+
420
+ it('should re-fetch when options actually change', async () => {
421
+ const typedClient = createTypedQueryClient<paths>({
422
+ baseURL: 'https://api.example.com',
423
+ });
424
+
425
+ // Track API calls using MSW request interception
426
+ let requestCount = 0;
427
+ const { server } = await import('./setup');
428
+ server.use(
429
+ http.get('https://api.example.com/users/:id', () => {
430
+ requestCount++;
431
+ return HttpResponse.json({ id: '123', name: 'John Doe' });
432
+ }),
433
+ );
434
+
435
+ const { rerender } = renderHook(
436
+ ({ enabled }: { enabled: boolean }) =>
437
+ typedClient.useQuery(
438
+ 'GET /users/{id}',
439
+ { params: { id: '123' } },
440
+ { enabled },
441
+ ),
442
+ {
443
+ wrapper,
444
+ initialProps: { enabled: true },
445
+ },
446
+ );
447
+
448
+ await waitFor(() => {
449
+ expect(requestCount).toBe(1);
450
+ });
451
+
452
+ // Disable the query
453
+ rerender({ enabled: false });
454
+
455
+ await act(async () => {
456
+ await new Promise((resolve) => setTimeout(resolve, 10));
457
+ });
458
+
459
+ // Should still only be called once since we disabled it
460
+ expect(requestCount).toBe(1);
461
+
462
+ // Re-enable the query
463
+ rerender({ enabled: true });
464
+
465
+ await waitFor(() => {
466
+ expect(requestCount).toBe(2);
467
+ });
468
+ });
469
+
470
+ it('should memoize mutation options correctly', async () => {
471
+ const typedClient = createTypedQueryClient<paths>({
472
+ baseURL: 'https://api.example.com',
473
+ });
474
+
475
+ // Track API calls using MSW request interception
476
+ let requestCount = 0;
477
+ const { server } = await import('./setup');
478
+ server.use(
479
+ http.post('https://api.example.com/users', async ({ request }) => {
480
+ requestCount++;
481
+ const body = await request.json();
482
+ return HttpResponse.json({ id: '123', name: (body as any).name });
483
+ }),
484
+ );
485
+
486
+ let renderCount = 0;
487
+ let successCallCount = 0;
488
+
489
+ const { result, rerender } = renderHook(
490
+ ({ options }: { options?: { onSuccess?: () => void } }) => {
491
+ renderCount++;
492
+ return typedClient.useMutation('POST /users', options);
493
+ },
494
+ {
495
+ wrapper,
496
+ initialProps: {
497
+ options: {
498
+ onSuccess: () => {
499
+ successCallCount++;
500
+ },
501
+ },
502
+ },
503
+ },
504
+ );
505
+
506
+ const initialRenderCount = renderCount;
507
+
508
+ // Trigger a mutation to test the options
509
+ await act(async () => {
510
+ result.current.mutate({
511
+ body: { name: 'Test User', email: 'test@example.com' },
512
+ });
513
+ });
514
+
515
+ await waitFor(() => {
516
+ expect(requestCount).toBe(1);
517
+ expect(successCallCount).toBe(1);
518
+ });
519
+
520
+ // Rerender with a new options object that has the same function reference
521
+ const onSuccess = () => {
522
+ successCallCount++;
523
+ };
524
+ rerender({ options: { onSuccess } });
525
+ rerender({ options: { onSuccess } });
526
+
527
+ // Component should re-render due to props change
528
+ expect(renderCount).toBeGreaterThan(initialRenderCount);
529
+ });
530
+
531
+ it('should handle config changes in useQuery correctly', async () => {
532
+ const typedClient = createTypedQueryClient<paths>({
533
+ baseURL: 'https://api.example.com',
534
+ });
535
+
536
+ // Track API calls using MSW request interception
537
+ let requestCount = 0;
538
+ const requestedIds: string[] = [];
539
+ const { server } = await import('./setup');
540
+ server.use(
541
+ http.get('https://api.example.com/users/:id', ({ params }) => {
542
+ requestCount++;
543
+ const { id } = params;
544
+ requestedIds.push(id as string);
545
+ return HttpResponse.json({ id, name: `User ${id}` });
546
+ }),
547
+ );
548
+
549
+ const { rerender } = renderHook(
550
+ ({ userId }: { userId: string }) =>
551
+ typedClient.useQuery('GET /users/{id}', { params: { id: userId } }),
552
+ {
553
+ wrapper,
554
+ initialProps: { userId: '123' },
555
+ },
556
+ );
557
+
558
+ await waitFor(() => {
559
+ expect(requestCount).toBe(1);
560
+ expect(requestedIds).toEqual(['123']);
561
+ });
562
+
563
+ // Change the user ID - should trigger a new fetch
564
+ rerender({ userId: '456' });
565
+
566
+ await waitFor(() => {
567
+ expect(requestCount).toBe(2);
568
+ expect(requestedIds).toEqual(['123', '456']);
569
+ });
570
+
571
+ // Call with same ID again - should not trigger new fetch due to React Query caching
572
+ rerender({ userId: '456' });
573
+
574
+ await act(async () => {
575
+ await new Promise((resolve) => setTimeout(resolve, 10));
576
+ });
577
+
578
+ expect(requestCount).toBe(2);
579
+ expect(requestedIds).toEqual(['123', '456']);
580
+ });
581
+ });
582
582
  });