@khanacademy/wonder-blocks-data 3.1.3 → 5.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 (49) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/es/index.js +408 -349
  3. package/dist/index.js +599 -494
  4. package/docs.md +17 -35
  5. package/package.json +1 -1
  6. package/src/__tests__/__snapshots__/generated-snapshot.test.js.snap +7 -46
  7. package/src/__tests__/generated-snapshot.test.js +60 -126
  8. package/src/components/__tests__/data.test.js +373 -313
  9. package/src/components/__tests__/intercept-requests.test.js +58 -0
  10. package/src/components/data.js +139 -21
  11. package/src/components/data.md +38 -69
  12. package/src/components/intercept-context.js +6 -3
  13. package/src/components/intercept-requests.js +69 -0
  14. package/src/components/intercept-requests.md +54 -0
  15. package/src/components/track-data.md +9 -23
  16. package/src/hooks/__tests__/__snapshots__/use-shared-cache.test.js.snap +17 -0
  17. package/src/hooks/__tests__/use-gql.test.js +1 -0
  18. package/src/hooks/__tests__/use-request-interception.test.js +255 -0
  19. package/src/hooks/__tests__/use-server-effect.test.js +217 -0
  20. package/src/hooks/__tests__/use-shared-cache.test.js +307 -0
  21. package/src/hooks/use-gql.js +36 -23
  22. package/src/hooks/use-request-interception.js +54 -0
  23. package/src/hooks/use-server-effect.js +45 -0
  24. package/src/hooks/use-shared-cache.js +106 -0
  25. package/src/index.js +18 -20
  26. package/src/util/__tests__/__snapshots__/scoped-in-memory-cache.test.js.snap +19 -0
  27. package/src/util/__tests__/request-fulfillment.test.js +42 -85
  28. package/src/util/__tests__/request-tracking.test.js +72 -191
  29. package/src/util/__tests__/{result-from-cache-entry.test.js → result-from-cache-response.test.js} +9 -10
  30. package/src/util/__tests__/scoped-in-memory-cache.test.js +396 -0
  31. package/src/util/__tests__/ssr-cache.test.js +639 -0
  32. package/src/util/request-fulfillment.js +36 -44
  33. package/src/util/request-tracking.js +62 -75
  34. package/src/util/{result-from-cache-entry.js → result-from-cache-response.js} +10 -13
  35. package/src/util/scoped-in-memory-cache.js +149 -0
  36. package/src/util/ssr-cache.js +206 -0
  37. package/src/util/types.js +43 -108
  38. package/src/components/__tests__/intercept-data.test.js +0 -87
  39. package/src/components/intercept-data.js +0 -77
  40. package/src/components/intercept-data.md +0 -65
  41. package/src/hooks/__tests__/use-data.test.js +0 -826
  42. package/src/hooks/use-data.js +0 -143
  43. package/src/util/__tests__/memory-cache.test.js +0 -446
  44. package/src/util/__tests__/request-handler.test.js +0 -121
  45. package/src/util/__tests__/response-cache.test.js +0 -879
  46. package/src/util/memory-cache.js +0 -187
  47. package/src/util/request-handler.js +0 -42
  48. package/src/util/request-handler.md +0 -51
  49. package/src/util/response-cache.js +0 -213
@@ -9,19 +9,15 @@ import {Server, View} from "@khanacademy/wonder-blocks-core";
9
9
 
10
10
  import TrackData from "../track-data.js";
11
11
  import {RequestFulfillment} from "../../util/request-fulfillment.js";
12
- import {ResponseCache} from "../../util/response-cache.js";
12
+ import {SsrCache} from "../../util/ssr-cache.js";
13
13
  import {RequestTracker} from "../../util/request-tracking.js";
14
- import InterceptData from "../intercept-data.js";
14
+ import InterceptRequests from "../intercept-requests.js";
15
15
  import Data from "../data.js";
16
16
 
