@sylphx/lens-react 2.0.1 → 2.1.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.
- package/dist/index.d.ts +104 -63
- package/dist/index.js +65 -23
- package/package.json +1 -1
- package/src/hooks.test.tsx +466 -367
- package/src/hooks.ts +255 -130
- package/src/index.ts +4 -3
package/src/hooks.test.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tests for React Hooks (
|
|
2
|
+
* Tests for React Hooks (Selector-based API)
|
|
3
3
|
*
|
|
4
4
|
* NOTE: These tests require DOM environment (happy-dom).
|
|
5
5
|
* Run from packages/react directory: cd packages/react && bun test
|
|
@@ -12,8 +12,10 @@ import { test as bunTest, describe, expect } from "bun:test";
|
|
|
12
12
|
|
|
13
13
|
const test = hasDom ? bunTest : bunTest.skip;
|
|
14
14
|
|
|
15
|
-
import type { MutationResult, QueryResult } from "@sylphx/lens-client";
|
|
15
|
+
import type { LensClient, MutationResult, QueryResult } from "@sylphx/lens-client";
|
|
16
16
|
import { act, renderHook, waitFor } from "@testing-library/react";
|
|
17
|
+
import type { ReactNode } from "react";
|
|
18
|
+
import { LensProvider } from "./context.js";
|
|
17
19
|
import { useLazyQuery, useMutation, useQuery } from "./hooks.js";
|
|
18
20
|
|
|
19
21
|
// =============================================================================
|
|
@@ -88,14 +90,31 @@ function createMockQueryResult<T>(initialValue: T | null = null): QueryResult<T>
|
|
|
88
90
|
}
|
|
89
91
|
|
|
90
92
|
// =============================================================================
|
|
91
|
-
//
|
|
93
|
+
// Test Wrapper with Mock Client
|
|
94
|
+
// =============================================================================
|
|
95
|
+
|
|
96
|
+
function createMockClient() {
|
|
97
|
+
return {} as LensClient<any, any>;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function createWrapper(mockClient: LensClient<any, any>) {
|
|
101
|
+
return function Wrapper({ children }: { children: ReactNode }) {
|
|
102
|
+
return <LensProvider client={mockClient}>{children}</LensProvider>;
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// =============================================================================
|
|
107
|
+
// Tests: useQuery (Accessor + Deps pattern)
|
|
92
108
|
// =============================================================================
|
|
93
109
|
|
|
94
110
|
describe("useQuery", () => {
|
|
95
111
|
test("returns loading state initially", () => {
|
|
112
|
+
const mockClient = createMockClient();
|
|
96
113
|
const mockQuery = createMockQueryResult<{ id: string; name: string }>();
|
|
97
114
|
|
|
98
|
-
const { result } = renderHook(() => useQuery(mockQuery)
|
|
115
|
+
const { result } = renderHook(() => useQuery(() => mockQuery, []), {
|
|
116
|
+
wrapper: createWrapper(mockClient),
|
|
117
|
+
});
|
|
99
118
|
|
|
100
119
|
expect(result.current.loading).toBe(true);
|
|
101
120
|
expect(result.current.data).toBe(null);
|
|
@@ -103,9 +122,12 @@ describe("useQuery", () => {
|
|
|
103
122
|
});
|
|
104
123
|
|
|
105
124
|
test("returns data when query resolves", async () => {
|
|
125
|
+
const mockClient = createMockClient();
|
|
106
126
|
const mockQuery = createMockQueryResult<{ id: string; name: string }>();
|
|
107
127
|
|
|
108
|
-
const { result } = renderHook(() => useQuery(mockQuery)
|
|
128
|
+
const { result } = renderHook(() => useQuery(() => mockQuery, []), {
|
|
129
|
+
wrapper: createWrapper(mockClient),
|
|
130
|
+
});
|
|
109
131
|
|
|
110
132
|
// Simulate data loading
|
|
111
133
|
act(() => {
|
|
@@ -121,9 +143,12 @@ describe("useQuery", () => {
|
|
|
121
143
|
});
|
|
122
144
|
|
|
123
145
|
test("returns error when query fails", async () => {
|
|
146
|
+
const mockClient = createMockClient();
|
|
124
147
|
const mockQuery = createMockQueryResult<{ id: string; name: string }>();
|
|
125
148
|
|
|
126
|
-
const { result } = renderHook(() => useQuery(mockQuery)
|
|
149
|
+
const { result } = renderHook(() => useQuery(() => mockQuery, []), {
|
|
150
|
+
wrapper: createWrapper(mockClient),
|
|
151
|
+
});
|
|
127
152
|
|
|
128
153
|
// Simulate error
|
|
129
154
|
act(() => {
|
|
@@ -139,6 +164,7 @@ describe("useQuery", () => {
|
|
|
139
164
|
});
|
|
140
165
|
|
|
141
166
|
test("handles non-Error rejection", async () => {
|
|
167
|
+
const mockClient = createMockClient();
|
|
142
168
|
const mockQuery = {
|
|
143
169
|
subscribe: () => () => {},
|
|
144
170
|
then: (onFulfilled: any, onRejected: any) => {
|
|
@@ -147,222 +173,220 @@ describe("useQuery", () => {
|
|
|
147
173
|
},
|
|
148
174
|
} as unknown as QueryResult<{ id: string }>;
|
|
149
175
|
|
|
150
|
-
const { result } = renderHook(() => useQuery(mockQuery)
|
|
176
|
+
const { result } = renderHook(() => useQuery(() => mockQuery, []), {
|
|
177
|
+
wrapper: createWrapper(mockClient),
|
|
178
|
+
});
|
|
151
179
|
|
|
152
180
|
await waitFor(() => {
|
|
153
|
-
expect(result.current.
|
|
181
|
+
expect(result.current.loading).toBe(false);
|
|
154
182
|
});
|
|
155
|
-
});
|
|
156
183
|
|
|
157
|
-
|
|
158
|
-
const mockQuery = createMockQueryResult<{ id: string; name: string }>();
|
|
159
|
-
|
|
160
|
-
const { result } = renderHook(() => useQuery(mockQuery, { skip: true }));
|
|
161
|
-
|
|
162
|
-
expect(result.current.loading).toBe(false);
|
|
163
|
-
expect(result.current.data).toBe(null);
|
|
184
|
+
expect(result.current.error?.message).toBe("String error");
|
|
164
185
|
});
|
|
165
186
|
|
|
166
|
-
test("
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
expect(result.current.loading).toBe(false);
|
|
170
|
-
expect(result.current.data).toBe(null);
|
|
171
|
-
expect(result.current.error).toBe(null);
|
|
172
|
-
});
|
|
187
|
+
test("skips query when skip option is true", () => {
|
|
188
|
+
const mockClient = createMockClient();
|
|
189
|
+
const mockQuery = createMockQueryResult<{ id: string }>();
|
|
173
190
|
|
|
174
|
-
|
|
175
|
-
|
|
191
|
+
const { result } = renderHook(() => useQuery(() => mockQuery, [], { skip: true }), {
|
|
192
|
+
wrapper: createWrapper(mockClient),
|
|
193
|
+
});
|
|
176
194
|
|
|
177
195
|
expect(result.current.loading).toBe(false);
|
|
178
196
|
expect(result.current.data).toBe(null);
|
|
179
197
|
expect(result.current.error).toBe(null);
|
|
180
198
|
});
|
|
181
199
|
|
|
182
|
-
test("handles
|
|
183
|
-
const
|
|
184
|
-
const accessor = () => mockQuery;
|
|
185
|
-
|
|
186
|
-
const { result } = renderHook(() => useQuery(accessor));
|
|
200
|
+
test("handles null query from accessor", () => {
|
|
201
|
+
const mockClient = createMockClient();
|
|
187
202
|
|
|
188
|
-
|
|
189
|
-
|
|
203
|
+
const { result } = renderHook(() => useQuery(() => null, []), {
|
|
204
|
+
wrapper: createWrapper(mockClient),
|
|
190
205
|
});
|
|
191
206
|
|
|
192
|
-
await waitFor(() => {
|
|
193
|
-
expect(result.current.data).toEqual({ id: "123", name: "John" });
|
|
194
|
-
});
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
test("handles accessor function returning null", () => {
|
|
198
|
-
const accessor = () => null;
|
|
199
|
-
|
|
200
|
-
const { result } = renderHook(() => useQuery(accessor));
|
|
201
|
-
|
|
202
207
|
expect(result.current.loading).toBe(false);
|
|
203
208
|
expect(result.current.data).toBe(null);
|
|
209
|
+
expect(result.current.error).toBe(null);
|
|
204
210
|
});
|
|
205
211
|
|
|
206
|
-
test("handles
|
|
207
|
-
const
|
|
212
|
+
test("handles undefined query from accessor", () => {
|
|
213
|
+
const mockClient = createMockClient();
|
|
208
214
|
|
|
209
|
-
const { result } = renderHook(() => useQuery(
|
|
215
|
+
const { result } = renderHook(() => useQuery(() => undefined, []), {
|
|
216
|
+
wrapper: createWrapper(mockClient),
|
|
217
|
+
});
|
|
210
218
|
|
|
211
219
|
expect(result.current.loading).toBe(false);
|
|
212
220
|
expect(result.current.data).toBe(null);
|
|
221
|
+
expect(result.current.error).toBe(null);
|
|
213
222
|
});
|
|
214
223
|
|
|
215
224
|
test("updates when query subscription emits", async () => {
|
|
216
|
-
const
|
|
225
|
+
const mockClient = createMockClient();
|
|
226
|
+
const mockQuery = createMockQueryResult<{ count: number }>();
|
|
217
227
|
|
|
218
|
-
const { result } = renderHook(() => useQuery(mockQuery)
|
|
228
|
+
const { result } = renderHook(() => useQuery(() => mockQuery, []), {
|
|
229
|
+
wrapper: createWrapper(mockClient),
|
|
230
|
+
});
|
|
219
231
|
|
|
220
|
-
//
|
|
232
|
+
// Initial value
|
|
221
233
|
act(() => {
|
|
222
|
-
mockQuery._setValue({
|
|
234
|
+
mockQuery._setValue({ count: 1 });
|
|
223
235
|
});
|
|
224
236
|
|
|
225
237
|
await waitFor(() => {
|
|
226
|
-
expect(result.current.data?.
|
|
238
|
+
expect(result.current.data?.count).toBe(1);
|
|
227
239
|
});
|
|
228
240
|
|
|
229
241
|
// Update value via subscription
|
|
230
242
|
act(() => {
|
|
231
|
-
mockQuery._setValue({
|
|
243
|
+
mockQuery._setValue({ count: 2 });
|
|
232
244
|
});
|
|
233
245
|
|
|
234
246
|
await waitFor(() => {
|
|
235
|
-
expect(result.current.data?.
|
|
247
|
+
expect(result.current.data?.count).toBe(2);
|
|
236
248
|
});
|
|
237
249
|
});
|
|
238
250
|
|
|
239
251
|
test("refetch reloads the query", async () => {
|
|
240
|
-
const
|
|
252
|
+
const mockClient = createMockClient();
|
|
253
|
+
const mockQuery = createMockQueryResult<{ id: string }>({ id: "initial" });
|
|
241
254
|
|
|
242
|
-
const { result } = renderHook(() => useQuery(mockQuery)
|
|
243
|
-
|
|
244
|
-
// Initial load
|
|
245
|
-
act(() => {
|
|
246
|
-
mockQuery._setValue({ id: "123", name: "John" });
|
|
255
|
+
const { result } = renderHook(() => useQuery(() => mockQuery, []), {
|
|
256
|
+
wrapper: createWrapper(mockClient),
|
|
247
257
|
});
|
|
248
258
|
|
|
249
259
|
await waitFor(() => {
|
|
250
|
-
expect(result.current.
|
|
260
|
+
expect(result.current.loading).toBe(false);
|
|
251
261
|
});
|
|
252
262
|
|
|
253
|
-
|
|
263
|
+
expect(result.current.data?.id).toBe("initial");
|
|
264
|
+
|
|
265
|
+
// Refetch
|
|
254
266
|
act(() => {
|
|
255
267
|
result.current.refetch();
|
|
256
268
|
});
|
|
257
269
|
|
|
258
|
-
|
|
259
|
-
|
|
270
|
+
expect(result.current.loading).toBe(true);
|
|
271
|
+
|
|
260
272
|
await waitFor(() => {
|
|
261
273
|
expect(result.current.loading).toBe(false);
|
|
262
274
|
});
|
|
263
|
-
|
|
264
|
-
expect(result.current.data).toEqual({ id: "123", name: "John" });
|
|
265
275
|
});
|
|
266
276
|
|
|
267
277
|
test("refetch handles errors", async () => {
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
const
|
|
278
|
+
const mockClient = createMockClient();
|
|
279
|
+
let shouldFail = false;
|
|
280
|
+
const mockQuery = {
|
|
281
|
+
subscribe: () => () => {},
|
|
282
|
+
then: (onFulfilled: any, onRejected: any) => {
|
|
283
|
+
if (shouldFail) {
|
|
284
|
+
return Promise.reject(new Error("Refetch failed")).then(onFulfilled, onRejected);
|
|
285
|
+
}
|
|
286
|
+
return Promise.resolve({ id: "test" }).then(onFulfilled, onRejected);
|
|
287
|
+
},
|
|
288
|
+
} as unknown as QueryResult<{ id: string }>;
|
|
271
289
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
mockQuery._setValue({ id: "123", name: "John" });
|
|
290
|
+
const { result } = renderHook(() => useQuery(() => mockQuery, []), {
|
|
291
|
+
wrapper: createWrapper(mockClient),
|
|
275
292
|
});
|
|
276
293
|
|
|
277
294
|
await waitFor(() => {
|
|
278
|
-
expect(result.current.
|
|
295
|
+
expect(result.current.loading).toBe(false);
|
|
279
296
|
});
|
|
280
297
|
|
|
281
|
-
|
|
282
|
-
const failingQuery = {
|
|
283
|
-
subscribe: () => () => {},
|
|
284
|
-
then: (onFulfilled: any, onRejected: any) => {
|
|
285
|
-
return Promise.reject(new Error("Refetch failed")).then(onFulfilled, onRejected);
|
|
286
|
-
},
|
|
287
|
-
} as unknown as QueryResult<{ id: string; name: string }>;
|
|
298
|
+
shouldFail = true;
|
|
288
299
|
|
|
289
|
-
|
|
290
|
-
|
|
300
|
+
act(() => {
|
|
301
|
+
result.current.refetch();
|
|
302
|
+
});
|
|
291
303
|
|
|
292
304
|
await waitFor(() => {
|
|
293
|
-
expect(
|
|
305
|
+
expect(result.current.loading).toBe(false);
|
|
294
306
|
});
|
|
307
|
+
|
|
308
|
+
expect(result.current.error?.message).toBe("Refetch failed");
|
|
295
309
|
});
|
|
296
310
|
|
|
297
|
-
test("refetch does nothing when query is null", () => {
|
|
298
|
-
const
|
|
311
|
+
test("refetch does nothing when query is null", async () => {
|
|
312
|
+
const mockClient = createMockClient();
|
|
313
|
+
|
|
314
|
+
const { result } = renderHook(() => useQuery(() => null, []), {
|
|
315
|
+
wrapper: createWrapper(mockClient),
|
|
316
|
+
});
|
|
299
317
|
|
|
318
|
+
// Should not throw
|
|
300
319
|
act(() => {
|
|
301
320
|
result.current.refetch();
|
|
302
321
|
});
|
|
303
322
|
|
|
304
323
|
expect(result.current.loading).toBe(false);
|
|
305
|
-
expect(result.current.data).toBe(null);
|
|
306
324
|
});
|
|
307
325
|
|
|
308
|
-
test("refetch does nothing when skip is true", () => {
|
|
309
|
-
const
|
|
326
|
+
test("refetch does nothing when skip is true", async () => {
|
|
327
|
+
const mockClient = createMockClient();
|
|
328
|
+
const mockQuery = createMockQueryResult<{ id: string }>();
|
|
310
329
|
|
|
311
|
-
const { result } = renderHook(() => useQuery(mockQuery, { skip: true })
|
|
330
|
+
const { result } = renderHook(() => useQuery(() => mockQuery, [], { skip: true }), {
|
|
331
|
+
wrapper: createWrapper(mockClient),
|
|
332
|
+
});
|
|
312
333
|
|
|
334
|
+
// Should not throw
|
|
313
335
|
act(() => {
|
|
314
336
|
result.current.refetch();
|
|
315
337
|
});
|
|
316
338
|
|
|
317
339
|
expect(result.current.loading).toBe(false);
|
|
318
|
-
expect(result.current.data).toBe(null);
|
|
319
340
|
});
|
|
320
341
|
|
|
321
342
|
test("refetch with non-Error rejection", async () => {
|
|
322
|
-
|
|
343
|
+
const mockClient = createMockClient();
|
|
344
|
+
let callCount = 0;
|
|
323
345
|
const mockQuery = {
|
|
324
346
|
subscribe: () => () => {},
|
|
325
347
|
then: (onFulfilled: any, onRejected: any) => {
|
|
326
|
-
|
|
327
|
-
|
|
348
|
+
callCount++;
|
|
349
|
+
if (callCount > 1) {
|
|
350
|
+
return Promise.reject("String error on refetch").then(onFulfilled, onRejected);
|
|
328
351
|
}
|
|
329
|
-
return Promise.resolve({ id: "
|
|
352
|
+
return Promise.resolve({ id: "test" }).then(onFulfilled, onRejected);
|
|
330
353
|
},
|
|
331
|
-
} as unknown as QueryResult<{ id: string
|
|
354
|
+
} as unknown as QueryResult<{ id: string }>;
|
|
332
355
|
|
|
333
|
-
const { result } = renderHook(() => useQuery(mockQuery)
|
|
356
|
+
const { result } = renderHook(() => useQuery(() => mockQuery, []), {
|
|
357
|
+
wrapper: createWrapper(mockClient),
|
|
358
|
+
});
|
|
334
359
|
|
|
335
360
|
await waitFor(() => {
|
|
336
|
-
expect(result.current.
|
|
361
|
+
expect(result.current.loading).toBe(false);
|
|
337
362
|
});
|
|
338
363
|
|
|
339
|
-
// Make it fail on refetch
|
|
340
|
-
shouldFail = true;
|
|
341
|
-
|
|
342
364
|
act(() => {
|
|
343
365
|
result.current.refetch();
|
|
344
366
|
});
|
|
345
367
|
|
|
346
368
|
await waitFor(() => {
|
|
347
|
-
expect(result.current.
|
|
369
|
+
expect(result.current.loading).toBe(false);
|
|
348
370
|
});
|
|
371
|
+
|
|
372
|
+
expect(result.current.error?.message).toBe("String error on refetch");
|
|
349
373
|
});
|
|
350
374
|
|
|
351
375
|
test("cleans up subscription on unmount", async () => {
|
|
352
|
-
const
|
|
376
|
+
const mockClient = createMockClient();
|
|
353
377
|
let unsubscribeCalled = false;
|
|
378
|
+
const mockQuery = {
|
|
379
|
+
subscribe: () => {
|
|
380
|
+
return () => {
|
|
381
|
+
unsubscribeCalled = true;
|
|
382
|
+
};
|
|
383
|
+
},
|
|
384
|
+
then: (onFulfilled: any) => Promise.resolve({ id: "test" }).then(onFulfilled),
|
|
385
|
+
} as unknown as QueryResult<{ id: string }>;
|
|
354
386
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
const originalUnsubscribe = originalSubscribe.call(mockQuery, callback);
|
|
359
|
-
return () => {
|
|
360
|
-
unsubscribeCalled = true;
|
|
361
|
-
originalUnsubscribe();
|
|
362
|
-
};
|
|
363
|
-
};
|
|
364
|
-
|
|
365
|
-
const { unmount } = renderHook(() => useQuery(mockQuery));
|
|
387
|
+
const { unmount } = renderHook(() => useQuery(() => mockQuery, []), {
|
|
388
|
+
wrapper: createWrapper(mockClient),
|
|
389
|
+
});
|
|
366
390
|
|
|
367
391
|
unmount();
|
|
368
392
|
|
|
@@ -370,99 +394,139 @@ describe("useQuery", () => {
|
|
|
370
394
|
});
|
|
371
395
|
|
|
372
396
|
test("does not update state after unmount", async () => {
|
|
373
|
-
const
|
|
397
|
+
const mockClient = createMockClient();
|
|
398
|
+
const mockQuery = createMockQueryResult<{ id: string }>();
|
|
374
399
|
|
|
375
|
-
const { unmount } = renderHook(() => useQuery(mockQuery)
|
|
400
|
+
const { result, unmount } = renderHook(() => useQuery(() => mockQuery, []), {
|
|
401
|
+
wrapper: createWrapper(mockClient),
|
|
402
|
+
});
|
|
376
403
|
|
|
377
|
-
// Unmount before query resolves
|
|
378
404
|
unmount();
|
|
379
405
|
|
|
380
|
-
//
|
|
406
|
+
// This should not cause errors or state updates
|
|
381
407
|
act(() => {
|
|
382
|
-
mockQuery._setValue({ id: "
|
|
408
|
+
mockQuery._setValue({ id: "after-unmount" });
|
|
383
409
|
});
|
|
384
410
|
|
|
385
|
-
//
|
|
386
|
-
|
|
387
|
-
expect(true).toBe(true);
|
|
411
|
+
// Result should still be from before unmount
|
|
412
|
+
expect(result.current.data).toBe(null);
|
|
388
413
|
});
|
|
389
414
|
|
|
390
|
-
test("handles query change", async () => {
|
|
391
|
-
const
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
let currentQuery = mockQuery1;
|
|
395
|
-
const { result, rerender } = renderHook(() => useQuery(currentQuery));
|
|
415
|
+
test("handles query change via deps", async () => {
|
|
416
|
+
const mockClient = createMockClient();
|
|
417
|
+
const mockQuery1 = createMockQueryResult<{ id: string }>({ id: "query1" });
|
|
418
|
+
const mockQuery2 = createMockQueryResult<{ id: string }>({ id: "query2" });
|
|
396
419
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
420
|
+
let useQuery1 = true;
|
|
421
|
+
const { result, rerender } = renderHook(() => useQuery(() => (useQuery1 ? mockQuery1 : mockQuery2), [useQuery1]), {
|
|
422
|
+
wrapper: createWrapper(mockClient),
|
|
400
423
|
});
|
|
401
424
|
|
|
402
425
|
await waitFor(() => {
|
|
403
|
-
expect(result.current.data?.
|
|
426
|
+
expect(result.current.data?.id).toBe("query1");
|
|
404
427
|
});
|
|
405
428
|
|
|
406
|
-
// Change to
|
|
407
|
-
|
|
429
|
+
// Change to query2
|
|
430
|
+
useQuery1 = false;
|
|
408
431
|
rerender();
|
|
409
432
|
|
|
410
|
-
expect(result.current.loading).toBe(true);
|
|
411
|
-
|
|
412
|
-
// Load second query
|
|
413
|
-
act(() => {
|
|
414
|
-
mockQuery2._setValue({ id: "2", name: "Second" });
|
|
415
|
-
});
|
|
416
|
-
|
|
417
433
|
await waitFor(() => {
|
|
418
|
-
expect(result.current.data?.
|
|
434
|
+
expect(result.current.data?.id).toBe("query2");
|
|
419
435
|
});
|
|
420
436
|
});
|
|
421
437
|
|
|
422
438
|
test("handles skip option change from true to false", async () => {
|
|
423
|
-
const
|
|
439
|
+
const mockClient = createMockClient();
|
|
440
|
+
const mockQuery = createMockQueryResult<{ id: string }>({ id: "test" });
|
|
424
441
|
|
|
425
442
|
let skip = true;
|
|
426
|
-
const { result, rerender } = renderHook(() => useQuery(mockQuery, { skip })
|
|
443
|
+
const { result, rerender } = renderHook(() => useQuery(() => mockQuery, [], { skip }), {
|
|
444
|
+
wrapper: createWrapper(mockClient),
|
|
445
|
+
});
|
|
427
446
|
|
|
428
447
|
expect(result.current.loading).toBe(false);
|
|
448
|
+
expect(result.current.data).toBe(null);
|
|
429
449
|
|
|
430
|
-
//
|
|
450
|
+
// Enable query
|
|
431
451
|
skip = false;
|
|
432
452
|
rerender();
|
|
433
453
|
|
|
434
|
-
expect(result.current.loading).toBe(true);
|
|
435
|
-
|
|
436
|
-
act(() => {
|
|
437
|
-
mockQuery._setValue({ id: "123", name: "John" });
|
|
438
|
-
});
|
|
439
|
-
|
|
440
454
|
await waitFor(() => {
|
|
441
|
-
expect(result.current.data).
|
|
455
|
+
expect(result.current.data?.id).toBe("test");
|
|
442
456
|
});
|
|
443
457
|
});
|
|
444
458
|
|
|
445
459
|
test("handles skip option change from false to true", async () => {
|
|
446
|
-
const
|
|
460
|
+
const mockClient = createMockClient();
|
|
461
|
+
const mockQuery = createMockQueryResult<{ id: string }>({ id: "test" });
|
|
447
462
|
|
|
448
463
|
let skip = false;
|
|
449
|
-
const { result, rerender } = renderHook(() => useQuery(mockQuery, { skip })
|
|
450
|
-
|
|
451
|
-
act(() => {
|
|
452
|
-
mockQuery._setValue({ id: "123", name: "John" });
|
|
464
|
+
const { result, rerender } = renderHook(() => useQuery(() => mockQuery, [], { skip }), {
|
|
465
|
+
wrapper: createWrapper(mockClient),
|
|
453
466
|
});
|
|
454
467
|
|
|
455
468
|
await waitFor(() => {
|
|
456
|
-
expect(result.current.data).
|
|
469
|
+
expect(result.current.data?.id).toBe("test");
|
|
457
470
|
});
|
|
458
471
|
|
|
459
|
-
//
|
|
472
|
+
// Disable query
|
|
460
473
|
skip = true;
|
|
461
474
|
rerender();
|
|
462
475
|
|
|
476
|
+
await waitFor(() => {
|
|
477
|
+
expect(result.current.data).toBe(null);
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
expect(result.current.loading).toBe(false);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
test("select transforms the data", async () => {
|
|
484
|
+
const mockClient = createMockClient();
|
|
485
|
+
const mockQuery = createMockQueryResult<{ id: string; name: string }>({
|
|
486
|
+
id: "123",
|
|
487
|
+
name: "John",
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
const { result } = renderHook(
|
|
491
|
+
() =>
|
|
492
|
+
useQuery(() => mockQuery, [], {
|
|
493
|
+
select: (data) => data.name.toUpperCase(),
|
|
494
|
+
}),
|
|
495
|
+
{ wrapper: createWrapper(mockClient) },
|
|
496
|
+
);
|
|
497
|
+
|
|
498
|
+
await waitFor(() => {
|
|
499
|
+
expect(result.current.loading).toBe(false);
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
expect(result.current.data).toBe("JOHN");
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
test("Route + Params pattern works", async () => {
|
|
506
|
+
const mockClient = createMockClient();
|
|
507
|
+
const mockQuery = createMockQueryResult<{ id: string }>({ id: "user-123" });
|
|
508
|
+
const route = (_params: { id: string }) => mockQuery;
|
|
509
|
+
|
|
510
|
+
const { result } = renderHook(() => useQuery(() => route, { id: "123" }), {
|
|
511
|
+
wrapper: createWrapper(mockClient),
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
await waitFor(() => {
|
|
515
|
+
expect(result.current.loading).toBe(false);
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
expect(result.current.data?.id).toBe("user-123");
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
test("Route + Params with null route", async () => {
|
|
522
|
+
const mockClient = createMockClient();
|
|
523
|
+
|
|
524
|
+
const { result } = renderHook(() => useQuery(() => null, { id: "123" }), {
|
|
525
|
+
wrapper: createWrapper(mockClient),
|
|
526
|
+
});
|
|
527
|
+
|
|
463
528
|
expect(result.current.loading).toBe(false);
|
|
464
529
|
expect(result.current.data).toBe(null);
|
|
465
|
-
expect(result.current.error).toBe(null);
|
|
466
530
|
});
|
|
467
531
|
});
|
|
468
532
|
|
|
@@ -472,39 +536,40 @@ describe("useQuery", () => {
|
|
|
472
536
|
|
|
473
537
|
describe("useMutation", () => {
|
|
474
538
|
test("executes mutation and returns result", async () => {
|
|
475
|
-
const
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
};
|
|
539
|
+
const mockClient = createMockClient();
|
|
540
|
+
const mockMutation = async (_input: { title: string }): Promise<MutationResult<{ id: string }>> => {
|
|
541
|
+
return { data: { id: "new-123" } };
|
|
479
542
|
};
|
|
480
543
|
|
|
481
|
-
const { result } = renderHook(() => useMutation(
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
expect(result.current.data).toBe(null);
|
|
544
|
+
const { result } = renderHook(() => useMutation(() => mockMutation), {
|
|
545
|
+
wrapper: createWrapper(mockClient),
|
|
546
|
+
});
|
|
485
547
|
|
|
486
|
-
let mutationResult: MutationResult<{ id: string
|
|
548
|
+
let mutationResult: MutationResult<{ id: string }> | undefined;
|
|
487
549
|
await act(async () => {
|
|
488
|
-
mutationResult = await result.current.mutate({
|
|
550
|
+
mutationResult = await result.current.mutate({ title: "Test" });
|
|
489
551
|
});
|
|
490
552
|
|
|
491
|
-
expect(mutationResult?.data).
|
|
492
|
-
expect(result.current.data).
|
|
553
|
+
expect(mutationResult?.data?.id).toBe("new-123");
|
|
554
|
+
expect(result.current.data?.id).toBe("new-123");
|
|
493
555
|
expect(result.current.loading).toBe(false);
|
|
494
556
|
});
|
|
495
557
|
|
|
496
558
|
test("handles mutation error", async () => {
|
|
497
|
-
const
|
|
559
|
+
const mockClient = createMockClient();
|
|
560
|
+
const mockMutation = async (): Promise<MutationResult<{ id: string }>> => {
|
|
498
561
|
throw new Error("Mutation failed");
|
|
499
562
|
};
|
|
500
563
|
|
|
501
|
-
const { result } = renderHook(() => useMutation(
|
|
564
|
+
const { result } = renderHook(() => useMutation(() => mockMutation), {
|
|
565
|
+
wrapper: createWrapper(mockClient),
|
|
566
|
+
});
|
|
502
567
|
|
|
503
568
|
await act(async () => {
|
|
504
569
|
try {
|
|
505
|
-
await result.current.mutate({
|
|
570
|
+
await result.current.mutate({ title: "Test" });
|
|
506
571
|
} catch {
|
|
507
|
-
// Expected
|
|
572
|
+
// Expected
|
|
508
573
|
}
|
|
509
574
|
});
|
|
510
575
|
|
|
@@ -513,46 +578,49 @@ describe("useMutation", () => {
|
|
|
513
578
|
});
|
|
514
579
|
|
|
515
580
|
test("handles non-Error exception in mutation", async () => {
|
|
516
|
-
const
|
|
581
|
+
const mockClient = createMockClient();
|
|
582
|
+
const mockMutation = async (): Promise<MutationResult<{ id: string }>> => {
|
|
517
583
|
throw "String error";
|
|
518
584
|
};
|
|
519
585
|
|
|
520
|
-
const { result } = renderHook(() => useMutation(
|
|
586
|
+
const { result } = renderHook(() => useMutation(() => mockMutation), {
|
|
587
|
+
wrapper: createWrapper(mockClient),
|
|
588
|
+
});
|
|
521
589
|
|
|
522
590
|
await act(async () => {
|
|
523
591
|
try {
|
|
524
|
-
await result.current.mutate({
|
|
592
|
+
await result.current.mutate({ title: "Test" });
|
|
525
593
|
} catch {
|
|
526
|
-
// Expected
|
|
594
|
+
// Expected
|
|
527
595
|
}
|
|
528
596
|
});
|
|
529
597
|
|
|
530
598
|
expect(result.current.error?.message).toBe("String error");
|
|
531
|
-
expect(result.current.loading).toBe(false);
|
|
532
599
|
});
|
|
533
600
|
|
|
534
601
|
test("shows loading state during mutation", async () => {
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
602
|
+
const mockClient = createMockClient();
|
|
603
|
+
let resolvePromise: () => void;
|
|
604
|
+
const mockMutation = async (): Promise<MutationResult<{ id: string }>> => {
|
|
605
|
+
await new Promise<void>((resolve) => {
|
|
606
|
+
resolvePromise = resolve;
|
|
539
607
|
});
|
|
608
|
+
return { data: { id: "test" } };
|
|
540
609
|
};
|
|
541
610
|
|
|
542
|
-
const { result } = renderHook(() => useMutation(
|
|
611
|
+
const { result } = renderHook(() => useMutation(() => mockMutation), {
|
|
612
|
+
wrapper: createWrapper(mockClient),
|
|
613
|
+
});
|
|
543
614
|
|
|
544
|
-
|
|
545
|
-
let mutationPromise: Promise<MutationResult<{ id: string }>> | undefined;
|
|
615
|
+
let mutationPromise: Promise<any>;
|
|
546
616
|
act(() => {
|
|
547
|
-
mutationPromise = result.current.mutate({
|
|
617
|
+
mutationPromise = result.current.mutate({ title: "Test" });
|
|
548
618
|
});
|
|
549
619
|
|
|
550
|
-
// Should be loading
|
|
551
620
|
expect(result.current.loading).toBe(true);
|
|
552
621
|
|
|
553
|
-
// Resolve mutation
|
|
554
622
|
await act(async () => {
|
|
555
|
-
|
|
623
|
+
resolvePromise!();
|
|
556
624
|
await mutationPromise;
|
|
557
625
|
});
|
|
558
626
|
|
|
@@ -560,14 +628,17 @@ describe("useMutation", () => {
|
|
|
560
628
|
});
|
|
561
629
|
|
|
562
630
|
test("reset clears mutation state", async () => {
|
|
563
|
-
const
|
|
564
|
-
|
|
631
|
+
const mockClient = createMockClient();
|
|
632
|
+
const mockMutation = async (): Promise<MutationResult<{ id: string }>> => {
|
|
633
|
+
return { data: { id: "test" } };
|
|
565
634
|
};
|
|
566
635
|
|
|
567
|
-
const { result } = renderHook(() => useMutation(
|
|
636
|
+
const { result } = renderHook(() => useMutation(() => mockMutation), {
|
|
637
|
+
wrapper: createWrapper(mockClient),
|
|
638
|
+
});
|
|
568
639
|
|
|
569
640
|
await act(async () => {
|
|
570
|
-
await result.current.mutate({
|
|
641
|
+
await result.current.mutate({ title: "Test" });
|
|
571
642
|
});
|
|
572
643
|
|
|
573
644
|
expect(result.current.data).not.toBe(null);
|
|
@@ -582,77 +653,89 @@ describe("useMutation", () => {
|
|
|
582
653
|
});
|
|
583
654
|
|
|
584
655
|
test("handles multiple mutations in sequence", async () => {
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
656
|
+
const mockClient = createMockClient();
|
|
657
|
+
let callCount = 0;
|
|
658
|
+
const mockMutation = async (_input: { title: string }): Promise<MutationResult<{ count: number }>> => {
|
|
659
|
+
callCount++;
|
|
660
|
+
return { data: { count: callCount } };
|
|
589
661
|
};
|
|
590
662
|
|
|
591
|
-
const { result } = renderHook(() => useMutation(
|
|
663
|
+
const { result } = renderHook(() => useMutation(() => mockMutation), {
|
|
664
|
+
wrapper: createWrapper(mockClient),
|
|
665
|
+
});
|
|
592
666
|
|
|
593
|
-
// First mutation
|
|
594
667
|
await act(async () => {
|
|
595
|
-
await result.current.mutate({
|
|
668
|
+
await result.current.mutate({ title: "First" });
|
|
596
669
|
});
|
|
670
|
+
expect(result.current.data?.count).toBe(1);
|
|
597
671
|
|
|
598
|
-
expect(result.current.data).toEqual({ id: "id-1", name: "First" });
|
|
599
|
-
|
|
600
|
-
// Second mutation
|
|
601
672
|
await act(async () => {
|
|
602
|
-
await result.current.mutate({
|
|
673
|
+
await result.current.mutate({ title: "Second" });
|
|
603
674
|
});
|
|
604
|
-
|
|
605
|
-
expect(result.current.data).toEqual({ id: "id-2", name: "Second" });
|
|
675
|
+
expect(result.current.data?.count).toBe(2);
|
|
606
676
|
});
|
|
607
677
|
|
|
608
678
|
test("clears error on successful mutation after previous error", async () => {
|
|
679
|
+
const mockClient = createMockClient();
|
|
609
680
|
let shouldFail = true;
|
|
610
|
-
const
|
|
681
|
+
const mockMutation = async (): Promise<MutationResult<{ id: string }>> => {
|
|
611
682
|
if (shouldFail) {
|
|
612
|
-
throw new Error("
|
|
683
|
+
throw new Error("Failed");
|
|
613
684
|
}
|
|
614
|
-
return { data: { id: "
|
|
685
|
+
return { data: { id: "success" } };
|
|
615
686
|
};
|
|
616
687
|
|
|
617
|
-
const { result } = renderHook(() => useMutation(
|
|
688
|
+
const { result } = renderHook(() => useMutation(() => mockMutation), {
|
|
689
|
+
wrapper: createWrapper(mockClient),
|
|
690
|
+
});
|
|
618
691
|
|
|
619
692
|
// First mutation fails
|
|
620
693
|
await act(async () => {
|
|
621
694
|
try {
|
|
622
|
-
await result.current.mutate({
|
|
695
|
+
await result.current.mutate({ title: "Test" });
|
|
623
696
|
} catch {
|
|
624
|
-
// Expected
|
|
697
|
+
// Expected
|
|
625
698
|
}
|
|
626
699
|
});
|
|
627
700
|
|
|
628
|
-
expect(result.current.error
|
|
701
|
+
expect(result.current.error).not.toBe(null);
|
|
629
702
|
|
|
630
703
|
// Second mutation succeeds
|
|
631
704
|
shouldFail = false;
|
|
632
705
|
await act(async () => {
|
|
633
|
-
await result.current.mutate({
|
|
706
|
+
await result.current.mutate({ title: "Test" });
|
|
634
707
|
});
|
|
635
708
|
|
|
636
709
|
expect(result.current.error).toBe(null);
|
|
637
|
-
expect(result.current.data).
|
|
710
|
+
expect(result.current.data?.id).toBe("success");
|
|
638
711
|
});
|
|
639
712
|
|
|
640
713
|
test("does not update state after unmount", async () => {
|
|
641
|
-
const
|
|
642
|
-
|
|
714
|
+
const mockClient = createMockClient();
|
|
715
|
+
let resolvePromise: () => void;
|
|
716
|
+
const mockMutation = async (): Promise<MutationResult<{ id: string }>> => {
|
|
717
|
+
await new Promise<void>((resolve) => {
|
|
718
|
+
resolvePromise = resolve;
|
|
719
|
+
});
|
|
720
|
+
return { data: { id: "test" } };
|
|
643
721
|
};
|
|
644
722
|
|
|
645
|
-
const { result, unmount } = renderHook(() => useMutation(
|
|
723
|
+
const { result, unmount } = renderHook(() => useMutation(() => mockMutation), {
|
|
724
|
+
wrapper: createWrapper(mockClient),
|
|
725
|
+
});
|
|
646
726
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
727
|
+
let mutationPromise: Promise<any>;
|
|
728
|
+
act(() => {
|
|
729
|
+
mutationPromise = result.current.mutate({ title: "Test" });
|
|
730
|
+
});
|
|
650
731
|
|
|
651
|
-
|
|
652
|
-
await mutationPromise;
|
|
732
|
+
unmount();
|
|
653
733
|
|
|
654
|
-
//
|
|
655
|
-
|
|
734
|
+
// Resolve after unmount - should not cause errors
|
|
735
|
+
await act(async () => {
|
|
736
|
+
resolvePromise!();
|
|
737
|
+
await mutationPromise;
|
|
738
|
+
});
|
|
656
739
|
});
|
|
657
740
|
});
|
|
658
741
|
|
|
@@ -662,48 +745,50 @@ describe("useMutation", () => {
|
|
|
662
745
|
|
|
663
746
|
describe("useLazyQuery", () => {
|
|
664
747
|
test("does not execute query on mount", () => {
|
|
665
|
-
const
|
|
748
|
+
const mockClient = createMockClient();
|
|
749
|
+
const mockQuery = createMockQueryResult<{ id: string }>();
|
|
666
750
|
|
|
667
|
-
const { result } = renderHook(() => useLazyQuery(mockQuery)
|
|
751
|
+
const { result } = renderHook(() => useLazyQuery(() => mockQuery, []), {
|
|
752
|
+
wrapper: createWrapper(mockClient),
|
|
753
|
+
});
|
|
668
754
|
|
|
669
755
|
expect(result.current.loading).toBe(false);
|
|
670
756
|
expect(result.current.data).toBe(null);
|
|
671
757
|
});
|
|
672
758
|
|
|
673
759
|
test("executes query when execute is called", async () => {
|
|
674
|
-
const
|
|
675
|
-
|
|
676
|
-
name: "John",
|
|
677
|
-
});
|
|
760
|
+
const mockClient = createMockClient();
|
|
761
|
+
const mockQuery = createMockQueryResult<{ id: string }>({ id: "lazy-123" });
|
|
678
762
|
|
|
679
|
-
const { result } = renderHook(() => useLazyQuery(mockQuery)
|
|
763
|
+
const { result } = renderHook(() => useLazyQuery(() => mockQuery, []), {
|
|
764
|
+
wrapper: createWrapper(mockClient),
|
|
765
|
+
});
|
|
680
766
|
|
|
681
|
-
let queryResult: { id: string; name: string } | undefined;
|
|
682
767
|
await act(async () => {
|
|
683
|
-
|
|
768
|
+
await result.current.execute();
|
|
684
769
|
});
|
|
685
770
|
|
|
686
|
-
expect(
|
|
687
|
-
expect(result.current.
|
|
771
|
+
expect(result.current.data?.id).toBe("lazy-123");
|
|
772
|
+
expect(result.current.loading).toBe(false);
|
|
688
773
|
});
|
|
689
774
|
|
|
690
775
|
test("handles query error", async () => {
|
|
691
|
-
|
|
692
|
-
const mockQuery =
|
|
693
|
-
|
|
694
|
-
|
|
776
|
+
const mockClient = createMockClient();
|
|
777
|
+
const mockQuery = {
|
|
778
|
+
then: (_: any, onRejected: any) => {
|
|
779
|
+
return Promise.reject(new Error("Query failed")).then(null, onRejected);
|
|
780
|
+
},
|
|
781
|
+
} as unknown as QueryResult<{ id: string }>;
|
|
695
782
|
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
mockQuery._setError(new Error("Query failed"));
|
|
783
|
+
const { result } = renderHook(() => useLazyQuery(() => mockQuery, []), {
|
|
784
|
+
wrapper: createWrapper(mockClient),
|
|
699
785
|
});
|
|
700
786
|
|
|
701
|
-
// Execute should throw
|
|
702
787
|
await act(async () => {
|
|
703
788
|
try {
|
|
704
789
|
await result.current.execute();
|
|
705
790
|
} catch {
|
|
706
|
-
// Expected
|
|
791
|
+
// Expected
|
|
707
792
|
}
|
|
708
793
|
});
|
|
709
794
|
|
|
@@ -711,19 +796,22 @@ describe("useLazyQuery", () => {
|
|
|
711
796
|
});
|
|
712
797
|
|
|
713
798
|
test("handles non-Error rejection", async () => {
|
|
799
|
+
const mockClient = createMockClient();
|
|
714
800
|
const mockQuery = {
|
|
715
|
-
then: (
|
|
716
|
-
return Promise.reject("String error").then(
|
|
801
|
+
then: (_: any, onRejected: any) => {
|
|
802
|
+
return Promise.reject("String error").then(null, onRejected);
|
|
717
803
|
},
|
|
718
804
|
} as unknown as QueryResult<{ id: string }>;
|
|
719
805
|
|
|
720
|
-
const { result } = renderHook(() => useLazyQuery(mockQuery)
|
|
806
|
+
const { result } = renderHook(() => useLazyQuery(() => mockQuery, []), {
|
|
807
|
+
wrapper: createWrapper(mockClient),
|
|
808
|
+
});
|
|
721
809
|
|
|
722
810
|
await act(async () => {
|
|
723
811
|
try {
|
|
724
812
|
await result.current.execute();
|
|
725
813
|
} catch {
|
|
726
|
-
// Expected
|
|
814
|
+
// Expected
|
|
727
815
|
}
|
|
728
816
|
});
|
|
729
817
|
|
|
@@ -731,12 +819,12 @@ describe("useLazyQuery", () => {
|
|
|
731
819
|
});
|
|
732
820
|
|
|
733
821
|
test("reset clears query state", async () => {
|
|
734
|
-
const
|
|
735
|
-
|
|
736
|
-
name: "John",
|
|
737
|
-
});
|
|
822
|
+
const mockClient = createMockClient();
|
|
823
|
+
const mockQuery = createMockQueryResult<{ id: string }>({ id: "test" });
|
|
738
824
|
|
|
739
|
-
const { result } = renderHook(() => useLazyQuery(mockQuery)
|
|
825
|
+
const { result } = renderHook(() => useLazyQuery(() => mockQuery, []), {
|
|
826
|
+
wrapper: createWrapper(mockClient),
|
|
827
|
+
});
|
|
740
828
|
|
|
741
829
|
await act(async () => {
|
|
742
830
|
await result.current.execute();
|
|
@@ -749,190 +837,201 @@ describe("useLazyQuery", () => {
|
|
|
749
837
|
});
|
|
750
838
|
|
|
751
839
|
expect(result.current.data).toBe(null);
|
|
752
|
-
expect(result.current.error).toBe(null);
|
|
753
|
-
expect(result.current.loading).toBe(false);
|
|
754
840
|
});
|
|
755
841
|
|
|
756
|
-
test("handles null query", async () => {
|
|
757
|
-
const
|
|
842
|
+
test("handles null query from accessor", async () => {
|
|
843
|
+
const mockClient = createMockClient();
|
|
758
844
|
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
queryResult = await result.current.execute();
|
|
845
|
+
const { result } = renderHook(() => useLazyQuery(() => null, []), {
|
|
846
|
+
wrapper: createWrapper(mockClient),
|
|
762
847
|
});
|
|
763
848
|
|
|
764
|
-
|
|
765
|
-
expect(result.current.data).toBe(null);
|
|
766
|
-
expect(result.current.loading).toBe(false);
|
|
767
|
-
});
|
|
768
|
-
|
|
769
|
-
test("handles undefined query", async () => {
|
|
770
|
-
const { result } = renderHook(() => useLazyQuery(undefined));
|
|
771
|
-
|
|
772
|
-
let queryResult: any;
|
|
849
|
+
let executeResult: any;
|
|
773
850
|
await act(async () => {
|
|
774
|
-
|
|
851
|
+
executeResult = await result.current.execute();
|
|
775
852
|
});
|
|
776
853
|
|
|
777
|
-
expect(
|
|
854
|
+
expect(executeResult).toBe(null);
|
|
778
855
|
expect(result.current.data).toBe(null);
|
|
779
|
-
expect(result.current.loading).toBe(false);
|
|
780
856
|
});
|
|
781
857
|
|
|
782
|
-
test("handles
|
|
783
|
-
const
|
|
784
|
-
id: "123",
|
|
785
|
-
name: "John",
|
|
786
|
-
});
|
|
787
|
-
const accessor = () => mockQuery;
|
|
788
|
-
|
|
789
|
-
const { result } = renderHook(() => useLazyQuery(accessor));
|
|
858
|
+
test("handles undefined query from accessor", async () => {
|
|
859
|
+
const mockClient = createMockClient();
|
|
790
860
|
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
queryResult = await result.current.execute();
|
|
861
|
+
const { result } = renderHook(() => useLazyQuery(() => undefined, []), {
|
|
862
|
+
wrapper: createWrapper(mockClient),
|
|
794
863
|
});
|
|
795
864
|
|
|
796
|
-
|
|
797
|
-
});
|
|
798
|
-
|
|
799
|
-
test("handles accessor function returning null", async () => {
|
|
800
|
-
const accessor = () => null;
|
|
801
|
-
|
|
802
|
-
const { result } = renderHook(() => useLazyQuery(accessor));
|
|
803
|
-
|
|
804
|
-
let queryResult: any;
|
|
865
|
+
let executeResult: any;
|
|
805
866
|
await act(async () => {
|
|
806
|
-
|
|
867
|
+
executeResult = await result.current.execute();
|
|
807
868
|
});
|
|
808
869
|
|
|
809
|
-
expect(
|
|
870
|
+
expect(executeResult).toBe(null);
|
|
871
|
+
expect(result.current.data).toBe(null);
|
|
810
872
|
});
|
|
811
873
|
|
|
812
874
|
test("uses latest query value from accessor on execute", async () => {
|
|
813
|
-
|
|
814
|
-
const mockQuery1 = createMockQueryResult<string>("
|
|
815
|
-
const mockQuery2 = createMockQueryResult<string>("
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
875
|
+
const mockClient = createMockClient();
|
|
876
|
+
const mockQuery1 = createMockQueryResult<{ id: string }>({ id: "query1" });
|
|
877
|
+
const mockQuery2 = createMockQueryResult<{ id: string }>({ id: "query2" });
|
|
878
|
+
|
|
879
|
+
let useQuery1 = true;
|
|
880
|
+
const { result, rerender } = renderHook(
|
|
881
|
+
() => useLazyQuery(() => (useQuery1 ? mockQuery1 : mockQuery2), [useQuery1]),
|
|
882
|
+
{ wrapper: createWrapper(mockClient) },
|
|
883
|
+
);
|
|
884
|
+
|
|
885
|
+
// Change to query2 before executing
|
|
886
|
+
useQuery1 = false;
|
|
887
|
+
rerender();
|
|
820
888
|
|
|
821
|
-
// First execute
|
|
822
|
-
let queryResult1: string | undefined;
|
|
823
889
|
await act(async () => {
|
|
824
|
-
|
|
890
|
+
await result.current.execute();
|
|
825
891
|
});
|
|
826
892
|
|
|
827
|
-
expect(
|
|
828
|
-
|
|
829
|
-
// Change accessor to return different query
|
|
830
|
-
currentValue = "second";
|
|
893
|
+
expect(result.current.data?.id).toBe("query2");
|
|
894
|
+
});
|
|
831
895
|
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
896
|
+
test("shows loading state during execution", async () => {
|
|
897
|
+
const mockClient = createMockClient();
|
|
898
|
+
// Create the pending promise upfront so resolvePromise is assigned immediately
|
|
899
|
+
let resolvePromise!: (value: { id: string }) => void;
|
|
900
|
+
const pendingPromise = new Promise<{ id: string }>((resolve) => {
|
|
901
|
+
resolvePromise = resolve;
|
|
836
902
|
});
|
|
903
|
+
const mockQuery = {
|
|
904
|
+
then: (onFulfilled: any, onRejected?: any) => {
|
|
905
|
+
return pendingPromise.then(onFulfilled, onRejected);
|
|
906
|
+
},
|
|
907
|
+
} as unknown as QueryResult<{ id: string }>;
|
|
837
908
|
|
|
838
|
-
|
|
839
|
-
|
|
909
|
+
const { result } = renderHook(() => useLazyQuery(() => mockQuery, []), {
|
|
910
|
+
wrapper: createWrapper(mockClient),
|
|
911
|
+
});
|
|
840
912
|
|
|
841
|
-
|
|
842
|
-
|
|
913
|
+
let executePromise: Promise<any>;
|
|
914
|
+
act(() => {
|
|
915
|
+
executePromise = result.current.execute();
|
|
916
|
+
});
|
|
843
917
|
|
|
844
|
-
|
|
918
|
+
expect(result.current.loading).toBe(true);
|
|
845
919
|
|
|
846
|
-
// Execute and set value
|
|
847
|
-
let executePromise: Promise<{ id: string }>;
|
|
848
920
|
await act(async () => {
|
|
849
|
-
|
|
850
|
-
mockQuery._setValue({ id: "123" });
|
|
921
|
+
resolvePromise({ id: "test" });
|
|
851
922
|
await executePromise;
|
|
852
923
|
});
|
|
853
924
|
|
|
854
925
|
expect(result.current.loading).toBe(false);
|
|
855
|
-
expect(result.current.data).toEqual({ id: "123" });
|
|
856
926
|
});
|
|
857
927
|
|
|
858
928
|
test("does not update state after unmount", async () => {
|
|
859
|
-
const
|
|
929
|
+
const mockClient = createMockClient();
|
|
930
|
+
// Create the pending promise upfront so resolvePromise is assigned immediately
|
|
931
|
+
let resolvePromise!: (value: { id: string }) => void;
|
|
932
|
+
const pendingPromise = new Promise<{ id: string }>((resolve) => {
|
|
933
|
+
resolvePromise = resolve;
|
|
934
|
+
});
|
|
935
|
+
const mockQuery = {
|
|
936
|
+
then: (onFulfilled: any, onRejected?: any) => {
|
|
937
|
+
return pendingPromise.then(onFulfilled, onRejected);
|
|
938
|
+
},
|
|
939
|
+
} as unknown as QueryResult<{ id: string }>;
|
|
860
940
|
|
|
861
|
-
const { result, unmount } = renderHook(() => useLazyQuery(mockQuery)
|
|
941
|
+
const { result, unmount } = renderHook(() => useLazyQuery(() => mockQuery, []), {
|
|
942
|
+
wrapper: createWrapper(mockClient),
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
let executePromise: Promise<any>;
|
|
946
|
+
act(() => {
|
|
947
|
+
executePromise = result.current.execute();
|
|
948
|
+
});
|
|
862
949
|
|
|
863
|
-
// Start execution, unmount, then resolve
|
|
864
|
-
const executePromise = result.current.execute();
|
|
865
950
|
unmount();
|
|
866
951
|
|
|
867
|
-
// Resolve after unmount
|
|
952
|
+
// Resolve after unmount - should not cause errors
|
|
868
953
|
await act(async () => {
|
|
869
|
-
|
|
954
|
+
resolvePromise({ id: "test" });
|
|
870
955
|
await executePromise;
|
|
871
956
|
});
|
|
872
|
-
|
|
873
|
-
// Test passes if no error is thrown (state update after unmount would cause error)
|
|
874
|
-
expect(true).toBe(true);
|
|
875
957
|
});
|
|
876
958
|
|
|
877
|
-
test("
|
|
878
|
-
const
|
|
879
|
-
|
|
959
|
+
test("can execute multiple times", async () => {
|
|
960
|
+
const mockClient = createMockClient();
|
|
961
|
+
let callCount = 0;
|
|
962
|
+
const mockQuery = {
|
|
963
|
+
then: (onFulfilled: any) => {
|
|
964
|
+
callCount++;
|
|
965
|
+
return Promise.resolve({ count: callCount }).then(onFulfilled);
|
|
966
|
+
},
|
|
967
|
+
} as unknown as QueryResult<{ count: number }>;
|
|
880
968
|
|
|
881
|
-
const { result
|
|
882
|
-
|
|
969
|
+
const { result } = renderHook(() => useLazyQuery(() => mockQuery, []), {
|
|
970
|
+
wrapper: createWrapper(mockClient),
|
|
883
971
|
});
|
|
884
972
|
|
|
885
|
-
// First execution fails
|
|
886
973
|
await act(async () => {
|
|
887
|
-
|
|
888
|
-
mockQuery1._setError(new Error("Query failed"));
|
|
889
|
-
try {
|
|
890
|
-
await executePromise;
|
|
891
|
-
} catch {
|
|
892
|
-
// Expected error
|
|
893
|
-
}
|
|
974
|
+
await result.current.execute();
|
|
894
975
|
});
|
|
976
|
+
expect(result.current.data?.count).toBe(1);
|
|
895
977
|
|
|
896
|
-
|
|
978
|
+
await act(async () => {
|
|
979
|
+
await result.current.execute();
|
|
980
|
+
});
|
|
981
|
+
expect(result.current.data?.count).toBe(2);
|
|
982
|
+
});
|
|
897
983
|
|
|
898
|
-
|
|
899
|
-
|
|
984
|
+
test("Route + Params pattern works", async () => {
|
|
985
|
+
const mockClient = createMockClient();
|
|
986
|
+
const mockQuery = createMockQueryResult<{ id: string }>({ id: "user-123" });
|
|
987
|
+
const route = (_params: { id: string }) => mockQuery;
|
|
988
|
+
|
|
989
|
+
const { result } = renderHook(() => useLazyQuery(() => route, { id: "123" }), {
|
|
990
|
+
wrapper: createWrapper(mockClient),
|
|
991
|
+
});
|
|
900
992
|
|
|
901
|
-
// Second execution succeeds
|
|
902
993
|
await act(async () => {
|
|
903
994
|
await result.current.execute();
|
|
904
995
|
});
|
|
905
996
|
|
|
906
|
-
expect(result.current.
|
|
907
|
-
expect(result.current.data).toEqual({ id: "123" });
|
|
997
|
+
expect(result.current.data?.id).toBe("user-123");
|
|
908
998
|
});
|
|
909
999
|
|
|
910
|
-
test("
|
|
911
|
-
const
|
|
912
|
-
const mockQuery2 = createMockQueryResult<{ count: number }>();
|
|
1000
|
+
test("Route + Params with null route", async () => {
|
|
1001
|
+
const mockClient = createMockClient();
|
|
913
1002
|
|
|
914
|
-
const { result
|
|
915
|
-
|
|
1003
|
+
const { result } = renderHook(() => useLazyQuery(() => null, { id: "123" }), {
|
|
1004
|
+
wrapper: createWrapper(mockClient),
|
|
916
1005
|
});
|
|
917
1006
|
|
|
918
|
-
|
|
1007
|
+
let executeResult: any;
|
|
919
1008
|
await act(async () => {
|
|
920
|
-
|
|
921
|
-
mockQuery1._setValue({ count: 1 });
|
|
922
|
-
await executePromise;
|
|
1009
|
+
executeResult = await result.current.execute();
|
|
923
1010
|
});
|
|
924
1011
|
|
|
925
|
-
expect(
|
|
1012
|
+
expect(executeResult).toBe(null);
|
|
1013
|
+
expect(result.current.data).toBe(null);
|
|
1014
|
+
});
|
|
926
1015
|
|
|
927
|
-
|
|
928
|
-
|
|
1016
|
+
test("select transforms the data", async () => {
|
|
1017
|
+
const mockClient = createMockClient();
|
|
1018
|
+
const mockQuery = createMockQueryResult<{ id: string; name: string }>({
|
|
1019
|
+
id: "123",
|
|
1020
|
+
name: "John",
|
|
1021
|
+
});
|
|
1022
|
+
|
|
1023
|
+
const { result } = renderHook(
|
|
1024
|
+
() =>
|
|
1025
|
+
useLazyQuery(() => mockQuery, [], {
|
|
1026
|
+
select: (data) => data.name.toUpperCase(),
|
|
1027
|
+
}),
|
|
1028
|
+
{ wrapper: createWrapper(mockClient) },
|
|
1029
|
+
);
|
|
929
1030
|
|
|
930
1031
|
await act(async () => {
|
|
931
|
-
|
|
932
|
-
mockQuery2._setValue({ count: 2 });
|
|
933
|
-
await executePromise;
|
|
1032
|
+
await result.current.execute();
|
|
934
1033
|
});
|
|
935
1034
|
|
|
936
|
-
expect(result.current.data
|
|
1035
|
+
expect(result.current.data).toBe("JOHN");
|
|
937
1036
|
});
|
|
938
1037
|
});
|