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