@khanacademy/wonder-blocks-data 7.0.1 → 8.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 (53) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/es/index.js +284 -100
  3. package/dist/index.js +1180 -800
  4. package/package.json +1 -1
  5. package/src/__docs__/_overview_ssr_.stories.mdx +13 -13
  6. package/src/__docs__/exports.abort-inflight-requests.stories.mdx +20 -0
  7. package/src/__docs__/exports.data.stories.mdx +3 -3
  8. package/src/__docs__/{exports.fulfill-all-data-requests.stories.mdx → exports.fetch-tracked-requests.stories.mdx} +5 -5
  9. package/src/__docs__/exports.get-gql-request-id.stories.mdx +24 -0
  10. package/src/__docs__/{exports.has-unfulfilled-requests.stories.mdx → exports.has-tracked-requests-to-be-fetched.stories.mdx} +4 -4
  11. package/src/__docs__/exports.intialize-hydration-cache.stories.mdx +29 -0
  12. package/src/__docs__/exports.purge-caches.stories.mdx +23 -0
  13. package/src/__docs__/{exports.remove-all-from-cache.stories.mdx → exports.purge-hydration-cache.stories.mdx} +4 -4
  14. package/src/__docs__/{exports.clear-shared-cache.stories.mdx → exports.purge-shared-cache.stories.mdx} +4 -4
  15. package/src/__docs__/exports.track-data.stories.mdx +4 -4
  16. package/src/__docs__/exports.use-cached-effect.stories.mdx +7 -4
  17. package/src/__docs__/exports.use-gql.stories.mdx +1 -33
  18. package/src/__docs__/exports.use-server-effect.stories.mdx +1 -1
  19. package/src/__docs__/exports.use-shared-cache.stories.mdx +2 -2
  20. package/src/__docs__/types.fetch-policy.stories.mdx +44 -0
  21. package/src/__docs__/types.response-cache.stories.mdx +1 -1
  22. package/src/__tests__/generated-snapshot.test.js +5 -5
  23. package/src/components/__tests__/data.test.js +2 -6
  24. package/src/hooks/__tests__/use-cached-effect.test.js +341 -100
  25. package/src/hooks/__tests__/use-hydratable-effect.test.js +15 -9
  26. package/src/hooks/__tests__/use-shared-cache.test.js +6 -6
  27. package/src/hooks/use-cached-effect.js +169 -93
  28. package/src/hooks/use-hydratable-effect.js +8 -1
  29. package/src/hooks/use-shared-cache.js +2 -2
  30. package/src/index.js +14 -78
  31. package/src/util/__tests__/get-gql-request-id.test.js +74 -0
  32. package/src/util/__tests__/graphql-document-node-parser.test.js +542 -0
  33. package/src/util/__tests__/hydration-cache-api.test.js +35 -0
  34. package/src/util/__tests__/purge-caches.test.js +29 -0
  35. package/src/util/__tests__/request-api.test.js +188 -0
  36. package/src/util/__tests__/request-fulfillment.test.js +42 -0
  37. package/src/util/__tests__/ssr-cache.test.js +10 -60
  38. package/src/util/__tests__/to-gql-operation.test.js +42 -0
  39. package/src/util/data-error.js +6 -0
  40. package/src/util/get-gql-request-id.js +50 -0
  41. package/src/util/graphql-document-node-parser.js +133 -0
  42. package/src/util/graphql-types.js +30 -0
  43. package/src/util/hydration-cache-api.js +28 -0
  44. package/src/util/purge-caches.js +15 -0
  45. package/src/util/request-api.js +66 -0
  46. package/src/util/request-fulfillment.js +32 -12
  47. package/src/util/request-tracking.js +1 -1
  48. package/src/util/ssr-cache.js +1 -21
  49. package/src/util/to-gql-operation.js +44 -0
  50. package/src/util/types.js +31 -0
  51. package/src/__docs__/exports.intialize-cache.stories.mdx +0 -29
  52. package/src/__docs__/exports.remove-from-cache.stories.mdx +0 -25
  53. package/src/__docs__/exports.request-fulfillment.stories.mdx +0 -36
