@khanacademy/wonder-blocks-data 3.2.0 → 4.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 (42) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/es/index.js +356 -332
  3. package/dist/index.js +507 -456
  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 +56 -122
  8. package/src/components/__tests__/data.test.js +372 -297
  9. package/src/components/__tests__/intercept-data.test.js +6 -30
  10. package/src/components/data.js +153 -21
  11. package/src/components/data.md +38 -69
  12. package/src/components/intercept-context.js +6 -2
  13. package/src/components/intercept-data.js +40 -51
  14. package/src/components/intercept-data.md +13 -27
  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-server-effect.test.js +217 -0
  18. package/src/hooks/__tests__/use-shared-cache.test.js +307 -0
  19. package/src/hooks/use-server-effect.js +45 -0
  20. package/src/hooks/use-shared-cache.js +106 -0
  21. package/src/index.js +15 -19
  22. package/src/util/__tests__/__snapshots__/scoped-in-memory-cache.test.js.snap +19 -0
  23. package/src/util/__tests__/request-fulfillment.test.js +42 -85
  24. package/src/util/__tests__/request-tracking.test.js +72 -191
  25. package/src/util/__tests__/{result-from-cache-entry.test.js → result-from-cache-response.test.js} +9 -10
  26. package/src/util/__tests__/scoped-in-memory-cache.test.js +396 -0
  27. package/src/util/__tests__/ssr-cache.test.js +639 -0
  28. package/src/util/request-fulfillment.js +36 -44
  29. package/src/util/request-tracking.js +62 -75
  30. package/src/util/{result-from-cache-entry.js → result-from-cache-response.js} +10 -13
  31. package/src/util/scoped-in-memory-cache.js +149 -0
  32. package/src/util/ssr-cache.js +206 -0
  33. package/src/util/types.js +43 -108
  34. package/src/hooks/__tests__/use-data.test.js +0 -826
  35. package/src/hooks/use-data.js +0 -143
  36. package/src/util/__tests__/memory-cache.test.js +0 -446
  37. package/src/util/__tests__/request-handler.test.js +0 -121
  38. package/src/util/__tests__/response-cache.test.js +0 -879
  39. package/src/util/memory-cache.js +0 -187
  40. package/src/util/request-handler.js +0 -42
  41. package/src/util/request-handler.md +0 -51
  42. 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
14
  import InterceptData from "../intercept-data.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,157 @@ 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
365
  <InterceptData
354
- handler={fakeHandler}
355
- fulfillRequest={fulfillRequestFn}
366
+ requestId="ID"
367
+ handler={interceptHandler}
356
368
  >
357
- <Data handler={fakeHandler} options={"options"}>
369
+ <Data handler={fakeHandler} requestId="ID">
358
370
  {fakeChildrenFn}
359
371
  </Data>
360
372
  </InterceptData>,
361
373
  );
362
374
 
363
375
  // Assert
364
- expect(fulfillRequestFn).toHaveBeenCalledWith("options");
365
- expect(fulfillRequestFn).toHaveBeenCalledTimes(1);
366
- expect(fulfillRequestSpy).not.toHaveBeenCalled();
376
+ expect(interceptHandler).toHaveBeenCalledTimes(1);
377
+ expect(fakeHandler).not.toHaveBeenCalled();
367
378
  });
368
379
 
369
380
  it("should invoke handler method if interceptor method returns null", () => {
370
381
  // 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
- };
382
+ const fakeHandler = jest.fn().mockResolvedValue("data");
380
383
  const fakeChildrenFn = jest.fn(() => null);
381
- const fulfillRequestFn = jest.fn(() => null);
384
+ const interceptHandler = jest.fn(() => null);
382
385
 
383
386
  // Act
384
387
  render(
385
388
  <InterceptData
386
- handler={fakeHandler}
387
- fulfillRequest={fulfillRequestFn}
389
+ handler={interceptHandler}
390
+ requestId="ID"
388
391
  >
389
- <Data handler={fakeHandler} options={"options"}>
392
+ <Data handler={fakeHandler} requestId="ID">
390
393
  {fakeChildrenFn}
391
394
  </Data>
392
395
  </InterceptData>,
393
396
  );
394
397
 
395
398
  // Assert
396
- expect(fulfillRequestFn).toHaveBeenCalledWith("options");
397
- expect(fulfillRequestFn).toHaveBeenCalledTimes(1);
398
- expect(fulfillRequestSpy).toHaveBeenCalled();
399
+ expect(interceptHandler).toHaveBeenCalledTimes(1);
400
+ expect(fakeHandler).toHaveBeenCalledTimes(1);
399
401
  });
