@geekmidas/client 9.0.2 → 10.0.0-alpha.1

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 (79) hide show
  1. package/dist/auth-fetcher-CBYuL6rx.d.cts +158 -0
  2. package/dist/auth-fetcher-CBYuL6rx.d.cts.map +1 -0
  3. package/dist/auth-fetcher-DOj0iz21.mjs +77 -0
  4. package/dist/auth-fetcher-DOj0iz21.mjs.map +1 -0
  5. package/dist/auth-fetcher-DxS8zOLc.d.mts +158 -0
  6. package/dist/auth-fetcher-DxS8zOLc.d.mts.map +1 -0
  7. package/dist/auth-fetcher-HILABmws.cjs +82 -0
  8. package/dist/auth-fetcher-HILABmws.cjs.map +1 -0
  9. package/dist/auth-fetcher.cjs +3 -76
  10. package/dist/auth-fetcher.d.cts +3 -158
  11. package/dist/auth-fetcher.d.mts +3 -158
  12. package/dist/auth-fetcher.mjs +3 -76
  13. package/dist/endpoint-hooks.d.mts +1 -1
  14. package/dist/fetcher-C1xtCKpT.d.mts +25 -0
  15. package/dist/fetcher-C1xtCKpT.d.mts.map +1 -0
  16. package/dist/fetcher-cDrfwN-Y.d.cts +25 -0
  17. package/dist/fetcher-cDrfwN-Y.d.cts.map +1 -0
  18. package/dist/fetcher.d.cts +3 -25
  19. package/dist/fetcher.d.mts +3 -25
  20. package/dist/index.cjs +8 -0
  21. package/dist/index.d.cts +5 -0
  22. package/dist/index.d.mts +5 -0
  23. package/dist/index.mjs +6 -0
  24. package/dist/infer-6KKCgmg1.cjs +0 -0
  25. package/{src/infer.ts → dist/infer-DfRq5kIb.d.cts} +54 -83
  26. package/dist/infer-DfRq5kIb.d.cts.map +1 -0
  27. package/dist/infer-bTirNeNW.d.mts +133 -0
  28. package/dist/infer-bTirNeNW.d.mts.map +1 -0
  29. package/dist/infer-uCUq1whM.mjs +0 -0
  30. package/dist/infer.cjs +1 -0
  31. package/dist/infer.d.cts +2 -133
  32. package/dist/infer.d.mts +2 -133
  33. package/dist/infer.mjs +1 -0
  34. package/dist/openapi-hooks.cjs.map +1 -1
  35. package/dist/openapi-hooks.d.cts.map +1 -1
  36. package/dist/openapi-hooks.d.mts +1 -1
  37. package/dist/openapi-hooks.d.mts.map +1 -1
  38. package/dist/openapi-hooks.mjs.map +1 -1
  39. package/dist/react-query.d.mts +1 -1
  40. package/dist/types-BH8WgGrm.mjs +0 -0
  41. package/dist/{types-D8Jrl4o3.d.mts → types-BMGM6sZc.d.mts} +1 -1
  42. package/dist/{types-D8Jrl4o3.d.mts.map → types-BMGM6sZc.d.mts.map} +1 -1
  43. package/dist/types-a8gm_IaQ.cjs +0 -0
  44. package/dist/types.cjs +1 -0
  45. package/dist/types.d.mts +1 -1
  46. package/dist/types.mjs +1 -0
  47. package/package.json +8 -5
  48. package/CHANGELOG.md +0 -146
  49. package/dist/auth-fetcher.cjs.map +0 -1
  50. package/dist/auth-fetcher.d.cts.map +0 -1
  51. package/dist/auth-fetcher.d.mts.map +0 -1
  52. package/dist/auth-fetcher.mjs.map +0 -1
  53. package/dist/fetcher.d.cts.map +0 -1
  54. package/dist/fetcher.d.mts.map +0 -1
  55. package/dist/infer.d.cts.map +0 -1
  56. package/dist/infer.d.mts.map +0 -1
  57. package/src/__tests__/auth-fetcher.spec.ts +0 -505
  58. package/src/__tests__/endpoint-hooks.spec.tsx +0 -374
  59. package/src/__tests__/fetcher.spec.ts +0 -580
  60. package/src/__tests__/infer.integration.spec.ts +0 -188
  61. package/src/__tests__/infer.spec.ts +0 -189
  62. package/src/__tests__/method-restrictions.spec.tsx +0 -227
  63. package/src/__tests__/mutation-inference.spec.tsx +0 -343
  64. package/src/__tests__/openapi-hooks.spec.tsx +0 -557
  65. package/src/__tests__/react-query-infinite.spec.tsx +0 -1003
  66. package/src/__tests__/react-query-invalidation.spec.tsx +0 -239
  67. package/src/__tests__/react-query.spec.tsx +0 -582
  68. package/src/__tests__/setup.ts +0 -281
  69. package/src/__tests__/types.spec.ts +0 -134
  70. package/src/__tests__/url-parsing.spec.ts +0 -196
  71. package/src/auth-fetcher.ts +0 -288
  72. package/src/endpoint-hooks.ts +0 -346
  73. package/src/fetcher.ts +0 -171
  74. package/src/openapi-hooks.ts +0 -193
  75. package/src/openapi-types.d.ts +0 -440
  76. package/src/openapi.json +0 -595
  77. package/src/react-query.ts +0 -341
  78. package/src/types.ts +0 -226
  79. package/tsconfig.json +0 -12
