@khanacademy/wonder-blocks-data 5.0.0 → 6.0.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 (85) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/es/index.js +778 -372
  3. package/dist/index.js +1203 -551
  4. package/legacy-docs.md +3 -0
  5. package/package.json +2 -2
  6. package/src/__docs__/_overview_.stories.mdx +18 -0
  7. package/src/__docs__/_overview_graphql.stories.mdx +35 -0
  8. package/src/__docs__/_overview_ssr_.stories.mdx +185 -0
  9. package/src/__docs__/_overview_testing_.stories.mdx +123 -0
  10. package/src/__docs__/exports.clear-shared-cache.stories.mdx +20 -0
  11. package/src/__docs__/exports.data-error.stories.mdx +23 -0
  12. package/src/__docs__/exports.data-errors.stories.mdx +23 -0
  13. package/src/{components/data.md → __docs__/exports.data.stories.mdx} +15 -18
  14. package/src/__docs__/exports.fulfill-all-data-requests.stories.mdx +24 -0
  15. package/src/__docs__/exports.gql-error.stories.mdx +23 -0
  16. package/src/__docs__/exports.gql-errors.stories.mdx +20 -0
  17. package/src/__docs__/exports.gql-router.stories.mdx +29 -0
  18. package/src/__docs__/exports.has-unfulfilled-requests.stories.mdx +20 -0
  19. package/src/{components/intercept-requests.md → __docs__/exports.intercept-requests.stories.mdx} +16 -1
  20. package/src/__docs__/exports.intialize-cache.stories.mdx +29 -0
  21. package/src/__docs__/exports.remove-all-from-cache.stories.mdx +24 -0
  22. package/src/__docs__/exports.remove-from-cache.stories.mdx +25 -0
  23. package/src/__docs__/exports.request-fulfillment.stories.mdx +36 -0
  24. package/src/__docs__/exports.scoped-in-memory-cache.stories.mdx +92 -0
  25. package/src/__docs__/exports.serializable-in-memory-cache.stories.mdx +112 -0
  26. package/src/__docs__/exports.status.stories.mdx +31 -0
  27. package/src/{components/track-data.md → __docs__/exports.track-data.stories.mdx} +15 -0
  28. package/src/__docs__/exports.use-cached-effect.stories.mdx +41 -0
  29. package/src/__docs__/exports.use-gql.stories.mdx +73 -0
  30. package/src/__docs__/exports.use-hydratable-effect.stories.mdx +43 -0
  31. package/src/__docs__/exports.use-server-effect.stories.mdx +38 -0
  32. package/src/__docs__/exports.use-shared-cache.stories.mdx +30 -0
  33. package/src/__docs__/exports.when-client-side.stories.mdx +33 -0
  34. package/src/__docs__/types.cached-response.stories.mdx +29 -0
  35. package/src/__docs__/types.error-options.stories.mdx +21 -0
  36. package/src/__docs__/types.gql-context.stories.mdx +20 -0
  37. package/src/__docs__/types.gql-fetch-fn.stories.mdx +24 -0
  38. package/src/__docs__/types.gql-fetch-options.stories.mdx +24 -0
  39. package/src/__docs__/types.gql-operation-type.stories.mdx +24 -0
  40. package/src/__docs__/types.gql-operation.stories.mdx +67 -0
  41. package/src/__docs__/types.response-cache.stories.mdx +33 -0
  42. package/src/__docs__/types.result.stories.mdx +39 -0
  43. package/src/__docs__/types.scoped-cache.stories.mdx +27 -0
  44. package/src/__docs__/types.valid-cache-data.stories.mdx +23 -0
  45. package/src/__tests__/__snapshots__/generated-snapshot.test.js.snap +0 -80
  46. package/src/__tests__/generated-snapshot.test.js +0 -24
  47. package/src/components/__tests__/data.test.js +149 -128
  48. package/src/components/data.js +22 -112
  49. package/src/components/intercept-requests.js +1 -1
  50. package/src/hooks/__tests__/__snapshots__/use-shared-cache.test.js.snap +8 -8
  51. package/src/hooks/__tests__/use-cached-effect.test.js +507 -0
  52. package/src/hooks/__tests__/use-gql-router-context.test.js +133 -0
  53. package/src/hooks/__tests__/use-gql.test.js +1 -30
  54. package/src/hooks/__tests__/use-hydratable-effect.test.js +728 -0
  55. package/src/hooks/__tests__/use-server-effect.test.js +39 -11
  56. package/src/hooks/use-cached-effect.js +225 -0
  57. package/src/hooks/use-gql-router-context.js +50 -0
  58. package/src/hooks/use-gql.js +22 -52
  59. package/src/hooks/use-hydratable-effect.js +213 -0
  60. package/src/hooks/use-request-interception.js +20 -23
  61. package/src/hooks/use-server-effect.js +12 -5
  62. package/src/hooks/use-shared-cache.js +13 -11
  63. package/src/index.js +53 -2
  64. package/src/util/__tests__/__snapshots__/serializable-in-memory-cache.test.js.snap +19 -0
  65. package/src/util/__tests__/merge-gql-context.test.js +74 -0
  66. package/src/util/__tests__/request-fulfillment.test.js +23 -42
  67. package/src/util/__tests__/request-tracking.test.js +26 -7
  68. package/src/util/__tests__/result-from-cache-response.test.js +19 -5
  69. package/src/util/__tests__/scoped-in-memory-cache.test.js +6 -85
  70. package/src/util/__tests__/serializable-in-memory-cache.test.js +398 -0
  71. package/src/util/__tests__/ssr-cache.test.js +52 -52
  72. package/src/util/abort-error.js +15 -0
  73. package/src/util/data-error.js +58 -0
  74. package/src/util/get-gql-data-from-response.js +3 -2
  75. package/src/util/gql-error.js +19 -11
  76. package/src/util/merge-gql-context.js +34 -0
  77. package/src/util/request-fulfillment.js +49 -46
  78. package/src/util/request-tracking.js +69 -15
  79. package/src/util/result-from-cache-response.js +12 -16
  80. package/src/util/scoped-in-memory-cache.js +24 -47
  81. package/src/util/serializable-in-memory-cache.js +49 -0
  82. package/src/util/ssr-cache.js +9 -8
  83. package/src/util/status.js +30 -0
  84. package/src/util/types.js +18 -1
  85. package/docs.md +0 -122