400
402
  });
401
403
  });
@@ -406,24 +408,24 @@ describe("Data", () => {
406
408
  * Each of these test cases will start out with some cached data
407
409
  * retrieved.
408
410
  */
409
- jest.spyOn(ResponseCache.Default, "getEntry").mockReturnValue({
411
+ jest.spyOn(
412
+ SsrCache.Default,
413
+ "getEntry",
414
+ // Fake once because that's how the cache would work,
415
+ // deleting the hydrated value as soon as it was used.
416
+ ).mockReturnValueOnce({
410
417
  data: "YAY! DATA!",
411
418
  });
412
419
  });
413
420
 
414
421
  it("should render first time with the cached data", () => {
415
422
  // Arrange
416
- const fakeHandler: IRequestHandler<string, string> = {
417
- fulfillRequest: () => Promise.resolve("data"),
418
- getKey: (o) => o,
419
- type: "MY_HANDLER",
420
- hydrate: true,
421
- };
423
+ const fakeHandler = () => Promise.resolve("data");
422
424
  const fakeChildrenFn = jest.fn(() => null);
423
425
 
424
426
  // Act
425
427
  render(
426
- <Data handler={fakeHandler} options={"options"}>
428
+ <Data handler={fakeHandler} requestId="ID">
427
429
  {fakeChildrenFn}
428
430
  </Data>,
429
431
  );
@@ -434,6 +436,144 @@ describe("Data", () => {
434
436
  data: "YAY! DATA!",
435
437
  });
436
438
  });
439
+
440
+ it("should retain old data while reloading if showOldDataWhileLoading is true", async () => {
441
+ // Arrange
442
+ const response1 = Promise.resolve("data1");
443
+ const response2 = Promise.resolve("data2");
444
+ const fakeHandler1 = () => response1;
445
+ const fakeHandler2 = () => response2;
446
+ const fakeChildrenFn = jest.fn(() => null);
447
+
448
+ // Act
449
+ const wrapper = render(
450
+ <Data
451
+ handler={fakeHandler1}
452
+ requestId="ID"
453
+ showOldDataWhileLoading={false}
454
+ >
455
+ {fakeChildrenFn}
456
+ </Data>,
457
+ );
458
+ wrapper.rerender(
459
+ <Data
460
+ handler={fakeHandler2}
461
+ requestId="ID"
462
+ showOldDataWhileLoading={true}
463
+ >
464
+ {fakeChildrenFn}
465
+ </Data>,
466
+ );
467
+
468
+ // Assert
469
+ expect(fakeChildrenFn).not.toHaveBeenCalledWith({
470
+ status: "loading",
471
+ });
472
+ });
473
+
474
+ it("should not request data when alwaysRequestOnHydration is false and cache has a valid data result", () => {
475
+ // Arrange
476
+ const fakeHandler = jest.fn().mockResolvedValue("data");
477
+ const fakeChildrenFn = jest.fn(() => null);
478
+
479
+ // Act
480
+ render(
481
+ <Data
482
+ handler={fakeHandler}
483
+ requestId="ID"
484
+ alwaysRequestOnHydration={false}
485
+ >
486
+ {fakeChildrenFn}
487
+ </Data>,
488
+ );
489
+
490
+ // Assert
491
+ expect(fakeHandler).not.toHaveBeenCalled();
492
+ });
493
+
494
+ it("should request data if cached data value is valid but alwaysRequestOnHydration is true", () => {
495
+ // Arrange
496
+ const fakeHandler = jest.fn().mockResolvedValue("data");
497
+ const fakeChildrenFn = jest.fn(() => null);
498
+
499
+ // Act
500
+ render(
501
+ <Data
502
+ handler={fakeHandler}
503
+ requestId="ID"
504
+ alwaysRequestOnHydration={true}
505
+ >
506
+ {fakeChildrenFn}
507
+ </Data>,
508
+ );
509
+
510
+ // Assert
511
+ expect(fakeHandler).toHaveBeenCalledTimes(1);
512
+ });
513
+ });
514
+
515
+ describe("with cached abort", () => {
516
+ beforeEach(() => {
517
+ /**
518
+ * Each of these test cases will start out with a cached abort.
519
+ */
520
+ jest.spyOn(
521
+ SsrCache.Default,
522
+ "getEntry",
523
+ // Fake once because that's how the cache would work,
524
+ // deleting the hydrated value as soon as it was used.
525
+ ).mockReturnValueOnce({
526
+ data: null,
527
+ });
528
+ });
529
+
530
+ it("should request data if cached data value is null (i.e. represents an aborted request)", () => {
531
+ // Arrange
532
+ const fakeHandler = jest.fn().mockResolvedValue("data");
533
+ const fakeChildrenFn = jest.fn(() => null);
534
+
535
+ // Act
536
+ render(
537
+ <Data handler={fakeHandler} requestId="ID">
538
+ {fakeChildrenFn}
539
+ </Data>,
540
+ );
541
+
542
+ // Assert
543
+ expect(fakeHandler).toHaveBeenCalledTimes(1);
544
+ });
545
+ });
546
+
547
+ describe("with cached error", () => {
548
+ beforeEach(() => {
549
+ /**
550
+ * Each of these test cases will start out with a cached error.
551
+ */
552
+ jest.spyOn(
553
+ SsrCache.Default,
554
+ "getEntry",
555
+ // Fake once because that's how the cache would work,
556
+ // deleting the hydrated value as soon as it was used.
557
+ ).mockReturnValueOnce({
558
+ error: "BOO! ERROR!",
559
+ });
560
+ });
561
+
562
+ it("should always request data if there's a cached error", () => {
563
+ // Arrange
564
+ const fakeHandler = jest.fn().mockResolvedValue("data");
565
+ const fakeChildrenFn = jest.fn(() => null);
566
+
567
+ // Act
568
+ render(
569
+ <Data handler={fakeHandler} requestId="ID">
570
+ {fakeChildrenFn}
571
+ </Data>,
572
+ );
573
+
574
+ // Assert
575
+ expect(fakeHandler).toHaveBeenCalledTimes(1);
576
+ });
437
577
  });
