@khanacademy/wonder-blocks-data 5.0.1 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/dist/es/index.js +771 -372
- package/dist/index.js +1191 -550
- package/legacy-docs.md +3 -0
- package/package.json +2 -2
- package/src/__docs__/_overview_.stories.mdx +18 -0
- package/src/__docs__/_overview_graphql.stories.mdx +35 -0
- package/src/__docs__/_overview_ssr_.stories.mdx +185 -0
- package/src/__docs__/_overview_testing_.stories.mdx +123 -0
- package/src/__docs__/exports.clear-shared-cache.stories.mdx +20 -0
- package/src/__docs__/exports.data-error.stories.mdx +23 -0
- package/src/__docs__/exports.data-errors.stories.mdx +23 -0
- package/src/{components/data.md → __docs__/exports.data.stories.mdx} +15 -18
- package/src/__docs__/exports.fulfill-all-data-requests.stories.mdx +24 -0
- package/src/__docs__/exports.gql-error.stories.mdx +23 -0
- package/src/__docs__/exports.gql-errors.stories.mdx +20 -0
- package/src/__docs__/exports.gql-router.stories.mdx +29 -0
- package/src/__docs__/exports.has-unfulfilled-requests.stories.mdx +20 -0
- package/src/{components/intercept-requests.md → __docs__/exports.intercept-requests.stories.mdx} +16 -1
- package/src/__docs__/exports.intialize-cache.stories.mdx +29 -0
- package/src/__docs__/exports.remove-all-from-cache.stories.mdx +24 -0
- package/src/__docs__/exports.remove-from-cache.stories.mdx +25 -0
- package/src/__docs__/exports.request-fulfillment.stories.mdx +36 -0
- package/src/__docs__/exports.scoped-in-memory-cache.stories.mdx +92 -0
- package/src/__docs__/exports.serializable-in-memory-cache.stories.mdx +112 -0
- package/src/__docs__/exports.status.stories.mdx +31 -0
- package/src/{components/track-data.md → __docs__/exports.track-data.stories.mdx} +15 -0
- package/src/__docs__/exports.use-cached-effect.stories.mdx +41 -0
- package/src/__docs__/exports.use-gql.stories.mdx +73 -0
- package/src/__docs__/exports.use-hydratable-effect.stories.mdx +43 -0
- package/src/__docs__/exports.use-server-effect.stories.mdx +38 -0
- package/src/__docs__/exports.use-shared-cache.stories.mdx +30 -0
- package/src/__docs__/exports.when-client-side.stories.mdx +33 -0
- package/src/__docs__/types.cached-response.stories.mdx +29 -0
- package/src/__docs__/types.error-options.stories.mdx +21 -0
- package/src/__docs__/types.gql-context.stories.mdx +20 -0
- package/src/__docs__/types.gql-fetch-fn.stories.mdx +24 -0
- package/src/__docs__/types.gql-fetch-options.stories.mdx +24 -0
- package/src/__docs__/types.gql-operation-type.stories.mdx +24 -0
- package/src/__docs__/types.gql-operation.stories.mdx +67 -0
- package/src/__docs__/types.response-cache.stories.mdx +33 -0
- package/src/__docs__/types.result.stories.mdx +39 -0
- package/src/__docs__/types.scoped-cache.stories.mdx +27 -0
- package/src/__docs__/types.valid-cache-data.stories.mdx +23 -0
- package/src/__tests__/__snapshots__/generated-snapshot.test.js.snap +0 -80
- package/src/__tests__/generated-snapshot.test.js +0 -24
- package/src/components/__tests__/data.test.js +149 -128
- package/src/components/data.js +22 -112
- package/src/components/intercept-requests.js +1 -1
- package/src/hooks/__tests__/__snapshots__/use-shared-cache.test.js.snap +8 -8
- package/src/hooks/__tests__/use-cached-effect.test.js +507 -0
- package/src/hooks/__tests__/use-gql-router-context.test.js +133 -0
- package/src/hooks/__tests__/use-gql.test.js +1 -30
- package/src/hooks/__tests__/use-hydratable-effect.test.js +708 -0
- package/src/hooks/__tests__/use-server-effect.test.js +39 -11
- package/src/hooks/use-cached-effect.js +225 -0
- package/src/hooks/use-gql-router-context.js +50 -0
- package/src/hooks/use-gql.js +22 -52
- package/src/hooks/use-hydratable-effect.js +206 -0
- package/src/hooks/use-request-interception.js +20 -23
- package/src/hooks/use-server-effect.js +12 -5
- package/src/hooks/use-shared-cache.js +13 -11
- package/src/index.js +53 -3
- package/src/util/__tests__/__snapshots__/serializable-in-memory-cache.test.js.snap +19 -0
- package/src/util/__tests__/merge-gql-context.test.js +74 -0
- package/src/util/__tests__/request-fulfillment.test.js +23 -42
- package/src/util/__tests__/request-tracking.test.js +26 -7
- package/src/util/__tests__/result-from-cache-response.test.js +19 -5
- package/src/util/__tests__/scoped-in-memory-cache.test.js +6 -85
- package/src/util/__tests__/serializable-in-memory-cache.test.js +398 -0
- package/src/util/__tests__/ssr-cache.test.js +52 -52
- package/src/util/abort-error.js +15 -0
- package/src/util/data-error.js +58 -0
- package/src/util/get-gql-data-from-response.js +3 -2
- package/src/util/gql-error.js +19 -11
- package/src/util/merge-gql-context.js +34 -0
- package/src/util/request-fulfillment.js +49 -46
- package/src/util/request-tracking.js +69 -15
- package/src/util/result-from-cache-response.js +12 -16
- package/src/util/scoped-in-memory-cache.js +24 -47
- package/src/util/serializable-in-memory-cache.js +49 -0
- package/src/util/ssr-cache.js +9 -8
- package/src/util/status.js +30 -0
- package/src/util/types.js +18 -1
- package/docs.md +0 -122
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import {
|
|
3
|
+
renderHook as clientRenderHook,
|
|
4
|
+
act,
|
|
5
|
+
} from "@testing-library/react-hooks";
|
|
6
|
+
import {renderHook as serverRenderHook} from "@testing-library/react-hooks/server";
|
|
7
|
+
|
|
8
|
+
import {Server} from "@khanacademy/wonder-blocks-core";
|
|
9
|
+
import {Status} from "../../util/status.js";
|
|
10
|
+
|
|
11
|
+
import {RequestFulfillment} from "../../util/request-fulfillment.js";
|
|
12
|
+
import * as UseRequestInterception from "../use-request-interception.js";
|
|
13
|
+
import * as UseSharedCache from "../use-shared-cache.js";
|
|
14
|
+
|
|
15
|
+
import {useCachedEffect} from "../use-cached-effect.js";
|
|
16
|
+
|
|
17
|
+
jest.mock("../use-request-interception.js");
|
|
18
|
+
jest.mock("../use-shared-cache.js");
|
|
19
|
+
|
|
20
|
+
describe("#useCachedEffect", () => {
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
jest.resetAllMocks();
|
|
23
|
+
|
|
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 = {};
|
|
28
|
+
|
|
29
|
+
// Simple implementation of request interception that just returns
|
|
30
|
+
// the handler.
|
|
31
|
+
jest.spyOn(
|
|
32
|
+
UseRequestInterception,
|
|
33
|
+
"useRequestInterception",
|
|
34
|
+
).mockImplementation((_, handler) => handler);
|
|
35
|
+
|
|
36
|
+
// We need the cache to work a little so that we get our result.
|
|
37
|
+
const cache = {};
|
|
38
|
+
jest.spyOn(UseSharedCache, "useSharedCache").mockImplementation(
|
|
39
|
+
(id, _, defaultValue) => {
|
|
40
|
+
const setCache = (v) => (cache[id] = v);
|
|
41
|
+
return [cache[id] ?? defaultValue, setCache];
|
|
42
|
+
},
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("when server-side", () => {
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
jest.spyOn(Server, "isServerSide").mockReturnValue(true);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("should call useRequestInterception", () => {
|
|
52
|
+
// Arrange
|
|
53
|
+
const useRequestInterceptSpy = jest
|
|
54
|
+
.spyOn(UseRequestInterception, "useRequestInterception")
|
|
55
|
+
.mockReturnValue(jest.fn());
|
|
56
|
+
const fakeHandler = jest.fn();
|
|
57
|
+
|
|
58
|
+
// Act
|
|
59
|
+
serverRenderHook(() => useCachedEffect("ID", fakeHandler));
|
|
60
|
+
|
|
61
|
+
// Assert
|
|
62
|
+
expect(useRequestInterceptSpy).toHaveBeenCalledWith(
|
|
63
|
+
"ID",
|
|
64
|
+
fakeHandler,
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it.each`
|
|
69
|
+
scope | expectedScope
|
|
70
|
+
${undefined} | ${"useCachedEffect"}
|
|
71
|
+
${"foo"} | ${"foo"}
|
|
72
|
+
`(
|
|
73
|
+
"should call useSharedCache with id, scope=$scope, without a default",
|
|
74
|
+
({scope, cachedResult, expectedScope}) => {
|
|
75
|
+
const fakeHandler = jest.fn();
|
|
76
|
+
const useSharedCacheSpy = jest.spyOn(
|
|
77
|
+
UseSharedCache,
|
|
78
|
+
"useSharedCache",
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
// Act
|
|
82
|
+
serverRenderHook(() =>
|
|
83
|
+
useCachedEffect("ID", fakeHandler, {scope}),
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
// Assert
|
|
87
|
+
expect(useSharedCacheSpy).toHaveBeenCalledWith(
|
|
88
|
+
"ID",
|
|
89
|
+
expectedScope,
|
|
90
|
+
);
|
|
91
|
+
},
|
|
92
|
+
);
|
|
93
|
+
|
|
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
|
+
// Arrange
|
|
108
|
+
const fakeHandler = jest.fn();
|
|
109
|
+
|
|
110
|
+
// Act
|
|
111
|
+
const {
|
|
112
|
+
result: {current: result},
|
|
113
|
+
} = serverRenderHook(() => useCachedEffect("ID", fakeHandler));
|
|
114
|
+
|
|
115
|
+
// Assert
|
|
116
|
+
expect(result).toStrictEqual(Status.loading());
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe("with cached result", () => {
|
|
121
|
+
it("should return the result", () => {
|
|
122
|
+
// Arrange
|
|
123
|
+
const fakeHandler = jest.fn();
|
|
124
|
+
const cachedResult = Status.success("data");
|
|
125
|
+
jest.spyOn(UseSharedCache, "useSharedCache").mockReturnValue([
|
|
126
|
+
cachedResult,
|
|
127
|
+
jest.fn(),
|
|
128
|
+
]);
|
|
129
|
+
|
|
130
|
+
// Act
|
|
131
|
+
const {
|
|
132
|
+
result: {current: result},
|
|
133
|
+
} = serverRenderHook(() => useCachedEffect("ID", fakeHandler));
|
|
134
|
+
|
|
135
|
+
// Assert
|
|
136
|
+
expect(result).toEqual(cachedResult);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe("when client-side", () => {
|
|
142
|
+
beforeEach(() => {
|
|
143
|
+
jest.spyOn(Server, "isServerSide").mockReturnValue(false);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("should call useRequestInterception", () => {
|
|
147
|
+
// Arrange
|
|
148
|
+
const useRequestInterceptSpy = jest
|
|
149
|
+
.spyOn(UseRequestInterception, "useRequestInterception")
|
|
150
|
+
.mockReturnValue(jest.fn());
|
|
151
|
+
const fakeHandler = jest.fn();
|
|
152
|
+
|
|
153
|
+
// Act
|
|
154
|
+
clientRenderHook(() => useCachedEffect("ID", fakeHandler));
|
|
155
|
+
|
|
156
|
+
// Assert
|
|
157
|
+
expect(useRequestInterceptSpy).toHaveBeenCalledWith(
|
|
158
|
+
"ID",
|
|
159
|
+
fakeHandler,
|
|
160
|
+
);
|
|
161
|
+
});
|
|
162
|
+
|
|
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
|
+
it("should share inflight requests for the same requestId", () => {
|
|
179
|
+
// Arrange
|
|
180
|
+
const pending = new Promise((resolve, reject) => {
|
|
181
|
+
/*pending*/
|
|
182
|
+
});
|
|
183
|
+
const fakeHandler = jest.fn().mockReturnValue(pending);
|
|
184
|
+
|
|
185
|
+
// Act
|
|
186
|
+
clientRenderHook(() => useCachedEffect("ID", fakeHandler));
|
|
187
|
+
clientRenderHook(() => useCachedEffect("ID", fakeHandler));
|
|
188
|
+
|
|
189
|
+
// Assert
|
|
190
|
+
expect(fakeHandler).toHaveBeenCalledTimes(1);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it.each`
|
|
194
|
+
cachedResult
|
|
195
|
+
${Status.error(new Error("some error"))}
|
|
196
|
+
${Status.success("data")}
|
|
197
|
+
${Status.aborted()}
|
|
198
|
+
`(
|
|
199
|
+
"should not fulfill request when there is a cached response of $cachedResult",
|
|
200
|
+
({cachedResult}) => {
|
|
201
|
+
const fakeHandler = jest.fn();
|
|
202
|
+
jest.spyOn(UseSharedCache, "useSharedCache").mockReturnValue([
|
|
203
|
+
cachedResult,
|
|
204
|
+
jest.fn(),
|
|
205
|
+
]);
|
|
206
|
+
|
|
207
|
+
// Act
|
|
208
|
+
clientRenderHook(() => useCachedEffect("ID", fakeHandler));
|
|
209
|
+
|
|
210
|
+
// Assert
|
|
211
|
+
expect(fakeHandler).not.toHaveBeenCalled();
|
|
212
|
+
},
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
it("should fulfill request once only if requestId does not change", async () => {
|
|
216
|
+
// Arrange
|
|
217
|
+
const fakeHandler = jest.fn().mockResolvedValue("data");
|
|
218
|
+
|
|
219
|
+
// Act
|
|
220
|
+
const {rerender, waitForNextUpdate} = clientRenderHook(() =>
|
|
221
|
+
useCachedEffect("ID", fakeHandler),
|
|
222
|
+
);
|
|
223
|
+
rerender();
|
|
224
|
+
await waitForNextUpdate();
|
|
225
|
+
|
|
226
|
+
// Assert
|
|
227
|
+
expect(fakeHandler).toHaveBeenCalledTimes(1);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("should fulfill request again if requestId changes", async () => {
|
|
231
|
+
// Arrange
|
|
232
|
+
const fakeHandler = jest.fn().mockResolvedValue("data");
|
|
233
|
+
|
|
234
|
+
// Act
|
|
235
|
+
const {rerender, waitForNextUpdate} = clientRenderHook(
|
|
236
|
+
({requestId}) => useCachedEffect(requestId, fakeHandler),
|
|
237
|
+
{
|
|
238
|
+
initialProps: {requestId: "ID"},
|
|
239
|
+
},
|
|
240
|
+
);
|
|
241
|
+
rerender({requestId: "ID2"});
|
|
242
|
+
await waitForNextUpdate();
|
|
243
|
+
|
|
244
|
+
// Assert
|
|
245
|
+
expect(fakeHandler).toHaveBeenCalledTimes(2);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("should update shared cache with result when request is fulfilled", async () => {
|
|
249
|
+
// Arrange
|
|
250
|
+
const setCacheFn = jest.fn();
|
|
251
|
+
jest.spyOn(UseSharedCache, "useSharedCache").mockReturnValue([
|
|
252
|
+
null,
|
|
253
|
+
setCacheFn,
|
|
254
|
+
]);
|
|
255
|
+
const fakeHandler = jest.fn().mockResolvedValue("DATA");
|
|
256
|
+
|
|
257
|
+
// Act
|
|
258
|
+
const {waitForNextUpdate} = clientRenderHook(() =>
|
|
259
|
+
useCachedEffect("ID", fakeHandler),
|
|
260
|
+
);
|
|
261
|
+
await waitForNextUpdate();
|
|
262
|
+
|
|
263
|
+
// Assert
|
|
264
|
+
expect(setCacheFn).toHaveBeenCalledWith(Status.success("DATA"));
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("should ignore inflight request if requestId changes", async () => {
|
|
268
|
+
// Arrange
|
|
269
|
+
const response1 = Promise.resolve("DATA1");
|
|
270
|
+
const response2 = Promise.resolve("DATA2");
|
|
271
|
+
const fakeHandler = jest
|
|
272
|
+
.fn()
|
|
273
|
+
.mockReturnValueOnce(response1)
|
|
274
|
+
.mockReturnValueOnce(response2);
|
|
275
|
+
|
|
276
|
+
// Act
|
|
277
|
+
const {rerender, result} = clientRenderHook(
|
|
278
|
+
({requestId}) => useCachedEffect(requestId, fakeHandler),
|
|
279
|
+
{
|
|
280
|
+
initialProps: {requestId: "ID"},
|
|
281
|
+
},
|
|
282
|
+
);
|
|
283
|
+
rerender({requestId: "ID2"});
|
|
284
|
+
await act((): Promise<mixed> =>
|
|
285
|
+
Promise.all([response1, response2]),
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
// Assert
|
|
289
|
+
expect(result.all).not.toContainEqual(Status.success("DATA1"));
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it("should return result of fulfilled request for current requestId", async () => {
|
|
293
|
+
// Arrange
|
|
294
|
+
const response1 = Promise.resolve("DATA1");
|
|
295
|
+
const response2 = Promise.resolve("DATA2");
|
|
296
|
+
const fakeHandler = jest
|
|
297
|
+
.fn()
|
|
298
|
+
.mockReturnValueOnce(response1)
|
|
299
|
+
.mockReturnValueOnce(response2);
|
|
300
|
+
|
|
301
|
+
// Act
|
|
302
|
+
const {rerender, result} = clientRenderHook(
|
|
303
|
+
({requestId}) => useCachedEffect(requestId, fakeHandler),
|
|
304
|
+
{
|
|
305
|
+
initialProps: {requestId: "ID"},
|
|
306
|
+
},
|
|
307
|
+
);
|
|
308
|
+
rerender({requestId: "ID2"});
|
|
309
|
+
await act((): Promise<mixed> =>
|
|
310
|
+
Promise.all([response1, response2]),
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
// Assert
|
|
314
|
+
expect(result.current).toStrictEqual(Status.success("DATA2"));
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it("should not fulfill request when skip is true", () => {
|
|
318
|
+
// Arrange
|
|
319
|
+
const fakeHandler = jest.fn();
|
|
320
|
+
|
|
321
|
+
// Act
|
|
322
|
+
clientRenderHook(() =>
|
|
323
|
+
useCachedEffect("ID", fakeHandler, {skip: true}),
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
// Assert
|
|
327
|
+
expect(fakeHandler).not.toHaveBeenCalled();
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it("should ignore inflight request if skip changes", async () => {
|
|
331
|
+
// Arrange
|
|
332
|
+
const response1 = Promise.resolve("DATA1");
|
|
333
|
+
const fakeHandler = jest.fn().mockReturnValueOnce(response1);
|
|
334
|
+
|
|
335
|
+
// Act
|
|
336
|
+
const {rerender, result} = clientRenderHook(
|
|
337
|
+
({skip}) => useCachedEffect("ID", fakeHandler, {skip}),
|
|
338
|
+
{
|
|
339
|
+
initialProps: {skip: false},
|
|
340
|
+
},
|
|
341
|
+
);
|
|
342
|
+
rerender({skip: true});
|
|
343
|
+
await act((): Promise<mixed> => response1);
|
|
344
|
+
|
|
345
|
+
// Assert
|
|
346
|
+
expect(result.all).not.toContainEqual(Status.success("DATA1"));
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it("should not ignore inflight request if handler changes", async () => {
|
|
350
|
+
// Arrange
|
|
351
|
+
const response1 = Promise.resolve("DATA1");
|
|
352
|
+
const response2 = Promise.resolve("DATA2");
|
|
353
|
+
const fakeHandler1 = jest.fn().mockReturnValueOnce(response1);
|
|
354
|
+
const fakeHandler2 = jest.fn().mockReturnValueOnce(response2);
|
|
355
|
+
|
|
356
|
+
// Act
|
|
357
|
+
const {rerender, result} = clientRenderHook(
|
|
358
|
+
({handler}) => useCachedEffect("ID", handler),
|
|
359
|
+
{
|
|
360
|
+
initialProps: {handler: fakeHandler1},
|
|
361
|
+
},
|
|
362
|
+
);
|
|
363
|
+
rerender({handler: fakeHandler2});
|
|
364
|
+
await act((): Promise<mixed> =>
|
|
365
|
+
Promise.all([response1, response2]),
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
// Assert
|
|
369
|
+
expect(result.current).toStrictEqual(Status.success("DATA1"));
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it("should not ignore inflight request if options (other than skip) change", async () => {
|
|
373
|
+
// Arrange
|
|
374
|
+
const response1 = Promise.resolve("DATA1");
|
|
375
|
+
const fakeHandler = jest.fn().mockReturnValueOnce(response1);
|
|
376
|
+
|
|
377
|
+
// Act
|
|
378
|
+
const {rerender, result} = clientRenderHook(
|
|
379
|
+
({options}) => useCachedEffect("ID", fakeHandler),
|
|
380
|
+
{
|
|
381
|
+
initialProps: {options: undefined},
|
|
382
|
+
},
|
|
383
|
+
);
|
|
384
|
+
rerender({
|
|
385
|
+
options: {
|
|
386
|
+
scope: "BLAH!",
|
|
387
|
+
},
|
|
388
|
+
});
|
|
389
|
+
await act((): Promise<mixed> => response1);
|
|
390
|
+
|
|
391
|
+
// Assert
|
|
392
|
+
expect(result.current).toStrictEqual(Status.success("DATA1"));
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it("should return previous result when requestId changes and retainResultOnChange is true", async () => {
|
|
396
|
+
// Arrange
|
|
397
|
+
const response1 = Promise.resolve("DATA1");
|
|
398
|
+
const response2 = Promise.resolve("DATA2");
|
|
399
|
+
const fakeHandler = jest
|
|
400
|
+
.fn()
|
|
401
|
+
.mockReturnValueOnce(response1)
|
|
402
|
+
.mockReturnValueOnce(response2);
|
|
403
|
+
|
|
404
|
+
// Act
|
|
405
|
+
const {
|
|
406
|
+
rerender,
|
|
407
|
+
result: hookResult,
|
|
408
|
+
waitForNextUpdate,
|
|
409
|
+
} = clientRenderHook(
|
|
410
|
+
({requestId}) =>
|
|
411
|
+
useCachedEffect(requestId, fakeHandler, {
|
|
412
|
+
retainResultOnChange: true,
|
|
413
|
+
}),
|
|
414
|
+
{
|
|
415
|
+
initialProps: {requestId: "ID"},
|
|
416
|
+
},
|
|
417
|
+
);
|
|
418
|
+
await act((): Promise<mixed> => response1);
|
|
419
|
+
rerender({requestId: "ID2"});
|
|
420
|
+
const result = hookResult.current;
|
|
421
|
+
await waitForNextUpdate();
|
|
422
|
+
|
|
423
|
+
// Assert
|
|
424
|
+
expect(result).toStrictEqual(Status.success("DATA1"));
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it("should return loading status when requestId changes and retainResultOnChange is false", async () => {
|
|
428
|
+
// Arrange
|
|
429
|
+
const response1 = Promise.resolve("DATA1");
|
|
430
|
+
const response2 = new Promise(() => {
|
|
431
|
+
/*pending*/
|
|
432
|
+
});
|
|
433
|
+
const fakeHandler = jest
|
|
434
|
+
.fn()
|
|
435
|
+
.mockReturnValueOnce(response1)
|
|
436
|
+
.mockReturnValueOnce(response2);
|
|
437
|
+
|
|
438
|
+
// Act
|
|
439
|
+
const {rerender, result} = clientRenderHook(
|
|
440
|
+
({requestId}) =>
|
|
441
|
+
useCachedEffect(requestId, fakeHandler, {
|
|
442
|
+
retainResultOnChange: false,
|
|
443
|
+
}),
|
|
444
|
+
{
|
|
445
|
+
initialProps: {requestId: "ID"},
|
|
446
|
+
},
|
|
447
|
+
);
|
|
448
|
+
await act((): Promise<mixed> => response1);
|
|
449
|
+
rerender({requestId: "ID2"});
|
|
450
|
+
|
|
451
|
+
// Assert
|
|
452
|
+
expect(result.current).toStrictEqual(Status.loading());
|
|
453
|
+
});
|
|
454
|
+
|
|
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);
|
|
459
|
+
|
|
460
|
+
// Act
|
|
461
|
+
const {result} = clientRenderHook(() =>
|
|
462
|
+
useCachedEffect("ID", fakeHandler),
|
|
463
|
+
);
|
|
464
|
+
await act((): Promise<mixed> => response);
|
|
465
|
+
|
|
466
|
+
// Assert
|
|
467
|
+
expect(result.current).toStrictEqual(Status.success("DATA"));
|
|
468
|
+
});
|
|
469
|
+
|
|
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);
|
|
474
|
+
|
|
475
|
+
// Act
|
|
476
|
+
const {result} = clientRenderHook(() =>
|
|
477
|
+
useCachedEffect("ID", fakeHandler, {
|
|
478
|
+
onResultChanged: () => {},
|
|
479
|
+
}),
|
|
480
|
+
);
|
|
481
|
+
await act((): Promise<mixed> => response);
|
|
482
|
+
|
|
483
|
+
// Assert
|
|
484
|
+
expect(result.current).toStrictEqual(Status.loading());
|
|
485
|
+
});
|
|
486
|
+
|
|
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();
|
|
492
|
+
|
|
493
|
+
// Act
|
|
494
|
+
clientRenderHook(() =>
|
|
495
|
+
useCachedEffect("ID", fakeHandler, {
|
|
496
|
+
onResultChanged,
|
|
497
|
+
}),
|
|
498
|
+
);
|
|
499
|
+
await act((): Promise<mixed> => response);
|
|
500
|
+
|
|
501
|
+
// Assert
|
|
502
|
+
expect(onResultChanged).toHaveBeenCalledWith(
|
|
503
|
+
Status.success("DATA"),
|
|
504
|
+
);
|
|
505
|
+
});
|
|
506
|
+
});
|
|
507
|
+
});
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import {renderHook} from "@testing-library/react-hooks";
|
|
4
|
+
|
|
5
|
+
import {GqlRouterContext} from "../../util/gql-router-context.js";
|
|
6
|
+
import {useGqlRouterContext} from "../use-gql-router-context.js";
|
|
7
|
+
|
|
8
|
+
describe("#useGqlRouterContext", () => {
|
|
9
|
+
it("should throw if there is no GqlRouterContext", () => {
|
|
10
|
+
// Arrange
|
|
11
|
+
|
|
12
|
+
// Act
|
|
13
|
+
const {
|
|
14
|
+
result: {error: result},
|
|
15
|
+
} = renderHook(() => useGqlRouterContext());
|
|
16
|
+
|
|
17
|
+
// Assert
|
|
18
|
+
expect(result).toMatchInlineSnapshot(
|
|
19
|
+
`[InternalGqlError: No GqlRouter]`,
|
|
20
|
+
);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("should return an equivalent to the GqlRouterContext if no overrides given", () => {
|
|
24
|
+
// Arrange
|
|
25
|
+
const baseContext = {
|
|
26
|
+
fetch: jest.fn(),
|
|
27
|
+
defaultContext: {
|
|
28
|
+
foo: "bar",
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
const Wrapper = ({children}: any) => (
|
|
32
|
+
<GqlRouterContext.Provider value={baseContext}>
|
|
33
|
+
{children}
|
|
34
|
+
</GqlRouterContext.Provider>
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
// Act
|
|
38
|
+
const {
|
|
39
|
+
result: {current: result},
|
|
40
|
+
} = renderHook(() => useGqlRouterContext(), {wrapper: Wrapper});
|
|
41
|
+
|
|
42
|
+
// Assert
|
|
43
|
+
expect(result).toStrictEqual(baseContext);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("should return the same object if nothing has changed", () => {
|
|
47
|
+
// Arrange
|
|
48
|
+
const baseContext = {
|
|
49
|
+
fetch: jest.fn(),
|
|
50
|
+
defaultContext: {
|
|
51
|
+
foo: "bar",
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
const Wrapper = ({children}: any) => (
|
|
55
|
+
<GqlRouterContext.Provider value={baseContext}>
|
|
56
|
+
{children}
|
|
57
|
+
</GqlRouterContext.Provider>
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
// Act
|
|
61
|
+
const wrapper = renderHook(() => useGqlRouterContext(), {
|
|
62
|
+
wrapper: Wrapper,
|
|
63
|
+
});
|
|
64
|
+
const result1 = wrapper.result.current;
|
|
65
|
+
wrapper.rerender();
|
|
66
|
+
const result2 = wrapper.result.current;
|
|
67
|
+
|
|
68
|
+
// Assert
|
|
69
|
+
expect(result1).toBe(result2);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("should return the same object if the object adds overrides that don't change the merged context", () => {
|
|
73
|
+
// Arrange
|
|
74
|
+
const baseContext = {
|
|
75
|
+
fetch: jest.fn(),
|
|
76
|
+
defaultContext: {
|
|
77
|
+
foo: "bar",
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
const Wrapper = ({children}: any) => (
|
|
81
|
+
<GqlRouterContext.Provider value={baseContext}>
|
|
82
|
+
{children}
|
|
83
|
+
</GqlRouterContext.Provider>
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
// Act
|
|
87
|
+
const wrapper = renderHook(
|
|
88
|
+
({overrides}) => useGqlRouterContext(overrides),
|
|
89
|
+
{
|
|
90
|
+
wrapper: Wrapper,
|
|
91
|
+
initialProps: {},
|
|
92
|
+
},
|
|
93
|
+
);
|
|
94
|
+
const result1 = wrapper.result.current;
|
|
95
|
+
wrapper.rerender({overrides: {foo: "bar"}});
|
|
96
|
+
const result2 = wrapper.result.current;
|
|
97
|
+
|
|
98
|
+
// Assert
|
|
99
|
+
expect(result1).toBe(result2);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("should return an updated object if the object adds overrides that change the merged context", () => {
|
|
103
|
+
// Arrange
|
|
104
|
+
const baseContext = {
|
|
105
|
+
fetch: jest.fn(),
|
|
106
|
+
defaultContext: {
|
|
107
|
+
foo: "bar",
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
const Wrapper = ({children}: any) => (
|
|
111
|
+
<GqlRouterContext.Provider value={baseContext}>
|
|
112
|
+
{children}
|
|
113
|
+
</GqlRouterContext.Provider>
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
// Act
|
|
117
|
+
const wrapper = renderHook(
|
|
118
|
+
({overrides}) => useGqlRouterContext(overrides),
|
|
119
|
+
{
|
|
120
|
+
wrapper: Wrapper,
|
|
121
|
+
initialProps: {
|
|
122
|
+
overrides: {fiz: "baz"},
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
);
|
|
126
|
+
const result1 = wrapper.result.current;
|
|
127
|
+
wrapper.rerender({overrides: {}});
|
|
128
|
+
const result2 = wrapper.result.current;
|
|
129
|
+
|
|
130
|
+
// Assert
|
|
131
|
+
expect(result1).not.toBe(result2);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
@@ -21,7 +21,7 @@ describe("#useGql", () => {
|
|
|
21
21
|
|
|
22
22
|
// Assert
|
|
23
23
|
expect(result).toMatchInlineSnapshot(
|
|
24
|
-
`[
|
|
24
|
+
`[InternalGqlError: No GqlRouter]`,
|
|
25
25
|
);
|
|
26
26
|
});
|
|
27
27
|
|
|
@@ -167,35 +167,6 @@ describe("#useGql", () => {
|
|
|
167
167
|
);
|
|
168
168
|
});
|
|
169
169
|
|
|
170
|
-
it("should resolve to null if the fetch was aborted", async () => {
|
|
171
|
-
// Arrange
|
|
172
|
-
const abortError = new Error("Aborted");
|
|
173
|
-
abortError.name = "AbortError";
|
|
174
|
-
const gqlRouterContext = {
|
|
175
|
-
fetch: jest.fn().mockRejectedValue(abortError),
|
|
176
|
-
defaultContext: {},
|
|
177
|
-
};
|
|
178
|
-
const {
|
|
179
|
-
result: {current: gqlFetch},
|
|
180
|
-
} = renderHook(() => useGql(), {
|
|
181
|
-
wrapper: ({children}) => (
|
|
182
|
-
<GqlRouterContext.Provider value={gqlRouterContext}>
|
|
183
|
-
{children}
|
|
184
|
-
</GqlRouterContext.Provider>
|
|
185
|
-
),
|
|
186
|
-
});
|
|
187
|
-
const gqlOp = {
|
|
188
|
-
type: "query",
|
|
189
|
-
id: "MyQuery",
|
|
190
|
-
};
|
|
191
|
-
|
|
192
|
-
// Act
|
|
193
|
-
const result = await gqlFetch(gqlOp);
|
|
194
|
-
|
|
195
|
-
// Assert
|
|
196
|
-
expect(result).toBeNull();
|
|
197
|
-
});
|
|
198
|
-
|
|
199
170
|
it("should resolve to the response data", async () => {
|
|
200
171
|
// Arrange
|
|
201
172
|
jest.spyOn(
|