@khanacademy/wonder-blocks-data 5.0.1 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/es/index.js +767 -371
  3. package/dist/index.js +1194 -564
  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 +50 -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 +705 -0
  55. package/src/hooks/__tests__/use-server-effect.test.js +90 -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 +206 -0
  60. package/src/hooks/use-request-interception.js +20 -23
  61. package/src/hooks/use-server-effect.js +42 -10
  62. package/src/hooks/use-shared-cache.js +13 -11
  63. package/src/index.js +53 -3
  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/data-error.js +58 -0
  73. package/src/util/get-gql-data-from-response.js +3 -2
  74. package/src/util/gql-error.js +19 -11
  75. package/src/util/merge-gql-context.js +34 -0
  76. package/src/util/request-fulfillment.js +49 -46
  77. package/src/util/request-tracking.js +69 -15
  78. package/src/util/result-from-cache-response.js +12 -16
  79. package/src/util/scoped-in-memory-cache.js +24 -47
  80. package/src/util/serializable-in-memory-cache.js +49 -0
  81. package/src/util/ssr-cache.js +9 -8
  82. package/src/util/status.js +30 -0
  83. package/src/util/types.js +18 -1
  84. package/docs.md +0 -122
@@ -0,0 +1,705 @@
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, skip: false},
111
+ );
112
+ },
113
+ );
114
+
115
+ it.each`
116
+ scope | expectedScope
117
+ ${undefined} | ${"useHydratableEffect"}
118
+ ${"foo"} | ${"foo"}
119
+ `(
120
+ "should call useSharedCache with id, scope=$scope, and a function to set the default",
121
+ ({scope, expectedScope}) => {
122
+ const fakeHandler = jest.fn();
123
+ jest.spyOn(UseServerEffect, "useServerEffect").mockReturnValue(
124
+ Status.success({thisIs: "some data"}),
125
+ );
126
+ const useSharedCacheSpy = jest.spyOn(
127
+ UseSharedCache,
128
+ "useSharedCache",
129
+ );
130
+
131
+ // Act
132
+ serverRenderHook(() =>
133
+ useHydratableEffect("ID", fakeHandler, {scope}),
134
+ );
135
+
136
+ // Assert
137
+ expect(useSharedCacheSpy).toHaveBeenCalledWith(
138
+ "ID",
139
+ expectedScope,
140
+ expect.any(Function),
141
+ );
142
+ },
143
+ );
144
+
145
+ it("should not request data", () => {
146
+ // Arrange
147
+ const fakeHandler = jest.fn().mockResolvedValue("data");
148
+
149
+ // Act
150
+ serverRenderHook(() => useHydratableEffect("ID", fakeHandler));
151
+
152
+ // Assert
153
+ expect(fakeHandler).not.toHaveBeenCalled();
154
+ });
155
+
156
+ describe("without server result", () => {
157
+ it("should return a loading result", () => {
158
+ // Arrange
159
+ const fakeHandler = jest.fn();
160
+
161
+ // Act
162
+ const {
163
+ result: {current: result},
164
+ } = serverRenderHook(() =>
165
+ useHydratableEffect("ID", fakeHandler),
166
+ );
167
+
168
+ // Assert
169
+ expect(result).toEqual(Status.loading());
170
+ });
171
+ });
172
+
173
+ describe("with server result", () => {
174
+ it("should return the result", () => {
175
+ // Arrange
176
+ const fakeHandler = jest.fn();
177
+ const serverResult = Status.success("data");
178
+ jest.spyOn(UseServerEffect, "useServerEffect").mockReturnValue(
179
+ serverResult,
180
+ );
181
+
182
+ // Act
183
+ const {
184
+ result: {current: result},
185
+ } = serverRenderHook(() =>
186
+ useHydratableEffect("ID", fakeHandler),
187
+ );
188
+
189
+ // Assert
190
+ expect(result).toEqual(serverResult);
191
+ });
192
+ });
193
+ });
194
+
195
+ describe("when client-side", () => {
196
+ beforeEach(() => {
197
+ jest.spyOn(Server, "isServerSide").mockReturnValue(false);
198
+ });
199
+
200
+ it.each`
201
+ clientBehavior | hydrate
202
+ ${WhenClientSide.DoNotHydrate} | ${false}
203
+ ${WhenClientSide.AlwaysExecute} | ${true}
204
+ ${WhenClientSide.ExecuteWhenNoResult} | ${true}
205
+ ${WhenClientSide.ExecuteWhenNoSuccessResult} | ${true}
206
+ ${undefined /*default*/} | ${true}
207
+ `(
208
+ "should call useServerEffect with the handler and hydrate=$hydrate for $clientBehavior",
209
+ ({hydrate, clientBehavior}) => {
210
+ // Arrange
211
+ const useServerEffectSpy = jest
212
+ .spyOn(UseServerEffect, "useServerEffect")
213
+ .mockReturnValue(null);
214
+ const fakeHandler = jest.fn();
215
+
216
+ // Act
217
+ clientRenderHook(() =>
218
+ useHydratableEffect("ID", fakeHandler, {
219
+ clientBehavior,
220
+ }),
221
+ );
222
+
223
+ // Assert
224
+ expect(useServerEffectSpy).toHaveBeenCalledWith(
225
+ "ID",
226
+ fakeHandler,
227
+ {hydrate, skip: false},
228
+ );
229
+ },
230
+ );
231
+
232
+ it("should fulfill request when there is no server value to hydrate", () => {
233
+ // Arrange
234
+ const fakeHandler = jest.fn();
235
+ jest.spyOn(UseServerEffect, "useServerEffect").mockReturnValue(
236
+ null,
237
+ );
238
+
239
+ // Act
240
+ clientRenderHook(() => useHydratableEffect("ID", fakeHandler));
241
+
242
+ // Assert
243
+ expect(fakeHandler).toHaveBeenCalled();
244
+ });
245
+
246
+ it("should share inflight requests for the same requestId", () => {
247
+ // Arrange
248
+ const pending = new Promise((resolve, reject) => {
249
+ /*pending*/
250
+ });
251
+ const fakeHandler = jest.fn().mockReturnValue(pending);
252
+
253
+ // Act
254
+ clientRenderHook(() => useHydratableEffect("ID", fakeHandler));
255
+ clientRenderHook(() => useHydratableEffect("ID", fakeHandler));
256
+
257
+ // Assert
258
+ expect(fakeHandler).toHaveBeenCalledTimes(1);
259
+ });
260
+
261
+ it.each`
262
+ serverResult
263
+ ${null}
264
+ ${Status.error(new Error("some error"))}
265
+ `(
266
+ "should fulfill request when server value is $serverResult and clientBehavior is ExecuteWhenNoSuccessResult",
267
+ ({serverResult}) => {
268
+ // Arrange
269
+ const fakeHandler = jest.fn();
270
+ jest.spyOn(UseServerEffect, "useServerEffect").mockReturnValue(
271
+ serverResult,
272
+ );
273
+
274
+ // Act
275
+ clientRenderHook(() =>
276
+ useHydratableEffect("ID", fakeHandler, {
277
+ clientBehavior:
278
+ WhenClientSide.ExecuteWhenNoSuccessResult,
279
+ }),
280
+ );
281
+
282
+ // Assert
283
+ expect(fakeHandler).toHaveBeenCalled();
284
+ },
285
+ );
286
+
287
+ it.each`
288
+ serverResult
289
+ ${null}
290
+ ${Status.error(new Error("some error"))}
291
+ ${Status.success("data")}
292
+ `(
293
+ "should fulfill request when server value is $serveResult and clientBehavior is AlwaysExecute",
294
+ ({serverResult}) => {
295
+ // Arrange
296
+ const fakeHandler = jest.fn();
297
+ jest.spyOn(UseServerEffect, "useServerEffect").mockReturnValue(
298
+ serverResult,
299
+ );
300
+
301
+ // Act
302
+ clientRenderHook(() =>
303
+ useHydratableEffect("ID", fakeHandler, {
304
+ clientBehavior: WhenClientSide.AlwaysExecute,
305
+ }),
306
+ );
307
+
308
+ // Assert
309
+ expect(fakeHandler).toHaveBeenCalled();
310
+ },
311
+ );
312
+
313
+ it("should not fulfill request when server value is success and clientBehavior is ExecuteWhenNoSuccessResult", () => {
314
+ // Arrange
315
+ const fakeHandler = jest.fn();
316
+ jest.spyOn(UseServerEffect, "useServerEffect").mockReturnValue(
317
+ Status.success("data"),
318
+ );
319
+
320
+ // Act
321
+ clientRenderHook(() =>
322
+ useHydratableEffect("ID", fakeHandler, {
323
+ clientBehavior: WhenClientSide.ExecuteWhenNoSuccessResult,
324
+ }),
325
+ );
326
+
327
+ // Assert
328
+ expect(fakeHandler).not.toHaveBeenCalled();
329
+ });
330
+
331
+ it.each`
332
+ serverResult
333
+ ${Status.error(new Error("some error"))}
334
+ ${Status.success("data")}
335
+ `(
336
+ "should not fulfill request when server value is $serverResult and clientBehavior is ExecuteWhenNoResult",
337
+ ({serverResult}) => {
338
+ const fakeHandler = jest.fn();
339
+ jest.spyOn(UseServerEffect, "useServerEffect").mockReturnValue(
340
+ serverResult,
341
+ );
342
+
343
+ // Act
344
+ clientRenderHook(() =>
345
+ useHydratableEffect("ID", fakeHandler, {
346
+ clientBehavior: WhenClientSide.ExecuteWhenNoResult,
347
+ }),
348
+ );
349
+
350
+ // Assert
351
+ expect(fakeHandler).not.toHaveBeenCalled();
352
+ },
353
+ );
354
+
355
+ it("should fulfill request once only if requestId does not change", async () => {
356
+ const fakeHandler = jest.fn().mockResolvedValue("data");
357
+ jest.spyOn(UseServerEffect, "useServerEffect").mockReturnValue(
358
+ null,
359
+ );
360
+
361
+ // Act
362
+ const {rerender, waitForNextUpdate} = clientRenderHook(() =>
363
+ useHydratableEffect("ID", fakeHandler),
364
+ );
365
+ rerender();
366
+ await waitForNextUpdate();
367
+
368
+ // Assert
369
+ expect(fakeHandler).toHaveBeenCalledTimes(1);
370
+ });
371
+
372
+ it("should fulfill request again if requestId changes", async () => {
373
+ // Arrange
374
+ const fakeHandler = jest.fn().mockResolvedValue("data");
375
+ jest.spyOn(UseServerEffect, "useServerEffect").mockReturnValue(
376
+ null,
377
+ );
378
+
379
+ // Act
380
+ const {rerender, waitForNextUpdate} = clientRenderHook(
381
+ ({requestId}) => useHydratableEffect(requestId, fakeHandler),
382
+ {
383
+ initialProps: {requestId: "ID"},
384
+ },
385
+ );
386
+ rerender({requestId: "ID2"});
387
+ await waitForNextUpdate();
388
+
389
+ // Assert
390
+ expect(fakeHandler).toHaveBeenCalledTimes(2);
391
+ });
392
+
393
+ it("should default shared cache to hydrate value for new requestId", async () => {
394
+ // Arrange
395
+ const fakeHandler = jest.fn().mockResolvedValue("data");
396
+ jest.spyOn(UseServerEffect, "useServerEffect")
397
+ .mockReturnValueOnce(Status.success("BADDATA"))
398
+ .mockReturnValue(null);
399
+
400
+ // Act
401
+ const {rerender, result} = clientRenderHook(
402
+ ({requestId}) => useHydratableEffect(requestId, fakeHandler),
403
+ {
404
+ initialProps: {requestId: "ID"},
405
+ },
406
+ );
407
+ rerender({requestId: "ID2"});
408
+
409
+ // Assert
410
+ expect(result.current).toStrictEqual(Status.loading());
411
+ });
412
+
413
+ it("should update shared cache with result when request is fulfilled", async () => {
414
+ // Arrange
415
+ const setCacheFn = jest.fn();
416
+ jest.spyOn(UseSharedCache, "useSharedCache").mockReturnValue([
417
+ null,
418
+ setCacheFn,
419
+ ]);
420
+ const fakeHandler = jest.fn().mockResolvedValue("DATA");
421
+
422
+ // Act
423
+ const {waitForNextUpdate} = clientRenderHook(() =>
424
+ useHydratableEffect("ID", fakeHandler),
425
+ );
426
+ await waitForNextUpdate();
427
+
428
+ // Assert
429
+ expect(setCacheFn).toHaveBeenCalledWith(Status.success("DATA"));
430
+ });
431
+
432
+ it("should ignore inflight request if requestId changes", async () => {
433
+ // Arrange
434
+ const response1 = Promise.resolve("DATA1");
435
+ const response2 = Promise.resolve("DATA2");
436
+ const fakeHandler = jest
437
+ .fn()
438
+ .mockReturnValueOnce(response1)
439
+ .mockReturnValueOnce(response2);
440
+ jest.spyOn(UseServerEffect, "useServerEffect").mockReturnValue(
441
+ null,
442
+ );
443
+
444
+ // Act
445
+ const {rerender, result} = clientRenderHook(
446
+ ({requestId}) => useHydratableEffect(requestId, fakeHandler),
447
+ {
448
+ initialProps: {requestId: "ID"},
449
+ },
450
+ );
451
+ rerender({requestId: "ID2"});
452
+ await act((): Promise<mixed> =>
453
+ Promise.all([response1, response2]),
454
+ );
455
+
456
+ // Assert
457
+ expect(result.all).not.toContainEqual(Status.success("DATA1"));
458
+ });
459
+
460
+ it("should return result of fulfilled request for current requestId", async () => {
461
+ // Arrange
462
+ const response1 = Promise.resolve("DATA1");
463
+ const response2 = Promise.resolve("DATA2");
464
+ const fakeHandler = jest
465
+ .fn()
466
+ .mockReturnValueOnce(response1)
467
+ .mockReturnValueOnce(response2);
468
+ jest.spyOn(UseServerEffect, "useServerEffect").mockReturnValue(
469
+ null,
470
+ );
471
+
472
+ // Act
473
+ const {rerender, result} = clientRenderHook(
474
+ ({requestId}) => useHydratableEffect(requestId, fakeHandler),
475
+ {
476
+ initialProps: {requestId: "ID"},
477
+ },
478
+ );
479
+ rerender({requestId: "ID2"});
480
+ await act((): Promise<mixed> =>
481
+ Promise.all([response1, response2]),
482
+ );
483
+
484
+ // Assert
485
+ expect(result.current).toStrictEqual(Status.success("DATA2"));
486
+ });
487
+
488
+ it("should not fulfill request when skip is true", () => {
489
+ // Arrange
490
+ const fakeHandler = jest.fn();
491
+ jest.spyOn(UseServerEffect, "useServerEffect").mockReturnValue(
492
+ null,
493
+ );
494
+
495
+ // Act
496
+ clientRenderHook(() =>
497
+ useHydratableEffect("ID", fakeHandler, {skip: true}),
498
+ );
499
+
500
+ // Assert
501
+ expect(fakeHandler).not.toHaveBeenCalled();
502
+ });
503
+
504
+ it("should ignore inflight request if skip changes", async () => {
505
+ // Arrange
506
+ const response1 = Promise.resolve("DATA1");
507
+ const fakeHandler = jest.fn().mockReturnValueOnce(response1);
508
+ jest.spyOn(UseServerEffect, "useServerEffect").mockReturnValue(
509
+ null,
510
+ );
511
+
512
+ // Act
513
+ const {rerender, result} = clientRenderHook(
514
+ ({skip}) => useHydratableEffect("ID", fakeHandler, {skip}),
515
+ {
516
+ initialProps: {skip: false},
517
+ },
518
+ );
519
+ rerender({skip: true});
520
+ await act((): Promise<mixed> => response1);
521
+
522
+ // Assert
523
+ expect(result.all).not.toContainEqual(Status.success("DATA1"));
524
+ });
525
+
526
+ it("should not ignore inflight request if handler changes", async () => {
527
+ // Arrange
528
+ const response1 = Promise.resolve("DATA1");
529
+ const response2 = Promise.resolve("DATA2");
530
+ const fakeHandler1 = jest.fn().mockReturnValueOnce(response1);
531
+ const fakeHandler2 = jest.fn().mockReturnValueOnce(response2);
532
+ jest.spyOn(UseServerEffect, "useServerEffect").mockReturnValue(
533
+ null,
534
+ );
535
+
536
+ // Act
537
+ const {rerender, result} = clientRenderHook(
538
+ ({handler}) => useHydratableEffect("ID", handler),
539
+ {
540
+ initialProps: {handler: fakeHandler1},
541
+ },
542
+ );
543
+ rerender({handler: fakeHandler2});
544
+ await act((): Promise<mixed> =>
545
+ Promise.all([response1, response2]),
546
+ );
547
+
548
+ // Assert
549
+ expect(result.current).toStrictEqual(Status.success("DATA1"));
550
+ });
551
+
552
+ it("should not ignore inflight request if options (other than skip) change", async () => {
553
+ // Arrange
554
+ const response1 = Promise.resolve("DATA1");
555
+ const fakeHandler = jest.fn().mockReturnValueOnce(response1);
556
+ jest.spyOn(UseServerEffect, "useServerEffect").mockReturnValue(
557
+ null,
558
+ );
559
+
560
+ // Act
561
+ const {rerender, result} = clientRenderHook(
562
+ ({options}) => useHydratableEffect("ID", fakeHandler),
563
+ {
564
+ initialProps: {options: undefined},
565
+ },
566
+ );
567
+ rerender({
568
+ options: {
569
+ scope: "BLAH!",
570
+ },
571
+ });
572
+ await act((): Promise<mixed> => response1);
573
+
574
+ // Assert
575
+ expect(result.current).toStrictEqual(Status.success("DATA1"));
576
+ });
577
+
578
+ it("should return previous result when requestId changes and retainResultOnChange is true", async () => {
579
+ // Arrange
580
+ const response1 = Promise.resolve("DATA1");
581
+ const response2 = Promise.resolve("DATA2");
582
+ const fakeHandler = jest
583
+ .fn()
584
+ .mockReturnValueOnce(response1)
585
+ .mockReturnValueOnce(response2);
586
+ jest.spyOn(UseServerEffect, "useServerEffect").mockReturnValue(
587
+ null,
588
+ );
589
+
590
+ // Act
591
+ const {
592
+ rerender,
593
+ result: hookResult,
594
+ waitForNextUpdate,
595
+ } = clientRenderHook(
596
+ ({requestId}) =>
597
+ useHydratableEffect(requestId, fakeHandler, {
598
+ retainResultOnChange: true,
599
+ }),
600
+ {
601
+ initialProps: {requestId: "ID"},
602
+ },
603
+ );
604
+ await act((): Promise<mixed> => response1);
605
+ rerender({requestId: "ID2"});
606
+ const result = hookResult.current;
607
+ await waitForNextUpdate();
608
+
609
+ // Assert
610
+ expect(result).toStrictEqual(Status.success("DATA1"));
611
+ });
612
+
613
+ it("should return loading status when requestId changes and retainResultOnChange is false", async () => {
614
+ // Arrange
615
+ const response1 = Promise.resolve("DATA1");
616
+ const response2 = new Promise(() => {
617
+ /*pending*/
618
+ });
619
+ const fakeHandler = jest
620
+ .fn()
621
+ .mockReturnValueOnce(response1)
622
+ .mockReturnValueOnce(response2);
623
+ jest.spyOn(UseServerEffect, "useServerEffect").mockReturnValue(
624
+ null,
625
+ );
626
+
627
+ // Act
628
+ const {rerender, result} = clientRenderHook(
629
+ ({requestId}) =>
630
+ useHydratableEffect(requestId, fakeHandler, {
631
+ retainResultOnChange: false,
632
+ }),
633
+ {
634
+ initialProps: {requestId: "ID"},
635
+ },
636
+ );
637
+ await act((): Promise<mixed> => response1);
638
+ rerender({requestId: "ID2"});
639
+
640
+ // Assert
641
+ expect(result.current).toStrictEqual(Status.loading());
642
+ });
643
+
644
+ it("should trigger render when request is fulfilled and onResultChanged is undefined", async () => {
645
+ // Arrange
646
+ const response = Promise.resolve("DATA");
647
+ const fakeHandler = jest.fn().mockReturnValue(response);
648
+ jest.spyOn(UseServerEffect, "useServerEffect").mockReturnValue(
649
+ null,
650
+ );
651
+
652
+ // Act
653
+ const {result} = clientRenderHook(() =>
654
+ useHydratableEffect("ID", fakeHandler),
655
+ );
656
+ await act((): Promise<mixed> => response);
657
+
658
+ // Assert
659
+ expect(result.current).toStrictEqual(Status.success("DATA"));
660
+ });
661
+
662
+ it("should not trigger render when request is fulfilled and onResultChanged is defined", async () => {
663
+ // Arrange
664
+ const response = Promise.resolve("DATA");
665
+ const fakeHandler = jest.fn().mockReturnValue(response);
666
+ jest.spyOn(UseServerEffect, "useServerEffect").mockReturnValue(
667
+ null,
668
+ );
669
+
670
+ // Act
671
+ const {result} = clientRenderHook(() =>
672
+ useHydratableEffect("ID", fakeHandler, {
673
+ onResultChanged: () => {},
674
+ }),
675
+ );
676
+ await act((): Promise<mixed> => response);
677
+
678
+ // Assert
679
+ expect(result.current).toStrictEqual(Status.loading());
680
+ });
681
+
682
+ it("should call onResultChanged when request is fulfilled and onResultChanged is defined", async () => {
683
+ // Arrange
684
+ const response = Promise.resolve("DATA");
685
+ const fakeHandler = jest.fn().mockReturnValue(response);
686
+ jest.spyOn(UseServerEffect, "useServerEffect").mockReturnValue(
687
+ null,
688
+ );
689
+ const onResultChanged = jest.fn();
690
+
691
+ // Act
692
+ clientRenderHook(() =>
693
+ useHydratableEffect("ID", fakeHandler, {
694
+ onResultChanged,
695
+ }),
696
+ );
697
+ await act((): Promise<mixed> => response);
698
+
699
+ // Assert
700
+ expect(onResultChanged).toHaveBeenCalledWith(
701
+ Status.success("DATA"),
702
+ );
703
+ });
704
+ });
705
+ });