@@ -1,9 +1,11 @@
1
1
  // @flow
2
+ import * as React from "react";
2
3
  import {
3
4
  renderHook as clientRenderHook,
4
5
  act,
5
6
  } from "@testing-library/react-hooks";
6
7
  import {renderHook as serverRenderHook} from "@testing-library/react-hooks/server";
8
+ import {render, act as reactAct} from "@testing-library/react";
7
9
 
8
10
  import {Server} from "@khanacademy/wonder-blocks-core";
9
11
  import {Status} from "../../util/status.js";
@@ -14,17 +16,25 @@ import * as UseSharedCache from "../use-shared-cache.js";
14
16
 
15
17
  import {useCachedEffect} from "../use-cached-effect.js";
16
18
 
19
+ // TODO(somewhatabstract, FEI-4174): Update eslint-plugin-import when they
20
+ // have fixed:
21
+ // https://github.com/import-js/eslint-plugin-import/issues/2073
22
+ // eslint-disable-next-line import/named
23
+ import {FetchPolicy} from "../../util/types.js";
24
+
17
25
  jest.mock("../use-request-interception.js");
18
26
  jest.mock("../use-shared-cache.js");
19
27
 
28
+ const allPolicies = Array.from(FetchPolicy.members());
29
+ const allPoliciesBut = (policy: FetchPolicy) =>
30
+ allPolicies.filter((p) => p !== policy);
31
+
20
32
  describe("#useCachedEffect", () => {
21
33
  beforeEach(() => {
22
34
  jest.resetAllMocks();
23
35
 
24
- // When we have request aborting and things, this can be nicer, but
25
- // for now, let's just clear out inflight requests between tests
26
- // by being cheeky.
27
- RequestFulfillment.Default._requests = {};
36
+ // Clear out inflight requests between tests.
37
+ RequestFulfillment.Default.abortAll();
28
38
 
29
39
  // Simple implementation of request interception that just returns
30
40
  // the handler.
@@ -37,7 +47,10 @@ describe("#useCachedEffect", () => {
37
47
  const cache = {};
38
48
  jest.spyOn(UseSharedCache, "useSharedCache").mockImplementation(
39
49
  (id, _, defaultValue) => {
40
- const setCache = (v) => (cache[id] = v);
50
+ const setCache = React.useCallback(
51
+ (v) => (cache[id] = v),
52
+ [id],
53
+ );
41
54
  return [cache[id] ?? defaultValue, setCache];
42
55
  },
43
56
  );
@@ -91,49 +104,93 @@ describe("#useCachedEffect", () => {
91
104
  },
92
105
  );
93
106
 
94
- it("should not request data", () => {
95
- // Arrange
96
- const fakeHandler = jest.fn().mockResolvedValue("data");
97
-
98
- // Act
99
- serverRenderHook(() => useCachedEffect("ID", fakeHandler));
100
-
101
- // Assert
102
- expect(fakeHandler).not.toHaveBeenCalled();
103
- });
104
-
105
- describe("without cached result", () => {
106
- it("should return a loading result", () => {
107
+ it.each(allPolicies)(
108
+ "should not request data for FetchPolicy.%s",
109
+ (fetchPolicy) => {
107
110
  // Arrange
108
- const fakeHandler = jest.fn();
111
+ const fakeHandler = jest.fn().mockResolvedValue("data");
109
112
 
110
113
  // Act
111
- const {
112
- result: {current: result},
113
- } = serverRenderHook(() => useCachedEffect("ID", fakeHandler));
114
+ serverRenderHook(() =>
115
+ useCachedEffect("ID", fakeHandler, {fetchPolicy}),
116
+ );
114
117
 
115
118
  // Assert
116
- expect(result).toStrictEqual(Status.loading());
117
- });
118
- });
119
+ expect(fakeHandler).not.toHaveBeenCalled();
120
+ },
121
+ );
122
+
123
+ describe.each(allPolicies)(
124
+ "with FetchPolicy.%s without cached result",
125
+ (fetchPolicy) => {
126
+ it("should return a loading result", () => {
127
+ // Arrange
128
+ const fakeHandler = jest.fn();
129
+
130
+ // Act
131
+ const {
132
+ result: {
133
+ current: [result],
134
+ },
135
+ } = serverRenderHook(() =>
136
+ useCachedEffect("ID", fakeHandler, {fetchPolicy}),
137
+ );
138
+
139
+ // Assert
140
+ expect(result).toStrictEqual(Status.loading());
141
+ });
142
+ },
143
+ );
144
+
145
+ describe.each(allPoliciesBut(FetchPolicy.NetworkOnly))(
146
+ "with FetchPolicy.%s with cached result",
147
+ (fetchPolicy) => {
148
+ it("should return the result", () => {
149
+ // Arrange
150
+ const fakeHandler = jest.fn();
151
+ const cachedResult = Status.success("data");
152
+ jest.spyOn(
153
+ UseSharedCache,
154
+ "useSharedCache",
155
+ ).mockReturnValue([cachedResult, jest.fn()]);
156
+
157
+ // Act
158
+ const {
159
+ result: {
160
+ current: [result],
161
+ },
162
+ } = serverRenderHook(() =>
163
+ useCachedEffect("ID", fakeHandler, {fetchPolicy}),
164
+ );
165
+
166
+ // Assert
167
+ expect(result).toEqual(cachedResult);
168
+ });
169
+ },
170
+ );
119
171
 
120
- describe("with cached result", () => {
121
- it("should return the result", () => {
172
+ describe("with FetchPolicy.NetworkOnly with cached result", () => {
173
+ it("should return a loading result", () => {
122
174
  // Arrange
123
175
  const fakeHandler = jest.fn();
124
- const cachedResult = Status.success("data");
125
176
  jest.spyOn(UseSharedCache, "useSharedCache").mockReturnValue([
126
- cachedResult,
177
+ Status.success("data"),
127
178
  jest.fn(),
128
179
  ]);
129
180
 
130
181
  // Act
131
182
  const {
132
- result: {current: result},
133
- } = serverRenderHook(() => useCachedEffect("ID", fakeHandler));
183
+ result: {
184
+ current: [result],
185
+ },
186
+ } = serverRenderHook(() =>
187
+ useCachedEffect("ID", fakeHandler, {
188
+ fetchPolicy: FetchPolicy.NetworkOnly,
189
+ }),
190
+ );
134
191
 
135
192
  // Assert
136
- expect(result).toEqual(cachedResult);
193
+ expect(result).toStrictEqual(Status.loading());
137
194
  });
138
195
  });
139
196
  });
@@ -160,21 +217,6 @@ describe("#useCachedEffect", () => {
160
217
  );
161
218
  });
162
219
 
163
- it("should fulfill request when there is no cached value", () => {
164
- // Arrange
165
- const fakeHandler = jest.fn();
166
- jest.spyOn(UseSharedCache, "useSharedCache").mockReturnValue([
167
- null,
168
- jest.fn(),
169
- ]);
170
-
171
- // Act
172
- clientRenderHook(() => useCachedEffect("ID", fakeHandler));
173
-
174
- // Assert
175
- expect(fakeHandler).toHaveBeenCalled();
176
- });
177
-
178
220
  it("should share inflight requests for the same requestId", () => {
179
221
  // Arrange
180
222
  const pending = new Promise((resolve, reject) => {
@@ -190,13 +232,102 @@ describe("#useCachedEffect", () => {
190
232
  expect(fakeHandler).toHaveBeenCalledTimes(1);
191
233
  });
192
234
 
235
+ it.each(allPoliciesBut(FetchPolicy.CacheOnly))(
236
+ "should provide function that causes refetch with FetchPolicy.%s",
237
+ async (fetchPolicy) => {
238
+ // Arrange
239
+ const response = Promise.resolve("DATA1");
240
+ const fakeHandler = jest.fn().mockReturnValue(response);
241
+
242
+ // Act
243
+ const {
244
+ result: {
245
+ current: [, refetch],
246
+ },
247
+ } = clientRenderHook(() =>
248
+ useCachedEffect("ID", fakeHandler, {fetchPolicy}),
249
+ );
250
+ fakeHandler.mockClear();
251
+ await act((): Promise<mixed> => response);
252
+ act(refetch);
253
+ await act((): Promise<mixed> => response);
254
+
255
+ // Assert
256
+ expect(fakeHandler).toHaveBeenCalledTimes(1);
257
+ },
258
+ );
259
+
260
+ it("should throw with FetchPolicy.CacheOnly", () => {
261
+ // Arrange
262
+ const fakeHandler = jest.fn();
263
+
264
+ // Act
265
+ const {
266
+ result: {
267
+ current: [, refetch],
268
+ },
269
+ } = clientRenderHook(() =>
270
+ useCachedEffect("ID", fakeHandler, {
271
+ fetchPolicy: FetchPolicy.CacheOnly,
272
+ }),
273
+ );
274
+ const underTest = () => refetch();
275
+
276
+ // Assert
277
+ expect(underTest).toThrowErrorMatchingInlineSnapshot(
278
+ `"Cannot fetch with CacheOnly policy"`,
279
+ );
280
+ });
281
+
282
+ it.each(allPoliciesBut(FetchPolicy.CacheOnly))(
283
+ "should fulfill request when there is no cached value and FetchPolicy.%s",
284
+ (fetchPolicy) => {
285
+ // Arrange
286
+ const fakeHandler = jest.fn();
287
+ jest.spyOn(UseSharedCache, "useSharedCache").mockReturnValue([
288
+ null,
289
+ jest.fn(),
290
+ ]);
291
+
292
+ // Act
293
+ clientRenderHook(() =>
294
+ useCachedEffect("ID", fakeHandler, {fetchPolicy}),
295
+ );
296
+
297
+ // Assert
298
+ expect(fakeHandler).toHaveBeenCalled();
299
+ },
300
+ );
301
+
302
+ it.each([FetchPolicy.CacheAndNetwork, FetchPolicy.NetworkOnly])(
303
+ "should fulfill request when there is a cached value and FetchPolicy.%s",
304
+ (fetchPolicy) => {
305
+ // Arrange
306
+ const fakeHandler = jest.fn();
307
+ jest.spyOn(UseSharedCache, "useSharedCache").mockReturnValue([
308
+ Status.success("data"),
309
+ jest.fn(),
310
+ ]);
311
+
312
+ // Act
313
+ clientRenderHook(() =>
314
+ useCachedEffect("ID", fakeHandler, {
315
+ fetchPolicy,
316
+ }),
317
+ );
318
+
319
+ // Assert
320
+ expect(fakeHandler).toHaveBeenCalled();
321
+ },
322
+ );
323
+
193
324
  it.each`
194
325
  cachedResult
195
326
  ${Status.error(new Error("some error"))}
196
327
  ${Status.success("data")}
197
328
  ${Status.aborted()}
198
329
  `(
199
- "should not fulfill request when there is a cached response of $cachedResult",
330
+ "should not fulfill request when there is a cached response of $cachedResult and FetchPolicy.CacheBeforeNetwork",
200
331
  ({cachedResult}) => {
201
332
  const fakeHandler = jest.fn();
202
333
  jest.spyOn(UseSharedCache, "useSharedCache").mockReturnValue([
@@ -205,14 +336,45 @@ describe("#useCachedEffect", () => {
205
336
  ]);
206
337
 
207
338
  // Act
208
- clientRenderHook(() => useCachedEffect("ID", fakeHandler));
339
+ clientRenderHook(() =>
340
+ useCachedEffect("ID", fakeHandler, {
341
+ fetchPolicy: FetchPolicy.CacheBeforeNetwork,
342
+ }),
343
+ );
209
344
 
210
345
  // Assert
211
346
  expect(fakeHandler).not.toHaveBeenCalled();
212
347
  },
213
348
  );
214
349
 
215
- it("should fulfill request once only if requestId does not change", async () => {
350
+ it.each`
351
+ cachedResult
352
+ ${null}
353
+ ${Status.error(new Error("some error"))}
354
+ ${Status.success("data")}
355
+ ${Status.aborted()}
356
+ `(
357
+ "should not fulfill request when there is a cached response of $cachedResult and FetchPolicy.CacheOnly",
358
+ ({cachedResult}) => {
359
+ const fakeHandler = jest.fn();
360
+ jest.spyOn(UseSharedCache, "useSharedCache").mockReturnValue([
361
+ cachedResult,
362
+ jest.fn(),
363
+ ]);
364
+
365
+ // Act
366
+ clientRenderHook(() =>
367
+ useCachedEffect("ID", fakeHandler, {
368
+ fetchPolicy: FetchPolicy.CacheOnly,
369
+ }),
370
+ );
371
+
372
+ // Assert
373
+ expect(fakeHandler).not.toHaveBeenCalled();
374
+ },
375
+ );
376
+
377
+ it("should fulfill request once-only if requestId does not change", async () => {
216
378
  // Arrange
217
379
  const fakeHandler = jest.fn().mockResolvedValue("data");
218
380
 
@@ -311,7 +473,7 @@ describe("#useCachedEffect", () => {
311
473
  );
312
474
 
313
475
  // Assert
314
- expect(result.current).toStrictEqual(Status.success("DATA2"));
476
+ expect(result.current[0]).toStrictEqual(Status.success("DATA2"));
315
477
  });
316
478
 
317
479
  it("should not fulfill request when skip is true", () => {
@@ -327,7 +489,7 @@ describe("#useCachedEffect", () => {
327
489
  expect(fakeHandler).not.toHaveBeenCalled();
328
490
  });
329
491
 
330
- it("should ignore inflight request if skip changes", async () => {
492
+ it("should ignore result of inflight request if skip changes", async () => {
331
493
  // Arrange
332
494
  const response1 = Promise.resolve("DATA1");
333
495
  const fakeHandler = jest.fn().mockReturnValueOnce(response1);
@@ -346,7 +508,7 @@ describe("#useCachedEffect", () => {
346
508
  expect(result.all).not.toContainEqual(Status.success("DATA1"));
347
509
  });
348
510
 
349
- it("should not ignore inflight request if handler changes", async () => {
511
+ it("should not ignore result of inflight request if handler changes", async () => {
350
512
  // Arrange
351
513
  const response1 = Promise.resolve("DATA1");
352
514
  const response2 = Promise.resolve("DATA2");
@@ -366,7 +528,7 @@ describe("#useCachedEffect", () => {
366
528
  );
367
529
 
368
530
  // Assert
369
- expect(result.current).toStrictEqual(Status.success("DATA1"));
531
+ expect(result.current[0]).toStrictEqual(Status.success("DATA1"));
370
532
  });
371
533
 
372
534
  it("should not ignore inflight request if options (other than skip) change", async () => {
@@ -389,7 +551,7 @@ describe("#useCachedEffect", () => {
389
551
  await act((): Promise<mixed> => response1);
390
552
 
391
553
  // Assert
392
- expect(result.current).toStrictEqual(Status.success("DATA1"));
554
+ expect(result.current[0]).toStrictEqual(Status.success("DATA1"));
393
555
  });
394
556
 
395
557
  it("should return previous result when requestId changes and retainResultOnChange is true", async () => {
@@ -417,7 +579,7 @@ describe("#useCachedEffect", () => {
417
579
  );
418
580
  await act((): Promise<mixed> => response1);
419
581
  rerender({requestId: "ID2"});
420
- const result = hookResult.current;
582
+ const [result] = hookResult.current;
421
583
  await waitForNextUpdate();
422
584
 
423
585
  // Assert
@@ -449,59 +611,138 @@ describe("#useCachedEffect", () => {
449
611
  rerender({requestId: "ID2"});
450
612
 
451
613
  // Assert
452
- expect(result.current).toStrictEqual(Status.loading());
614
+ expect(result.current[0]).toStrictEqual(Status.loading());
453
615
  });
454
616
 
455
- it("should trigger render when request is fulfilled and onResultChanged is undefined", async () => {
456
- // Arrange
457
- const response = Promise.resolve("DATA");
458
- const fakeHandler = jest.fn().mockReturnValue(response);
617
+ it.each(allPoliciesBut(FetchPolicy.CacheOnly))(
618
+ "should trigger render when request is fulfilled and onResultChanged is undefined for FetchPolicy.%s",
619
+ async (fetchPolicy) => {
620
+ // Arrange
621
+ const response = Promise.resolve("DATA");
622
+ const fakeHandler = jest.fn().mockReturnValue(response);
623
+ let renderCount = 0;
624
+ const Component = React.memo(() => {
625
+ useCachedEffect("ID", fakeHandler, {fetchPolicy});
626
+ renderCount++;
627
+ return <div>Hello :)</div>;
628
+ });
459
629
 
460
- // Act
461
- const {result} = clientRenderHook(() =>
462
- useCachedEffect("ID", fakeHandler),
463
- );
464
- await act((): Promise<mixed> => response);
630
+ // Act
631
+ render(<Component />);
632
+ await reactAct((): Promise<mixed> => response);
465
633
 
466
- // Assert
467
- expect(result.current).toStrictEqual(Status.success("DATA"));
468
- });
634
+ // Assert
635
+ expect(renderCount).toBe(2);
636
+ },
637
+ );
469
638
 
470
- it("should not trigger render when request is fulfilled and onResultChanged is defined", async () => {
471
- // Arrange
472
- const response = Promise.resolve("DATA");
473
- const fakeHandler = jest.fn().mockReturnValue(response);
639
+ it.each(allPoliciesBut(FetchPolicy.CacheOnly))(
640
+ "should trigger render once per inflight request being fulfilled and onResultChanged is undefined for FetchPolicy.%s",
641
+ async (fetchPolicy) => {
642
+ // Arrange
643
+ const response = Promise.resolve("DATA");
644
+ const fakeHandler = jest.fn().mockReturnValue(response);
645
+ let renderCount = 0;
646
+ const Component = React.memo(() => {
647
+ const [, refetch] = useCachedEffect("ID", fakeHandler, {
648
+ fetchPolicy,
649
+ });
650
+ React.useEffect(() => {
651
+ refetch();
652
+ refetch();
653
+ refetch();
654
+ refetch();
655
+ }, [refetch]);
656
+ renderCount++;
657
+ return <div>Hello :)</div>;
658
+ });
474
659
 
475
- // Act
476
- const {result} = clientRenderHook(() =>
477
- useCachedEffect("ID", fakeHandler, {
478
- onResultChanged: () => {},
479
- }),
480
- );
481
- await act((): Promise<mixed> => response);
660
+ // Act
661
+ render(<Component />);
662
+ await reactAct((): Promise<mixed> => response);
482
663
 
483
- // Assert
484
- expect(result.current).toStrictEqual(Status.loading());
485
- });
664
+ // Assert
665
+ expect(renderCount).toBe(2);
666
+ },
667
+ );
486
668
 
487
- it("should call onResultChanged when request is fulfilled and onResultChanged is defined", async () => {
488
- // Arrange
489
- const response = Promise.resolve("DATA");
490
- const fakeHandler = jest.fn().mockReturnValue(response);
491
- const onResultChanged = jest.fn();
669
+ it.each(allPoliciesBut(FetchPolicy.CacheOnly))(
670
+ "should not trigger render when request is fulfilled and onResultChanged is defined for FetchPolicy.%s",
671
+ async (fetchPolicy) => {
672
+ // Arrange
673
+ const response = Promise.resolve("DATA");
674
+ const fakeHandler = jest.fn().mockReturnValue(response);
675
+ let renderCount = 0;
676
+ const Component = React.memo(() => {
677
+ useCachedEffect("ID", fakeHandler, {
678
+ onResultChanged: () => {},
679
+ fetchPolicy,
680
+ });
681
+ renderCount++;
682
+ return <div>Hello :)</div>;
683
+ });
492
684
 
493
- // Act
494
- clientRenderHook(() =>
495
- useCachedEffect("ID", fakeHandler, {
496
- onResultChanged,
497
- }),
498
- );
499
- await act((): Promise<mixed> => response);
685
+ // Act
686
+ render(<Component />);
687
+ await reactAct((): Promise<mixed> => response);
500
688
 
501
- // Assert
502
- expect(onResultChanged).toHaveBeenCalledWith(
503
- Status.success("DATA"),
504
- );
505
- });
689
+ // Assert
690
+ expect(renderCount).toBe(1);
691
+ },
692
+ );
693
+
694
+ it.each(allPoliciesBut(FetchPolicy.CacheOnly))(
695
+ "should call onResultChanged when request is fulfilled and onResultChanged is defined for FetchPolicy.%s",
696
+ async (fetchPolicy) => {
697
+ // Arrange
698
+ const response = Promise.resolve("DATA");
699
+ const fakeHandler = jest.fn().mockReturnValue(response);
700
+ const onResultChanged = jest.fn();
701
+
702
+ // Act
703
+ clientRenderHook(() =>
704
+ useCachedEffect("ID", fakeHandler, {
705
+ onResultChanged,
706
+ fetchPolicy,
707
+ }),
708
+ );
709
+ await act((): Promise<mixed> => response);
710
+
711
+ // Assert
712
+ expect(onResultChanged).toHaveBeenCalledWith(
713
+ Status.success("DATA"),
714
+ );
715
+ },
716
+ );
717
+
718
+ it.each(allPoliciesBut(FetchPolicy.CacheOnly))(
719
+ "should call onResultChanged once per inflight request being fulfilled and onResultChanged is defined for FetchPolicy.%s",
720
+ async (fetchPolicy) => {
721
+ // Arrange
722
+ const response = Promise.resolve("DATA");
723
+ const fakeHandler = jest.fn().mockReturnValue(response);
724
+ const onResultChanged = jest.fn();
725
+
726
+ // Act
727
+ const {
728
+ result: {
729
+ current: [, refetch],
730
+ },
731
+ } = clientRenderHook(() =>
732
+ useCachedEffect("ID", fakeHandler, {
733
+ onResultChanged,
734
+ fetchPolicy,
735
+ }),
736
+ );
737
+ act(refetch);
738
+ act(refetch);
739
+ act(refetch);
740
+ act(refetch);
741
+ await act((): Promise<mixed> => response);
742
+
743
+ // Assert
744
+ expect(onResultChanged).toHaveBeenCalledTimes(1);
745
+ },
746
+ );
506
747
  });
507
748
  });
@@ -1,4 +1,5 @@
1
1
  // @flow
2
+ import * as React from "react";
2
3
  import {
3
4
  renderHook as clientRenderHook,
4
5
  act,
@@ -30,10 +31,8 @@ describe("#useHydratableEffect", () => {
30
31
  beforeEach(() => {
31
32
  jest.resetAllMocks();
32
33
 
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 = {};
34
+ // Clear out inflight requests between tests.
35
+ RequestFulfillment.Default.abortAll();
37
36
 
38
37
  // Simple implementation of request interception that just returns
39
38
  // the handler.
@@ -46,7 +45,10 @@ describe("#useHydratableEffect", () => {
46
45
  const cache = {};
47
46
  jest.spyOn(UseSharedCache, "useSharedCache").mockImplementation(
48
47
  (id, _, defaultValue) => {
49
- const setCache = (v) => (cache[id] = v);
48
+ const setCache = React.useCallback(
49
+ (v) => (cache[id] = v),
50
+ [id],
51
+ );
50
52
  const currentValue =
51
53
  cache[id] ??
52
54
  (typeof defaultValue === "function"
@@ -390,12 +392,16 @@ describe("#useHydratableEffect", () => {
390
392
  expect(fakeHandler).toHaveBeenCalledTimes(2);
391
393
  });
392
394
 
393
- it("should default shared cache to hydrate value for new requestId", async () => {
395
+ it("should default shared cache to hydrate value for new requestId", () => {
394
396
  // Arrange
395
- const fakeHandler = jest.fn().mockResolvedValue("data");
397
+ const fakeHandler = jest.fn().mockResolvedValue("NEVER CALLED");
396
398
  jest.spyOn(UseServerEffect, "useServerEffect")
399
+ // First requestId will get hydrated value. No fetch will occur.
400
+ // The hook result will be this value.
397
401
  .mockReturnValueOnce(Status.success("BADDATA"))
398
- .mockReturnValue(null);
402
+ // Second requestId will get a different hydrated value.
403
+ // No fetch will occur. The hook will then be this value.
404
+ .mockReturnValueOnce(Status.success("GOODDATA"));
399
405
 
400
406
  // Act
401
407
  const {rerender, result} = clientRenderHook(
@@ -407,7 +413,7 @@ describe("#useHydratableEffect", () => {
407
413
  rerender({requestId: "ID2"});
408
414
 
409
415
  // Assert
410
- expect(result.current).toStrictEqual(Status.loading());
416
+ expect(result.current).toStrictEqual(Status.success("GOODDATA"));
411
417
  });
412
418
 
413
419
  it("should update shared cache with result when request is fulfilled", async () => {
@@ -1,11 +1,11 @@
1
1
  // @flow
2
2
  import {renderHook as clientRenderHook} from "@testing-library/react-hooks";
3
3
 
4
- import {useSharedCache, clearSharedCache} from "../use-shared-cache.js";
4
+ import {useSharedCache, purgeSharedCache} from "../use-shared-cache.js";
5
5
 
6
6
  describe("#useSharedCache", () => {
7
7
  beforeEach(() => {
8
- clearSharedCache();
8
+ purgeSharedCache();
9
9
  });
10
10
 
11
11
  it.each`
@@ -258,9 +258,9 @@ describe("#useSharedCache", () => {
258
258
  });
259
259
  });
260
260
 
261
- describe("#clearSharedCache", () => {
261
+ describe("#purgeSharedCache", () => {
262
262
  beforeEach(() => {
263
- clearSharedCache();
263
+ purgeSharedCache();
264
264
  });
265
265
 
266
266
  it("should clear the entire cache if no scope given", () => {
@@ -274,7 +274,7 @@ describe("#clearSharedCache", () => {
274
274
  hook2.rerender();
275
275
 
276
276
  // Act
277
- clearSharedCache();
277
+ purgeSharedCache();
278
278
  // Make sure we refresh the hook results.
279
279
  hook1.rerender();
280
280
  hook2.rerender();
@@ -295,7 +295,7 @@ describe("#clearSharedCache", () => {
295
295
  hook2.rerender();
296
296
 
297
297
  // Act
298
- clearSharedCache("scope2");
298
+ purgeSharedCache("scope2");
299
299
  // Make sure we refresh the hook results.
300
300
  hook1.rerender();
301
301
  hook2.rerender();