@@ -1,1003 +0,0 @@
1
- /**
2
- * @vitest-environment jsdom
3
- */
4
- import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
5
- import { act, renderHook, waitFor } from '@testing-library/react';
6
- import { HttpResponse, http } from 'msw';
7
- // biome-ignore lint/style/useImportType: <explanation>
8
- import React from 'react';
9
- import { beforeEach, describe, expect, it, vi } from 'vitest';
10
- import type { paths } from '../openapi-types';
11
- import { createTypedQueryClient } from '../react-query';
12
- import './setup';
13
-
14
- describe('TypedQueryClient - useInfiniteQuery', () => {
15
- const queryClient = new QueryClient({
16
- defaultOptions: {
17
- queries: {
18
- retry: false,
19
- gcTime: 0,
20
- staleTime: 0,
21
- },
22
- },
23
- });
24
-
25
- const wrapper = ({ children }: { children: React.ReactNode }) => (
26
- <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
27
- );
28
-
29
- beforeEach(() => {
30
- queryClient.clear();
31
- });
32
-
33
- it('should correctly merge query parameters with pageParam', async () => {
34
- // Mock fetch to verify the URL
35
- const originalFetch = global.fetch;
36
- const fetchSpy = vi.fn(originalFetch);
37
- global.fetch = fetchSpy;
38
-
39
- const typedClient = createTypedQueryClient<paths>({
40
- baseURL: 'https://api.example.com',
41
- fetch: fetchSpy,
42
- });
43
-
44
- const { result } = renderHook(
45
- () =>
46
- typedClient.useInfiniteQuery(
47
- 'GET /posts',
48
- {
49
- initialPageParam: 1,
50
- getNextPageParam: () => undefined,
51
- },
52
- {
53
- query: {
54
- sort: 'desc',
55
- limit: 10,
56
- },
57
- },
58
- ),
59
- { wrapper },
60
- );
61
-
62
- await waitFor(() => expect(result.current.isSuccess).toBe(true));
63
-
64
- // Restore original fetch
65
- global.fetch = originalFetch;
66
-
67
- // Verify the fetch URL contains all query parameters
68
- expect(fetchSpy).toHaveBeenCalled();
69
- const callArg = fetchSpy.mock.calls[0][0];
70
- const url = new URL(
71
- callArg instanceof Request ? callArg.url : callArg.toString(),
72
- );
73
-
74
- expect(url.searchParams.get('page')).toBe('1');
75
- expect(url.searchParams.get('sort')).toBe('desc');
76
- expect(url.searchParams.get('limit')).toBe('10');
77
-
78
- // Verify response data
79
- expect(result.current.data?.pages[0]).toBeDefined();
80
- expect(result.current.data?.pages[0].sort).toBe('desc');
81
- });
82
-
83
- it('should correctly type the data structure with pages array', () => {
84
- const typedClient = createTypedQueryClient<paths>({
85
- baseURL: 'https://api.example.com',
86
- });
87
-
88
- const { result } = renderHook(
89
- () => {
90
- const query = typedClient.useInfiniteQuery('GET /users/paginated', {
91
- initialPageParam: 1,
92
- getNextPageParam: (lastPage, allPages, lastPageParam) => {
93
- if (!lastPage) return undefined;
94
- // Type tests - these should compile without error
95
- const _pageNum: number = lastPage.pagination.page;
96
- const hasMore: boolean = lastPage.pagination.hasMore;
97
- const _users: Array<{ id: string; name: string; email: string }> =
98
- lastPage.users;
99
-
100
- // allPages should be an array
101
- const _pagesArray: (typeof lastPage)[] = allPages;
102
-
103
- // lastPageParam should be number (as we defined)
104
- const param: number = lastPageParam;
105
-
106
- return hasMore ? param + 1 : undefined;
107
- },
108
- });
109
-
110
- // Type test for the returned data structure
111
- if (query.data) {
112
- // This should compile - data.pages should exist and be an array
113
- const pages: Array<{
114
- users: Array<{ id: string; name: string; email: string }>;
115
- pagination: {
116
- page: number;
117
- limit: number;
118
- total: number;
119
- hasMore: boolean;
120
- };
121
- }> = query.data.pages;
122
-
123
- // pageParams should also be typed correctly
124
- const _pageParams: number[] = query.data.pageParams;
125
-
126
- // Test accessing nested data
127
- if (pages.length > 0) {
128
- const firstPageUsers = pages[0].users;
129
- const firstUser = firstPageUsers[0];
130
- if (firstUser) {
131
- const _userId: string = firstUser.id;
132
- const _userName: string = firstUser.name;
133
- const _userEmail: string = firstUser.email;
134
- }
135
- }
136
- }
137
-
138
- return query;
139
- },
140
- { wrapper },
141
- );
142
-
143
- // The test passes if TypeScript compiles without error
144
- expect(result.current).toBeDefined();
145
- });
146
-
147
- it('should work with cursor-based pagination types', () => {
148
- const typedClient = createTypedQueryClient<paths>({
149
- baseURL: 'https://api.example.com',
150
- });
151
-
152
- const { result } = renderHook(
153
- () => {
154
- const query = typedClient.useInfiniteQuery('GET /messages', {
155
- initialPageParam: undefined as string | undefined,
156
- getNextPageParam: (lastPage) => {
157
- // Type test - nextCursor should be string | null
158
- const cursor: string | null = lastPage.nextCursor;
159
- return cursor ?? undefined;
160
- },
161
- });
162
-
163
- // Type test for cursor-based data
164
- if (query.data) {
165
- const _pages: Array<{
166
- messages: Array<{ id: string; text: string; timestamp: string }>;
167
- nextCursor: string | null;
168
- }> = query.data.pages;
169
-
170
- const _pageParams: (string | undefined)[] = query.data.pageParams;
171
- }
172
-
173
- return query;
174
- },
175
- { wrapper },
176
- );
177
-
178
- expect(result.current).toBeDefined();
179
- });
180
-
181
- it('should preserve existing query parameters when paginating', async () => {
182
- const typedClient = createTypedQueryClient<paths>({
183
- baseURL: 'https://api.example.com',
184
- });
185
-
186
- const { result } = renderHook(
187
- () =>
188
- typedClient.useInfiniteQuery(
189
- 'GET /posts',
190
- {
191
- initialPageParam: 1,
192
- getNextPageParam: (lastPage) => {
193
- const nextPage = lastPage?.pagination?.page
194
- ? lastPage.pagination.page + 1
195
- : undefined;
196
- return nextPage && nextPage <= 3 ? nextPage : undefined;
197
- },
198
- },
199
- {
200
- query: {
201
- limit: 5,
202
- sort: 'asc',
203
- },
204
- },
205
- ),
206
- { wrapper },
207
- );
208
-
209
- // Wait for initial load
210
- await waitFor(() => expect(result.current.isSuccess).toBe(true));
211
-
212
- // Verify initial data has query params applied
213
- expect(result.current.data?.pages[0]).toBeDefined();
214
- expect(result.current.data?.pages[0].pagination.limit).toBe(5);
215
- expect(result.current.data?.pages[0].sort).toBe('asc');
216
- });
217
-
218
- it('should merge query parameters with pageParam correctly', async () => {
219
- const typedClient = createTypedQueryClient<paths>({
220
- baseURL: 'https://api.example.com',
221
- });
222
-
223
- const { result } = renderHook(
224
- () =>
225
- typedClient.useInfiniteQuery(
226
- 'GET /posts',
227
- {
228
- initialPageParam: 1,
229
- getNextPageParam: (_lastPage, _allPages, lastPageParam) =>
230
- lastPageParam < 3 ? lastPageParam + 1 : undefined,
231
- },
232
- {
233
- query: {
234
- sort: 'desc',
235
- limit: 15,
236
- },
237
- },
238
- ),
239
- { wrapper },
240
- );
241
-
242
- await waitFor(() => expect(result.current.isSuccess).toBe(true));
243
-
244
- // Check that initial query parameters are present
245
- expect(result.current.data?.pages[0].sort).toBe('desc');
246
- expect(result.current.data?.pages[0].pagination.limit).toBe(15);
247
- expect(result.current.data?.pages[0].pagination.page).toBe(1);
248
- });
249
-
250
- it('should handle object pageParam with existing query parameters', async () => {
251
- const typedClient = createTypedQueryClient<paths>({
252
- baseURL: 'https://api.example.com',
253
- });
254
-
255
- const { result } = renderHook(
256
- () =>
257
- typedClient.useInfiniteQuery(
258
- 'GET /messages',
259
- {
260
- initialPageParam: {
261
- cursor: undefined as string | undefined,
262
- limit: 10,
263
- },
264
- getNextPageParam: (lastPage) =>
265
- lastPage.nextCursor
266
- ? { cursor: lastPage.nextCursor, limit: 10 }
267
- : undefined,
268
- },
269
- {
270
- query: {
271
- limit: 20, // This should be overridden by pageParam
272
- },
273
- },
274
- ),
275
- { wrapper },
276
- );
277
-
278
- await waitFor(() => expect(result.current.isSuccess).toBe(true));
279
-
280
- // The pageParam should override the initial query parameter
281
- expect(result.current.data?.pages[0].messages).toBeDefined();
282
- });
283
-
284
- it('should handle numeric pageParam without config', async () => {
285
- const typedClient = createTypedQueryClient<paths>({
286
- baseURL: 'https://api.example.com',
287
- });
288
-
289
- const { result } = renderHook(
290
- () =>
291
- typedClient.useInfiniteQuery('GET /posts', {
292
- initialPageParam: 1,
293
- getNextPageParam: () => undefined,
294
- }),
295
- { wrapper },
296
- );
297
-
298
- await waitFor(() => expect(result.current.isSuccess).toBe(true));
299
-
300
- expect(result.current.data?.pages[0].pagination.page).toBe(1);
301
- });
302
-
303
- it('should include query parameters in cache key', async () => {
304
- const typedClient = createTypedQueryClient<paths>({
305
- baseURL: 'https://api.example.com',
306
- });
307
-
308
- const { result: result1 } = renderHook(
309
- () =>
310
- typedClient.useInfiniteQuery(
311
- 'GET /posts',
312
- {
313
- initialPageParam: 1,
314
- getNextPageParam: () => undefined,
315
- },
316
- {
317
- query: { sort: 'asc' },
318
- },
319
- ),
320
- { wrapper },
321
- );
322
-
323
- const { result: result2 } = renderHook(
324
- () =>
325
- typedClient.useInfiniteQuery(
326
- 'GET /posts',
327
- {
328
- initialPageParam: 1,
329
- getNextPageParam: () => undefined,
330
- },
331
- {
332
- query: { sort: 'desc' },
333
- },
334
- ),
335
- { wrapper },
336
- );
337
-
338
- await waitFor(() => {
339
- expect(result1.current.isSuccess).toBe(true);
340
- expect(result2.current.isSuccess).toBe(true);
341
- });
342
-
343
- // Different query parameters should result in different cache entries
344
- expect(result1.current.data?.pages[0].sort).toBe('asc');
345
- expect(result2.current.data?.pages[0].sort).toBe('desc');
346
- });
347
-
348
- it('should maintain all query parameters across multiple page fetches', async () => {
349
- const typedClient = createTypedQueryClient<paths>({
350
- baseURL: 'https://api.example.com',
351
- });
352
-
353
- // Mock fetch to verify URLs
354
- const originalFetch = global.fetch;
355
- const fetchCalls: string[] = [];
356
- const fetchSpy = vi.fn((url, ...args) => {
357
- const urlString = url instanceof Request ? url.url : url.toString();
358
- fetchCalls.push(urlString);
359
- return originalFetch(url, ...args);
360
- });
361
- global.fetch = fetchSpy;
362
-
363
- const { result } = renderHook(
364
- () =>
365
- typedClient.useInfiniteQuery(
366
- 'GET /posts',
367
- {
368
- initialPageParam: 1,
369
- getNextPageParam: (_lastPage, _allPages, lastPageParam) => {
370
- return lastPageParam < 3 ? lastPageParam + 1 : undefined;
371
- },
372
- },
373
- {
374
- query: {
375
- sort: 'desc',
376
- limit: 10,
377
- },
378
- },
379
- ),
380
- { wrapper },
381
- );
382
-
383
- // Wait for initial page
384
- await waitFor(() => expect(result.current.isSuccess).toBe(true));
385
-
386
- // Fetch next pages
387
- await result.current.fetchNextPage();
388
- await result.current.fetchNextPage();
389
-
390
- // Restore original fetch
391
- global.fetch = originalFetch;
392
-
393
- // Verify all requests had the correct query parameters
394
- fetchCalls.forEach((urlString, index) => {
395
- const url = new URL(urlString);
396
- expect(url.searchParams.get('page')).toBe(String(index + 1));
397
- expect(url.searchParams.get('sort')).toBe('desc');
398
- expect(url.searchParams.get('limit')).toBe('10');
399
- });
400
- });
401
-
402
- it.skip('should handle complex object pageParam merging', async () => {
403
- const typedClient = createTypedQueryClient<paths>({
404
- baseURL: 'https://api.example.com',
405
- });
406
-
407
- const { result } = renderHook(
408
- () =>
409
- typedClient.useInfiniteQuery(
410
- 'GET /messages',
411
- {
412
- initialPageParam: {
413
- cursor: undefined as string | undefined,
414
- limit: 5,
415
- type: 'initial',
416
- },
417
- getNextPageParam: (lastPage) =>
418
- lastPage.nextCursor
419
- ? {
420
- cursor: lastPage.nextCursor,
421
- limit: 5,
422
- type: 'next',
423
- }
424
- : undefined,
425
- },
426
- {
427
- query: {
428
- limit: 10, // Should be overridden by pageParam
429
- },
430
- },
431
- ),
432
- { wrapper },
433
- );
434
-
435
- await waitFor(() => expect(result.current.isSuccess).toBe(true));
436
-
437
- // The complex pageParam should properly merge with config
438
- expect(result.current.data?.pages[0]).toBeDefined();
439
-
440
- // Fetch next page to ensure complex objects continue to work
441
- if (result.current.hasNextPage) {
442
- await act(() => result.current.fetchNextPage());
443
- await waitFor(() => {
444
- expect(result.current.data?.pages).toHaveLength(2);
445
- });
446
- }
447
- });
448
-
449
- it('should correctly build query key with query parameters', () => {
450
- const typedClient = createTypedQueryClient<paths>({
451
- baseURL: 'https://api.example.com',
452
- });
453
-
454
- // Test with query parameters
455
- const queryKey1 = typedClient.buildQueryKey('GET /posts', {
456
- query: { sort: 'asc', limit: 10 },
457
- });
458
-
459
- expect(queryKey1).toEqual([
460
- 'GET /posts',
461
- { query: { sort: 'asc', limit: 10 } },
462
- ]);
463
-
464
- // Test with both params and query
465
- const queryKey2 = typedClient.buildQueryKey('GET /users/{id}', {
466
- params: { id: '123' },
467
- });
468
-
469
- expect(queryKey2).toEqual(['GET /users/{id}', { params: { id: '123' } }]);
470
- });
471
-
472
- it('should handle undefined pageParam correctly', async () => {
473
- const typedClient = createTypedQueryClient<paths>({
474
- baseURL: 'https://api.example.com',
475
- });
476
-
477
- const { result } = renderHook(
478
- () =>
479
- typedClient.useInfiniteQuery(
480
- 'GET /messages',
481
- {
482
- initialPageParam: undefined,
483
- getNextPageParam: (lastPage) => lastPage.nextCursor || undefined,
484
- },
485
- {
486
- query: {
487
- limit: 15,
488
- },
489
- },
490
- ),
491
- { wrapper },
492
- );
493
-
494
- await waitFor(() => expect(result.current.isSuccess).toBe(true));
495
-
496
- // Should still include the initial query parameters
497
- expect(result.current.data?.pages[0].messages).toBeDefined();
498
- });
499
-
500
- it('should validate query parameter types are preserved in responses', async () => {
501
- const typedClient = createTypedQueryClient<paths>({
502
- baseURL: 'https://api.example.com',
503
- });
504
-
505
- const { result } = renderHook(
506
- () =>
507
- typedClient.useInfiniteQuery(
508
- 'GET /posts',
509
- {
510
- initialPageParam: 1,
511
- getNextPageParam: () => undefined,
512
- },
513
- {
514
- query: {
515
- page: 1,
516
- limit: 20,
517
- sort: 'asc',
518
- },
519
- },
520
- ),
521
- { wrapper },
522
- );
523
-
524
- await waitFor(() => expect(result.current.isSuccess).toBe(true));
525
-
526
- const firstPage = result.current.data?.pages[0];
527
-
528
- // Type checks - these should compile
529
- if (firstPage) {
530
- const posts: Array<{
531
- id: string;
532
- title: string;
533
- content: string;
534
- authorId: string;
535
- createdAt: string;
536
- }> = firstPage.posts;
537
-
538
- const pagination: {
539
- page: number;
540
- limit: number;
541
- total: number;
542
- } = firstPage.pagination;
543
-
544
- const sort: 'asc' | 'desc' = firstPage.sort;
545
-
546
- // Runtime checks
547
- expect(posts).toBeInstanceOf(Array);
548
- expect(typeof pagination.page).toBe('number');
549
- expect(typeof pagination.limit).toBe('number');
550
- expect(typeof pagination.total).toBe('number');
551
- expect(['asc', 'desc']).toContain(sort);
552
- }
553
- });
554
-
555
- it('should send all query parameters in HTTP request', async () => {
556
- // Mock fetch to capture the actual request
557
- const originalFetch = global.fetch;
558
- const fetchSpy = vi.fn(originalFetch);
559
-
560
- const typedClient = createTypedQueryClient<paths>({
561
- baseURL: 'https://api.example.com',
562
- fetch: fetchSpy,
563
- });
564
-
565
- const { result } = renderHook(
566
- () =>
567
- typedClient.useInfiniteQuery(
568
- 'GET /posts',
569
- {
570
- initialPageParam: 2,
571
- getNextPageParam: () => undefined,
572
- },
573
- {
574
- query: {
575
- sort: 'desc',
576
- limit: 25,
577
- },
578
- },
579
- ),
580
- { wrapper },
581
- );
582
-
583
- await waitFor(() => expect(result.current.isSuccess).toBe(true));
584
-
585
- // Check that the fetch was called with correct URL including all query params
586
- expect(fetchSpy).toHaveBeenCalled();
587
- const callArg = fetchSpy.mock.calls[0][0];
588
- const url = new URL(
589
- callArg instanceof Request ? callArg.url : callArg.toString(),
590
- );
591
-
592
- expect(url.searchParams.get('page')).toBe('2');
593
- expect(url.searchParams.get('sort')).toBe('desc');
594
- expect(url.searchParams.get('limit')).toBe('25');
595
- });
596
-
597
- it('should handle empty query config gracefully', async () => {
598
- const typedClient = createTypedQueryClient<paths>({
599
- baseURL: 'https://api.example.com',
600
- });
601
-
602
- const { result } = renderHook(
603
- () =>
604
- typedClient.useInfiniteQuery('GET /posts', {
605
- initialPageParam: 1,
606
- getNextPageParam: () => undefined,
607
- }),
608
- { wrapper },
609
- );
610
-
611
- await waitFor(() => expect(result.current.isSuccess).toBe(true));
612
-
613
- expect(result.current.data?.pages[0]).toBeDefined();
614
- expect(result.current.data?.pages[0].pagination.page).toBe(1);
615
- });
616
-
617
- it('should handle query parameters with special characters', async () => {
618
- const typedClient = createTypedQueryClient<paths>({
619
- baseURL: 'https://api.example.com',
620
- });
621
-
622
- const { result } = renderHook(
623
- () =>
624
- typedClient.useInfiniteQuery(
625
- 'GET /messages',
626
- {
627
- initialPageParam: 'cursor%20with%20spaces',
628
- getNextPageParam: () => undefined,
629
- },
630
- {
631
- query: {
632
- limit: 10,
633
- },
634
- },
635
- ),
636
- { wrapper },
637
- );
638
-
639
- await waitFor(() => expect(result.current.isSuccess).toBe(true));
640
-
641
- // Should handle special characters in pageParam correctly
642
- expect(result.current.data?.pages[0]).toBeDefined();
643
- });
644
-
645
- it('should verify object pageParam merging with echo endpoint', async () => {
646
- const typedClient = createTypedQueryClient<paths>({
647
- baseURL: 'https://api.example.com',
648
- });
649
-
650
- const { result } = renderHook(
651
- () =>
652
- typedClient.useInfiniteQuery(
653
- 'GET /echo',
654
- {
655
- initialPageParam: {
656
- page: 1,
657
- size: 20,
658
- offset: 0,
659
- },
660
- getNextPageParam: (lastPage) =>
661
- lastPage.pagination.hasMore
662
- ? {
663
- page: lastPage.pagination.page + 1,
664
- size: 20,
665
- offset: lastPage.pagination.page * 20,
666
- }
667
- : undefined,
668
- },
669
- {
670
- query: {
671
- sort: 'asc',
672
- type: 'premium',
673
- } as any,
674
- },
675
- ),
676
- { wrapper },
677
- );
678
-
679
- await waitFor(() => expect(result.current.isSuccess).toBe(true));
680
-
681
- // Verify object pageParam is spread into query params
682
- const firstPage = result.current.data?.pages[0];
683
- expect(firstPage?.queryParams).toEqual({
684
- page: '1',
685
- size: '20',
686
- offset: '0',
687
- sort: 'asc',
688
- type: 'premium',
689
- });
690
-
691
- // Fetch next page
692
- await result.current.fetchNextPage();
693
- await waitFor(() => expect(result.current.data?.pages).toHaveLength(2));
694
-
695
- const secondPage = result.current.data?.pages[1];
696
- expect(secondPage?.queryParams).toEqual({
697
- page: '2',
698
- size: '20',
699
- offset: '20',
700
- sort: 'asc',
701
- type: 'premium',
702
- });
703
- });
704
-
705
- it('should verify all query params are sent using echo endpoint', async () => {
706
- const typedClient = createTypedQueryClient<paths>({
707
- baseURL: 'https://api.example.com',
708
- });
709
-
710
- const { result } = renderHook(
711
- () =>
712
- typedClient.useInfiniteQuery(
713
- 'GET /echo',
714
- {
715
- initialPageParam: 1,
716
- getNextPageParam: (lastPage) =>
717
- lastPage.pagination.hasMore
718
- ? lastPage.pagination.page + 1
719
- : undefined,
720
- },
721
- {
722
- query: {
723
- sort: 'desc',
724
- filter: 'active',
725
- category: 'tech',
726
- includeArchived: false,
727
- limit: 25,
728
- } as any, // Using any to allow arbitrary query params for echo endpoint
729
- },
730
- ),
731
- { wrapper },
732
- );
733
-
734
- // Wait for initial load
735
- await waitFor(() => expect(result.current.isSuccess).toBe(true));
736
-
737
- // Verify the echo endpoint returned all our query parameters
738
- const firstPage = result.current.data?.pages[0];
739
- expect(firstPage?.queryParams).toEqual({
740
- page: '1', // From pageParam
741
- sort: 'desc',
742
- filter: 'active',
743
- category: 'tech',
744
- includeArchived: 'false',
745
- limit: '25',
746
- });
747
-
748
- // Fetch next page
749
- await result.current.fetchNextPage();
750
- await waitFor(() => expect(result.current.data?.pages).toHaveLength(2));
751
-
752
- // Verify second page also has all parameters with updated page
753
- const secondPage = result.current.data?.pages[1];
754
- expect(secondPage?.queryParams).toEqual({
755
- page: '2', // Updated pageParam
756
- sort: 'desc',
757
- filter: 'active',
758
- category: 'tech',
759
- includeArchived: 'false',
760
- limit: '25',
761
- });
762
-
763
- // Verify the actual data and pagination info
764
- expect(firstPage?.pagination).toMatchObject({
765
- page: 1,
766
- limit: 25,
767
- total: 50,
768
- hasMore: true,
769
- });
770
- });
771
-
772
- it('should distinguish between queries with different query parameters in cache', async () => {
773
- const typedClient = createTypedQueryClient<paths>({
774
- baseURL: 'https://api.example.com',
775
- });
776
-
777
- // Query 1: sort=asc, limit=5
778
- const { result: result1 } = renderHook(
779
- () =>
780
- typedClient.useInfiniteQuery(
781
- 'GET /posts',
782
- {
783
- initialPageParam: 1,
784
- getNextPageParam: () => 2,
785
- },
786
- {
787
- query: { sort: 'asc', limit: 5 },
788
- },
789
- ),
790
- { wrapper },
791
- );
792
-
793
- // Query 2: sort=desc, limit=5
794
- const { result: result2 } = renderHook(
795
- () =>
796
- typedClient.useInfiniteQuery(
797
- 'GET /posts',
798
- {
799
- initialPageParam: 1,
800
- getNextPageParam: () => 2,
801
- },
802
- {
803
- query: { sort: 'desc', limit: 5 },
804
- },
805
- ),
806
- { wrapper },
807
- );
808
-
809
- // Query 3: sort=asc, limit=10
810
- const { result: result3 } = renderHook(
811
- () =>
812
- typedClient.useInfiniteQuery(
813
- 'GET /posts',
814
- {
815
- initialPageParam: 1,
816
- getNextPageParam: () => 2,
817
- },
818
- {
819
- query: { sort: 'asc', limit: 10 },
820
- },
821
- ),
822
- { wrapper },
823
- );
824
-
825
- await waitFor(() => {
826
- expect(result1.current.isSuccess).toBe(true);
827
- expect(result2.current.isSuccess).toBe(true);
828
- expect(result3.current.isSuccess).toBe(true);
829
- });
830
-
831
- // Each query should have different data based on query params
832
- expect(result1.current.data?.pages[0].sort).toBe('asc');
833
- expect(result1.current.data?.pages[0].pagination.limit).toBe(5);
834
-
835
- expect(result2.current.data?.pages[0].sort).toBe('desc');
836
- expect(result2.current.data?.pages[0].pagination.limit).toBe(5);
837
-
838
- expect(result3.current.data?.pages[0].sort).toBe('asc');
839
- expect(result3.current.data?.pages[0].pagination.limit).toBe(10);
840
-
841
- // Fetch next page for first query
842
- await waitFor(async () => {
843
- await result1.current.fetchNextPage();
844
- expect(result1.current.data?.pages).toHaveLength(2);
845
- });
846
-
847
- // Verify the second page also has correct query params
848
- expect(result1.current.data?.pages[1].sort).toBe('asc');
849
- expect(result1.current.data?.pages[1].pagination.limit).toBe(5);
850
- expect(result1.current.data?.pages[1].pagination.page).toBe(2);
851
- });
852
-
853
- describe('Memoization', () => {
854
- it('should not cause unnecessary re-renders when options object is recreated with same values', async () => {
855
- const typedClient = createTypedQueryClient<paths>({
856
- baseURL: 'https://api.example.com',
857
- });
858
-
859
- // Track API calls using MSW request interception
860
- let requestCount = 0;
861
- const { server } = await import('./setup');
862
- server.use(
863
- http.get('https://api.example.com/users/paginated', ({ request }) => {
864
- requestCount++;
865
- const url = new URL(request.url);
866
- const page = parseInt(url.searchParams.get('page') || '1', 10);
867
-
868
- return HttpResponse.json({
869
- users: [
870
- {
871
- id: `user-${page}`,
872
- name: `User ${page}`,
873
- email: `user${page}@example.com`,
874
- },
875
- ],
876
- pagination: {
877
- page,
878
- limit: 10,
879
- total: 50,
880
- hasMore: page < 5,
881
- },
882
- });
883
- }),
884
- );
885
-
886
- let renderCount = 0;
887
-
888
- const { rerender } = renderHook(
889
- ({ options }: { options?: { enabled?: boolean } }) => {
890
- renderCount++;
891
- return typedClient.useInfiniteQuery(
892
- 'GET /users/paginated',
893
- {
894
- initialPageParam: 1,
895
- getNextPageParam: (lastPage) =>
896
- lastPage.pagination.hasMore
897
- ? lastPage.pagination.page + 1
898
- : undefined,
899
- },
900
- { query: { limit: 10 }, ...options },
901
- );
902
- },
903
- {
904
- wrapper,
905
- initialProps: { options: { enabled: true } },
906
- },
907
- );
908
-
909
- await waitFor(() => {
910
- expect(requestCount).toBe(1);
911
- });
912
-
913
- const initialRenderCount = renderCount;
914
-
915
- // Rerender with a new options object that has the same values
916
- rerender({ options: { enabled: true } });
917
-
918
- // Give React time to potentially trigger re-renders
919
- await act(async () => {
920
- await new Promise((resolve) => setTimeout(resolve, 10));
921
- });
922
-
923
- // The API should not be called again because options haven't actually changed
924
- expect(requestCount).toBe(1);
925
-
926
- // Component should re-render due to props change, but React Query shouldn't refetch
927
- expect(renderCount).toBeGreaterThan(initialRenderCount);
928
- });
929
-
930
- it('should handle pageParam changes correctly in infinite queries', async () => {
931
- const typedClient = createTypedQueryClient<paths>({
932
- baseURL: 'https://api.example.com',
933
- });
934
-
935
- // Track API calls using MSW request interception
936
- let requestCount = 0;
937
- const requestedPages: number[] = [];
938
- const { server } = await import('./setup');
939
- server.use(
940
- http.get('https://api.example.com/users/paginated', ({ request }) => {
941
- requestCount++;
942
- const url = new URL(request.url);
943
- const page = parseInt(url.searchParams.get('page') || '1', 10);
944
- requestedPages.push(page);
945
-
946
- return HttpResponse.json({
947
- users: Array.from({ length: 10 }, (_, i) => ({
948
- id: `user-${(page - 1) * 10 + i + 1}`,
949
- name: `User ${(page - 1) * 10 + i + 1}`,
950
- email: `user${(page - 1) * 10 + i + 1}@example.com`,
951
- })),
952
- pagination: {
953
- page,
954
- limit: 10,
955
- total: 50,
956
- hasMore: page < 3, // Reduced to avoid complexity
957
- },
958
- });
959
- }),
960
- );
961
-
962
- const { result } = renderHook(
963
- () =>
964
- typedClient.useInfiniteQuery(
965
- 'GET /users/paginated',
966
- {
967
- initialPageParam: 1,
968
- getNextPageParam: (lastPage) =>
969
- lastPage.pagination.hasMore
970
- ? lastPage.pagination.page + 1
971
- : undefined,
972
- },
973
- { query: { limit: 10 } },
974
- ),
975
- { wrapper },
976
- );
977
-
978
- // Wait for initial load
979
- await waitFor(() => {
980
- expect(result.current.isSuccess).toBe(true);
981
- expect(requestCount).toBe(1);
982
- expect(requestedPages).toEqual([1]);
983
- });
984
-
985
- // Test the memoization by checking that multiple calls to fetchNextPage work
986
- if (result.current.hasNextPage) {
987
- await act(async () => {
988
- await result.current.fetchNextPage();
989
- });
990
-
991
- await waitFor(() => {
992
- expect(requestCount).toBe(2);
993
- expect(requestedPages).toEqual([1, 2]);
994
- });
995
- }
996
-
997
- // Verify that pageParam changes trigger new requests
998
- expect(requestCount).toBe(2);
999
- expect(result.current.data?.pages).toBeDefined();
1000
- expect(result.current.data?.pages.length).toBeGreaterThan(0);
1001
- });
1002
- });
1003
- });