438
578
  });
439
579
 
@@ -448,46 +588,33 @@ describe("Data", () => {
448
588
  * Each of these test cases will never have cached data
449
589
  * retrieved.
450
590
  */
451
- jest.spyOn(ResponseCache.Default, "getEntry").mockReturnValue(
452
- null,
453
- );
591
+ jest.spyOn(SsrCache.Default, "getEntry").mockReturnValue(null);
454
592
  });
455
593
 
456
594
  it("should not request data", () => {
457
595
  // 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
- };
596
+ const fakeHandler = jest.fn().mockResolvedValue("data");
465
597
  const fakeChildrenFn = jest.fn(() => null);
466
598
 
467
599
  // Act
468
600
  ReactDOMServer.renderToString(
469
- <Data handler={fakeHandler} options={"options"}>
601
+ <Data handler={fakeHandler} requestId="ID">
470
602
  {fakeChildrenFn}
471
603
  </Data>,
472
604
  );
473
605
 
474
606
  // Assert
475
- expect(fulfillRequestSpy).not.toHaveBeenCalled();
607
+ expect(fakeHandler).not.toHaveBeenCalled();
476
608
  });
477
609
 
478
610
  it("should render children with loading", () => {
479
611
  // Arrange
480
- const fakeHandler: IRequestHandler<string, string> = {
481
- fulfillRequest: () => Promise.resolve("data"),
482
- getKey: (o) => o,
483
- type: "MY_HANDLER",
484
- hydrate: true,
485
- };
612
+ const fakeHandler = jest.fn().mockResolvedValue("data");
486
613
  const fakeChildrenFn = jest.fn(() => null);
487
614
 
488
615
  // Act
489
616
  ReactDOMServer.renderToString(
490
- <Data handler={fakeHandler} options={"options"}>
617
+ <Data handler={fakeHandler} requestId="ID">
491
618
  {fakeChildrenFn}
492
619
  </Data>,
493
620
  );
@@ -504,59 +631,50 @@ describe("Data", () => {
504
631
  RequestTracker.Default,
505
632
  "trackDataRequest",
506
633
  );
507
- const fakeHandler: IRequestHandler<string, string> = {
508
- fulfillRequest: () => Promise.resolve("data"),
509
- getKey: (o) => o,
510
- type: "MY_HANDLER",
511
- hydrate: true,
512
- };
634
+ const fakeHandler = jest.fn().mockResolvedValue("data");
513
635
  const fakeChildrenFn = jest.fn(() => null);
514
636
 
515
637
  // Act
516
638
  ReactDOMServer.renderToString(
517
639
  <TrackData>
518
- <Data handler={fakeHandler} options={"options"}>
640
+ <Data handler={fakeHandler} requestId="ID">
519
641
  {fakeChildrenFn}
520
642
  </Data>
521
643
  </TrackData>,
522
644
  );
523
645
 
524
646
  // Assert
525
- expect(trackSpy).toHaveBeenCalledWith(fakeHandler, "options");
647
+ expect(trackSpy).toHaveBeenCalledWith(
648
+ "ID",
649
+ expect.any(Function),
650
+ true,
651
+ );
526
652
  });
527
653
 
528
654
  describe("with data interceptor", () => {
529
655
  it("should not request data from the interceptor", () => {
530
656
  // 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
- };
657
+ const fakeHandler = jest.fn().mockResolvedValue("data");
540
658
  const fakeChildrenFn = jest.fn(() => null);
541
- const fulfillRequestFn = jest.fn(() =>
659
+ const interceptedHandler = jest.fn(() =>
542
660
  Promise.resolve("DATA!"),
543
661
  );
544
662
 
545
663
  // Act
546
664
  ReactDOMServer.renderToString(
547
665
  <InterceptData
548
- handler={fakeHandler}
549
- fulfillRequest={fulfillRequestFn}
666
+ handler={interceptedHandler}
667
+ requestId="ID"
550
668
  >
551
- <Data handler={fakeHandler} options={"options"}>
669
+ <Data handler={fakeHandler} requestId="ID">
552
670
  {fakeChildrenFn}
553
671
  </Data>
554
672
  </InterceptData>,
555
673
  );
556
674
 
557
675
  // Assert
558
- expect(fulfillRequestFn).not.toHaveBeenCalled();
559
- expect(fulfillRequestSpy).not.toHaveBeenCalled();
676
+ expect(fakeHandler).not.toHaveBeenCalled();
677
+ expect(interceptedHandler).not.toHaveBeenCalled();
560
678
  });
561
679
 
562
680
  it("should invoke the tracking call", () => {
@@ -565,30 +683,20 @@ describe("Data", () => {
565
683
  RequestTracker.Default,
566
684
  "trackDataRequest",
567
685
  );
568
- const fakeHandler = {
569
- fulfillRequest: () => Promise.resolve("data"),
570
- getKey: (o) => o,
571
- type: "MY_HANDLER",
572
- hydrate: true,
573
- };
686
+ const fakeHandler = jest.fn().mockResolvedValue("data");
574
687
  const fakeChildrenFn = jest.fn(() => null);
575
- const fulfillRequestFn = jest.fn(() =>
576
- Promise.resolve("DATA!"),
577
- );
688
+ const interceptedHandler = jest
689
+ .fn()
690
+ .mockResolvedValue("INTERCEPTED");
578
691
 
579
692
  // Act
580
693
  ReactDOMServer.renderToString(
581
694
  <TrackData>
582
695
  <InterceptData
583
- handler={
584
- (fakeHandler: IRequestHandler<
585
- string,
586
- string,
587
- >)
588
- }
589
- fulfillRequest={fulfillRequestFn}
696
+ requestId="ID"
697
+ handler={interceptedHandler}
590
698
  >
591
- <Data handler={fakeHandler} options={"options"}>
699
+ <Data handler={fakeHandler} requestId="ID">
592
700
  {fakeChildrenFn}
593
701
  </Data>
594
702
  </InterceptData>
@@ -597,13 +705,9 @@ describe("Data", () => {
597
705
 
598
706
  // Assert
599
707
  expect(trackSpy).toHaveBeenCalledWith(
600
- {
601
- fulfillRequest: expect.any(Function),
602
- getKey: expect.any(Function),
603
- type: "MY_HANDLER",
604
- hydrate: true,
605
- },
606
- "options",
708
+ "ID",
709
+ expect.any(Function),
710
+ true,
607
711
  );
608
712
  });
609
713
  });
@@ -615,46 +719,35 @@ describe("Data", () => {
615
719
  * Each of these test cases will start out with some cached data
616
720
  * retrieved.
617
721
  */
618
- jest.spyOn(ResponseCache.Default, "getEntry").mockReturnValue({
722
+ jest.spyOn(SsrCache.Default, "getEntry").mockReturnValue({
619
723
  data: "YAY! DATA!",
620
724
  });
621
725
  });
622
726
 
623
727
  it("should not request data", () => {
624
728
  // 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
- };
729
+ const fakeHandler = jest.fn().mockResolvedValue("data");
632
730
  const fakeChildrenFn = jest.fn(() => null);
633
731
 
634
732
  // Act
635
733
  ReactDOMServer.renderToString(
636
- <Data handler={fakeHandler} options={"options"}>
734
+ <Data handler={fakeHandler} requestId="ID">
637
735
  {fakeChildrenFn}
638
736
  </Data>,
639
737
  );
640
738
 
641
739
  // Assert
642
- expect(fulfillRequestSpy).not.toHaveBeenCalled();
740
+ expect(fakeHandler).not.toHaveBeenCalled();
643
741
  });
644
742
 
645
743
  it("should render children with data", () => {
646
744
  // Arrange
647
- const fakeHandler: IRequestHandler<string, string> = {
648
- fulfillRequest: () => Promise.resolve("data"),
649
- getKey: (o) => o,
650
- type: "MY_HANDLER",
651
- hydrate: true,
652
- };
745
+ const fakeHandler = jest.fn().mockResolvedValue("data");
653
746
  const fakeChildrenFn = jest.fn(() => null);
654
747
 
655
748
  // Act
656
749
  ReactDOMServer.renderToString(
657
- <Data handler={fakeHandler} options={"options"}>
750
+ <Data handler={fakeHandler} requestId="ID">
658
751
  {fakeChildrenFn}
659
752
  </Data>,
660
753
  );
@@ -668,20 +761,15 @@ describe("Data", () => {
668
761
 
669
762
  it("should render children with error", () => {
670
763
  // Arrange
671
- jest.spyOn(ResponseCache.Default, "getEntry").mockReturnValue({
764
+ jest.spyOn(SsrCache.Default, "getEntry").mockReturnValue({
672
765
  error: "OH NO! IT GO BOOM",
673
766
  });
674
- const fakeHandler: IRequestHandler<string, string> = {
675
- fulfillRequest: () => Promise.resolve("data"),
676
- getKey: (o) => o,
677
- type: "MY_HANDLER",
678
- hydrate: true,
679
- };
767
+ const fakeHandler = jest.fn().mockResolvedValue("data");
680
768
  const fakeChildrenFn = jest.fn(() => null);
681
769
 
682
770
  // Act
683
771
  ReactDOMServer.renderToString(
684
- <Data handler={fakeHandler} options={"options"}>
772
+ <Data handler={fakeHandler} requestId="ID">
685
773
  {fakeChildrenFn}
686
774
  </Data>,
687
775
  );
@@ -699,18 +787,13 @@ describe("Data", () => {
699
787
  RequestTracker.Default,
700
788
  "trackDataRequest",
701
789
  );
702
- const fakeHandler: IRequestHandler<string, string> = {
703
- fulfillRequest: () => Promise.resolve("data"),
704
- getKey: (o) => o,
705
- type: "MY_HANDLER",
706
- hydrate: true,
707
- };
790
+ const fakeHandler = jest.fn().mockResolvedValue("data");
708
791
  const fakeChildrenFn = jest.fn(() => null);
709
792
 
710
793
  // Act
711
794
  ReactDOMServer.renderToString(
712
795
  <TrackData>
713
- <Data handler={fakeHandler} options={"options"}>
796
+ <Data handler={fakeHandler} requestId="ID">
714
797
  {fakeChildrenFn}
715
798
  </Data>
716
799
  </TrackData>,
@@ -726,35 +809,27 @@ describe("Data", () => {
726
809
  describe("with data interceptor", () => {
727
810
  it("should not request data from interceptor", () => {
728
811
  // 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
- };
812
+ const fakeHandler = jest.fn().mockResolvedValue("data");
738
813
  const fakeChildrenFn = jest.fn(() => null);
739
- const fulfillRequestFn = jest.fn(() =>
740
- Promise.resolve("data2"),
741
- );
814
+ const interceptHandler = jest
815
+ .fn()
816
+ .mockResolvedValue("INTERCEPTED");
742
817
 
743
818
  // Act
744
819
  ReactDOMServer.renderToString(
745
820
  <InterceptData
746
- handler={fakeHandler}
747
- fulfillRequest={fulfillRequestFn}
821
+ handler={interceptHandler}
822
+ requestId="ID"
748
823
  >
749
- <Data handler={fakeHandler} options={"options"}>
824
+ <Data handler={fakeHandler} requestId="ID">
750
825
  {fakeChildrenFn}
751
826
  </Data>
752
827
  </InterceptData>,
753
828
  );
754
829
 
755
830
  // Assert
756
- expect(fulfillRequestSpy).not.toHaveBeenCalled();
757
- expect(fulfillRequestFn).not.toHaveBeenCalled();
831
+ expect(fakeHandler).not.toHaveBeenCalled();
832
+ expect(interceptHandler).not.toHaveBeenCalled();
758
833
  });
759
834
  });
760
835
  });