@khanacademy/wonder-blocks-data 2.3.4 → 3.1.1

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.
Files changed (48) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/es/index.js +368 -429
  3. package/dist/index.js +457 -460
  4. package/docs.md +19 -13
  5. package/package.json +3 -3
  6. package/src/__tests__/__snapshots__/generated-snapshot.test.js.snap +40 -160
  7. package/src/__tests__/generated-snapshot.test.js +15 -195
  8. package/src/components/__tests__/data.test.js +159 -965
  9. package/src/components/__tests__/gql-router.test.js +64 -0
  10. package/src/components/__tests__/intercept-data.test.js +9 -66
  11. package/src/components/__tests__/track-data.test.js +6 -5
  12. package/src/components/data.js +9 -117
  13. package/src/components/data.md +38 -60
  14. package/src/components/gql-router.js +66 -0
  15. package/src/components/intercept-data.js +2 -34
  16. package/src/components/intercept-data.md +7 -105
  17. package/src/hooks/__tests__/use-data.test.js +826 -0
  18. package/src/hooks/__tests__/use-gql.test.js +233 -0
  19. package/src/hooks/use-data.js +143 -0
  20. package/src/hooks/use-gql.js +77 -0
  21. package/src/index.js +13 -9
  22. package/src/util/__tests__/get-gql-data-from-response.test.js +187 -0
  23. package/src/util/__tests__/memory-cache.test.js +134 -35
  24. package/src/util/__tests__/request-fulfillment.test.js +21 -36
  25. package/src/util/__tests__/request-handler.test.js +30 -30
  26. package/src/util/__tests__/request-tracking.test.js +29 -30
  27. package/src/util/__tests__/response-cache.test.js +521 -561
  28. package/src/util/__tests__/result-from-cache-entry.test.js +68 -0
  29. package/src/util/get-gql-data-from-response.js +69 -0
  30. package/src/util/gql-error.js +36 -0
  31. package/src/util/gql-router-context.js +6 -0
  32. package/src/util/gql-types.js +65 -0
  33. package/src/util/memory-cache.js +18 -14
  34. package/src/util/request-fulfillment.js +4 -0
  35. package/src/util/request-handler.js +2 -27
  36. package/src/util/request-handler.md +0 -32
  37. package/src/util/response-cache.js +50 -110
  38. package/src/util/result-from-cache-entry.js +38 -0
  39. package/src/util/types.js +14 -35
  40. package/LICENSE +0 -21
  41. package/src/components/__tests__/intercept-cache.test.js +0 -124
  42. package/src/components/__tests__/internal-data.test.js +0 -1030
  43. package/src/components/intercept-cache.js +0 -79
  44. package/src/components/intercept-cache.md +0 -103
  45. package/src/components/internal-data.js +0 -219
  46. package/src/util/__tests__/no-cache.test.js +0 -112
  47. package/src/util/no-cache.js +0 -67
  48. package/src/util/no-cache.md +0 -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,143 @@
