@khanacademy/wonder-blocks-data 4.0.0 → 6.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 (91) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/es/index.js +793 -375
  3. package/dist/index.js +1203 -523
  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/__docs__/exports.intercept-requests.stories.mdx +69 -0
  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 +7 -31
  47. package/src/components/__tests__/data.test.js +160 -154
  48. package/src/components/__tests__/intercept-requests.test.js +58 -0
  49. package/src/components/data.js +22 -126
  50. package/src/components/intercept-context.js +4 -5
  51. package/src/components/intercept-requests.js +69 -0
  52. package/src/hooks/__tests__/__snapshots__/use-shared-cache.test.js.snap +8 -8
  53. package/src/hooks/__tests__/use-cached-effect.test.js +507 -0
  54. package/src/hooks/__tests__/use-gql-router-context.test.js +133 -0
  55. package/src/hooks/__tests__/use-gql.test.js +1 -30
  56. package/src/hooks/__tests__/use-hydratable-effect.test.js +708 -0
  57. package/src/hooks/__tests__/use-request-interception.test.js +255 -0
  58. package/src/hooks/__tests__/use-server-effect.test.js +39 -11
  59. package/src/hooks/use-cached-effect.js +225 -0
  60. package/src/hooks/use-gql-router-context.js +50 -0
  61. package/src/hooks/use-gql.js +22 -52
  62. package/src/hooks/use-hydratable-effect.js +206 -0
  63. package/src/hooks/use-request-interception.js +51 -0
  64. package/src/hooks/use-server-effect.js +14 -7
  65. package/src/hooks/use-shared-cache.js +13 -11
  66. package/src/index.js +54 -2
  67. package/src/util/__tests__/__snapshots__/serializable-in-memory-cache.test.js.snap +19 -0
  68. package/src/util/__tests__/merge-gql-context.test.js +74 -0
  69. package/src/util/__tests__/request-fulfillment.test.js +23 -42
  70. package/src/util/__tests__/request-tracking.test.js +26 -7
  71. package/src/util/__tests__/result-from-cache-response.test.js +19 -5
  72. package/src/util/__tests__/scoped-in-memory-cache.test.js +6 -85
  73. package/src/util/__tests__/serializable-in-memory-cache.test.js +398 -0
  74. package/src/util/__tests__/ssr-cache.test.js +52 -52
  75. package/src/util/abort-error.js +15 -0
  76. package/src/util/data-error.js +58 -0
  77. package/src/util/get-gql-data-from-response.js +3 -2
  78. package/src/util/gql-error.js +19 -11
  79. package/src/util/merge-gql-context.js +34 -0
  80. package/src/util/request-fulfillment.js +49 -46
  81. package/src/util/request-tracking.js +69 -15
  82. package/src/util/result-from-cache-response.js +12 -16
  83. package/src/util/scoped-in-memory-cache.js +24 -47
  84. package/src/util/serializable-in-memory-cache.js +49 -0
  85. package/src/util/ssr-cache.js +9 -8
  86. package/src/util/status.js +30 -0
  87. package/src/util/types.js +18 -1
  88. package/docs.md +0 -122
  89. package/src/components/__tests__/intercept-data.test.js +0 -63
  90. package/src/components/intercept-data.js +0 -66
  91. package/src/components/intercept-data.md +0 -51
