@khanacademy/wonder-blocks-data 4.0.0 → 5.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 +6 -0
- package/dist/es/index.js +64 -45
- package/dist/index.js +109 -74
- package/package.json +1 -1
- package/src/__tests__/generated-snapshot.test.js +7 -7
- package/src/components/__tests__/data.test.js +11 -26
- package/src/components/__tests__/intercept-requests.test.js +58 -0
- package/src/components/data.js +4 -18
- package/src/components/intercept-context.js +4 -5
- package/src/components/intercept-requests.js +69 -0
- package/src/components/{intercept-data.md → intercept-requests.md} +18 -15
- package/src/hooks/__tests__/use-request-interception.test.js +255 -0
- package/src/hooks/use-request-interception.js +54 -0
- package/src/hooks/use-server-effect.js +3 -3
- package/src/index.js +2 -1
- package/src/components/__tests__/intercept-data.test.js +0 -63
- package/src/components/intercept-data.js +0 -66
|
@@ -11,7 +11,7 @@ import TrackData from "../track-data.js";
|
|
|
11
11
|
import {RequestFulfillment} from "../../util/request-fulfillment.js";
|
|
12
12
|
import {SsrCache} from "../../util/ssr-cache.js";
|
|
13
13
|
import {RequestTracker} from "../../util/request-tracking.js";
|
|
14
|
-
import
|
|
14
|
+
import InterceptRequests from "../intercept-requests.js";
|
|
15
15
|
import Data from "../data.js";
|
|
16
16
|
|
|
17
17
|
describe("Data", () => {
|
|
@@ -362,14 +362,11 @@ describe("Data", () => {
|
|
|
362
362
|
|
|
363
363
|
// Act
|
|
364
364
|
render(
|
|
365
|
-
<
|
|
366
|
-
requestId="ID"
|
|
367
|
-
handler={interceptHandler}
|
|
368
|
-
>
|
|
365
|
+
<InterceptRequests interceptor={interceptHandler}>
|
|
369
366
|
<Data handler={fakeHandler} requestId="ID">
|
|
370
367
|
{fakeChildrenFn}
|
|
371
368
|
</Data>
|
|
372
|
-
</
|
|
369
|
+
</InterceptRequests>,
|
|
373
370
|
);
|
|
374
371
|
|
|
375
372
|
// Assert
|
|
@@ -385,14 +382,11 @@ describe("Data", () => {
|
|
|
385
382
|
|
|
386
383
|
// Act
|
|
387
384
|
render(
|
|
388
|
-
<
|
|
389
|
-
handler={interceptHandler}
|
|
390
|
-
requestId="ID"
|
|
391
|
-
>
|
|
385
|
+
<InterceptRequests interceptor={interceptHandler}>
|
|
392
386
|
<Data handler={fakeHandler} requestId="ID">
|
|
393
387
|
{fakeChildrenFn}
|
|
394
388
|
</Data>
|
|
395
|
-
</
|
|
389
|
+
</InterceptRequests>,
|
|
396
390
|
);
|
|
397
391
|
|
|
398
392
|
// Assert
|
|
@@ -662,14 +656,11 @@ describe("Data", () => {
|
|
|
662
656
|
|
|
663
657
|
// Act
|
|
664
658
|
ReactDOMServer.renderToString(
|
|
665
|
-
<
|
|
666
|
-
handler={interceptedHandler}
|
|
667
|
-
requestId="ID"
|
|
668
|
-
>
|
|
659
|
+
<InterceptRequests interceptor={interceptedHandler}>
|
|
669
660
|
<Data handler={fakeHandler} requestId="ID">
|
|
670
661
|
{fakeChildrenFn}
|
|
671
662
|
</Data>
|
|
672
|
-
</
|
|
663
|
+
</InterceptRequests>,
|
|
673
664
|
);
|
|
674
665
|
|
|
675
666
|
// Assert
|
|
@@ -692,14 +683,11 @@ describe("Data", () => {
|
|
|
692
683
|
// Act
|
|
693
684
|
ReactDOMServer.renderToString(
|
|
694
685
|
<TrackData>
|
|
695
|
-
<
|
|
696
|
-
requestId="ID"
|
|
697
|
-
handler={interceptedHandler}
|
|
698
|
-
>
|
|
686
|
+
<InterceptRequests interceptor={interceptedHandler}>
|
|
699
687
|
<Data handler={fakeHandler} requestId="ID">
|
|
700
688
|
{fakeChildrenFn}
|
|
701
689
|
</Data>
|
|
702
|
-
</
|
|
690
|
+
</InterceptRequests>
|
|
703
691
|
</TrackData>,
|
|
704
692
|
);
|
|
705
693
|
|
|
@@ -817,14 +805,11 @@ describe("Data", () => {
|
|
|
817
805
|
|
|
818
806
|
// Act
|
|
819
807
|
ReactDOMServer.renderToString(
|
|
820
|
-
<
|
|
821
|
-
handler={interceptHandler}
|
|
822
|
-
requestId="ID"
|
|
823
|
-
>
|
|
808
|
+
<InterceptRequests interceptor={interceptHandler}>
|
|
824
809
|
<Data handler={fakeHandler} requestId="ID">
|
|
825
810
|
{fakeChildrenFn}
|
|
826
811
|
</Data>
|
|
827
|
-
</
|
|
812
|
+
</InterceptRequests>,
|
|
828
813
|
);
|
|
829
814
|
|
|
830
815
|
// Assert
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import {render} from "@testing-library/react";
|
|
4
|
+
|
|
5
|
+
import InterceptContext from "../intercept-context.js";
|
|
6
|
+
import InterceptRequests from "../intercept-requests.js";
|
|
7
|
+
|
|
8
|
+
describe("InterceptRequests", () => {
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
jest.resetAllMocks();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("should update context with fulfillRequest method", () => {
|
|
14
|
+
// Arrange
|
|
15
|
+
const fakeHandler = (requestId): Promise<string> =>
|
|
16
|
+
Promise.resolve("data");
|
|
17
|
+
const props = {
|
|
18
|
+
interceptor: fakeHandler,
|
|
19
|
+
};
|
|
20
|
+
const captureContextFn = jest.fn();
|
|
21
|
+
|
|
22
|
+
// Act
|
|
23
|
+
render(
|
|
24
|
+
<InterceptRequests {...props}>
|
|
25
|
+
<InterceptContext.Consumer>
|
|
26
|
+
{captureContextFn}
|
|
27
|
+
</InterceptContext.Consumer>
|
|
28
|
+
</InterceptRequests>,
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
// Assert
|
|
32
|
+
expect(captureContextFn).toHaveBeenCalledWith([fakeHandler]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("should override parent InterceptRequests", () => {
|
|
36
|
+
// Arrange
|
|
37
|
+
const fakeHandler1 = jest.fn();
|
|
38
|
+
const fakeHandler2 = jest.fn();
|
|
39
|
+
const captureContextFn = jest.fn();
|
|
40
|
+
|
|
41
|
+
// Act
|
|
42
|
+
render(
|
|
43
|
+
<InterceptRequests interceptor={fakeHandler1}>
|
|
44
|
+
<InterceptRequests interceptor={fakeHandler2}>
|
|
45
|
+
<InterceptContext.Consumer>
|
|
46
|
+
{captureContextFn}
|
|
47
|
+
</InterceptContext.Consumer>
|
|
48
|
+
</InterceptRequests>
|
|
49
|
+
</InterceptRequests>,
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
// Assert
|
|
53
|
+
expect(captureContextFn).toHaveBeenCalledWith([
|
|
54
|
+
fakeHandler1,
|
|
55
|
+
fakeHandler2,
|
|
56
|
+
]);
|
|
57
|
+
});
|
|
58
|
+
});
|
package/src/components/data.js
CHANGED
|
@@ -3,8 +3,8 @@ import * as React from "react";
|
|
|
3
3
|
|
|
4
4
|
import {Server} from "@khanacademy/wonder-blocks-core";
|
|
5
5
|
import {RequestFulfillment} from "../util/request-fulfillment.js";
|
|
6
|
-
import InterceptContext from "./intercept-context.js";
|
|
7
6
|
import {useServerEffect} from "../hooks/use-server-effect.js";
|
|
7
|
+
import {useRequestInterception} from "../hooks/use-request-interception.js";
|
|
8
8
|
import {resultFromCachedResponse} from "../util/result-from-cache-response.js";
|
|
9
9
|
|
|
10
10
|
import type {Result, ValidCacheData} from "../util/types.js";
|
|
@@ -80,25 +80,11 @@ const Data = <TData: ValidCacheData>({
|
|
|
80
80
|
showOldDataWhileLoading,
|
|
81
81
|
alwaysRequestOnHydration,
|
|
82
82
|
}: Props<TData>): React.Node => {
|
|
83
|
-
|
|
84
|
-
// If we have one, we need to replace the handler with one that
|
|
85
|
-
// uses the interceptor.
|
|
86
|
-
const interceptorMap = React.useContext(InterceptContext);
|
|
87
|
-
|
|
88
|
-
// If we have an interceptor, we need to replace the handler with one
|
|
89
|
-
// that uses the interceptor. This helper function generates a new
|
|
90
|
-
// handler.
|
|
91
|
-
const maybeInterceptedHandler = React.useMemo(() => {
|
|
92
|
-
const interceptor = interceptorMap[requestId];
|
|
93
|
-
if (interceptor == null) {
|
|
94
|
-
return handler;
|
|
95
|
-
}
|
|
96
|
-
return () => interceptor() ?? handler();
|
|
97
|
-
}, [handler, interceptorMap, requestId]);
|
|
83
|
+
const interceptedHandler = useRequestInterception(requestId, handler);
|
|
98
84
|
|
|
99
85
|
const hydrateResult = useServerEffect(
|
|
100
86
|
requestId,
|
|
101
|
-
|
|
87
|
+
interceptedHandler,
|
|
102
88
|
hydrate,
|
|
103
89
|
);
|
|
104
90
|
const [currentResult, setResult] = React.useState(hydrateResult);
|
|
@@ -137,7 +123,7 @@ const Data = <TData: ValidCacheData>({
|
|
|
137
123
|
// hydrated value.
|
|
138
124
|
let cancel = false;
|
|
139
125
|
RequestFulfillment.Default.fulfill(requestId, {
|
|
140
|
-
handler:
|
|
126
|
+
handler: interceptedHandler,
|
|
141
127
|
})
|
|
142
128
|
.then((result) => {
|
|
143
129
|
if (cancel) {
|
|
@@ -2,10 +2,9 @@
|
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
import type {ValidCacheData} from "../util/types.js";
|
|
4
4
|
|
|
5
|
-
type InterceptContextData =
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
};
|
|
5
|
+
type InterceptContextData = $ReadOnlyArray<
|
|
6
|
+
(requestId: string) => ?Promise<?ValidCacheData>,
|
|
7
|
+
>;
|
|
9
8
|
|
|
10
9
|
/**
|
|
11
10
|
* InterceptContext defines a map from request ID to interception methods.
|
|
@@ -13,6 +12,6 @@ type InterceptContextData = {
|
|
|
13
12
|
* INTERNAL USE ONLY
|
|
14
13
|
*/
|
|
15
14
|
const InterceptContext: React.Context<InterceptContextData> =
|
|
16
|
-
React.createContext<InterceptContextData>(
|
|
15
|
+
React.createContext<InterceptContextData>([]);
|
|
17
16
|
|
|
18
17
|
export default InterceptContext;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
|
|
4
|
+
import InterceptContext from "./intercept-context.js";
|
|
5
|
+
|
|
6
|
+
import type {ValidCacheData} from "../util/types.js";
|
|
7
|
+
|
|
8
|
+
type Props<TData: ValidCacheData> = {|
|
|
9
|
+
/**
|
|
10
|
+
* Called to intercept and possibly handle the request.
|
|
11
|
+
* If this returns null, the request will be handled by ancestor
|
|
12
|
+
* any ancestor interceptors, and ultimately, the original request
|
|
13
|
+
* handler, otherwise, this interceptor is handling the request.
|
|
14
|
+
*
|
|
15
|
+
* Interceptors are called in ancestor precedence, with the closest
|
|
16
|
+
* interceptor ancestor being called first, and the furthest ancestor
|
|
17
|
+
* being called last.
|
|
18
|
+
*
|
|
19
|
+
* Beware: Interceptors do not care about what data they are intercepting,
|
|
20
|
+
* so make sure to only intercept requests that you recognize from the
|
|
21
|
+
* identifier.
|
|
22
|
+
*/
|
|
23
|
+
interceptor: (requestId: string) => ?Promise<?TData>,
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The children to render within this component. Any requests by `Data`
|
|
27
|
+
* components that use same ID as this component will be intercepted.
|
|
28
|
+
* If `InterceptRequests` is used within `children`, that interception will
|
|
29
|
+
* be given a chance to intercept first.
|
|
30
|
+
*/
|
|
31
|
+
children: React.Node,
|
|
32
|
+
|};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* This component provides a mechanism to intercept data requests.
|
|
36
|
+
* This is for use in testing.
|
|
37
|
+
*
|
|
38
|
+
* This component is not recommended for use in production code as it
|
|
39
|
+
* can prevent predictable functioning of the Wonder Blocks Data framework.
|
|
40
|
+
* One possible side-effect is that inflight requests from the interceptor could
|
|
41
|
+
* be picked up by `Data` component requests from outside the children of this
|
|
42
|
+
* component.
|
|
43
|
+
*
|
|
44
|
+
* Interceptions within the same component tree are chained such that the
|
|
45
|
+
* interceptor closest to the intercepted request is called first, and the
|
|
46
|
+
* furthest interceptor is called last.
|
|
47
|
+
*/
|
|
48
|
+
const InterceptRequests = <TData: ValidCacheData>({
|
|
49
|
+
interceptor,
|
|
50
|
+
children,
|
|
51
|
+
}: Props<TData>): React.Node => {
|
|
52
|
+
const interceptors = React.useContext(InterceptContext);
|
|
53
|
+
|
|
54
|
+
const updatedInterceptors = React.useMemo(
|
|
55
|
+
// We could build this in reverse order so that our hook that does
|
|
56
|
+
// the interception didn't have to use reduceRight, but I think it
|
|
57
|
+
// is easier to think about if we do this in component tree order.
|
|
58
|
+
() => [...interceptors, interceptor],
|
|
59
|
+
[interceptors, interceptor],
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<InterceptContext.Provider value={updatedInterceptors}>
|
|
64
|
+
{children}
|
|
65
|
+
</InterceptContext.Provider>
|
|
66
|
+
);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export default InterceptRequests;
|
|
@@ -1,38 +1,41 @@
|
|
|
1
1
|
When you want to generate tests that check the loading state and
|
|
2
2
|
subsequent loaded state are working correctly for your uses of `Data` you can
|
|
3
|
-
use the `
|
|
3
|
+
use the `InterceptRequests` component. You can also use this component to
|
|
4
|
+
register request interceptors for any code that uses the `useRequestInterception`
|
|
5
|
+
hook.
|
|
4
6
|
|
|
5
|
-
This component takes
|
|
6
|
-
to fulfill the request, and the id of the request that is being intercepted.
|
|
7
|
+
This component takes the children to be rendered, and an interceptor function.
|
|
7
8
|
|
|
8
|
-
Note that this component is expected to be used only within test cases
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
Note that this component is expected to be used only within test cases or
|
|
10
|
+
stories. Be careful want request IDs are matched to avoid intercepting the
|
|
11
|
+
wrong requests and remember that in-flight requests for a given request ID
|
|
12
|
+
can be shared - which means a bad request ID match could share requests across
|
|
13
|
+
different request IDs..
|
|
12
14
|
|
|
13
|
-
The `
|
|
15
|
+
The `interceptor` intercept function has the form:
|
|
14
16
|
|
|
15
17
|
```js static
|
|
16
|
-
() => ?Promise<?TData>;
|
|
18
|
+
(requestId: string) => ?Promise<?TData>;
|
|
17
19
|
```
|
|
18
20
|
|
|
19
|
-
If this method returns `null`, the
|
|
21
|
+
If this method returns `null`, then the next interceptor in the chain is
|
|
22
|
+
invoked, ultimately ending with the original handler. This
|
|
20
23
|
means that a request will be made for data via the handler assigned to the
|
|
21
|
-
`Data` component being intercepted.
|
|
24
|
+
`Data` component being intercepted if no interceptor handles the request first.
|
|
22
25
|
|
|
23
26
|
```jsx
|
|
24
27
|
import {Body, BodyMonospace} from "@khanacademy/wonder-blocks-typography";
|
|
25
28
|
import {View} from "@khanacademy/wonder-blocks-core";
|
|
26
|
-
import {
|
|
29
|
+
import {InterceptRequests, Data} from "@khanacademy/wonder-blocks-data";
|
|
27
30
|
import {Strut} from "@khanacademy/wonder-blocks-layout";
|
|
28
31
|
import Color from "@khanacademy/wonder-blocks-color";
|
|
29
32
|
import Spacing from "@khanacademy/wonder-blocks-spacing";
|
|
30
33
|
|
|
31
34
|
const myHandler = () => Promise.reject(new Error("You should not see this!"));
|
|
32
35
|
|
|
33
|
-
const
|
|
36
|
+
const interceptor = (requestId) => requestId === "INTERCEPT_EXAMPLE" ? Promise.resolve("INTERCEPTED DATA!") : null;
|
|
34
37
|
|
|
35
|
-
<
|
|
38
|
+
<InterceptRequests interceptor={interceptor}>
|
|
36
39
|
<View>
|
|
37
40
|
<Body>This received intercepted data!</Body>
|
|
38
41
|
<Data handler={myHandler} requestId="INTERCEPT_EXAMPLE">
|
|
@@ -47,5 +50,5 @@ const interceptHandler = () => Promise.resolve("INTERCEPTED DATA!");
|
|
|
47
50
|
}}
|
|
48
51
|
</Data>
|
|
49
52
|
</View>
|
|
50
|
-
</
|
|
53
|
+
</InterceptRequests>
|
|
51
54
|
```
|
|
@@ -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,54 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
|
|
4
|
+
import InterceptContext from "../components/intercept-context.js";
|
|
5
|
+
import type {ValidCacheData} from "../util/types.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Allow request handling to be intercepted.
|
|
9
|
+
*
|
|
10
|
+
* Hook to take a uniquely identified request handler and return a
|
|
11
|
+
* method that will support request interception from the InterceptRequest
|
|
12
|
+
* component.
|
|
13
|
+
*
|
|
14
|
+
* If you want request interception to be supported with `useServerEffect` or
|
|
15
|
+
* any client-side effect that uses the handler, call this first to generate
|
|
16
|
+
* an intercepted handler, and then invoke `useServerEffect` (or other things)
|
|
17
|
+
* with that intercepted handler.
|
|
18
|
+
*/
|
|
19
|
+
export const useRequestInterception = <TData: ValidCacheData>(
|
|
20
|
+
requestId: string,
|
|
21
|
+
handler: () => Promise<?TData>,
|
|
22
|
+
): (() => Promise<?TData>) => {
|
|
23
|
+
// Get the interceptors that have been registered.
|
|
24
|
+
const interceptors = React.useContext(InterceptContext);
|
|
25
|
+
|
|
26
|
+
// Now, we need to create a new handler that will check if the
|
|
27
|
+
// request is intercepted before ultimately calling the original handler
|
|
28
|
+
// if nothing intercepted it.
|
|
29
|
+
// We memoize this so that it only changes if something related to it
|
|
30
|
+
// changes.
|
|
31
|
+
const interceptedHandler = React.useMemo(
|
|
32
|
+
() => (): Promise<?TData> => {
|
|
33
|
+
// Call the interceptors from closest to furthest.
|
|
34
|
+
// If one returns a non-null result, then we keep that.
|
|
35
|
+
const interceptResponse = interceptors.reduceRight(
|
|
36
|
+
(prev, interceptor) => {
|
|
37
|
+
if (prev != null) {
|
|
38
|
+
return prev;
|
|
39
|
+
}
|
|
40
|
+
return interceptor(requestId);
|
|
41
|
+
},
|
|
42
|
+
null,
|
|
43
|
+
);
|
|
44
|
+
// If nothing intercepted this request, invoke the original handler.
|
|
45
|
+
// NOTE: We can't guarantee all interceptors return the same type
|
|
46
|
+
// as our handler, so how can flow know? Let's just suppress that.
|
|
47
|
+
// $FlowFixMe[incompatible-return]
|
|
48
|
+
return interceptResponse ?? handler();
|
|
49
|
+
},
|
|
50
|
+
[handler, interceptors, requestId],
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
return interceptedHandler;
|
|
54
|
+
};
|