1
+ // @flow
2
+ import {Server} from "@khanacademy/wonder-blocks-core";
3
+ import {useState, useEffect, useContext, useRef} from "react";
4
+ import {RequestFulfillment} from "../util/request-fulfillment.js";
5
+ import InterceptContext from "../components/intercept-context.js";
6
+ import {TrackerContext} from "../util/request-tracking.js";
7
+ import {resultFromCacheEntry} from "../util/result-from-cache-entry.js";
8
+ import {ResponseCache} from "../util/response-cache.js";
9
+
10
+ import type {
11
+ Result,
12
+ IRequestHandler,
13
+ ValidData,
14
+ CacheEntry,
15
+ } from "../util/types.js";
16
+
17
+ export const useData = <TOptions, TData: ValidData>(
18
+ handler: IRequestHandler<TOptions, TData>,
19
+ options: TOptions,
20
+ ): Result<TData> => {
21
+ // If we're server-side or hydrating, we'll have a cached entry to use.
22
+ // So we get that and use it to initialize our state.
23
+ // This works in both hydration and SSR because the very first call to
24
+ // this will have cached data in those cases as it will be present on the
25
+ // initial render - and subsequent renders on the client it will be null.
26
+ const cachedResult = ResponseCache.Default.getEntry<TOptions, TData>(
27
+ handler,
28
+ options,
29
+ );
30
+ const [result, setResult] = useState<?CacheEntry<TData>>(cachedResult);
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
+
59
+ // We only track data requests when we are server-side and we don't
60
+ // already have a result, as given by the cachedData (which is also the
61
+ // initial value for the result state).
62
+ const maybeTrack = useContext(TrackerContext);
63
+ if (result == null && Server.isServerSide()) {
64
+ maybeTrack?.(getMaybeInterceptedHandler(), options);
65
+ }
66
+
67
+ // We need to update our request when the handler changes or the key
68
+ // to the options change, so we keep track of those.
69
+ // However, even if we are hydrating from cache, we still need to make the
70
+ // request at least once, so we do not initialize these references.
71
+ const handlerRef = useRef();
72
+ const keyRef = useRef();
73
+ const interceptorRef = useRef();
74
+
75
+ // This effect will ensure that we fulfill the request as desired.
76
+ useEffect(() => {
77
+ // If we are server-side, then just skip the effect. We track requests
78
+ // during SSR and fulfill them outside of the React render cycle.
79
+ // NOTE: This shouldn't happen since effects would not run on the server
80
+ // but let's be defensive - I think it makes the code clearer.
81
+ /* istanbul ignore next */
82
+ if (Server.isServerSide()) {
83
+ return;
84
+ }
85
+
86
+ // Update our refs to the current handler and key.
87
+ handlerRef.current = handler;
88
+ keyRef.current = handler.getKey(options);
89
+ interceptorRef.current = interceptor;
90
+
91
+ // If we're not hydrating a result, we want to make sure we set our
92
+ // result to null so that we're in the loading state.
93
+ if (cachedResult == null) {
94
+ // Mark ourselves as loading.
95
+ setResult(null);
96
+ }
97
+
98
+ // We aren't server-side, so let's make the request.
99
+ // The request handler is in control of whether that request actually
100
+ // happens or not.
101
+ let cancel = false;
102
+ RequestFulfillment.Default.fulfill(
103
+ getMaybeInterceptedHandler(),
104
+ options,
105
+ )
106
+ .then((updateEntry) => {
107
+ if (cancel) {
108
+ return;
109
+ }
110
+ setResult(updateEntry);
111
+ return;
112
+ })
113
+ .catch((e) => {
114
+ if (cancel) {
115
+ return;
116
+ }
117
+ /**
118
+ * We should never get here as errors in fulfillment are part
119
+ * of the `then`, but if we do.
120
+ */
121
+ // eslint-disable-next-line no-console
122
+ console.error(
123
+ `Unexpected error occurred during data fulfillment: ${e}`,
124
+ );
125
+ setResult({
126
+ data: null,
127
+ error: typeof e === "string" ? e : e.message,
128
+ });
129
+ return;
130
+ });
131
+
132
+ return () => {
133
+ cancel = true;
134
+ };
135
+ // - handler.getKey is a proxy for options
136
+ // - We don't want to trigger on cachedResult changing, we're
137
+ // just using that as a flag for render state if the other things
138
+ // trigger this effect.
139
+ // eslint-disable-next-line react-hooks/exhaustive-deps
140
+ }, [handler, handler.getKey(options), interceptor]);
141
+
142
+ return resultFromCacheEntry(result);
143
+ };
@@ -0,0 +1,77 @@
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
+ // Need to make sure we pass other errors along.
71
+ throw error;
72
+ });
73
+ },
74
+ [fetch, defaultContext],
75
+ );
76
+ return gqlFetch;
77
+ };
package/src/index.js CHANGED
@@ -15,7 +15,6 @@ export type {
15
15
  CacheEntry,
16
16
  Result,
17
17
  IRequestHandler,
18
- ICache,
19
18
  ResponseCache,
20
19
  } from "./util/types.js";
21
20
 
@@ -51,15 +50,20 @@ export const removeAllFromCache = <TOptions, TData: ValidData>(
51
50
  ) => boolean,
52
51
  ): number => ResCache.Default.removeAll<TOptions, TData>(handler, predicate);
53
52
 
54
- /**
55
- * TODO(somewhatabstract): Export each cache type we implement.
56
- *
57
- * Is there a base type we export, like we do for RequestHandler?
58
- */
59
-
60
53
  export {default as RequestHandler} from "./util/request-handler.js";
61
54
  export {default as TrackData} from "./components/track-data.js";
62
55
  export {default as Data} from "./components/data.js";
63
56
  export {default as InterceptData} from "./components/intercept-data.js";
64
- export {default as InterceptCache} from "./components/intercept-cache.js";
65
- export {default as NoCache} from "./util/no-cache.js";
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 {
64
+ GqlContext,
65
+ GqlOperation,
66
+ GqlOperationType,
67
+ GqlFetchOptions,
68
+ GqlFetchFn,
69
+ } 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
+ });