@@ -0,0 +1,728 @@
1
+ // @flow
2
+ import {
3
+ renderHook as clientRenderHook,
4
+ act,
5
+ } from "@testing-library/react-hooks";
6
+ import {renderHook as serverRenderHook} from "@testing-library/react-hooks/server";
7
+
8
+ import {Server} from "@khanacademy/wonder-blocks-core";
9
+ import {Status} from "../../util/status.js";
10
+
11
+ import {RequestFulfillment} from "../../util/request-fulfillment.js";
12
+ import * as UseRequestInterception from "../use-request-interception.js";
13
+ import * as UseServerEffect from "../use-server-effect.js";
14
+ import * as UseSharedCache from "../use-shared-cache.js";
15
+
16
+ import {
17
+ useHydratableEffect,
18
+ // TODO(somewhatabstract, FEI-4174): Update eslint-plugin-import when they
19
+ // have fixed:
20
+ // https://github.com/import-js/eslint-plugin-import/issues/2073
21
+ // eslint-disable-next-line import/named
22
+ WhenClientSide,
23
+ } from "../use-hydratable-effect.js";
24
+
25
+ jest.mock("../use-request-interception.js");
26
+ jest.mock("../use-server-effect.js");
27
+ jest.mock("../use-shared-cache.js");
28
+
29
+ describe("#useHydratableEffect", () => {
30
+ beforeEach(() => {
31
+ jest.resetAllMocks();
32
+
33
+ // When we have request aborting and things, this can be nicer, but
34
+ // for now, let's just clear out inflight requests between tests
35
+ // by being cheeky.
36
+ RequestFulfillment.Default._requests = {};
37
+
38
+ // Simple implementation of request interception that just returns
39
+ // the handler.
40
+ jest.spyOn(
41
+ UseRequestInterception,
42
+ "useRequestInterception",
43
+ ).mockImplementation((_, handler) => handler);
44
+
45
+ // We need the cache to work a little so that we get our result.
46
+ const cache = {};
47
+ jest.spyOn(UseSharedCache, "useSharedCache").mockImplementation(
48
+ (id, _, defaultValue) => {
49
+ const setCache = (v) => (cache[id] = v);
50
+ const currentValue =
51
+ cache[id] ??
52
+ (typeof defaultValue === "function"
53
+ ? defaultValue()
54
+ : defaultValue);
55
+ cache[id] = currentValue;
56
+ return [currentValue, setCache];
57
+ },
58
+ );
59
+ });
60
+
61
+ describe("when server-side", () => {
62
+ beforeEach(() => {
63
+ jest.spyOn(Server, "isServerSide").mockReturnValue(true);
64
+ });
65
+
66
+ it("should call useRequestInterception", () => {
67
+ // Arrange
68
+ const useRequestInterceptSpy = jest
69
+ .spyOn(UseRequestInterception, "useRequestInterception")
70
+ .mockReturnValue(jest.fn());
71
+ const fakeHandler = jest.fn();
72
+
73
+ // Act
74
+ serverRenderHook(() => useHydratableEffect("ID", fakeHandler));
75
+
76
+ // Assert
77
+ expect(useRequestInterceptSpy).toHaveBeenCalledWith(
78
+ "ID",
79
+ fakeHandler,
80
+ );
81
+ });
82
+
83
+ it.each`
84
+ clientBehavior | hydrate
85
+ ${WhenClientSide.DoNotHydrate} | ${false}
86
+ ${WhenClientSide.AlwaysExecute} | ${true}
87
+ ${WhenClientSide.ExecuteWhenNoResult} | ${true}
88
+ ${WhenClientSide.ExecuteWhenNoSuccessResult} | ${true}
89
+ ${undefined /*default*/} | ${true}
90
+ `(
91
+ "should call useServerEffect with the handler and hydrate=$hydrate for $clientBehavior",
92
+ ({hydrate, clientBehavior}) => {
93
+ // Arrange
94
+ const useServerEffectSpy = jest
95
+ .spyOn(UseServerEffect, "useServerEffect")
96
+ .mockReturnValue(null);
97
+ const fakeHandler = jest.fn();
98
+
99
+ // Act
100
+ serverRenderHook(() =>
101
+ useHydratableEffect("ID", fakeHandler, {
102
+ clientBehavior,
103
+ }),
104
+ );
105
+
106
+ // Assert
107
+ expect(useServerEffectSpy).toHaveBeenCalledWith(
108
+ "ID",
109
+ fakeHandler,
110
+ hydrate,
111
+ );
112
+ },
113
+ );
114
+
115
+ it("should pass an abort handler to useServerEffect when skip is true", async () => {
116
+ // Arrange
117
+ jest.spyOn(
118
+ UseRequestInterception,
119
+ "useRequestInterception",
120
+ ).mockReturnValue(jest.fn());
121
+ const fakeHandler = jest.fn();
122
+ const useServerEffectSpy = jest
123
+ .spyOn(UseServerEffect, "useServerEffect")
124
+ .mockReturnValue(null);
125
+
126
+ // Act
127
+ serverRenderHook(() =>
128
+ useHydratableEffect("ID", fakeHandler, {skip: true}),
129
+ );
130
+ const underTest = useServerEffectSpy.mock.calls[0][1]();
131
+
132
+ // Assert
133
+ await expect(underTest).rejects.toMatchInlineSnapshot(
134
+ `[AbortError: skipped]`,
135
+ );
136
+ });
137
+
138
+ it.each`
139
+ scope | expectedScope
140
+ ${undefined} | ${"useHydratableEffect"}
141
+ ${"foo"} | ${"foo"}
142
+ `(
143
+ "should call useSharedCache with id, scope=$scope, and a function to set the default",
144
+ ({scope, expectedScope}) => {
145
+ const fakeHandler = jest.fn();
146
+ jest.spyOn(UseServerEffect, "useServerEffect").mockReturnValue(
147
+ Status.success({thisIs: "some data"}),
148
+ );
149
+ const useSharedCacheSpy = jest.spyOn(
150
+ UseSharedCache,
151
+ "useSharedCache",
152
+ );
153
+
154
+ // Act
155
+ serverRenderHook(() =>
156
+ useHydratableEffect("ID", fakeHandler, {scope}),
157
+ );
158
+
159
+ // Assert
160
+ expect(useSharedCacheSpy).toHaveBeenCalledWith(
161
+ "ID",
162
+ expectedScope,
163
+ expect.any(Function),
164
+ );
165
+ },
166
+ );
167
+
168
+ it("should not request data", () => {
169
+ // Arrange
170
+ const fakeHandler = jest.fn().mockResolvedValue("data");
171
+
172
+ // Act
173
+ serverRenderHook(() => useHydratableEffect("ID", fakeHandler));
174
+
175
+ // Assert
176
+ expect(fakeHandler).not.toHaveBeenCalled();
177
+ });
178
+
179
+ describe("without server result", () => {
180
+ it("should return a loading result", () => {
181
+ // Arrange
182
+ const fakeHandler = jest.fn();
183
+
184
+ // Act
185
+ const {
186
+ result: {current: result},
187
+ } = serverRenderHook(() =>
188
+ useHydratableEffect("ID", fakeHandler),
189
+ );
190
+
191
+ // Assert
192
+ expect(result).toEqual(Status.loading());
193
+ });
194
+ });
195
+
196
+ describe("with server result", () => {
197
+ it("should return the result", () => {
198
+ // Arrange
199
+ const fakeHandler = jest.fn();
200
+ const serverResult = Status.success("data");
201
+ jest.spyOn(UseServerEffect, "useServerEffect").mockReturnValue(
202
+ serverResult,
203
+ );
204
+
205
+ // Act
206
+ const {
207
+ result: {current: result},
208
+ } = serverRenderHook(() =>
209
+ useHydratableEffect("ID", fakeHandler),
210
+ );
211
+
212
+ // Assert
213
+ expect(result).toEqual(serverResult);
214
+ });
215
+ });
216
+ });
217
+
218
+ describe("when client-side", () => {
219
+ beforeEach(() => {
220
+ jest.spyOn(Server, "isServerSide").mockReturnValue(false);
221
+ });
222
+
223
+ it.each`
224
+ clientBehavior | hydrate
225
+ ${WhenClientSide.DoNotHydrate} | ${false}
226
+ ${WhenClientSide.AlwaysExecute} | ${true}
227
+ ${WhenClientSide.ExecuteWhenNoResult} | ${true}
228
+ ${WhenClientSide.ExecuteWhenNoSuccessResult} | ${true}
229
+ ${undefined /*default*/} | ${true}
230
+ `(
231
+ "should call useServerEffect with the handler and hydrate=$hydrate for $clientBehavior",
232
+ ({hydrate, clientBehavior}) => {
233
+ // Arrange
234
+ const useServerEffectSpy = jest
235
+ .spyOn(UseServerEffect, "useServerEffect")
236
+ .mockReturnValue(null);
237
+ const fakeHandler = jest.fn();
238
+
239
+ // Act
240
+ clientRenderHook(() =>
241
+ useHydratableEffect("ID", fakeHandler, {
242
+ clientBehavior,
243
+ }),
244
+ );
245
+
246
+ // Assert
247
+ expect(useServerEffectSpy).toHaveBeenCalledWith(
248
+ "ID",
249
+ fakeHandler,
250
+ hydrate,
251
+ );
252
+ },
253
+ );
254
+
255
+ it("should fulfill request when there is no server value to hydrate", () => {
256
+ // Arrange
257
+ const fakeHandler = jest.fn();
258
+ jest.spyOn(UseServerEffect, "useServerEffect").mockReturnValue(
259
+ null,
260
+ );
261
+
262
+ // Act
263
+ clientRenderHook(() => useHydratableEffect("ID", fakeHandler));
264
+
265
+ // Assert
266
+ expect(fakeHandler).toHaveBeenCalled();
267
+ });
268
+
269
+ it("should share inflight requests for the same requestId", () => {
270
+ // Arrange
271
+ const pending = new Promise((resolve, reject) => {
272
+ /*pending*/
273
+ });
274
+ const fakeHandler = jest.fn().mockReturnValue(pending);
275
+
276
+ // Act
277
+ clientRenderHook(() => useHydratableEffect("ID", fakeHandler));
278
+ clientRenderHook(() => useHydratableEffect("ID", fakeHandler));
279
+
280
+ // Assert
281
+ expect(fakeHandler).toHaveBeenCalledTimes(1);
282
+ });
283
+
284
+ it.each`
285
+ serverResult
286
+ ${null}
287
+ ${Status.error(new Error("some error"))}
288
+ `(
289
+ "should fulfill request when server value is $serverResult and clientBehavior is ExecuteWhenNoSuccessResult",
290
+ ({serverResult}) => {
291
+ // Arrange
292
+ const fakeHandler = jest.fn();
293
+ jest.spyOn(UseServerEffect, "useServerEffect").mockReturnValue(
294
+ serverResult,
295
+ );
296
+
297
+ // Act
298
+ clientRenderHook(() =>
299
+ useHydratableEffect("ID", fakeHandler, {
300
+ clientBehavior:
301
+ WhenClientSide.ExecuteWhenNoSuccessResult,
302
+ }),
303
+ );
304
+
305
+ // Assert
306
+ expect(fakeHandler).toHaveBeenCalled();
307
+ },
308
+ );
309
+
310
+ it.each`
311
+ serverResult
312
+ ${null}
313
+ ${Status.error(new Error("some error"))}
314
+ ${Status.success("data")}
315
+ `(
316
+ "should fulfill request when server value is $serveResult and clientBehavior is AlwaysExecute",
317
+ ({serverResult}) => {
318
+ // Arrange
319
+ const fakeHandler = jest.fn();
320
+ jest.spyOn(UseServerEffect, "useServerEffect").mockReturnValue(
321
+ serverResult,
322
+ );
323
+
324
+ // Act
325
+ clientRenderHook(() =>
326
+ useHydratableEffect("ID", fakeHandler, {
327
+ clientBehavior: WhenClientSide.AlwaysExecute,
328
+ }),
329
+ );
330
+
331
+ // Assert
332
+ expect(fakeHandler).toHaveBeenCalled();
333
+ },
334
+ );
335
+
336
+ it("should not fulfill request when server value is success and clientBehavior is ExecuteWhenNoSuccessResult", () => {
337
+ // Arrange
338
+ const fakeHandler = jest.fn();
339
+ jest.spyOn(UseServerEffect, "useServerEffect").mockReturnValue(
340
+ Status.success("data"),
341
+ );
342
+
343
+ // Act
344
+ clientRenderHook(() =>
345
+ useHydratableEffect("ID", fakeHandler, {
346
+ clientBehavior: WhenClientSide.ExecuteWhenNoSuccessResult,
347
+ }),
348
+ );
349
+
350
+ // Assert
351
+ expect(fakeHandler).not.toHaveBeenCalled();
352
+ });
353
+
354
+ it.each`
355
+ serverResult
356
+ ${Status.error(new Error("some error"))}
357
+ ${Status.success("data")}
358
+ `(
359
+ "should not fulfill request when server value is $serverResult and clientBehavior is ExecuteWhenNoResult",
360
+ ({serverResult}) => {
361
+ const fakeHandler = jest.fn();
362
+ jest.spyOn(UseServerEffect, "useServerEffect").mockReturnValue(
363
+ serverResult,
364
+ );
365
+
366
+ // Act
367
+ clientRenderHook(() =>
368
+ useHydratableEffect("ID", fakeHandler, {
369
+ clientBehavior: WhenClientSide.ExecuteWhenNoResult,
370
+ }),
371
+ );
372
+
373
+ // Assert
374
+ expect(fakeHandler).not.toHaveBeenCalled();
375
+ },
376
+ );
377
+
378
+ it("should fulfill request once only if requestId does not change", async () => {
379
+ const fakeHandler = jest.fn().mockResolvedValue("data");
380
+ jest.spyOn(UseServerEffect, "useServerEffect").mockReturnValue(
381
+ null,
382
+ );
383
+
384
+ // Act
385
+ const {rerender, waitForNextUpdate} = clientRenderHook(() =>
386
+ useHydratableEffect("ID", fakeHandler),
387
+ );
388
+ rerender();
389
+ await waitForNextUpdate();
390
+
391
+ // Assert
392
+ expect(fakeHandler).toHaveBeenCalledTimes(1);
393
+ });
394
+
395
+ it("should fulfill request again if requestId changes", async () => {
396
+ // Arrange
397
+ const fakeHandler = jest.fn().mockResolvedValue("data");
398
+ jest.spyOn(UseServerEffect, "useServerEffect").mockReturnValue(
399
+ null,
400
+ );
401
+
402
+ // Act
403
+ const {rerender, waitForNextUpdate} = clientRenderHook(
404
+ ({requestId}) => useHydratableEffect(requestId, fakeHandler),
405
+ {
406
+ initialProps: {requestId: "ID"},
407
+ },
408
+ );
409
+ rerender({requestId: "ID2"});
410
+ await waitForNextUpdate();
411
+
412
+ // Assert
413
+ expect(fakeHandler).toHaveBeenCalledTimes(2);
414
+ });
415
+
416
+ it("should default shared cache to hydrate value for new requestId", async () => {
417
+ // Arrange
418
+ const fakeHandler = jest.fn().mockResolvedValue("data");
419
+ jest.spyOn(UseServerEffect, "useServerEffect")
420
+ .mockReturnValueOnce(Status.success("BADDATA"))
421
+ .mockReturnValue(null);
422
+
423
+ // Act
424
+ const {rerender, result} = clientRenderHook(
425
+ ({requestId}) => useHydratableEffect(requestId, fakeHandler),
426
+ {
427
+ initialProps: {requestId: "ID"},
428
+ },
429
+ );
430
+ rerender({requestId: "ID2"});
431
+
432
+ // Assert
433
+ expect(result.current).toStrictEqual(Status.loading());
434
+ });
435
+
436
+ it("should update shared cache with result when request is fulfilled", async () => {
437
+ // Arrange
438
+ const setCacheFn = jest.fn();
439
+ jest.spyOn(UseSharedCache, "useSharedCache").mockReturnValue([
440
+ null,
441
+ setCacheFn,
442
+ ]);
443
+ const fakeHandler = jest.fn().mockResolvedValue("DATA");
444
+
445
+ // Act
446
+ const {waitForNextUpdate} = clientRenderHook(() =>
447
+ useHydratableEffect("ID", fakeHandler),
448
+ );
449
+ await waitForNextUpdate();
450
+
451
+ // Assert
452
+ expect(setCacheFn).toHaveBeenCalledWith(Status.success("DATA"));
453
+ });
454
+
455
+ it("should ignore inflight request if requestId changes", async () => {
456
+ // Arrange
457
+ const response1 = Promise.resolve("DATA1");
458
+ const response2 = Promise.resolve("DATA2");
459
+ const fakeHandler = jest
460
+ .fn()
461
+ .mockReturnValueOnce(response1)
462
+ .mockReturnValueOnce(response2);
463
+ jest.spyOn(UseServerEffect, "useServerEffect").mockReturnValue(
464
+ null,
465
+ );
466
+
467
+ // Act
468
+ const {rerender, result} = clientRenderHook(
469
+ ({requestId}) => useHydratableEffect(requestId, fakeHandler),
470
+ {
471
+ initialProps: {requestId: "ID"},
472
+ },
473
+ );
474
+ rerender({requestId: "ID2"});
475
+ await act((): Promise<mixed> =>
476
+ Promise.all([response1, response2]),
477
+ );
478
+
479
+ // Assert
480
+ expect(result.all).not.toContainEqual(Status.success("DATA1"));
481
+ });
482
+
483
+ it("should return result of fulfilled request for current requestId", async () => {
484
+ // Arrange
485
+ const response1 = Promise.resolve("DATA1");
486
+ const response2 = Promise.resolve("DATA2");
487
+ const fakeHandler = jest
488
+ .fn()
489
+ .mockReturnValueOnce(response1)
490
+ .mockReturnValueOnce(response2);
491
+ jest.spyOn(UseServerEffect, "useServerEffect").mockReturnValue(
492
+ null,
493
+ );
494
+
495
+ // Act
496
+ const {rerender, result} = clientRenderHook(
497
+ ({requestId}) => useHydratableEffect(requestId, fakeHandler),
498
+ {
499
+ initialProps: {requestId: "ID"},
500
+ },
501
+ );
502
+ rerender({requestId: "ID2"});
503
+ await act((): Promise<mixed> =>
504
+ Promise.all([response1, response2]),
505
+ );
506
+
507
+ // Assert
508
+ expect(result.current).toStrictEqual(Status.success("DATA2"));
509
+ });
510
+
511
+ it("should not fulfill request when skip is true", () => {
512
+ // Arrange
513
+ const fakeHandler = jest.fn();
514
+ jest.spyOn(UseServerEffect, "useServerEffect").mockReturnValue(
515
+ null,
516
+ );
517
+
518
+ // Act
519
+ clientRenderHook(() =>
520
+ useHydratableEffect("ID", fakeHandler, {skip: true}),
521
+ );
522
+
523
+ // Assert
524
+ expect(fakeHandler).not.toHaveBeenCalled();
525
+ });
526
+
527
+ it("should ignore inflight request if skip changes", async () => {
528
+ // Arrange
529
+ const response1 = Promise.resolve("DATA1");
530
+ const fakeHandler = jest.fn().mockReturnValueOnce(response1);
531
+ jest.spyOn(UseServerEffect, "useServerEffect").mockReturnValue(
532
+ null,
533
+ );
534
+
535
+ // Act
536
+ const {rerender, result} = clientRenderHook(
537
+ ({skip}) => useHydratableEffect("ID", fakeHandler, {skip}),
538
+ {
539
+ initialProps: {skip: false},
540
+ },
541
+ );
542
+ rerender({skip: true});
543
+ await act((): Promise<mixed> => response1);
544
+
545
+ // Assert
546
+ expect(result.all).not.toContainEqual(Status.success("DATA1"));
547
+ });
548
+
549
+ it("should not ignore inflight request if handler changes", async () => {
550
+ // Arrange
551
+ const response1 = Promise.resolve("DATA1");
552
+ const response2 = Promise.resolve("DATA2");
553
+ const fakeHandler1 = jest.fn().mockReturnValueOnce(response1);
554
+ const fakeHandler2 = jest.fn().mockReturnValueOnce(response2);
555
+ jest.spyOn(UseServerEffect, "useServerEffect").mockReturnValue(
556
+ null,
557
+ );
558
+
559
+ // Act
560
+ const {rerender, result} = clientRenderHook(
561
+ ({handler}) => useHydratableEffect("ID", handler),
562
+ {
563
+ initialProps: {handler: fakeHandler1},
564
+ },
565
+ );
566
+ rerender({handler: fakeHandler2});
567
+ await act((): Promise<mixed> =>
568
+ Promise.all([response1, response2]),
569
+ );
570
+
571
+ // Assert
572
+ expect(result.current).toStrictEqual(Status.success("DATA1"));
573
+ });
574
+
575
+ it("should not ignore inflight request if options (other than skip) change", async () => {
576
+ // Arrange
577
+ const response1 = Promise.resolve("DATA1");
578
+ const fakeHandler = jest.fn().mockReturnValueOnce(response1);
579
+ jest.spyOn(UseServerEffect, "useServerEffect").mockReturnValue(
580
+ null,
581
+ );
582
+
583
+ // Act
584
+ const {rerender, result} = clientRenderHook(
585
+ ({options}) => useHydratableEffect("ID", fakeHandler),
586
+ {
587
+ initialProps: {options: undefined},
588
+ },
589
+ );
590
+ rerender({
591
+ options: {
592
+ scope: "BLAH!",
593
+ },
594
+ });
595
+ await act((): Promise<mixed> => response1);
596
+
597
+ // Assert
598
+ expect(result.current).toStrictEqual(Status.success("DATA1"));
599
+ });
600
+
601
+ it("should return previous result when requestId changes and retainResultOnChange is true", async () => {
602
+ // Arrange
603
+ const response1 = Promise.resolve("DATA1");
604
+ const response2 = Promise.resolve("DATA2");
605
+ const fakeHandler = jest
606
+ .fn()
607
+ .mockReturnValueOnce(response1)
608
+ .mockReturnValueOnce(response2);
609
+ jest.spyOn(UseServerEffect, "useServerEffect").mockReturnValue(
610
+ null,
611
+ );
612
+
613
+ // Act
614
+ const {
615
+ rerender,
616
+ result: hookResult,
617
+ waitForNextUpdate,
618
+ } = clientRenderHook(
619
+ ({requestId}) =>
620
+ useHydratableEffect(requestId, fakeHandler, {
621
+ retainResultOnChange: true,
622
+ }),
623
+ {
624
+ initialProps: {requestId: "ID"},
625
+ },
626
+ );
627
+ await act((): Promise<mixed> => response1);
628
+ rerender({requestId: "ID2"});
629
+ const result = hookResult.current;
630
+ await waitForNextUpdate();
631
+
632
+ // Assert
633
+ expect(result).toStrictEqual(Status.success("DATA1"));
634
+ });
635
+
636
+ it("should return loading status when requestId changes and retainResultOnChange is false", async () => {
637
+ // Arrange
638
+ const response1 = Promise.resolve("DATA1");
639
+ const response2 = new Promise(() => {
640
+ /*pending*/
641
+ });
642
+ const fakeHandler = jest
643
+ .fn()
644
+ .mockReturnValueOnce(response1)
645
+ .mockReturnValueOnce(response2);
646
+ jest.spyOn(UseServerEffect, "useServerEffect").mockReturnValue(
647
+ null,
648
+ );
649
+
650
+ // Act
651
+ const {rerender, result} = clientRenderHook(
652
+ ({requestId}) =>
653
+ useHydratableEffect(requestId, fakeHandler, {
654
+ retainResultOnChange: false,
655
+ }),
656
+ {
657
+ initialProps: {requestId: "ID"},
658
+ },
659
+ );
660
+ await act((): Promise<mixed> => response1);
661
+ rerender({requestId: "ID2"});
662
+
663
+ // Assert
664
+ expect(result.current).toStrictEqual(Status.loading());
665
+ });
666
+
667
+ it("should trigger render when request is fulfilled and onResultChanged is undefined", async () => {
668
+ // Arrange
669
+ const response = Promise.resolve("DATA");
670
+ const fakeHandler = jest.fn().mockReturnValue(response);
671
+ jest.spyOn(UseServerEffect, "useServerEffect").mockReturnValue(
672
+ null,
673
+ );
674
+
675
+ // Act
676
+ const {result} = clientRenderHook(() =>
677
+ useHydratableEffect("ID", fakeHandler),
678
+ );
679
+ await act((): Promise<mixed> => response);
680
+
681
+ // Assert
682
+ expect(result.current).toStrictEqual(Status.success("DATA"));
683
+ });
684
+
685
+ it("should not trigger render when request is fulfilled and onResultChanged is defined", async () => {
686
+ // Arrange
687
+ const response = Promise.resolve("DATA");
688
+ const fakeHandler = jest.fn().mockReturnValue(response);
689
+ jest.spyOn(UseServerEffect, "useServerEffect").mockReturnValue(
690
+ null,
691
+ );
692
+
693
+ // Act
694
+ const {result} = clientRenderHook(() =>
695
+ useHydratableEffect("ID", fakeHandler, {
696
+ onResultChanged: () => {},
697
+ }),
698
+ );
699
+ await act((): Promise<mixed> => response);
700
+
701
+ // Assert
702
+ expect(result.current).toStrictEqual(Status.loading());
703
+ });
704
+
705
+ it("should call onResultChanged when request is fulfilled and onResultChanged is defined", async () => {
706
+ // Arrange
707
+ const response = Promise.resolve("DATA");
708
+ const fakeHandler = jest.fn().mockReturnValue(response);
709
+ jest.spyOn(UseServerEffect, "useServerEffect").mockReturnValue(
710
+ null,
711
+ );
712
+ const onResultChanged = jest.fn();
713
+
714
+ // Act
715
+ clientRenderHook(() =>
716
+ useHydratableEffect("ID", fakeHandler, {
717
+ onResultChanged,
718
+ }),
719
+ );
720
+ await act((): Promise<mixed> => response);
721
+
722
+ // Assert
723
+ expect(onResultChanged).toHaveBeenCalledWith(
724
+ Status.success("DATA"),
725
+ );
726
+ });
727
+ });
728
+ });