17
- import type {IRequestHandler} from "../../util/types.js";
18
-
19
17
  describe("Data", () => {
20
18
  beforeEach(() => {
21
- const responseCache = new ResponseCache();
22
- jest.spyOn(ResponseCache, "Default", "get").mockReturnValue(
23
- responseCache,
24
- );
19
+ const responseCache = new SsrCache();
20
+ jest.spyOn(SsrCache, "Default", "get").mockReturnValue(responseCache);
25
21
  jest.spyOn(RequestFulfillment, "Default", "get").mockReturnValue(
26
22
  new RequestFulfillment(responseCache),
27
23
  );
@@ -45,53 +41,42 @@ describe("Data", () => {
45
41
  * Each of these test cases will not have cached data to be
46
42
  * retrieved in the beginning.
47
43
  */
48
- jest.spyOn(
49
- ResponseCache.Default,
50
- "getEntry",
51
- ).mockReturnValueOnce(null);
44
+ jest.spyOn(SsrCache.Default, "getEntry").mockReturnValueOnce(
45
+ null,
46
+ );
52
47
  });
53
48
 
54
- it("should make request for data on construction", () => {
49
+ it("should make request for data on construction", async () => {
55
50
  // Arrange
56
- const fulfillRequestSpy = jest.fn(() =>
57
- Promise.resolve("data"),
58
- );
59
- const fakeHandler: IRequestHandler<string, string> = {
60
- fulfillRequest: fulfillRequestSpy,
61
- getKey: (o) => o,
62
- type: "MY_HANDLER",
63
- hydrate: true,
64
- };
51
+ const response = Promise.resolve("data");
52
+ const fakeHandler = jest.fn().mockReturnValue(response);
65
53
  const fakeChildrenFn = jest.fn(() => null);
66
54
 
67
55
  // Act
68
56
  render(
69
- <Data handler={fakeHandler} options={"options"}>
57
+ <Data handler={fakeHandler} requestId="ID">
70
58
  {fakeChildrenFn}
71
59
  </Data>,
72
60
  );
61
+ await act(() => response);
73
62
 
74
63
  // Assert
75
- expect(fulfillRequestSpy).toHaveBeenCalledWith("options");
76
- expect(fulfillRequestSpy).toHaveBeenCalledTimes(1);
64
+ expect(fakeHandler).toHaveBeenCalledTimes(1);
77
65
  });
78
66
 
79
- it("should initially render children with loading", () => {
67
+ it("should initially render children with loading", async () => {
80
68
  // Arrange
81
- const fakeHandler: IRequestHandler<string, string> = {
82
- fulfillRequest: () => Promise.resolve("data"),
83
- getKey: (o) => o,
84
- type: "MY_HANDLER",
85
- hydrate: true,
86
- };
69
+ const response = Promise.resolve("data");
70
+ const fakeHandler = jest.fn().mockReturnValue(response);
87
71
  const fakeChildrenFn = jest.fn(() => null);
88
72
 
89
73
  // Act
90
74
  render(
91
- <Data handler={fakeHandler} options={"options"}>
75
+ <Data handler={fakeHandler} requestId="ID">
92
76
  {fakeChildrenFn}
93
77
  </Data>,
94
78
  );
79
+ await act(() => response);
95
80
 
96
81
  // Assert
97
82
  expect(fakeChildrenFn).toHaveBeenCalledWith({
@@ -101,32 +86,25 @@ describe("Data", () => {
101
86
 
102
87
  it("should share single request across all uses", () => {
103
88
  // Arrange
104
- const fulfillRequestSpy = jest.fn(
89
+ const fakeHandler = jest.fn(
105
90
  () => new Promise((resolve, reject) => {}),
106
91
  );
107
- const fakeHandler: IRequestHandler<string, string> = {
108
- fulfillRequest: fulfillRequestSpy,
109
- getKey: (o) => o,
110
- type: "MY_HANDLER",
111
- hydrate: true,
112
- };
113
92
  const fakeChildrenFn = jest.fn(() => null);
114
93
 
115
94
  // Act
116
95
  render(
117
96
  <View>
118
- <Data handler={fakeHandler} options={"options"}>
97
+ <Data handler={fakeHandler} requestId="ID">
119
98
  {fakeChildrenFn}
120
99
  </Data>
121
- <Data handler={fakeHandler} options={"options"}>
100
+ <Data handler={fakeHandler} requestId="ID">
122
101
  {fakeChildrenFn}
123
102
  </Data>
124
103
  </View>,
125
104
  );
126
105
 
127
106
  // Assert
128
- expect(fulfillRequestSpy).toHaveBeenCalledWith("options");
129
- expect(fulfillRequestSpy).toHaveBeenCalledTimes(1);
107
+ expect(fakeHandler).toHaveBeenCalledTimes(1);
130
108
  });
131
109
 
132
110
  it("should render with an error if the request rejects to an error", async () => {
@@ -135,18 +113,12 @@ describe("Data", () => {
135
113
  RequestFulfillment.Default,
136
114
  "fulfill",
137
115
  );
138
-
139
- const fakeHandler: IRequestHandler<string, string> = {
140
- fulfillRequest: () => Promise.reject(new Error("OH NOES!")),
141
- getKey: (o) => o,
142
- type: "MY_HANDLER",
143
- hydrate: true,
144
- };
116
+ const fakeHandler = () => Promise.reject(new Error("OH NOES!"));
145
117
  const fakeChildrenFn = jest.fn(() => null);
146
118
 
147
119
  // Act
148
120
  render(
149
- <Data handler={fakeHandler} options={"options"}>
121
+ <Data handler={fakeHandler} requestId="ID">
150
122
  {fakeChildrenFn}
151
123
  </Data>,
152
124
  );
@@ -170,17 +142,12 @@ describe("Data", () => {
170
142
  "fulfill",
171
143
  );
172
144
 
173
- const fakeHandler: IRequestHandler<string, string> = {
174
- fulfillRequest: () => Promise.resolve("YAY! DATA!"),
175
- getKey: (o) => o,
176
- type: "MY_HANDLER",
177
- hydrate: true,
178
- };
145
+ const fakeHandler = () => Promise.resolve("YAY! DATA!");
179
146
  const fakeChildrenFn = jest.fn(() => null);
180
147
 
181
148
  // Act
182
149
  render(
183
- <Data handler={fakeHandler} options={"options"}>
150
+ <Data handler={fakeHandler} requestId="ID">
184
151
  {fakeChildrenFn}
185
152
  </Data>,
186
153
  );
@@ -197,86 +164,76 @@ describe("Data", () => {
197
164
  });
198
165
  });
199
166
 
200
- it("should render with an error if the request rejects", async () => {
201
- // Arrange
202
- const fulfillSpy = jest
203
- .spyOn(RequestFulfillment.Default, "fulfill")
204
- .mockReturnValue(Promise.reject("CATASTROPHE!"));
205
-
206
- const fakeHandler: IRequestHandler<string, string> = {
207
- fulfillRequest: () => Promise.resolve("YAY!"),
208
- getKey: (o) => o,
209
- type: "MY_HANDLER",
210
- hydrate: true,
211
- };
212
- const fakeChildrenFn = jest.fn(() => null);
213
- const consoleSpy = jest
214
- .spyOn(console, "error")
215
- .mockImplementation(() => {
216
- /* Just to shut it up */
217
- });
167
+ it.each`
168
+ error
169
+ ${"CATASTROPHE!"}
170
+ ${new Error("CATASTROPHE!")}
171
+ `(
172
+ "should render with an error if the request rejects with $error",
173
+ async ({error}) => {
174
+ // Arrange
175
+ const fulfillSpy = jest
176
+ .spyOn(RequestFulfillment.Default, "fulfill")
177
+ .mockReturnValue(Promise.reject(error));
218
178
 
219
- // Act
220
- render(
221
- <Data handler={fakeHandler} options={"options"}>
222
- {fakeChildrenFn}
223
- </Data>,
224
- );
225
- /**
226
- * We wait for the fulfillment to reject.
227
- */
228
- await act(() =>
229
- fulfillSpy.mock.results[0].value.catch(() => {}),
230
- );
179
+ const fakeHandler = () => Promise.resolve("YAY!");
180
+ const fakeChildrenFn = jest.fn(() => null);
181
+ const consoleSpy = jest
182
+ .spyOn(console, "error")
183
+ .mockImplementation(() => {
184
+ /* Just to shut it up */
185
+ });
231
186
 
232
- // Assert
233
- expect(fakeChildrenFn).toHaveBeenCalledTimes(2);
234
- expect(fakeChildrenFn).toHaveBeenLastCalledWith({
235
- status: "error",
236
- error: "CATASTROPHE!",
237
- });
238
- expect(consoleSpy).toHaveBeenCalledWith(
239
- "Unexpected error occurred during data fulfillment: CATASTROPHE!",
240
- );
241
- });
187
+ // Act
188
+ render(
189
+ <Data handler={fakeHandler} requestId="ID">
190
+ {fakeChildrenFn}
191
+ </Data>,
192
+ );
193
+ /**
194
+ * We wait for the fulfillment to reject.
195
+ */
196
+ await act(() =>
197
+ fulfillSpy.mock.results[0].value.catch(() => {}),
198
+ );
199
+
200
+ // Assert
201
+ expect(fakeChildrenFn).toHaveBeenCalledTimes(2);
202
+ expect(fakeChildrenFn).toHaveBeenLastCalledWith({
203
+ status: "error",
204
+ error: "CATASTROPHE!",
205
+ });
206
+ expect(consoleSpy).toHaveBeenCalledWith(
207
+ expect.stringMatching(
208
+ "Unexpected error occurred during data fulfillment:(?: Error:)? CATASTROPHE!",
209
+ ),
210
+ );
211
+ },
212
+ );
242
213
 
243
- it("should render loading if the handler changes and request not cached", async () => {
214
+ it("should start loading if the id changes and request not cached", async () => {
244
215
  // Arrange
245
216
  const fulfillSpy = jest.spyOn(
246
217
  RequestFulfillment.Default,
247
218
  "fulfill",
248
219
  );
249
- const fakeHandler: IRequestHandler<string, string> = {
250
- fulfillRequest: () => Promise.reject(new Error("OH NOES!")),
251
- getKey: (o) => o,
252
- type: "TYPE1",
253
- hydrate: true,
254
- };
255
- const fakeHandler2: IRequestHandler<string, string> = {
256
- fulfillRequest: () =>
257
- new Promise(() => {
258
- /*pending*/
259
- }),
260
- getKey: (o) => o,
261
- type: "TYPE2",
262
- hydrate: true,
263
- };
220
+
221
+ const fakeHandler = () => Promise.resolve("HELLO!");
264
222
  const fakeChildrenFn = jest.fn(() => null);
265
223
  const wrapper = render(
266
- <Data handler={fakeHandler} options={"options"}>
224
+ <Data handler={fakeHandler} requestId="ID">
267
225
  {fakeChildrenFn}
268
226
  </Data>,
269
227
  );
270
- // We want to make sure we render the error state so we can
228
+ // We want to make sure we render the data state so we can
271
229
  // see our switch back to loading.
272
230
  await act(() => fulfillSpy.mock.results[0].value);
273
- // Clear out calls so everything is from the props change.
274
231
  fulfillSpy.mockClear();
275
232
  fakeChildrenFn.mockClear();
276
233
 
277
234
  // Act
278
235
  wrapper.rerender(
279
- <Data handler={fakeHandler2} options={"options"}>
236
+ <Data handler={fakeHandler} requestId="NEW_ID">
280
237
  {fakeChildrenFn}
281
238
  </Data>,
282
239
  );
@@ -290,112 +247,151 @@ describe("Data", () => {
290
247
  });
291
248
  });
292
249
 
293
- it("should start loading if the options key changes and is not cached", async () => {
250
+ it("should ignore resolution of pending handler fulfillment when id changes", async () => {
294
251
  // Arrange
295
- const fulfillSpy = jest.spyOn(
296
- RequestFulfillment.Default,
297
- "fulfill",
252
+ const oldRequest = Promise.resolve("OLD DATA");
253
+ const oldHandler = jest
254
+ .fn()
255
+ .mockReturnValueOnce(oldRequest)
256
+ .mockReturnValue(
257
+ new Promise(() => {
258
+ /*let's have the new request remain pending*/
259
+ }),
260
+ );
261
+
262
+ // Act
263
+ const fakeChildrenFn = jest.fn(() => null);
264
+ const wrapper = render(
265
+ <Data handler={oldHandler} requestId="ID">
266
+ {fakeChildrenFn}
267
+ </Data>,
298
268
  );
269
+ wrapper.rerender(
270
+ <Data handler={oldHandler} requestId="NEW_ID">
271
+ {fakeChildrenFn}
272
+ </Data>,
273
+ );
274
+ await act(() => oldRequest);
275
+
276
+ // Assert
277
+ expect(fakeChildrenFn).not.toHaveBeenCalledWith({
278
+ status: "success",
279
+ data: "OLD DATA",
280
+ });
281
+ });
282
+
283
+ it("should ignore rejection of pending handler fulfillment when id changes", async () => {
284
+ // Arrange
285
+ const oldRequest = Promise.reject(new Error("BOOM!"));
286
+ const oldHandler = jest
287
+ .fn()
288
+ .mockReturnValueOnce(oldRequest)
289
+ .mockReturnValue(
290
+ new Promise(() => {
291
+ /*let's have the new request remain pending*/
292
+ }),
293
+ );
299
294
 
300
- const fakeHandler: IRequestHandler<string, string> = {
301
- fulfillRequest: () => Promise.resolve("HELLO!"),
302
- getKey: (o) => o,
303
- type: "MY_HANDLER",
304
- hydrate: true,
305
- };
295
+ // Act
306
296
  const fakeChildrenFn = jest.fn(() => null);
307
297
  const wrapper = render(
308
- <Data handler={fakeHandler} options={"options"}>
298
+ <Data handler={oldHandler} requestId="ID">
309
299
  {fakeChildrenFn}
310
300
  </Data>,
311
301
  );
312
- // We want to make sure we render the data state so we can
313
- // see our switch back to loading.
314
- await act(() => fulfillSpy.mock.results[0].value);
315
- fulfillSpy.mockClear();
316
- fakeChildrenFn.mockClear();
302
+ wrapper.rerender(
303
+ <Data handler={oldHandler} requestId="NEW_ID">
304
+ {fakeChildrenFn}
305
+ </Data>,
306
+ );
307
+ await act(() =>
308
+ oldRequest.catch(() => {
309
+ /*ignore*/
310
+ }),
311
+ );
312
+
313
+ // Assert
314
+ expect(fakeChildrenFn).not.toHaveBeenCalledWith({
315
+ status: "error",
316
+ error: "BOOM!",
317
+ });
318
+ });
319
+
320
+ it("should ignore catastrophic request fulfillment when id changes", async () => {
321
+ // Arrange
322
+ const catastrophe = Promise.reject("CATASTROPHE!");
323
+ jest.spyOn(
324
+ RequestFulfillment.Default,
325
+ "fulfill",
326
+ ).mockReturnValueOnce(catastrophe);
327
+ const oldHandler = jest.fn().mockResolvedValue("OLD DATA");
317
328
 
318
329
  // Act
330
+ const fakeChildrenFn = jest.fn(() => null);
331
+ const wrapper = render(
332
+ <Data handler={oldHandler} requestId="ID">
333
+ {fakeChildrenFn}
334
+ </Data>,
335
+ );
319
336
  wrapper.rerender(
320
- <Data handler={fakeHandler} options={"new-options"}>
337
+ <Data handler={oldHandler} requestId="NEW_ID">
321
338
  {fakeChildrenFn}
322
339
  </Data>,
323
340
  );
341
+ await act(() =>
342
+ catastrophe.catch(() => {
343
+ /* ignore */
344
+ }),
345
+ );
324
346
 
325
347
  // Assert
326
- // Render 1: Caused by handler changed
327
- // Render 2: Caused by result state changing to null
328
- expect(fakeChildrenFn).toHaveBeenCalledTimes(2);
329
- expect(fakeChildrenFn).toHaveBeenLastCalledWith({
330
- status: "loading",
348
+ expect(fakeChildrenFn).not.toHaveBeenCalledWith({
349
+ status: "error",
350
+ error: "CATASTROPHE!",
331
351
  });
332
352
  });
333
353
 
334
354
  describe("with data interceptor", () => {
335
355
  it("should request data from interceptor", () => {
336
356
  // Arrange
337
- const fulfillRequestSpy = jest
338
- .fn()
339
- .mockResolvedValue("data");
340
- const fakeHandler: IRequestHandler<string, string> = {
341
- fulfillRequest: fulfillRequestSpy,
342
- getKey: (o) => o,
343
- type: "MY_HANDLER",
344
- hydrate: true,
345
- };
357
+ const fakeHandler = jest.fn().mockResolvedValue("data");
346
358
  const fakeChildrenFn = jest.fn(() => null);
347
- const fulfillRequestFn = jest.fn(() =>
348
- Promise.resolve("DATA!"),
349
- );
359
+ const interceptHandler = jest
360
+ .fn()
361
+ .mockResolvedValue("INTERCEPTED DATA");
350
362
 
351
363
  // Act
352
364
  render(
353
- <InterceptData
354
- handler={fakeHandler}
355
- fulfillRequest={fulfillRequestFn}
356
- >
357
- <Data handler={fakeHandler} options={"options"}>
365
+ <InterceptRequests interceptor={interceptHandler}>
366
+ <Data handler={fakeHandler} requestId="ID">
358
367
  {fakeChildrenFn}
359
368
  </Data>
360
- </InterceptData>,
369
+ </InterceptRequests>,
361
370
  );
362
371
 
363
372
  // Assert
364
- expect(fulfillRequestFn).toHaveBeenCalledWith("options");
365
- expect(fulfillRequestFn).toHaveBeenCalledTimes(1);
366
- expect(fulfillRequestSpy).not.toHaveBeenCalled();
373
+ expect(interceptHandler).toHaveBeenCalledTimes(1);
374
+ expect(fakeHandler).not.toHaveBeenCalled();
367
375
  });
368
376
 
369
377
  it("should invoke handler method if interceptor method returns null", () => {
370
378
  // Arrange
371
- const fulfillRequestSpy = jest
372
- .fn()
373
- .mockResolvedValue("data");
374
- const fakeHandler: IRequestHandler<string, string> = {
375
- fulfillRequest: fulfillRequestSpy,
376
- getKey: (o) => o,
377
- type: "MY_HANDLER",
378
- hydrate: true,
379
- };
379
+ const fakeHandler = jest.fn().mockResolvedValue("data");
380
380
  const fakeChildrenFn = jest.fn(() => null);
381
- const fulfillRequestFn = jest.fn(() => null);
381
+ const interceptHandler = jest.fn(() => null);
382
382
 
383
383
  // Act
384
384
  render(
385
- <InterceptData
386
- handler={fakeHandler}
387
- fulfillRequest={fulfillRequestFn}
388
- >
389
- <Data handler={fakeHandler} options={"options"}>
385
+ <InterceptRequests interceptor={interceptHandler}>
386
+ <Data handler={fakeHandler} requestId="ID">
390
387
  {fakeChildrenFn}
391
388
  </Data>
392
- </InterceptData>,
389
+ </InterceptRequests>,
393
390
  );
394
391
 
395
392
  // Assert
396
- expect(fulfillRequestFn).toHaveBeenCalledWith("options");
397
- expect(fulfillRequestFn).toHaveBeenCalledTimes(1);
398
- expect(fulfillRequestSpy).toHaveBeenCalled();
393
+ expect(interceptHandler).toHaveBeenCalledTimes(1);
394
+ expect(fakeHandler).toHaveBeenCalledTimes(1);
399
395
  });
400
396
  });
401
397
  });
@@ -406,24 +402,24 @@ describe("Data", () => {
406
402
  * Each of these test cases will start out with some cached data
407
403
  * retrieved.
408
404
  */
409
- jest.spyOn(ResponseCache.Default, "getEntry").mockReturnValue({
405
+ jest.spyOn(
406
+ SsrCache.Default,
407
+ "getEntry",
408
+ // Fake once because that's how the cache would work,
409
+ // deleting the hydrated value as soon as it was used.
410
+ ).mockReturnValueOnce({
410
411
  data: "YAY! DATA!",
411
412
  });
412
413
  });
413
414
 
414
415
  it("should render first time with the cached data", () => {
415
416
  // Arrange
416
- const fakeHandler: IRequestHandler<string, string> = {
417
- fulfillRequest: () => Promise.resolve("data"),
418
- getKey: (o) => o,
419
- type: "MY_HANDLER",
420
- hydrate: true,
421
- };
417
+ const fakeHandler = () => Promise.resolve("data");
422
418
  const fakeChildrenFn = jest.fn(() => null);
423
419
 
424
420
  // Act
425
421
  render(
426
- <Data handler={fakeHandler} options={"options"}>
422
+ <Data handler={fakeHandler} requestId="ID">
427
423
  {fakeChildrenFn}
428
424
  </Data>,
429
425
  );
@@ -434,6 +430,144 @@ describe("Data", () => {
434
430
  data: "YAY! DATA!",
435
431
  });
436
432
  });
433
+
434
+ it("should retain old data while reloading if showOldDataWhileLoading is true", async () => {
435
+ // Arrange
436
+ const response1 = Promise.resolve("data1");
437
+ const response2 = Promise.resolve("data2");
438
+ const fakeHandler1 = () => response1;
439
+ const fakeHandler2 = () => response2;
440
+ const fakeChildrenFn = jest.fn(() => null);
441
+
442
+ // Act
443
+ const wrapper = render(
444
+ <Data
445
+ handler={fakeHandler1}
446
+ requestId="ID"
447
+ showOldDataWhileLoading={false}
448
+ >
449
+ {fakeChildrenFn}
450
+ </Data>,
451
+ );
452
+ wrapper.rerender(
453
+ <Data
454
+ handler={fakeHandler2}
455
+ requestId="ID"
456
+ showOldDataWhileLoading={true}
457
+ >
458
+ {fakeChildrenFn}
459
+ </Data>,
460
+ );
461
+
462
+ // Assert
463
+ expect(fakeChildrenFn).not.toHaveBeenCalledWith({
464
+ status: "loading",
465
+ });
466
+ });
467
+
468
+ it("should not request data when alwaysRequestOnHydration is false and cache has a valid data result", () => {
469
+ // Arrange
470
+ const fakeHandler = jest.fn().mockResolvedValue("data");
471
+ const fakeChildrenFn = jest.fn(() => null);
472
+
473
+ // Act
474
+ render(
475
+ <Data
476
+ handler={fakeHandler}
477
+ requestId="ID"
478
+ alwaysRequestOnHydration={false}
479
+ >
480
+ {fakeChildrenFn}
481
+ </Data>,
482
+ );
483
+
484
+ // Assert
485
+ expect(fakeHandler).not.toHaveBeenCalled();
486
+ });
487
+
488
+ it("should request data if cached data value is valid but alwaysRequestOnHydration is true", () => {
489
+ // Arrange
490
+ const fakeHandler = jest.fn().mockResolvedValue("data");
491
+ const fakeChildrenFn = jest.fn(() => null);
492
+
493
+ // Act
494
+ render(
495
+ <Data
496
+ handler={fakeHandler}
497
+ requestId="ID"
498
+ alwaysRequestOnHydration={true}
499
+ >
500
+ {fakeChildrenFn}
501
+ </Data>,
502
+ );
503
+
504
+ // Assert
505
+ expect(fakeHandler).toHaveBeenCalledTimes(1);
506
+ });
507
+ });
508
+
509
+ describe("with cached abort", () => {
510
+ beforeEach(() => {
511
+ /**
512
+ * Each of these test cases will start out with a cached abort.
513
+ */
514
+ jest.spyOn(
515
+ SsrCache.Default,
516
+ "getEntry",
517
+ // Fake once because that's how the cache would work,
518
+ // deleting the hydrated value as soon as it was used.
519
+ ).mockReturnValueOnce({
520
+ data: null,
521
+ });
522
+ });
523
+
524
+ it("should request data if cached data value is null (i.e. represents an aborted request)", () => {
525
+ // Arrange
526
+ const fakeHandler = jest.fn().mockResolvedValue("data");
527
+ const fakeChildrenFn = jest.fn(() => null);
528
+
529
+ // Act
530
+ render(
531
+ <Data handler={fakeHandler} requestId="ID">
532
+ {fakeChildrenFn}
533
+ </Data>,
534
+ );
535
+
536
+ // Assert
537
+ expect(fakeHandler).toHaveBeenCalledTimes(1);
538
+ });
539
+ });
540
+
541
+ describe("with cached error", () => {
542
+ beforeEach(() => {
543
+ /**
544
+ * Each of these test cases will start out with a cached error.
545
+ */
546
+ jest.spyOn(
547
+ SsrCache.Default,
548
+ "getEntry",
549
+ // Fake once because that's how the cache would work,
550
+ // deleting the hydrated value as soon as it was used.
551
+ ).mockReturnValueOnce({
552
+ error: "BOO! ERROR!",
553
+ });
554
+ });
555
+
556
+ it("should always request data if there's a cached error", () => {
557
+ // Arrange
558
+ const fakeHandler = jest.fn().mockResolvedValue("data");
559
+ const fakeChildrenFn = jest.fn(() => null);
560
+
561
+ // Act
562
+ render(
563
+ <Data handler={fakeHandler} requestId="ID">
564
+ {fakeChildrenFn}
565
+ </Data>,
566
+ );
567
+
568
+ // Assert
569
+ expect(fakeHandler).toHaveBeenCalledTimes(1);
570
+ });
437
571
  });
438
572
  });
439
573
 
@@ -448,46 +582,33 @@ describe("Data", () => {
448
582
  * Each of these test cases will never have cached data
449
583
  * retrieved.
450
584
  */
451
- jest.spyOn(ResponseCache.Default, "getEntry").mockReturnValue(
452
- null,
453
- );
585
+ jest.spyOn(SsrCache.Default, "getEntry").mockReturnValue(null);
454
586
  });
455
587
 
456
588
  it("should not request data", () => {
457
589
  // Arrange
458
- const fulfillRequestSpy = jest.fn().mockResolvedValue("data");
459
- const fakeHandler: IRequestHandler<string, string> = {
460
- fulfillRequest: fulfillRequestSpy,
461
- getKey: (o) => o,
462
- type: "MY_HANDLER",
463
- hydrate: true,
464
- };
590
+ const fakeHandler = jest.fn().mockResolvedValue("data");
465
591
  const fakeChildrenFn = jest.fn(() => null);
466
592
 
467
593
  // Act
468
594
  ReactDOMServer.renderToString(
469
- <Data handler={fakeHandler} options={"options"}>
595
+ <Data handler={fakeHandler} requestId="ID">
470
596
  {fakeChildrenFn}
471
597
  </Data>,
472
598
  );
473
599
 
474
600
  // Assert
475
- expect(fulfillRequestSpy).not.toHaveBeenCalled();
601
+ expect(fakeHandler).not.toHaveBeenCalled();
476
602
  });
477
603
 
478
604
  it("should render children with loading", () => {
479
605
  // Arrange
480
- const fakeHandler: IRequestHandler<string, string> = {
481
- fulfillRequest: () => Promise.resolve("data"),
482
- getKey: (o) => o,
483
- type: "MY_HANDLER",
484
- hydrate: true,
485
- };
606
+ const fakeHandler = jest.fn().mockResolvedValue("data");
486
607
  const fakeChildrenFn = jest.fn(() => null);
487
608
 
488
609
  // Act
489
610
  ReactDOMServer.renderToString(
490
- <Data handler={fakeHandler} options={"options"}>
611
+ <Data handler={fakeHandler} requestId="ID">
491
612
  {fakeChildrenFn}
492
613
  </Data>,
493
614
  );
@@ -504,59 +625,47 @@ describe("Data", () => {
504
625
  RequestTracker.Default,
505
626
  "trackDataRequest",
506
627
  );
507
- const fakeHandler: IRequestHandler<string, string> = {
508
- fulfillRequest: () => Promise.resolve("data"),
509
- getKey: (o) => o,
510
- type: "MY_HANDLER",
511
- hydrate: true,
512
- };
628
+ const fakeHandler = jest.fn().mockResolvedValue("data");
513
629
  const fakeChildrenFn = jest.fn(() => null);
514
630
 
515
631
  // Act
516
632
  ReactDOMServer.renderToString(
517
633
  <TrackData>
518
- <Data handler={fakeHandler} options={"options"}>
634
+ <Data handler={fakeHandler} requestId="ID">
519
635
  {fakeChildrenFn}
520
636
  </Data>
521
637
  </TrackData>,
522
638
  );
523
639
 
524
640
  // Assert
525
- expect(trackSpy).toHaveBeenCalledWith(fakeHandler, "options");
641
+ expect(trackSpy).toHaveBeenCalledWith(
642
+ "ID",
643
+ expect.any(Function),
644
+ true,
645
+ );
526
646
  });
527
647
 
528
648
  describe("with data interceptor", () => {
529
649
  it("should not request data from the interceptor", () => {
530
650
  // Arrange
531
- const fulfillRequestSpy = jest
532
- .fn()
533
- .mockResolvedValue("data");
534
- const fakeHandler: IRequestHandler<string, string> = {
535
- fulfillRequest: fulfillRequestSpy,
536
- getKey: (o) => o,
537
- type: "MY_HANDLER",
538
- hydrate: true,
539
- };
651
+ const fakeHandler = jest.fn().mockResolvedValue("data");
540
652
  const fakeChildrenFn = jest.fn(() => null);
541
- const fulfillRequestFn = jest.fn(() =>
653
+ const interceptedHandler = jest.fn(() =>
542
654
  Promise.resolve("DATA!"),
543
655
  );
544
656
 
545
657
  // Act
546
658
  ReactDOMServer.renderToString(
547
- <InterceptData
548
- handler={fakeHandler}
549
- fulfillRequest={fulfillRequestFn}
550
- >
551
- <Data handler={fakeHandler} options={"options"}>
659
+ <InterceptRequests interceptor={interceptedHandler}>
660
+ <Data handler={fakeHandler} requestId="ID">
552
661
  {fakeChildrenFn}
553
662
  </Data>
554
- </InterceptData>,
663
+ </InterceptRequests>,
555
664
  );
556
665
 
557
666
  // Assert
558
- expect(fulfillRequestFn).not.toHaveBeenCalled();
559
- expect(fulfillRequestSpy).not.toHaveBeenCalled();
667
+ expect(fakeHandler).not.toHaveBeenCalled();
668
+ expect(interceptedHandler).not.toHaveBeenCalled();
560
669
  });
561
670
 
562
671
  it("should invoke the tracking call", () => {
@@ -565,45 +674,28 @@ describe("Data", () => {
565
674
  RequestTracker.Default,
566
675
  "trackDataRequest",
567
676
  );
568
- const fakeHandler = {
569
- fulfillRequest: () => Promise.resolve("data"),
570
- getKey: (o) => o,
571
- type: "MY_HANDLER",
572
- hydrate: true,
573
- };
677
+ const fakeHandler = jest.fn().mockResolvedValue("data");
574
678
  const fakeChildrenFn = jest.fn(() => null);
575
- const fulfillRequestFn = jest.fn(() =>
576
- Promise.resolve("DATA!"),
577
- );
679
+ const interceptedHandler = jest
680
+ .fn()
681
+ .mockResolvedValue("INTERCEPTED");
578
682
 
579
683
  // Act
580
684
  ReactDOMServer.renderToString(
581
685
  <TrackData>
582
- <InterceptData
583
- handler={
584
- (fakeHandler: IRequestHandler<
585
- string,
586
- string,
587
- >)
588
- }
589
- fulfillRequest={fulfillRequestFn}
590
- >
591
- <Data handler={fakeHandler} options={"options"}>
686
+ <InterceptRequests interceptor={interceptedHandler}>
687
+ <Data handler={fakeHandler} requestId="ID">
592
688
  {fakeChildrenFn}
593
689
  </Data>
594
- </InterceptData>
690
+ </InterceptRequests>
595
691
  </TrackData>,
596
692
  );
597
693
 
598
694
  // Assert
599
695
  expect(trackSpy).toHaveBeenCalledWith(
600
- {
601
- fulfillRequest: expect.any(Function),
602
- getKey: expect.any(Function),
603
- type: "MY_HANDLER",
604
- hydrate: true,
605
- },
606
- "options",
696
+ "ID",
697
+ expect.any(Function),
698
+ true,
607
699
  );
608
700
  });
609
701
  });
@@ -615,46 +707,35 @@ describe("Data", () => {
615
707
  * Each of these test cases will start out with some cached data
616
708
  * retrieved.
617
709
  */
618
- jest.spyOn(ResponseCache.Default, "getEntry").mockReturnValue({
710
+ jest.spyOn(SsrCache.Default, "getEntry").mockReturnValue({
619
711
  data: "YAY! DATA!",
620
712
  });
621
713
  });
622
714
 
623
715
  it("should not request data", () => {
624
716
  // Arrange
625
- const fulfillRequestSpy = jest.fn().mockResolvedValue("data");
626
- const fakeHandler: IRequestHandler<string, string> = {
627
- fulfillRequest: fulfillRequestSpy,
628
- getKey: (o) => o,
629
- type: "MY_HANDLER",
630
- hydrate: true,
631
- };
717
+ const fakeHandler = jest.fn().mockResolvedValue("data");
632
718
  const fakeChildrenFn = jest.fn(() => null);
633
719
 
634
720
  // Act
635
721
  ReactDOMServer.renderToString(
636
- <Data handler={fakeHandler} options={"options"}>
722
+ <Data handler={fakeHandler} requestId="ID">
637
723
  {fakeChildrenFn}
638
724
  </Data>,
639
725
  );
640
726
 
641
727
  // Assert
642
- expect(fulfillRequestSpy).not.toHaveBeenCalled();
728
+ expect(fakeHandler).not.toHaveBeenCalled();
643
729
  });
644
730
 
645
731
  it("should render children with data", () => {
646
732
  // Arrange
647
- const fakeHandler: IRequestHandler<string, string> = {
648
- fulfillRequest: () => Promise.resolve("data"),
649
- getKey: (o) => o,
650
- type: "MY_HANDLER",
651
- hydrate: true,
652
- };
733
+ const fakeHandler = jest.fn().mockResolvedValue("data");
653
734
  const fakeChildrenFn = jest.fn(() => null);
654
735
 
655
736
  // Act
656
737
  ReactDOMServer.renderToString(
657
- <Data handler={fakeHandler} options={"options"}>
738
+ <Data handler={fakeHandler} requestId="ID">
658
739
  {fakeChildrenFn}
659
740
  </Data>,
660
741
  );
@@ -668,20 +749,15 @@ describe("Data", () => {
668
749
 
669
750
  it("should render children with error", () => {
670
751
  // Arrange
671
- jest.spyOn(ResponseCache.Default, "getEntry").mockReturnValue({
752
+ jest.spyOn(SsrCache.Default, "getEntry").mockReturnValue({
672
753
  error: "OH NO! IT GO BOOM",
673
754
  });
674
- const fakeHandler: IRequestHandler<string, string> = {
675
- fulfillRequest: () => Promise.resolve("data"),
676
- getKey: (o) => o,
677
- type: "MY_HANDLER",
678
- hydrate: true,
679
- };
755
+ const fakeHandler = jest.fn().mockResolvedValue("data");
680
756
  const fakeChildrenFn = jest.fn(() => null);
681
757
 
682
758
  // Act
683
759
  ReactDOMServer.renderToString(
684
- <Data handler={fakeHandler} options={"options"}>
760
+ <Data handler={fakeHandler} requestId="ID">
685
761
  {fakeChildrenFn}
686
762
  </Data>,
687
763
  );
@@ -699,18 +775,13 @@ describe("Data", () => {
699
775
  RequestTracker.Default,
700
776
  "trackDataRequest",
701
777
  );
702
- const fakeHandler: IRequestHandler<string, string> = {
703
- fulfillRequest: () => Promise.resolve("data"),
704
- getKey: (o) => o,
705
- type: "MY_HANDLER",
706
- hydrate: true,
707
- };
778
+ const fakeHandler = jest.fn().mockResolvedValue("data");
708
779
  const fakeChildrenFn = jest.fn(() => null);
709
780
 
710
781
  // Act
711
782
  ReactDOMServer.renderToString(
712
783
  <TrackData>
713
- <Data handler={fakeHandler} options={"options"}>
784
+ <Data handler={fakeHandler} requestId="ID">
714
785
  {fakeChildrenFn}
715
786
  </Data>
716
787
  </TrackData>,
@@ -726,35 +797,24 @@ describe("Data", () => {
726
797
  describe("with data interceptor", () => {
727
798
  it("should not request data from interceptor", () => {
728
799
  // Arrange
729
- const fulfillRequestSpy = jest
730
- .fn()
731
- .mockResolvedValue("data");
732
- const fakeHandler: IRequestHandler<string, string> = {
733
- fulfillRequest: fulfillRequestSpy,
734
- getKey: (o) => o,
735
- type: "MY_HANDLER",
736
- hydrate: true,
737
- };
800
+ const fakeHandler = jest.fn().mockResolvedValue("data");
738
801
  const fakeChildrenFn = jest.fn(() => null);
739
- const fulfillRequestFn = jest.fn(() =>
740
- Promise.resolve("data2"),
741
- );
802
+ const interceptHandler = jest
803
+ .fn()
804
+ .mockResolvedValue("INTERCEPTED");
742
805
 
743
806
  // Act
744
807
  ReactDOMServer.renderToString(
745
- <InterceptData
746
- handler={fakeHandler}
747
- fulfillRequest={fulfillRequestFn}
748
- >
749
- <Data handler={fakeHandler} options={"options"}>
808
+ <InterceptRequests interceptor={interceptHandler}>
809
+ <Data handler={fakeHandler} requestId="ID">
750
810
  {fakeChildrenFn}
751
811
  </Data>
752
- </InterceptData>,
812
+ </InterceptRequests>,
753
813
  );
754
814
 
755
815
  // Assert
756
- expect(fulfillRequestSpy).not.toHaveBeenCalled();
757
- expect(fulfillRequestFn).not.toHaveBeenCalled();
816
+ expect(fakeHandler).not.toHaveBeenCalled();
817
+ expect(interceptHandler).not.toHaveBeenCalled();
758
818
  });
759
819
  });
760
820
  });