@khanacademy/wonder-blocks-data 3.0.0 → 3.1.2
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 +28 -2
- package/dist/es/index.js +204 -31
- package/dist/index.js +315 -70
- package/package.json +4 -3
- package/src/components/__tests__/gql-router.test.js +64 -0
- package/src/components/gql-router.js +66 -0
- package/src/hooks/__tests__/use-data.test.js +142 -106
- package/src/hooks/__tests__/use-gql.test.js +233 -0
- package/src/hooks/use-data.js +28 -23
- package/src/hooks/use-gql.js +77 -0
- package/src/index.js +12 -6
- package/src/util/__tests__/get-gql-data-from-response.test.js +187 -0
- package/src/util/get-gql-data-from-response.js +69 -0
- package/src/util/gql-error.js +36 -0
- package/src/util/gql-router-context.js +6 -0
- package/src/util/gql-types.js +65 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
|
|
4
|
+
import {GqlRouterContext} from "../util/gql-router-context.js";
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
GqlContext,
|
|
8
|
+
GqlFetchFn,
|
|
9
|
+
GqlRouterConfiguration,
|
|
10
|
+
} from "../util/gql-types.js";
|
|
11
|
+
|
|
12
|
+
type Props<TContext: GqlContext> = {|
|
|
13
|
+
/**
|
|
14
|
+
* The default context to be used by operations when no context is provided.
|
|
15
|
+
*/
|
|
16
|
+
defaultContext: TContext,
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The function to use when fetching requests.
|
|
20
|
+
*/
|
|
21
|
+
fetch: GqlFetchFn<any, any, any, TContext>,
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The children to be rendered inside the router.
|
|
25
|
+
*/
|
|
26
|
+
children: React.Node,
|
|
27
|
+
|};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Configure GraphQL routing for GraphQL hooks and components.
|
|
31
|
+
*
|
|
32
|
+
* These can be nested. Components and hooks relying on the GraphQL routing
|
|
33
|
+
* will use the configuration from their closest ancestral GqlRouter.
|
|
34
|
+
*/
|
|
35
|
+
export const GqlRouter = <TContext: GqlContext>({
|
|
36
|
+
defaultContext: thisDefaultContext,
|
|
37
|
+
fetch: thisFetch,
|
|
38
|
+
children,
|
|
39
|
+
}: Props<TContext>): React.Node => {
|
|
40
|
+
// We don't care if we're nested. We always force our callers to define
|
|
41
|
+
// everything. It makes for a clearer API and requires less error checking
|
|
42
|
+
// code (assuming our flow types are correct). We also don't default fetch
|
|
43
|
+
// to anything - our callers can tell us what function to use quite easily.
|
|
44
|
+
// If code that consumes this wants more nuanced nesting, it can implement
|
|
45
|
+
// it within its own GqlRouter than then defers to this one.
|
|
46
|
+
|
|
47
|
+
// We want to always use the same object if things haven't changed to avoid
|
|
48
|
+
// over-rendering consumers of our context, let's memoize the configuration.
|
|
49
|
+
// By doing this, if a component under children that uses this context
|
|
50
|
+
// uses React.memo, we won't force it to re-render every time we render
|
|
51
|
+
// because we'll only change the context value if something has actually
|
|
52
|
+
// changed.
|
|
53
|
+
const configuration: GqlRouterConfiguration<TContext> = React.useMemo(
|
|
54
|
+
() => ({
|
|
55
|
+
fetch: thisFetch,
|
|
56
|
+
defaultContext: thisDefaultContext,
|
|
57
|
+
}),
|
|
58
|
+
[thisDefaultContext, thisFetch],
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<GqlRouterContext.Provider value={configuration}>
|
|
63
|
+
{children}
|
|
64
|
+
</GqlRouterContext.Provider>
|
|
65
|
+
);
|
|
66
|
+
};
|
|
@@ -155,6 +155,42 @@ describe("#useData", () => {
|
|
|
155
155
|
error: "ERROR",
|
|
156
156
|
});
|
|
157
157
|
});
|
|
158
|
+
|
|
159
|
+
it("should track the intercepted request", async () => {
|
|
160
|
+
// Arrange
|
|
161
|
+
const intercepted = Promise.resolve("INTERCEPTED");
|
|
162
|
+
const notIntercepted = Promise.resolve("NOT INTERCEPTED");
|
|
163
|
+
const fakeHandler: IRequestHandler<string, string> = {
|
|
164
|
+
fulfillRequest: jest.fn().mockReturnValue(notIntercepted),
|
|
165
|
+
getKey: (o) => o,
|
|
166
|
+
type: "MY_HANDLER",
|
|
167
|
+
hydrate: true,
|
|
168
|
+
};
|
|
169
|
+
const trackDataRequestSpy = jest.spyOn(
|
|
170
|
+
RequestTracker.Default,
|
|
171
|
+
"trackDataRequest",
|
|
172
|
+
);
|
|
173
|
+
const wrapper = ({children}) => (
|
|
174
|
+
<TrackData>
|
|
175
|
+
<InterceptData
|
|
176
|
+
fulfillRequest={() => intercepted}
|
|
177
|
+
handler={fakeHandler}
|
|
178
|
+
>
|
|
179
|
+
{children}
|
|
180
|
+
</InterceptData>
|
|
181
|
+
</TrackData>
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
// Act
|
|
185
|
+
serverRenderHook(() => useData(fakeHandler, "options"), {
|
|
186
|
+
wrapper,
|
|
187
|
+
});
|
|
188
|
+
const trackedHandler = trackDataRequestSpy.mock.calls[0][0];
|
|
189
|
+
const result = await trackedHandler.fulfillRequest();
|
|
190
|
+
|
|
191
|
+
// Assert
|
|
192
|
+
expect(result).toBe("INTERCEPTED");
|
|
193
|
+
});
|
|
158
194
|
});
|
|
159
195
|
|
|
160
196
|
describe("when client-side", () => {
|
|
@@ -668,122 +704,122 @@ describe("#useData", () => {
|
|
|
668
704
|
data: "DATA",
|
|
669
705
|
});
|
|
670
706
|
});
|
|
671
|
-
});
|
|
672
707
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
708
|
+
describe("with interceptor", () => {
|
|
709
|
+
it("should return the result of the interceptor request resolution", async () => {
|
|
710
|
+
// Arrange
|
|
711
|
+
const intercepted = Promise.resolve("INTERCEPTED");
|
|
712
|
+
const notIntercepted = Promise.resolve("NOT INTERCEPTED");
|
|
713
|
+
const fakeHandler: IRequestHandler<string, string> = {
|
|
714
|
+
fulfillRequest: jest.fn().mockReturnValue(notIntercepted),
|
|
715
|
+
getKey: (o) => o,
|
|
716
|
+
type: "MY_HANDLER",
|
|
717
|
+
hydrate: true,
|
|
718
|
+
};
|
|
719
|
+
const wrapper = ({children}) => (
|
|
720
|
+
<InterceptData
|
|
721
|
+
fulfillRequest={() => intercepted}
|
|
722
|
+
handler={fakeHandler}
|
|
723
|
+
>
|
|
724
|
+
{children}
|
|
725
|
+
</InterceptData>
|
|
726
|
+
);
|
|
692
727
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
728
|
+
// Act
|
|
729
|
+
const render = clientRenderHook(
|
|
730
|
+
() => useData(fakeHandler, "options"),
|
|
731
|
+
{
|
|
732
|
+
wrapper,
|
|
733
|
+
},
|
|
734
|
+
);
|
|
735
|
+
await act((): Promise<mixed> =>
|
|
736
|
+
Promise.all([notIntercepted, intercepted]),
|
|
737
|
+
);
|
|
738
|
+
const result = render.result.current;
|
|
704
739
|
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
740
|
+
// Assert
|
|
741
|
+
expect(result).toEqual({
|
|
742
|
+
status: "success",
|
|
743
|
+
data: "INTERCEPTED",
|
|
744
|
+
});
|
|
709
745
|
});
|
|
710
|
-
});
|
|
711
|
-
|
|
712
|
-
it("should return the result of the interceptor request rejection", async () => {
|
|
713
|
-
// Arrange
|
|
714
|
-
const intercepted = Promise.reject("INTERCEPTED");
|
|
715
|
-
const notIntercepted = Promise.resolve("NOT INTERCEPTED");
|
|
716
|
-
const fakeHandler: IRequestHandler<string, string> = {
|
|
717
|
-
fulfillRequest: jest.fn().mockReturnValue(notIntercepted),
|
|
718
|
-
getKey: (o) => o,
|
|
719
|
-
type: "MY_HANDLER",
|
|
720
|
-
hydrate: true,
|
|
721
|
-
};
|
|
722
|
-
const wrapper = ({children}) => (
|
|
723
|
-
<InterceptData
|
|
724
|
-
fulfillRequest={() => intercepted}
|
|
725
|
-
handler={fakeHandler}
|
|
726
|
-
>
|
|
727
|
-
{children}
|
|
728
|
-
</InterceptData>
|
|
729
|
-
);
|
|
730
746
|
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
+
it("should return the result of the interceptor request rejection", async () => {
|
|
748
|
+
// Arrange
|
|
749
|
+
const intercepted = Promise.reject("INTERCEPTED");
|
|
750
|
+
const notIntercepted = Promise.resolve("NOT INTERCEPTED");
|
|
751
|
+
const fakeHandler: IRequestHandler<string, string> = {
|
|
752
|
+
fulfillRequest: jest.fn().mockReturnValue(notIntercepted),
|
|
753
|
+
getKey: (o) => o,
|
|
754
|
+
type: "MY_HANDLER",
|
|
755
|
+
hydrate: true,
|
|
756
|
+
};
|
|
757
|
+
const wrapper = ({children}) => (
|
|
758
|
+
<InterceptData
|
|
759
|
+
fulfillRequest={() => intercepted}
|
|
760
|
+
handler={fakeHandler}
|
|
761
|
+
>
|
|
762
|
+
{children}
|
|
763
|
+
</InterceptData>
|
|
764
|
+
);
|
|
747
765
|
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
766
|
+
// Act
|
|
767
|
+
const render = clientRenderHook(
|
|
768
|
+
() => useData(fakeHandler, "options"),
|
|
769
|
+
{
|
|
770
|
+
wrapper,
|
|
771
|
+
},
|
|
772
|
+
);
|
|
773
|
+
await notIntercepted;
|
|
774
|
+
await act(async (): Promise<mixed> => {
|
|
775
|
+
try {
|
|
776
|
+
await intercepted;
|
|
777
|
+
} catch (e) {
|
|
778
|
+
/* ignore, it's ok */
|
|
779
|
+
}
|
|
780
|
+
});
|
|
781
|
+
const result = render.result.current;
|
|
782
|
+
|
|
783
|
+
// Assert
|
|
784
|
+
expect(result).toEqual({
|
|
785
|
+
status: "error",
|
|
786
|
+
error: "INTERCEPTED",
|
|
787
|
+
});
|
|
752
788
|
});
|
|
753
|
-
});
|
|
754
789
|
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
790
|
+
it("should return the result of the handler if the interceptor returns null", async () => {
|
|
791
|
+
// Arrange
|
|
792
|
+
const notIntercepted = Promise.resolve("NOT INTERCEPTED");
|
|
793
|
+
const fakeHandler: IRequestHandler<string, string> = {
|
|
794
|
+
fulfillRequest: jest.fn().mockReturnValue(notIntercepted),
|
|
795
|
+
getKey: (o) => o,
|
|
796
|
+
type: "MY_HANDLER",
|
|
797
|
+
hydrate: true,
|
|
798
|
+
};
|
|
799
|
+
const wrapper = ({children}) => (
|
|
800
|
+
<InterceptData
|
|
801
|
+
fulfillRequest={() => null}
|
|
802
|
+
handler={fakeHandler}
|
|
803
|
+
>
|
|
804
|
+
{children}
|
|
805
|
+
</InterceptData>
|
|
806
|
+
);
|
|
772
807
|
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
808
|
+
// Act
|
|
809
|
+
const render = clientRenderHook(
|
|
810
|
+
() => useData(fakeHandler, "options"),
|
|
811
|
+
{
|
|
812
|
+
wrapper,
|
|
813
|
+
},
|
|
814
|
+
);
|
|
815
|
+
await act((): Promise<mixed> => notIntercepted);
|
|
816
|
+
const result = render.result.current;
|
|
782
817
|
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
818
|
+
// Assert
|
|
819
|
+
expect(result).toEqual({
|
|
820
|
+
status: "success",
|
|
821
|
+
data: "NOT INTERCEPTED",
|
|
822
|
+
});
|
|
787
823
|
});
|
|
788
824
|
});
|
|
789
825
|
});
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import {renderHook} from "@testing-library/react-hooks";
|
|
4
|
+
|
|
5
|
+
import * as GetGqlDataFromResponse from "../../util/get-gql-data-from-response.js";
|
|
6
|
+
import {GqlRouterContext} from "../../util/gql-router-context.js";
|
|
7
|
+
import {useGql} from "../use-gql.js";
|
|
8
|
+
|
|
9
|
+
describe("#useGql", () => {
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
jest.resetAllMocks();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("should throw if there is no GqlRouterContext available", () => {
|
|
15
|
+
// Arrange
|
|
16
|
+
|
|
17
|
+
// Act
|
|
18
|
+
const {
|
|
19
|
+
result: {error: result},
|
|
20
|
+
} = renderHook(() => useGql());
|
|
21
|
+
|
|
22
|
+
// Assert
|
|
23
|
+
expect(result).toMatchInlineSnapshot(
|
|
24
|
+
`[GqlInternalError: No GqlRouter]`,
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("should return a function", () => {
|
|
29
|
+
// Arrange
|
|
30
|
+
const gqlRouterContext = {
|
|
31
|
+
fetch: jest.fn(),
|
|
32
|
+
defaultContext: {},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Act
|
|
36
|
+
const {
|
|
37
|
+
result: {current: result},
|
|
38
|
+
} = renderHook(() => useGql(), {
|
|
39
|
+
wrapper: ({children}) => (
|
|
40
|
+
<GqlRouterContext.Provider value={gqlRouterContext}>
|
|
41
|
+
{children}
|
|
42
|
+
</GqlRouterContext.Provider>
|
|
43
|
+
),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Assert
|
|
47
|
+
expect(result).toBeInstanceOf(Function);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe("returned gqlFetch", () => {
|
|
51
|
+
it("should fetch the operation with combined context", async () => {
|
|
52
|
+
// Arrange
|
|
53
|
+
jest.spyOn(
|
|
54
|
+
GetGqlDataFromResponse,
|
|
55
|
+
"getGqlDataFromResponse",
|
|
56
|
+
).mockResolvedValue({
|
|
57
|
+
some: "data",
|
|
58
|
+
});
|
|
59
|
+
const fetchFake = jest
|
|
60
|
+
.fn()
|
|
61
|
+
.mockResolvedValue(("FAKE_RESPONSE": any));
|
|
62
|
+
const gqlRouterContext = {
|
|
63
|
+
fetch: fetchFake,
|
|
64
|
+
defaultContext: {
|
|
65
|
+
a: "defaultA",
|
|
66
|
+
b: "defaultB",
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
const {
|
|
70
|
+
result: {current: gqlFetch},
|
|
71
|
+
} = renderHook(() => useGql(), {
|
|
72
|
+
wrapper: ({children}) => (
|
|
73
|
+
<GqlRouterContext.Provider value={gqlRouterContext}>
|
|
74
|
+
{children}
|
|
75
|
+
</GqlRouterContext.Provider>
|
|
76
|
+
),
|
|
77
|
+
});
|
|
78
|
+
const gqlOp = {
|
|
79
|
+
type: "query",
|
|
80
|
+
id: "MyQuery",
|
|
81
|
+
};
|
|
82
|
+
const gqlOpContext = {
|
|
83
|
+
b: "overrideB",
|
|
84
|
+
};
|
|
85
|
+
const gqlOpVariables = {
|
|
86
|
+
var1: "val1",
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// Act
|
|
90
|
+
await gqlFetch(gqlOp, {
|
|
91
|
+
context: gqlOpContext,
|
|
92
|
+
variables: gqlOpVariables,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Assert
|
|
96
|
+
expect(fetchFake).toHaveBeenCalledWith(gqlOp, gqlOpVariables, {
|
|
97
|
+
a: "defaultA",
|
|
98
|
+
b: "overrideB",
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("should parse the response", async () => {
|
|
103
|
+
// Arrange
|
|
104
|
+
const getGqlDataFromResponseSpy = jest
|
|
105
|
+
.spyOn(GetGqlDataFromResponse, "getGqlDataFromResponse")
|
|
106
|
+
.mockResolvedValue({
|
|
107
|
+
some: "data",
|
|
108
|
+
});
|
|
109
|
+
const gqlRouterContext = {
|
|
110
|
+
fetch: jest.fn().mockResolvedValue(("FAKE_RESPONSE": any)),
|
|
111
|
+
defaultContext: {},
|
|
112
|
+
};
|
|
113
|
+
const {
|
|
114
|
+
result: {current: gqlFetch},
|
|
115
|
+
} = renderHook(() => useGql(), {
|
|
116
|
+
wrapper: ({children}) => (
|
|
117
|
+
<GqlRouterContext.Provider value={gqlRouterContext}>
|
|
118
|
+
{children}
|
|
119
|
+
</GqlRouterContext.Provider>
|
|
120
|
+
),
|
|
121
|
+
});
|
|
122
|
+
const gqlOp = {
|
|
123
|
+
type: "query",
|
|
124
|
+
id: "MyQuery",
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// Act
|
|
128
|
+
await gqlFetch(gqlOp);
|
|
129
|
+
|
|
130
|
+
// Assert
|
|
131
|
+
expect(getGqlDataFromResponseSpy).toHaveBeenCalledWith(
|
|
132
|
+
"FAKE_RESPONSE",
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("should reject if the response parse rejects", async () => {
|
|
137
|
+
// Arrange
|
|
138
|
+
jest.spyOn(
|
|
139
|
+
GetGqlDataFromResponse,
|
|
140
|
+
"getGqlDataFromResponse",
|
|
141
|
+
).mockRejectedValue(new Error("FAKE_ERROR"));
|
|
142
|
+
const gqlRouterContext = {
|
|
143
|
+
fetch: jest.fn().mockResolvedValue(("FAKE_RESPONSE": any)),
|
|
144
|
+
defaultContext: {},
|
|
145
|
+
};
|
|
146
|
+
const {
|
|
147
|
+
result: {current: gqlFetch},
|
|
148
|
+
} = renderHook(() => useGql(), {
|
|
149
|
+
wrapper: ({children}) => (
|
|
150
|
+
<GqlRouterContext.Provider value={gqlRouterContext}>
|
|
151
|
+
{children}
|
|
152
|
+
</GqlRouterContext.Provider>
|
|
153
|
+
),
|
|
154
|
+
});
|
|
155
|
+
const gqlOp = {
|
|
156
|
+
type: "query",
|
|
157
|
+
id: "MyQuery",
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// Act
|
|
161
|
+
const act = gqlFetch(gqlOp);
|
|
162
|
+
|
|
163
|
+
// Assert
|
|
164
|
+
await expect(act).rejects.toThrowErrorMatchingInlineSnapshot(
|
|
165
|
+
`"FAKE_ERROR"`,
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("should resolve to null if the fetch was aborted", async () => {
|
|
170
|
+
// Arrange
|
|
171
|
+
const abortError = new Error("Aborted");
|
|
172
|
+
abortError.name = "AbortError";
|
|
173
|
+
const gqlRouterContext = {
|
|
174
|
+
fetch: jest.fn().mockRejectedValue(abortError),
|
|
175
|
+
defaultContext: {},
|
|
176
|
+
};
|
|
177
|
+
const {
|
|
178
|
+
result: {current: gqlFetch},
|
|
179
|
+
} = renderHook(() => useGql(), {
|
|
180
|
+
wrapper: ({children}) => (
|
|
181
|
+
<GqlRouterContext.Provider value={gqlRouterContext}>
|
|
182
|
+
{children}
|
|
183
|
+
</GqlRouterContext.Provider>
|
|
184
|
+
),
|
|
185
|
+
});
|
|
186
|
+
const gqlOp = {
|
|
187
|
+
type: "query",
|
|
188
|
+
id: "MyQuery",
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// Act
|
|
192
|
+
const result = await gqlFetch(gqlOp);
|
|
193
|
+
|
|
194
|
+
// Assert
|
|
195
|
+
expect(result).toBeNull();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("should resolve to the response data", async () => {
|
|
199
|
+
// Arrange
|
|
200
|
+
jest.spyOn(
|
|
201
|
+
GetGqlDataFromResponse,
|
|
202
|
+
"getGqlDataFromResponse",
|
|
203
|
+
).mockResolvedValue({
|
|
204
|
+
some: "data",
|
|
205
|
+
});
|
|
206
|
+
const gqlRouterContext = {
|
|
207
|
+
fetch: jest.fn().mockResolvedValue(("FAKE_RESPONSE": any)),
|
|
208
|
+
defaultContext: {},
|
|
209
|
+
};
|
|
210
|
+
const {
|
|
211
|
+
result: {current: gqlFetch},
|
|
212
|
+
} = renderHook(() => useGql(), {
|
|
213
|
+
wrapper: ({children}) => (
|
|
214
|
+
<GqlRouterContext.Provider value={gqlRouterContext}>
|
|
215
|
+
{children}
|
|
216
|
+
</GqlRouterContext.Provider>
|
|
217
|
+
),
|
|
218
|
+
});
|
|
219
|
+
const gqlOp = {
|
|
220
|
+
type: "mutation",
|
|
221
|
+
id: "MyMutation",
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
// Act
|
|
225
|
+
const result = await gqlFetch(gqlOp);
|
|
226
|
+
|
|
227
|
+
// Assert
|
|
228
|
+
expect(result).toEqual({
|
|
229
|
+
some: "data",
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
});
|
package/src/hooks/use-data.js
CHANGED
|
@@ -29,20 +29,41 @@ export const useData = <TOptions, TData: ValidData>(
|
|
|
29
29
|
);
|
|
30
30
|
const [result, setResult] = useState<?CacheEntry<TData>>(cachedResult);
|
|
31
31
|
|
|
32
|
+
// Lookup to see if there's an interceptor for the handler.
|
|
33
|
+
// If we have one, we need to replace the handler with one that
|
|
34
|
+
// uses the interceptor.
|
|
35
|
+
const interceptorMap = useContext(InterceptContext);
|
|
36
|
+
const interceptor = interceptorMap[handler.type];
|
|
37
|
+
|
|
38
|
+
// If we have an interceptor, we need to replace the handler with one that
|
|
39
|
+
// uses the interceptor. This helper function generates a new handler.
|
|
40
|
+
// We need this before we track the request as we want the interceptor
|
|
41
|
+
// to also work for tracked requests to simplify testing the server-side
|
|
42
|
+
// request fulfillment.
|
|
43
|
+
const getMaybeInterceptedHandler = () => {
|
|
44
|
+
if (interceptor == null) {
|
|
45
|
+
return handler;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const fulfillRequestFn = (options) =>
|
|
49
|
+
interceptor.fulfillRequest(options) ??
|
|
50
|
+
handler.fulfillRequest(options);
|
|
51
|
+
return {
|
|
52
|
+
fulfillRequest: fulfillRequestFn,
|
|
53
|
+
getKey: (options) => handler.getKey(options),
|
|
54
|
+
type: handler.type,
|
|
55
|
+
hydrate: handler.hydrate,
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
|
|
32
59
|
// We only track data requests when we are server-side and we don't
|
|
33
60
|
// already have a result, as given by the cachedData (which is also the
|
|
34
61
|
// initial value for the result state).
|
|
35
62
|
const maybeTrack = useContext(TrackerContext);
|
|
36
63
|
if (result == null && Server.isServerSide()) {
|
|
37
|
-
maybeTrack?.(
|
|
64
|
+
maybeTrack?.(getMaybeInterceptedHandler(), options);
|
|
38
65
|
}
|
|
39
66
|
|
|
40
|
-
// Lookup to see if there's an interceptor for the handler.
|
|
41
|
-
// If we have one, we need to replace the handler with one that
|
|
42
|
-
// uses the interceptor.
|
|
43
|
-
const interceptorMap = useContext(InterceptContext);
|
|
44
|
-
const interceptor = interceptorMap[handler.type];
|
|
45
|
-
|
|
46
67
|
// We need to update our request when the handler changes or the key
|
|
47
68
|
// to the options change, so we keep track of those.
|
|
48
69
|
// However, even if we are hydrating from cache, we still need to make the
|
|
@@ -74,22 +95,6 @@ export const useData = <TOptions, TData: ValidData>(
|
|
|
74
95
|
setResult(null);
|
|
75
96
|
}
|
|
76
97
|
|
|
77
|
-
const getMaybeInterceptedHandler = () => {
|
|
78
|
-
if (interceptor == null) {
|
|
79
|
-
return handler;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const fulfillRequestFn = (options) =>
|
|
83
|
-
interceptor.fulfillRequest(options) ??
|
|
84
|
-
handler.fulfillRequest(options);
|
|
85
|
-
return {
|
|
86
|
-
fulfillRequest: fulfillRequestFn,
|
|
87
|
-
getKey: (options) => handler.getKey(options),
|
|
88
|
-
type: handler.type,
|
|
89
|
-
hydrate: handler.hydrate,
|
|
90
|
-
};
|
|
91
|
-
};
|
|
92
|
-
|
|
93
98
|
// We aren't server-side, so let's make the request.
|
|
94
99
|
// The request handler is in control of whether that request actually
|
|
95
100
|
// happens or not.
|