@khanacademy/wonder-blocks-data 3.0.1 → 3.1.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 +7 -0
- package/dist/es/index.js +167 -2
- package/dist/index.js +278 -41
- package/package.json +2 -1
- package/src/components/__tests__/gql-router.test.js +64 -0
- package/src/components/gql-router.js +66 -0
- package/src/hooks/__tests__/use-gql.test.js +233 -0
- package/src/hooks/use-gql.js +75 -0
- package/src/index.js +6 -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 +60 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@khanacademy/wonder-blocks-data",
|
|
3
|
-
"version": "3.0
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"design": "v1",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"@khanacademy/wonder-blocks-core": "^4.0.0"
|
|
18
18
|
},
|
|
19
19
|
"peerDependencies": {
|
|
20
|
+
"@khanacademy/wonder-stuff-core": "^0.1.2",
|
|
20
21
|
"react": "16.14.0"
|
|
21
22
|
},
|
|
22
23
|
"devDependencies": {
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import {render} from "@testing-library/react";
|
|
4
|
+
|
|
5
|
+
import {GqlRouterContext} from "../../util/gql-router-context.js";
|
|
6
|
+
import {GqlRouter} from "../gql-router.js";
|
|
7
|
+
|
|
8
|
+
describe("GqlRouter", () => {
|
|
9
|
+
it("should provide the GqlRouterContext as configured", async () => {
|
|
10
|
+
// Arrange
|
|
11
|
+
const defaultContext = {
|
|
12
|
+
foo: "bar",
|
|
13
|
+
};
|
|
14
|
+
const fetch = jest.fn();
|
|
15
|
+
const CaptureContext = ({captureFn}) => {
|
|
16
|
+
captureFn(React.useContext(GqlRouterContext));
|
|
17
|
+
return null;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// Act
|
|
21
|
+
const result = await new Promise((resolve, reject) => {
|
|
22
|
+
render(
|
|
23
|
+
<GqlRouter defaultContext={defaultContext} fetch={fetch}>
|
|
24
|
+
<CaptureContext captureFn={resolve} />
|
|
25
|
+
</GqlRouter>,
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Assert
|
|
30
|
+
expect(result).toStrictEqual({
|
|
31
|
+
defaultContext,
|
|
32
|
+
fetch,
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("should not render React.memo-ized children if props remain the same", () => {
|
|
37
|
+
// Arrange
|
|
38
|
+
const defaultContext = {
|
|
39
|
+
foo: "bar",
|
|
40
|
+
};
|
|
41
|
+
const fetch = jest.fn();
|
|
42
|
+
let renderCount = 0;
|
|
43
|
+
const Child = React.memo(() => {
|
|
44
|
+
const context = React.useContext(GqlRouterContext);
|
|
45
|
+
renderCount++;
|
|
46
|
+
return <div>{JSON.stringify(context)}</div>;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Act
|
|
50
|
+
const {rerender} = render(
|
|
51
|
+
<GqlRouter defaultContext={defaultContext} fetch={fetch}>
|
|
52
|
+
<Child />
|
|
53
|
+
</GqlRouter>,
|
|
54
|
+
);
|
|
55
|
+
rerender(
|
|
56
|
+
<GqlRouter defaultContext={defaultContext} fetch={fetch}>
|
|
57
|
+
<Child />
|
|
58
|
+
</GqlRouter>,
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
// Assert
|
|
62
|
+
expect(renderCount).toBe(1);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -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
|
+
FetchFn,
|
|
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: FetchFn<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
|
+
};
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import {useContext, useMemo} from "react";
|
|
3
|
+
|
|
4
|
+
import {GqlRouterContext} from "../util/gql-router-context.js";
|
|
5
|
+
import {getGqlDataFromResponse} from "../util/get-gql-data-from-response.js";
|
|
6
|
+
import {GqlError, GqlErrors} from "../util/gql-error.js";
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
GqlContext,
|
|
10
|
+
GqlOperation,
|
|
11
|
+
GqlFetchOptions,
|
|
12
|
+
GqlOperationType,
|
|
13
|
+
} from "../util/gql-types.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Hook to obtain a gqlFetch function for performing GraphQL requests.
|
|
17
|
+
*
|
|
18
|
+
* The fetch function will resolve null if the request was aborted, otherwise
|
|
19
|
+
* it will resolve the data returned by the GraphQL server.
|
|
20
|
+
*/
|
|
21
|
+
export const useGql = (): (<
|
|
22
|
+
TType: GqlOperationType,
|
|
23
|
+
TData,
|
|
24
|
+
TVariables: {...},
|
|
25
|
+
TContext: GqlContext,
|
|
26
|
+
>(
|
|
27
|
+
operation: GqlOperation<TType, TData, TVariables>,
|
|
28
|
+
options?: GqlFetchOptions<TVariables, TContext>,
|
|
29
|
+
) => Promise<?TData>) => {
|
|
30
|
+
// This hook only works if the `GqlRouter` has been used to setup context.
|
|
31
|
+
const gqlRouterContext = useContext(GqlRouterContext);
|
|
32
|
+
if (gqlRouterContext == null) {
|
|
33
|
+
throw new GqlError("No GqlRouter", GqlErrors.Internal);
|
|
34
|
+
}
|
|
35
|
+
const {fetch, defaultContext} = gqlRouterContext;
|
|
36
|
+
|
|
37
|
+
// Let's memoize the gqlFetch function we create based off our context.
|
|
38
|
+
// That way, even if the context happens to change, if its values don't
|
|
39
|
+
// we give the same function instance back to our callers instead of
|
|
40
|
+
// making a new one. That then means they can safely use the return value
|
|
41
|
+
// in hooks deps without fear of it triggering extra renders.
|
|
42
|
+
const gqlFetch = useMemo(
|
|
43
|
+
() =>
|
|
44
|
+
<
|
|
45
|
+
TType: GqlOperationType,
|
|
46
|
+
TData,
|
|
47
|
+
TVariables: {...},
|
|
48
|
+
TContext: GqlContext,
|
|
49
|
+
>(
|
|
50
|
+
operation: GqlOperation<TType, TData, TVariables>,
|
|
51
|
+
options: GqlFetchOptions<TVariables, TContext> = Object.freeze(
|
|
52
|
+
{},
|
|
53
|
+
),
|
|
54
|
+
) => {
|
|
55
|
+
const {variables, context} = options;
|
|
56
|
+
|
|
57
|
+
// Invoke the fetch and extract the data.
|
|
58
|
+
return fetch(operation, variables, {
|
|
59
|
+
...defaultContext,
|
|
60
|
+
...context,
|
|
61
|
+
}).then(getGqlDataFromResponse, (error) => {
|
|
62
|
+
// Return null if the request was aborted.
|
|
63
|
+
// The only way to detect this reliably, it seems, is to
|
|
64
|
+
// check the error name and see if it's "AbortError" (this
|
|
65
|
+
// is also what Apollo does).
|
|
66
|
+
// Even then, it's reliant on the fetch supporting aborts.
|
|
67
|
+
if (error.name === "AbortError") {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
},
|
|
72
|
+
[fetch, defaultContext],
|
|
73
|
+
);
|
|
74
|
+
return gqlFetch;
|
|
75
|
+
};
|
package/src/index.js
CHANGED
|
@@ -50,14 +50,14 @@ export const removeAllFromCache = <TOptions, TData: ValidData>(
|
|
|
50
50
|
) => boolean,
|
|
51
51
|
): number => ResCache.Default.removeAll<TOptions, TData>(handler, predicate);
|
|
52
52
|
|
|
53
|
-
/**
|
|
54
|
-
* TODO(somewhatabstract): Export each cache type we implement.
|
|
55
|
-
*
|
|
56
|
-
* Is there a base type we export, like we do for RequestHandler?
|
|
57
|
-
*/
|
|
58
|
-
|
|
59
53
|
export {default as RequestHandler} from "./util/request-handler.js";
|
|
60
54
|
export {default as TrackData} from "./components/track-data.js";
|
|
61
55
|
export {default as Data} from "./components/data.js";
|
|
62
56
|
export {default as InterceptData} from "./components/intercept-data.js";
|
|
63
57
|
export {useData} from "./hooks/use-data.js";
|
|
58
|
+
|
|
59
|
+
// GraphQL
|
|
60
|
+
export {GqlRouter} from "./components/gql-router.js";
|
|
61
|
+
export {useGql} from "./hooks/use-gql.js";
|
|
62
|
+
export * from "./util/gql-error.js";
|
|
63
|
+
export type {GqlContext, GqlOperation} from "./util/gql-types.js";
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import {getGqlDataFromResponse} from "../get-gql-data-from-response.js";
|
|
3
|
+
|
|
4
|
+
describe("#getGqlDataFromReponse", () => {
|
|
5
|
+
it("should throw if the response cannot be parsed", async () => {
|
|
6
|
+
// Arrange
|
|
7
|
+
const response: any = {
|
|
8
|
+
status: 200,
|
|
9
|
+
text: jest.fn(() => Promise.resolve("BAD JSON")),
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// Act
|
|
13
|
+
const result = getGqlDataFromResponse(response);
|
|
14
|
+
|
|
15
|
+
// Assert
|
|
16
|
+
await expect(result).rejects.toThrowErrorMatchingInlineSnapshot(`
|
|
17
|
+
"Failed to parse response
|
|
18
|
+
caused by
|
|
19
|
+
SyntaxError: Unexpected token B in JSON at position 0"
|
|
20
|
+
`);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("should include status code and body text in parse error metadata", async () => {
|
|
24
|
+
// Arrange
|
|
25
|
+
const response: any = {
|
|
26
|
+
status: 200,
|
|
27
|
+
text: jest.fn(() => Promise.resolve("BAD JSON")),
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Act
|
|
31
|
+
const result = getGqlDataFromResponse(response);
|
|
32
|
+
|
|
33
|
+
// Assert
|
|
34
|
+
await expect(result).rejects.toHaveProperty("metadata", {
|
|
35
|
+
statusCode: 200,
|
|
36
|
+
bodyText: "BAD JSON",
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("should throw if the status code is not <300", async () => {
|
|
41
|
+
// Arrange
|
|
42
|
+
const response: any = {
|
|
43
|
+
status: 400,
|
|
44
|
+
text: jest.fn(() => Promise.resolve("{}")),
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Act
|
|
48
|
+
const result = getGqlDataFromResponse(response);
|
|
49
|
+
|
|
50
|
+
// Assert
|
|
51
|
+
await expect(result).rejects.toThrowErrorMatchingInlineSnapshot(
|
|
52
|
+
`"Response unsuccessful"`,
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("should include status code and result in response error metadata", async () => {
|
|
57
|
+
// Arrange
|
|
58
|
+
const response: any = {
|
|
59
|
+
status: 400,
|
|
60
|
+
text: jest.fn(() =>
|
|
61
|
+
Promise.resolve(JSON.stringify({data: "DATA"})),
|
|
62
|
+
),
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// Act
|
|
66
|
+
const result = getGqlDataFromResponse(response);
|
|
67
|
+
|
|
68
|
+
// Assert
|
|
69
|
+
await expect(result).rejects.toHaveProperty("metadata", {
|
|
70
|
+
statusCode: 400,
|
|
71
|
+
result: {
|
|
72
|
+
data: "DATA",
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("should throw if the response is malformed", async () => {
|
|
78
|
+
// Arrange
|
|
79
|
+
const response: any = {
|
|
80
|
+
status: 200,
|
|
81
|
+
text: jest.fn(() => Promise.resolve("{}")),
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// Act
|
|
85
|
+
const result = getGqlDataFromResponse(response);
|
|
86
|
+
|
|
87
|
+
// Assert
|
|
88
|
+
await expect(result).rejects.toThrowErrorMatchingInlineSnapshot(
|
|
89
|
+
`"Server response missing"`,
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("should include the status code and the result in the malformed response error", async () => {
|
|
94
|
+
// Arrange
|
|
95
|
+
const response: any = {
|
|
96
|
+
status: 200,
|
|
97
|
+
text: jest.fn(() =>
|
|
98
|
+
Promise.resolve(JSON.stringify({malformed: "response"})),
|
|
99
|
+
),
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// Act
|
|
103
|
+
const result = getGqlDataFromResponse(response);
|
|
104
|
+
|
|
105
|
+
// Assert
|
|
106
|
+
await expect(result).rejects.toHaveProperty("metadata", {
|
|
107
|
+
statusCode: 200,
|
|
108
|
+
result: {
|
|
109
|
+
malformed: "response",
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("should throw if the response has GraphQL errors", async () => {
|
|
115
|
+
// Arrange
|
|
116
|
+
const response: any = {
|
|
117
|
+
status: 200,
|
|
118
|
+
text: jest.fn(() =>
|
|
119
|
+
Promise.resolve(
|
|
120
|
+
JSON.stringify({
|
|
121
|
+
data: {},
|
|
122
|
+
errors: [{message: "GraphQL error"}],
|
|
123
|
+
}),
|
|
124
|
+
),
|
|
125
|
+
),
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// Act
|
|
129
|
+
const result = getGqlDataFromResponse(response);
|
|
130
|
+
|
|
131
|
+
// Assert
|
|
132
|
+
await expect(result).rejects.toThrowErrorMatchingInlineSnapshot(
|
|
133
|
+
`"GraphQL errors"`,
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("should include the status code and result in the metadata", async () => {
|
|
138
|
+
// Arrange
|
|
139
|
+
const response: any = {
|
|
140
|
+
status: 200,
|
|
141
|
+
text: jest.fn(() =>
|
|
142
|
+
Promise.resolve(
|
|
143
|
+
JSON.stringify({
|
|
144
|
+
data: {},
|
|
145
|
+
errors: [{message: "GraphQL error"}],
|
|
146
|
+
}),
|
|
147
|
+
),
|
|
148
|
+
),
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
// Act
|
|
152
|
+
const result = getGqlDataFromResponse(response);
|
|
153
|
+
|
|
154
|
+
// Assert
|
|
155
|
+
await expect(result).rejects.toHaveProperty("metadata", {
|
|
156
|
+
statusCode: 200,
|
|
157
|
+
result: {
|
|
158
|
+
data: {},
|
|
159
|
+
errors: [{message: "GraphQL error"}],
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("should resolve to the response data", async () => {
|
|
165
|
+
// Arrange
|
|
166
|
+
const response: any = {
|
|
167
|
+
status: 200,
|
|
168
|
+
text: jest.fn(() =>
|
|
169
|
+
Promise.resolve(
|
|
170
|
+
JSON.stringify({
|
|
171
|
+
data: {
|
|
172
|
+
test: "test",
|
|
173
|
+
},
|
|
174
|
+
}),
|
|
175
|
+
),
|
|
176
|
+
),
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// Act
|
|
180
|
+
const result = getGqlDataFromResponse(response);
|
|
181
|
+
|
|
182
|
+
// Assert
|
|
183
|
+
await expect(result).resolves.toEqual({
|
|
184
|
+
test: "test",
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
});
|