@@ -0,0 +1,708 @@
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 update shared cache with result when request is fulfilled", async () => {
417
+ // Arrange
418
+ const setCacheFn = jest.fn();
419
+ jest.spyOn(UseSharedCache, "useSharedCache").mockReturnValue([
420
+ null,
421
+ setCacheFn,
422
+ ]);
423
+ const fakeHandler = jest.fn().mockResolvedValue("DATA");
424
+
425
+ // Act
426
+ const {waitForNextUpdate} = clientRenderHook(() =>
427
+ useHydratableEffect("ID", fakeHandler),
428
+ );
429
+ await waitForNextUpdate();
430
+
431
+ // Assert
432
+ expect(setCacheFn).toHaveBeenCalledWith(Status.success("DATA"));
433
+ });
434
+
435
+ it("should ignore inflight request if requestId changes", async () => {
436
+ // Arrange
437
+ const response1 = Promise.resolve("DATA1");
438
+ const response2 = Promise.resolve("DATA2");
439
+ const fakeHandler = jest
440
+ .fn()
441
+ .mockReturnValueOnce(response1)
442
+ .mockReturnValueOnce(response2);
443
+ jest.spyOn(UseServerEffect, "useServerEffect").mockReturnValue(
444
+ null,
445
+ );
446
+
447
+ // Act
448
+ const {rerender, result} = clientRenderHook(
449
+ ({requestId}) => useHydratableEffect(requestId, fakeHandler),
450
+ {
451
+ initialProps: {requestId: "ID"},
452
+ },
453
+ );
454
+ rerender({requestId: "ID2"});
455
+ await act((): Promise<mixed> =>
456
+ Promise.all([response1, response2]),
457
+ );
458
+
459
+ // Assert
460
+ expect(result.all).not.toContainEqual(Status.success("DATA1"));
461
+ });
462
+
463
+ it("should return result of fulfilled request for current requestId", async () => {
464
+ // Arrange
465
+ const response1 = Promise.resolve("DATA1");
466
+ const response2 = Promise.resolve("DATA2");
467
+ const fakeHandler = jest
468
+ .fn()
469
+ .mockReturnValueOnce(response1)
470
+ .mockReturnValueOnce(response2);
471
+ jest.spyOn(UseServerEffect, "useServerEffect").mockReturnValue(
472
+ null,
473
+ );
474
+
475
+ // Act
476
+ const {rerender, result} = clientRenderHook(
477
+ ({requestId}) => useHydratableEffect(requestId, fakeHandler),
478
+ {
479
+ initialProps: {requestId: "ID"},
480
+ },
481
+ );
482
+ rerender({requestId: "ID2"});
483
+ await act((): Promise<mixed> =>
484
+ Promise.all([response1, response2]),
485
+ );
486
+
487
+ // Assert
488
+ expect(result.current).toStrictEqual(Status.success("DATA2"));
489
+ });
490
+
491
+ it("should not fulfill request when skip is true", () => {
492
+ // Arrange
493
+ const fakeHandler = jest.fn();
494
+ jest.spyOn(UseServerEffect, "useServerEffect").mockReturnValue(
495
+ null,
496
+ );
497
+
498
+ // Act
499
+ clientRenderHook(() =>
500
+ useHydratableEffect("ID", fakeHandler, {skip: true}),
501
+ );
502
+
503
+ // Assert
504
+ expect(fakeHandler).not.toHaveBeenCalled();
505
+ });
506
+
507
+ it("should ignore inflight request if skip changes", async () => {
508
+ // Arrange
509
+ const response1 = Promise.resolve("DATA1");
510
+ const fakeHandler = jest.fn().mockReturnValueOnce(response1);
511
+ jest.spyOn(UseServerEffect, "useServerEffect").mockReturnValue(
512
+ null,
513
+ );
514
+
515
+ // Act
516
+ const {rerender, result} = clientRenderHook(
517
+ ({skip}) => useHydratableEffect("ID", fakeHandler, {skip}),
518
+ {
519
+ initialProps: {skip: false},
520
+ },
521
+ );
522
+ rerender({skip: true});
523
+ await act((): Promise<mixed> => response1);
524
+
525
+ // Assert
526
+ expect(result.all).not.toContainEqual(Status.success("DATA1"));
527
+ });
528
+
529
+ it("should not ignore inflight request if handler changes", async () => {
530
+ // Arrange
531
+ const response1 = Promise.resolve("DATA1");
532
+ const response2 = Promise.resolve("DATA2");
533
+ const fakeHandler1 = jest.fn().mockReturnValueOnce(response1);
534
+ const fakeHandler2 = jest.fn().mockReturnValueOnce(response2);
535
+ jest.spyOn(UseServerEffect, "useServerEffect").mockReturnValue(
536
+ null,
537
+ );
538
+
539
+ // Act
540
+ const {rerender, result} = clientRenderHook(
541
+ ({handler}) => useHydratableEffect("ID", handler),
542
+ {
543
+ initialProps: {handler: fakeHandler1},
544
+ },
545
+ );
546
+ rerender({handler: fakeHandler2});
547
+ await act((): Promise<mixed> =>
548
+ Promise.all([response1, response2]),
549
+ );
550
+
551
+ // Assert
552
+ expect(result.current).toStrictEqual(Status.success("DATA1"));
553
+ });
554
+
555
+ it("should not ignore inflight request if options (other than skip) change", async () => {
556
+ // Arrange
557
+ const response1 = Promise.resolve("DATA1");
558
+ const fakeHandler = jest.fn().mockReturnValueOnce(response1);
559
+ jest.spyOn(UseServerEffect, "useServerEffect").mockReturnValue(
560
+ null,
561
+ );
562
+
563
+ // Act
564
+ const {rerender, result} = clientRenderHook(
565
+ ({options}) => useHydratableEffect("ID", fakeHandler),
566
+ {
567
+ initialProps: {options: undefined},
568
+ },
569
+ );
570
+ rerender({
571
+ options: {
572
+ scope: "BLAH!",
573
+ },
574
+ });
575
+ await act((): Promise<mixed> => response1);
576
+
577
+ // Assert
578
+ expect(result.current).toStrictEqual(Status.success("DATA1"));
579
+ });
580
+
581
+ it("should return previous result when requestId changes and retainResultOnChange is true", async () => {
582
+ // Arrange
583
+ const response1 = Promise.resolve("DATA1");
584
+ const response2 = Promise.resolve("DATA2");
585
+ const fakeHandler = jest
586
+ .fn()
587
+ .mockReturnValueOnce(response1)
588
+ .mockReturnValueOnce(response2);
589
+ jest.spyOn(UseServerEffect, "useServerEffect").mockReturnValue(
590
+ null,
591
+ );
592
+
593
+ // Act
594
+ const {
595
+ rerender,
596
+ result: hookResult,
597
+ waitForNextUpdate,
598
+ } = clientRenderHook(
599
+ ({requestId}) =>
600
+ useHydratableEffect(requestId, fakeHandler, {
601
+ retainResultOnChange: true,
602
+ }),
603
+ {
604
+ initialProps: {requestId: "ID"},
605
+ },
606
+ );
607
+ await act((): Promise<mixed> => response1);
608
+ rerender({requestId: "ID2"});
609
+ const result = hookResult.current;
610
+ await waitForNextUpdate();
611
+
612
+ // Assert
613
+ expect(result).toStrictEqual(Status.success("DATA1"));
614
+ });
615
+
616
+ it("should return loading status when requestId changes and retainResultOnChange is false", async () => {
617
+ // Arrange
618
+ const response1 = Promise.resolve("DATA1");
619
+ const response2 = new Promise(() => {
620
+ /*pending*/
621
+ });
622
+ const fakeHandler = jest
623
+ .fn()
624
+ .mockReturnValueOnce(response1)
625
+ .mockReturnValueOnce(response2);
626
+ jest.spyOn(UseServerEffect, "useServerEffect").mockReturnValue(
627
+ null,
628
+ );
629
+
630
+ // Act
631
+ const {rerender, result} = clientRenderHook(
632
+ ({requestId}) =>
633
+ useHydratableEffect(requestId, fakeHandler, {
634
+ retainResultOnChange: false,
635
+ }),
636
+ {
637
+ initialProps: {requestId: "ID"},
638
+ },
639
+ );
640
+ await act((): Promise<mixed> => response1);
641
+ rerender({requestId: "ID2"});
642
+
643
+ // Assert
644
+ expect(result.current).toStrictEqual(Status.loading());
645
+ });
646
+
647
+ it("should trigger render when request is fulfilled and onResultChanged is undefined", async () => {
648
+ // Arrange
649
+ const response = Promise.resolve("DATA");
650
+ const fakeHandler = jest.fn().mockReturnValue(response);
651
+ jest.spyOn(UseServerEffect, "useServerEffect").mockReturnValue(
652
+ null,
653
+ );
654
+
655
+ // Act
656
+ const {result} = clientRenderHook(() =>
657
+ useHydratableEffect("ID", fakeHandler),
658
+ );
659
+ await act((): Promise<mixed> => response);
660
+
661
+ // Assert
662
+ expect(result.current).toStrictEqual(Status.success("DATA"));
663
+ });
664
+
665
+ it("should not trigger render when request is fulfilled and onResultChanged is defined", async () => {
666
+ // Arrange
667
+ const response = Promise.resolve("DATA");
668
+ const fakeHandler = jest.fn().mockReturnValue(response);
669
+ jest.spyOn(UseServerEffect, "useServerEffect").mockReturnValue(
670
+ null,
671
+ );
672
+
673
+ // Act
674
+ const {result} = clientRenderHook(() =>
675
+ useHydratableEffect("ID", fakeHandler, {
676
+ onResultChanged: () => {},
677
+ }),
678
+ );
679
+ await act((): Promise<mixed> => response);
680
+
681
+ // Assert
682
+ expect(result.current).toStrictEqual(Status.loading());
683
+ });
684
+
685
+ it("should call onResultChanged 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
+ const onResultChanged = jest.fn();
693
+
694
+ // Act
695
+ clientRenderHook(() =>
696
+ useHydratableEffect("ID", fakeHandler, {
697
+ onResultChanged,
698
+ }),
699
+ );
700
+ await act((): Promise<mixed> => response);
701
+
702
+ // Assert
703
+ expect(onResultChanged).toHaveBeenCalledWith(
704
+ Status.success("DATA"),
705
+ );
706
+ });
707
+ });
708
+ });