@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
@@ -0,0 +1,255 @@
1
+ // @flow
2
+ import * as React from "react";
3
+ import {renderHook} from "@testing-library/react-hooks";
4
+ import InterceptRequests from "../../components/intercept-requests.js";
5
+ import {useRequestInterception} from "../use-request-interception.js";
6
+
7
+ describe("#useRequestInterception", () => {
8
+ it("should return a function", () => {
9
+ // Arrange
10
+
11
+ // Act
12
+ const {
13
+ result: {current: result},
14
+ } = renderHook(() => useRequestInterception("ID", jest.fn()));
15
+
16
+ // Assert
17
+ expect(result).toBeInstanceOf(Function);
18
+ });
19
+
20
+ it("should return the same function if the arguments and context don't change", () => {
21
+ // Arrange
22
+ const handler = jest.fn();
23
+
24
+ // Act
25
+ const wrapper = renderHook(() => useRequestInterception("ID", handler));
26
+ const result1 = wrapper.result.current;
27
+ wrapper.rerender();
28
+ const result2 = wrapper.result.current;
29
+
30
+ // Assert
31
+ expect(result1).toBe(result2);
32
+ });
33
+
34
+ it("should return a new function if the requestId changes", () => {
35
+ // Arrange
36
+ const handler = jest.fn();
37
+
38
+ // Act
39
+ const wrapper = renderHook(
40
+ ({requestId}) => useRequestInterception(requestId, handler),
41
+ {initialProps: {requestId: "ID"}},
42
+ );
43
+ const result1 = wrapper.result.current;
44
+ wrapper.rerender({requestId: "ID2"});
45
+ const result2 = wrapper.result.current;
46
+
47
+ // Assert
48
+ expect(result1).not.toBe(result2);
49
+ });
50
+
51
+ it("should return a new function if the handler changes", () => {
52
+ // Arrange
53
+
54
+ // Act
55
+ const wrapper = renderHook(
56
+ ({handler}) => useRequestInterception("ID", handler),
57
+ {initialProps: {handler: jest.fn()}},
58
+ );
59
+ const result1 = wrapper.result.current;
60
+ wrapper.rerender({handler: jest.fn()});
61
+ const result2 = wrapper.result.current;
62
+
63
+ // Assert
64
+ expect(result1).not.toBe(result2);
65
+ });
66
+
67
+ it("should return a new function if the context changes", () => {
68
+ // Arrange
69
+ const handler = jest.fn();
70
+ const interceptor1 = jest.fn();
71
+ const interceptor2 = jest.fn();
72
+ const Wrapper = ({children, interceptor}: any) => (
73
+ <InterceptRequests interceptor={interceptor}>
74
+ {children}
75
+ </InterceptRequests>
76
+ );
77
+
78
+ // Act
79
+ const wrapper = renderHook(
80
+ () => useRequestInterception("ID", handler),
81
+ {wrapper: Wrapper, initialProps: {interceptor: interceptor1}},
82
+ );
83
+ const result1 = wrapper.result.current;
84
+ wrapper.rerender({wrapper: Wrapper, interceptor: interceptor2});
85
+ const result2 = wrapper.result.current;
86
+
87
+ // Assert
88
+ expect(result1).not.toBe(result2);
89
+ });
90
+
91
+ describe("returned function", () => {
92
+ it("should invoke the original handler when there are no interceptors", () => {
93
+ // Arrange
94
+ const handler = jest.fn();
95
+ const requestId = "ID";
96
+ const {
97
+ result: {current: interceptedHandler},
98
+ } = renderHook(() => useRequestInterception(requestId, handler));
99
+
100
+ // Act
101
+ interceptedHandler();
102
+
103
+ // Assert
104
+ expect(handler).toHaveBeenCalledTimes(1);
105
+ });
106
+
107
+ it("should invoke interceptors nearest to furthest", () => {
108
+ // Arrange
109
+ const handler = jest.fn();
110
+ const interceptorFurthest = jest.fn(() => null);
111
+ const interceptorNearest = jest.fn(() => null);
112
+ const Wrapper = ({children}: any) => (
113
+ <InterceptRequests interceptor={interceptorFurthest}>
114
+ <InterceptRequests interceptor={interceptorNearest}>
115
+ {children}
116
+ </InterceptRequests>
117
+ </InterceptRequests>
118
+ );
119
+ const {
120
+ result: {current: interceptedHandler},
121
+ } = renderHook(() => useRequestInterception("ID", handler), {
122
+ wrapper: Wrapper,
123
+ });
124
+
125
+ // Act
126
+ interceptedHandler();
127
+
128
+ // Assert
129
+ expect(interceptorNearest).toHaveBeenCalledBefore(
130
+ interceptorFurthest,
131
+ );
132
+ });
133
+
134
+ it("should invoke the handler last", () => {
135
+ // Arrange
136
+ const handler = jest.fn();
137
+ const interceptorFurthest = jest.fn(() => null);
138
+ const interceptorNearest = jest.fn(() => null);
139
+ const Wrapper = ({children}: any) => (
140
+ <InterceptRequests interceptor={interceptorFurthest}>
141
+ <InterceptRequests interceptor={interceptorNearest}>
142
+ {children}
143
+ </InterceptRequests>
144
+ </InterceptRequests>
145
+ );
146
+ const {
147
+ result: {current: interceptedHandler},
148
+ } = renderHook(() => useRequestInterception("ID", handler), {
149
+ wrapper: Wrapper,
150
+ });
151
+
152
+ // Act
153
+ interceptedHandler();
154
+
155
+ // Assert
156
+ expect(interceptorFurthest).toHaveBeenCalledBefore(handler);
157
+ });
158
+
159
+ it("should invoke the original handler when there all interceptors return null", () => {
160
+ // Arrange
161
+ const handler = jest.fn();
162
+ const interceptor1 = jest.fn(() => null);
163
+ const interceptor2 = jest.fn(() => null);
164
+ const Wrapper = ({children}: any) => (
165
+ <InterceptRequests interceptor={interceptor1}>
166
+ <InterceptRequests interceptor={interceptor2}>
167
+ {children}
168
+ </InterceptRequests>
169
+ </InterceptRequests>
170
+ );
171
+ const {
172
+ result: {current: interceptedHandler},
173
+ } = renderHook(() => useRequestInterception("ID", handler), {
174
+ wrapper: Wrapper,
175
+ });
176
+
177
+ // Act
178
+ interceptedHandler();
179
+
180
+ // Assert
181
+ expect(handler).toHaveBeenCalledTimes(1);
182
+ });
183
+
184
+ it("should return the result of the nearest interceptor that returns a non-null result", async () => {
185
+ // Arrange
186
+ const handler = jest
187
+ .fn()
188
+ .mockRejectedValue(
189
+ new Error("This handler should have been intercepted"),
190
+ );
191
+ const interceptorFurthest = jest
192
+ .fn()
193
+ .mockRejectedValue(
194
+ new Error("This interceptor should not get called"),
195
+ );
196
+ const interceptorNearest = jest
197
+ .fn()
198
+ .mockResolvedValue("INTERCEPTED_DATA");
199
+ const Wrapper = ({children}: any) => (
200
+ <InterceptRequests interceptor={interceptorFurthest}>
201
+ <InterceptRequests interceptor={interceptorNearest}>
202
+ {children}
203
+ </InterceptRequests>
204
+ </InterceptRequests>
205
+ );
206
+ const {
207
+ result: {current: interceptedHandler},
208
+ } = renderHook(() => useRequestInterception("ID", handler), {
209
+ wrapper: Wrapper,
210
+ });
211
+
212
+ // Act
213
+ const result = await interceptedHandler();
214
+
215
+ // Assert
216
+ expect(result).toBe("INTERCEPTED_DATA");
217
+ });
218
+
219
+ it("should not invoke interceptors or handlers beyond a non-null interception", () => {
220
+ // Arrange
221
+ const handler = jest
222
+ .fn()
223
+ .mockRejectedValue(
224
+ new Error("This handler should have been intercepted"),
225
+ );
226
+ const interceptorFurthest = jest
227
+ .fn()
228
+ .mockRejectedValue(
229
+ new Error("This interceptor should not get called"),
230
+ );
231
+ const interceptorNearest = jest
232
+ .fn()
233
+ .mockResolvedValue("INTERCEPTED_DATA");
234
+ const Wrapper = ({children}: any) => (
235
+ <InterceptRequests interceptor={interceptorFurthest}>
236
+ <InterceptRequests interceptor={interceptorNearest}>
237
+ {children}
238
+ </InterceptRequests>
239
+ </InterceptRequests>
240
+ );
241
+ const {
242
+ result: {current: interceptedHandler},
243
+ } = renderHook(() => useRequestInterception("ID", handler), {
244
+ wrapper: Wrapper,
245
+ });
246
+
247
+ // Act
248
+ interceptedHandler();
249
+
250
+ // Assert
251
+ expect(handler).not.toHaveBeenCalled();
252
+ expect(interceptorFurthest).not.toHaveBeenCalled();
253
+ });
254
+ });
255
+ });
@@ -0,0 +1,217 @@
1
+ // @flow
2
+ import {renderHook as clientRenderHook} from "@testing-library/react-hooks";
3
+ import {renderHook as serverRenderHook} from "@testing-library/react-hooks/server";
4
+
5
+ import {Server} from "@khanacademy/wonder-blocks-core";
6
+
7
+ import TrackData from "../../components/track-data.js";
8
+ import {RequestFulfillment} from "../../util/request-fulfillment.js";
9
+ import {SsrCache} from "../../util/ssr-cache.js";
10
+ import {RequestTracker} from "../../util/request-tracking.js";
11
+
12
+ import {useServerEffect} from "../use-server-effect.js";
13
+
14
+ describe("#useServerEffect", () => {
15
+ beforeEach(() => {
16
+ const responseCache = new SsrCache();
17
+ jest.spyOn(SsrCache, "Default", "get").mockReturnValue(responseCache);
18
+ jest.spyOn(RequestFulfillment, "Default", "get").mockReturnValue(
19
+ new RequestFulfillment(responseCache),
20
+ );
21
+ jest.spyOn(RequestTracker, "Default", "get").mockReturnValue(
22
+ new RequestTracker(responseCache),
23
+ );
24
+ });
25
+
26
+ afterEach(() => {
27
+ jest.resetAllMocks();
28
+ });
29
+
30
+ describe("when server-side", () => {
31
+ beforeEach(() => {
32
+ jest.spyOn(Server, "isServerSide").mockReturnValue(true);
33
+ });
34
+
35
+ it("should return null if no cached result", () => {
36
+ // Arrange
37
+ const fakeHandler = jest.fn();
38
+
39
+ // Act
40
+ const {
41
+ result: {current: result},
42
+ } = serverRenderHook(() => useServerEffect("ID", fakeHandler));
43
+
44
+ // Assert
45
+ expect(result).toBeNull();
46
+ });
47
+
48
+ it("should not directly request fulfillment", () => {
49
+ // Arrange
50
+ const fakeHandler = jest.fn();
51
+ const fulfillRequestSpy = jest.spyOn(
52
+ RequestFulfillment.Default,
53
+ "fulfill",
54
+ );
55
+
56
+ // Act
57
+ serverRenderHook(() => useServerEffect("ID", fakeHandler));
58
+
59
+ // Assert
60
+ expect(fulfillRequestSpy).not.toHaveBeenCalled();
61
+ });
62
+
63
+ it("should track the request", () => {
64
+ // Arrange
65
+ const fakeHandler = jest.fn();
66
+ const trackDataRequestSpy = jest.spyOn(
67
+ RequestTracker.Default,
68
+ "trackDataRequest",
69
+ );
70
+
71
+ // Act
72
+ serverRenderHook(() => useServerEffect("ID", fakeHandler), {
73
+ wrapper: TrackData,
74
+ });
75
+
76
+ // Assert
77
+ expect(trackDataRequestSpy).toHaveBeenCalledWith(
78
+ "ID",
79
+ fakeHandler,
80
+ true,
81
+ );
82
+ });
83
+
84
+ it("should return data cached result", () => {
85
+ // Arrange
86
+ const fakeHandler = jest.fn();
87
+ jest.spyOn(SsrCache.Default, "getEntry").mockReturnValueOnce({
88
+ data: "DATA",
89
+ error: null,
90
+ });
91
+
92
+ // Act
93
+ const {
94
+ result: {current: result},
95
+ } = serverRenderHook(() => useServerEffect("ID", fakeHandler));
96
+
97
+ // Assert
98
+ expect(result).toEqual({data: "DATA", error: null});
99
+ });
100
+
101
+ it("should return error cached result", () => {
102
+ // Arrange
103
+ const fakeHandler = jest.fn();
104
+ jest.spyOn(SsrCache.Default, "getEntry").mockReturnValueOnce({
105
+ data: null,
106
+ error: "ERROR",
107
+ });
108
+
109
+ // Act
110
+ const {
111
+ result: {current: result},
112
+ } = serverRenderHook(() => useServerEffect("ID", fakeHandler));
113
+
114
+ // Assert
115
+ expect(result).toEqual({
116
+ data: null,
117
+ error: "ERROR",
118
+ });
119
+ });
120
+ });
121
+
122
+ describe("when client-side", () => {
123
+ beforeEach(() => {
124
+ jest.spyOn(Server, "isServerSide").mockReturnValue(false);
125
+ });
126
+
127
+ it("should return null if no cached result", () => {
128
+ // Arrange
129
+ const fakeHandler = jest.fn();
130
+
131
+ // Act
132
+ const {
133
+ result: {current: result},
134
+ } = clientRenderHook(() => useServerEffect("ID", fakeHandler));
135
+
136
+ // Assert
137
+ expect(result).toBeNull();
138
+ });
139
+
140
+ it("should return data cached result", () => {
141
+ // Arrange
142
+ const fakeHandler = jest.fn();
143
+ jest.spyOn(SsrCache.Default, "getEntry").mockReturnValueOnce({
144
+ data: "DATA",
145
+ error: null,
146
+ });
147
+
148
+ // Act
149
+ const {
150
+ result: {current: result},
151
+ } = clientRenderHook(() => useServerEffect("ID", fakeHandler));
152
+
153
+ // Assert
154
+ expect(result).toEqual({data: "DATA", error: null});
155
+ });
156
+
157
+ it("should return error cached result", () => {
158
+ // Arrange
159
+ const fakeHandler = jest.fn();
160
+ jest.spyOn(SsrCache.Default, "getEntry").mockReturnValueOnce({
161
+ data: null,
162
+ error: "ERROR",
163
+ });
164
+
165
+ // Act
166
+ const {
167
+ result: {current: result},
168
+ } = clientRenderHook(() => useServerEffect("ID", fakeHandler));
169
+
170
+ // Assert
171
+ expect(result).toEqual({
172
+ data: null,
173
+ error: "ERROR",
174
+ });
175
+ });
176
+
177
+ it("should not track the request", () => {
178
+ // Arrange
179
+ const fakeHandler = jest.fn().mockReturnValue(
180
+ new Promise(() => {
181
+ /*prevent act() warning*/
182
+ }),
183
+ );
184
+ const trackDataRequestSpy = jest.spyOn(
185
+ RequestTracker.Default,
186
+ "trackDataRequest",
187
+ );
188
+
189
+ // Act
190
+ clientRenderHook(() => useServerEffect("ID", fakeHandler), {
191
+ wrapper: TrackData,
192
+ });
193
+
194
+ // Assert
195
+ expect(trackDataRequestSpy).not.toHaveBeenCalled();
196
+ });
197
+
198
+ it("should not request fulfillment", () => {
199
+ // Arrange
200
+ const fakeHandler = jest.fn().mockReturnValue(
201
+ new Promise(() => {
202
+ /*prevent act() warning*/
203
+ }),
204
+ );
205
+ const fulfillRequestSpy = jest.spyOn(
206
+ RequestFulfillment.Default,
207
+ "fulfill",
208
+ );
209
+
210
+ // Act
211
+ clientRenderHook(() => useServerEffect("ID", fakeHandler));
212
+
213
+ // Assert
214
+ expect(fulfillRequestSpy).not.toHaveBeenCalled();
215
+ });
216
+ });
217
+ });