@khanacademy/wonder-blocks-testing 0.0.2 → 1.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 +14 -2
- package/package.json +4 -3
- package/src/gql/__tests__/gql-request-matches-mock.test.js +235 -0
- package/src/gql/__tests__/make-gql-mock-response.test.js +298 -0
- package/src/gql/__tests__/mock-gql-fetch.test.js +469 -0
- package/src/gql/__tests__/wb-data-integration.test.js +267 -0
- package/src/gql/gql-request-matches-mock.js +74 -0
- package/src/gql/make-gql-mock-response.js +124 -0
- package/src/gql/mock-gql-fetch.js +96 -0
- package/src/gql/types.js +41 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import {render, screen, waitFor} from "@testing-library/react";
|
|
4
|
+
|
|
5
|
+
import {GqlRouter, useGql} from "@khanacademy/wonder-blocks-data";
|
|
6
|
+
import {RespondWith} from "../make-gql-mock-response.js";
|
|
7
|
+
import {mockGqlFetch} from "../mock-gql-fetch.js";
|
|
8
|
+
|
|
9
|
+
describe("integrating mockGqlFetch, RespondWith, GqlRouter and useGql", () => {
|
|
10
|
+
it("should reject with error indicating there are no mocks", async () => {
|
|
11
|
+
// Arrange
|
|
12
|
+
const mockFetch = mockGqlFetch();
|
|
13
|
+
const RenderError = () => {
|
|
14
|
+
const [result, setResult] = React.useState(null);
|
|
15
|
+
const gqlFetch = useGql();
|
|
16
|
+
React.useEffect(() => {
|
|
17
|
+
gqlFetch({
|
|
18
|
+
type: "query",
|
|
19
|
+
id: "getMyStuff",
|
|
20
|
+
}).catch((e) => {
|
|
21
|
+
setResult(e.message);
|
|
22
|
+
});
|
|
23
|
+
}, [gqlFetch]);
|
|
24
|
+
|
|
25
|
+
return <div data-test-id="result">{result}</div>;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// Act
|
|
29
|
+
render(
|
|
30
|
+
<GqlRouter defaultContext={{}} fetch={mockFetch}>
|
|
31
|
+
<RenderError />
|
|
32
|
+
</GqlRouter>,
|
|
33
|
+
);
|
|
34
|
+
const result = screen.getByTestId("result");
|
|
35
|
+
|
|
36
|
+
// Assert
|
|
37
|
+
await waitFor(() =>
|
|
38
|
+
expect(result).toHaveTextContent(
|
|
39
|
+
"No matching GraphQL mock response found for request",
|
|
40
|
+
),
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("should resolve with data for RespondWith.data", async () => {
|
|
45
|
+
// Arrange
|
|
46
|
+
const mockFetch = mockGqlFetch();
|
|
47
|
+
const query = {
|
|
48
|
+
type: "query",
|
|
49
|
+
id: "getMyStuff",
|
|
50
|
+
};
|
|
51
|
+
const data = {myStuff: "stuff"};
|
|
52
|
+
const RenderError = () => {
|
|
53
|
+
const [result, setResult] = React.useState(null);
|
|
54
|
+
const gqlFetch = useGql();
|
|
55
|
+
React.useEffect(() => {
|
|
56
|
+
// eslint-disable-next-line promise/catch-or-return
|
|
57
|
+
gqlFetch(query).then((r) => {
|
|
58
|
+
setResult(JSON.stringify(r ?? "(null)"));
|
|
59
|
+
return;
|
|
60
|
+
});
|
|
61
|
+
}, [gqlFetch]);
|
|
62
|
+
|
|
63
|
+
return <div data-test-id="result">{result}</div>;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// Act
|
|
67
|
+
mockFetch.mockOperation({operation: query}, RespondWith.data(data));
|
|
68
|
+
render(
|
|
69
|
+
<GqlRouter defaultContext={{}} fetch={mockFetch}>
|
|
70
|
+
<RenderError />
|
|
71
|
+
</GqlRouter>,
|
|
72
|
+
);
|
|
73
|
+
const result = screen.getByTestId("result");
|
|
74
|
+
|
|
75
|
+
// Assert
|
|
76
|
+
await waitFor(() =>
|
|
77
|
+
expect(result).toHaveTextContent(JSON.stringify(data)),
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("should respond with null data for RespondWith.abortedRequest", async () => {
|
|
82
|
+
// Arrange
|
|
83
|
+
const mockFetch = mockGqlFetch();
|
|
84
|
+
const query = {
|
|
85
|
+
type: "query",
|
|
86
|
+
id: "getMyStuff",
|
|
87
|
+
};
|
|
88
|
+
const RenderError = () => {
|
|
89
|
+
const [result, setResult] = React.useState(null);
|
|
90
|
+
const gqlFetch = useGql();
|
|
91
|
+
React.useEffect(() => {
|
|
92
|
+
// eslint-disable-next-line promise/catch-or-return
|
|
93
|
+
gqlFetch(query).then((r) => {
|
|
94
|
+
setResult(JSON.stringify(r ?? "(null)"));
|
|
95
|
+
return;
|
|
96
|
+
});
|
|
97
|
+
}, [gqlFetch]);
|
|
98
|
+
|
|
99
|
+
return <div data-test-id="result">{result}</div>;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// Act
|
|
103
|
+
mockFetch.mockOperation(
|
|
104
|
+
{operation: query},
|
|
105
|
+
RespondWith.abortedRequest(),
|
|
106
|
+
);
|
|
107
|
+
render(
|
|
108
|
+
<GqlRouter defaultContext={{}} fetch={mockFetch}>
|
|
109
|
+
<RenderError />
|
|
110
|
+
</GqlRouter>,
|
|
111
|
+
);
|
|
112
|
+
const result = screen.getByTestId("result");
|
|
113
|
+
|
|
114
|
+
// Assert
|
|
115
|
+
await waitFor(() => expect(result).toHaveTextContent('"(null)"'));
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("should reject with unsuccessful response error for RespondWith.errorStatusCode", async () => {
|
|
119
|
+
// Arrange
|
|
120
|
+
const mockFetch = mockGqlFetch();
|
|
121
|
+
const query = {
|
|
122
|
+
type: "query",
|
|
123
|
+
id: "getMyStuff",
|
|
124
|
+
};
|
|
125
|
+
const RenderError = () => {
|
|
126
|
+
const [result, setResult] = React.useState(null);
|
|
127
|
+
const gqlFetch = useGql();
|
|
128
|
+
React.useEffect(() => {
|
|
129
|
+
// eslint-disable-next-line promise/catch-or-return
|
|
130
|
+
gqlFetch(query).catch((e) => {
|
|
131
|
+
setResult(e.message);
|
|
132
|
+
});
|
|
133
|
+
}, [gqlFetch]);
|
|
134
|
+
|
|
135
|
+
return <div data-test-id="result">{result}</div>;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// Act
|
|
139
|
+
mockFetch.mockOperation(
|
|
140
|
+
{operation: query},
|
|
141
|
+
RespondWith.errorStatusCode(404),
|
|
142
|
+
);
|
|
143
|
+
render(
|
|
144
|
+
<GqlRouter defaultContext={{}} fetch={mockFetch}>
|
|
145
|
+
<RenderError />
|
|
146
|
+
</GqlRouter>,
|
|
147
|
+
);
|
|
148
|
+
const result = screen.getByTestId("result");
|
|
149
|
+
|
|
150
|
+
// Assert
|
|
151
|
+
await waitFor(() =>
|
|
152
|
+
expect(result).toHaveTextContent("Response unsuccessful"),
|
|
153
|
+
);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("should reject with parse error for RespondWith.unparseableBody", async () => {
|
|
157
|
+
// Arrange
|
|
158
|
+
const mockFetch = mockGqlFetch();
|
|
159
|
+
const query = {
|
|
160
|
+
type: "query",
|
|
161
|
+
id: "getMyStuff",
|
|
162
|
+
};
|
|
163
|
+
const RenderError = () => {
|
|
164
|
+
const [result, setResult] = React.useState(null);
|
|
165
|
+
const gqlFetch = useGql();
|
|
166
|
+
React.useEffect(() => {
|
|
167
|
+
// eslint-disable-next-line promise/catch-or-return
|
|
168
|
+
gqlFetch(query).catch((e) => {
|
|
169
|
+
setResult(e.message);
|
|
170
|
+
});
|
|
171
|
+
}, [gqlFetch]);
|
|
172
|
+
|
|
173
|
+
return <div data-test-id="result">{result}</div>;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// Act
|
|
177
|
+
mockFetch.mockOperation(
|
|
178
|
+
{operation: query},
|
|
179
|
+
RespondWith.unparseableBody(),
|
|
180
|
+
);
|
|
181
|
+
render(
|
|
182
|
+
<GqlRouter defaultContext={{}} fetch={mockFetch}>
|
|
183
|
+
<RenderError />
|
|
184
|
+
</GqlRouter>,
|
|
185
|
+
);
|
|
186
|
+
const result = screen.getByTestId("result");
|
|
187
|
+
|
|
188
|
+
// Assert
|
|
189
|
+
await waitFor(() =>
|
|
190
|
+
expect(result).toHaveTextContent("Failed to parse response"),
|
|
191
|
+
);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("should reject with missing response error for RespondWith.nonGraphQLBody", async () => {
|
|
195
|
+
// Arrange
|
|
196
|
+
const mockFetch = mockGqlFetch();
|
|
197
|
+
const query = {
|
|
198
|
+
type: "query",
|
|
199
|
+
id: "getMyStuff",
|
|
200
|
+
};
|
|
201
|
+
const RenderError = () => {
|
|
202
|
+
const [result, setResult] = React.useState(null);
|
|
203
|
+
const gqlFetch = useGql();
|
|
204
|
+
React.useEffect(() => {
|
|
205
|
+
// eslint-disable-next-line promise/catch-or-return
|
|
206
|
+
gqlFetch(query).catch((e) => {
|
|
207
|
+
setResult(e.message);
|
|
208
|
+
});
|
|
209
|
+
}, [gqlFetch]);
|
|
210
|
+
|
|
211
|
+
return <div data-test-id="result">{result}</div>;
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
// Act
|
|
215
|
+
mockFetch.mockOperation(
|
|
216
|
+
{operation: query},
|
|
217
|
+
RespondWith.nonGraphQLBody(),
|
|
218
|
+
);
|
|
219
|
+
render(
|
|
220
|
+
<GqlRouter defaultContext={{}} fetch={mockFetch}>
|
|
221
|
+
<RenderError />
|
|
222
|
+
</GqlRouter>,
|
|
223
|
+
);
|
|
224
|
+
const result = screen.getByTestId("result");
|
|
225
|
+
|
|
226
|
+
// Assert
|
|
227
|
+
await waitFor(() =>
|
|
228
|
+
expect(result).toHaveTextContent("Server response missing"),
|
|
229
|
+
);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("should reject with GraphQL error for RespondWith.graphQLErrors", async () => {
|
|
233
|
+
// Arrange
|
|
234
|
+
const mockFetch = mockGqlFetch();
|
|
235
|
+
const query = {
|
|
236
|
+
type: "query",
|
|
237
|
+
id: "getMyStuff",
|
|
238
|
+
};
|
|
239
|
+
const RenderError = () => {
|
|
240
|
+
const [result, setResult] = React.useState(null);
|
|
241
|
+
const gqlFetch = useGql();
|
|
242
|
+
React.useEffect(() => {
|
|
243
|
+
// eslint-disable-next-line promise/catch-or-return
|
|
244
|
+
gqlFetch(query).catch((e) => {
|
|
245
|
+
setResult(e.message);
|
|
246
|
+
});
|
|
247
|
+
}, [gqlFetch]);
|
|
248
|
+
|
|
249
|
+
return <div data-test-id="result">{result}</div>;
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
// Act
|
|
253
|
+
mockFetch.mockOperation(
|
|
254
|
+
{operation: query},
|
|
255
|
+
RespondWith.graphQLErrors(["error 1", "error 2"]),
|
|
256
|
+
);
|
|
257
|
+
render(
|
|
258
|
+
<GqlRouter defaultContext={{}} fetch={mockFetch}>
|
|
259
|
+
<RenderError />
|
|
260
|
+
</GqlRouter>,
|
|
261
|
+
);
|
|
262
|
+
const result = screen.getByTestId("result");
|
|
263
|
+
|
|
264
|
+
// Assert
|
|
265
|
+
await waitFor(() => expect(result).toHaveTextContent("GraphQL errors"));
|
|
266
|
+
});
|
|
267
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import type {GqlOperation, GqlContext} from "@khanacademy/wonder-blocks-data";
|
|
3
|
+
import type {GqlMockOperation} from "./types.js";
|
|
4
|
+
|
|
5
|
+
const safeHasOwnProperty = (obj: any, prop: string): boolean =>
|
|
6
|
+
// Flow really shouldn't be raising this error here.
|
|
7
|
+
// $FlowFixMe[method-unbinding]
|
|
8
|
+
Object.prototype.hasOwnProperty.call(obj, prop);
|
|
9
|
+
|
|
10
|
+
// TODO(somewhatabstract, FEI-4268): use a third-party library to do this and
|
|
11
|
+
// possibly make it also support the jest `jest.objectContaining` type matching
|
|
12
|
+
// to simplify mock declaration (note that it would need to work in regular
|
|
13
|
+
// tests and stories/fixtures).
|
|
14
|
+
const areObjectsEqual = (a: any, b: any): boolean => {
|
|
15
|
+
if (a === b) {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
if (a == null || b == null) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
if (typeof a !== "object" || typeof b !== "object") {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
const aKeys = Object.keys(a);
|
|
25
|
+
const bKeys = Object.keys(b);
|
|
26
|
+
if (aKeys.length !== bKeys.length) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
for (let i = 0; i < aKeys.length; i++) {
|
|
30
|
+
const key = aKeys[i];
|
|
31
|
+
if (!safeHasOwnProperty(b, key) || !areObjectsEqual(a[key], b[key])) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return true;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const gqlRequestMatchesMock = (
|
|
39
|
+
mock: GqlMockOperation<any, any, any, any>,
|
|
40
|
+
operation: GqlOperation<any, any, any>,
|
|
41
|
+
variables: ?{...},
|
|
42
|
+
context: GqlContext,
|
|
43
|
+
): boolean => {
|
|
44
|
+
// If they don't represent the same operation, then they can't match.
|
|
45
|
+
// NOTE: Operations can include more fields than id and type, but we only
|
|
46
|
+
// care about id and type. The rest is ignored.
|
|
47
|
+
if (
|
|
48
|
+
mock.operation.id !== operation.id ||
|
|
49
|
+
mock.operation.type !== operation.type
|
|
50
|
+
) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// We do a loose match, so if the lhs doesn't define variables,
|
|
55
|
+
// we just assume it matches everything.
|
|
56
|
+
if (mock.variables != null) {
|
|
57
|
+
// Variables have to match.
|
|
58
|
+
if (!areObjectsEqual(mock.variables, variables)) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// We do a loose match, so if the lhs doesn't define context,
|
|
64
|
+
// we just assume it matches everything.
|
|
65
|
+
if (mock.context != null) {
|
|
66
|
+
// Context has to match.
|
|
67
|
+
if (!areObjectsEqual(mock.context, context)) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// If we get here, we have a match.
|
|
73
|
+
return true;
|
|
74
|
+
};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
export opaque type GqlMockResponse<TData> =
|
|
3
|
+
| {|
|
|
4
|
+
type: "data",
|
|
5
|
+
data: TData,
|
|
6
|
+
|}
|
|
7
|
+
| {|
|
|
8
|
+
type: "parse",
|
|
9
|
+
|}
|
|
10
|
+
| {|
|
|
11
|
+
type: "abort",
|
|
12
|
+
|}
|
|
13
|
+
| {|
|
|
14
|
+
type: "status",
|
|
15
|
+
statusCode: number,
|
|
16
|
+
|}
|
|
17
|
+
| {|
|
|
18
|
+
type: "invalid",
|
|
19
|
+
|}
|
|
20
|
+
| {|type: "graphql", errors: $ReadOnlyArray<string>|};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Helpers to define rejection states for mocking GQL requests.
|
|
24
|
+
*/
|
|
25
|
+
export const RespondWith = Object.freeze({
|
|
26
|
+
data: <TData>(data: TData): GqlMockResponse<TData> => ({
|
|
27
|
+
type: "data",
|
|
28
|
+
data,
|
|
29
|
+
}),
|
|
30
|
+
unparseableBody: (): GqlMockResponse<any> => ({type: "parse"}),
|
|
31
|
+
abortedRequest: (): GqlMockResponse<any> => ({type: "abort"}),
|
|
32
|
+
errorStatusCode: (statusCode: number): GqlMockResponse<any> => {
|
|
33
|
+
if (statusCode < 300) {
|
|
34
|
+
throw new Error(`${statusCode} is not a valid error status code`);
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
type: "status",
|
|
38
|
+
statusCode,
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
nonGraphQLBody: (): GqlMockResponse<any> => ({type: "invalid"}),
|
|
42
|
+
graphQLErrors: (
|
|
43
|
+
errorMessages: $ReadOnlyArray<string>,
|
|
44
|
+
): GqlMockResponse<any> => ({
|
|
45
|
+
type: "graphql",
|
|
46
|
+
errors: errorMessages,
|
|
47
|
+
}),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Turns an ErrorResponse value in an actual Response that will invoke
|
|
52
|
+
* that error.
|
|
53
|
+
*/
|
|
54
|
+
export const makeGqlMockResponse = <TData>(
|
|
55
|
+
response: GqlMockResponse<TData>,
|
|
56
|
+
): Promise<Response> => {
|
|
57
|
+
switch (response.type) {
|
|
58
|
+
case "data":
|
|
59
|
+
return Promise.resolve(
|
|
60
|
+
({
|
|
61
|
+
status: 200,
|
|
62
|
+
text: () =>
|
|
63
|
+
Promise.resolve(
|
|
64
|
+
JSON.stringify({
|
|
65
|
+
data: response.data,
|
|
66
|
+
}),
|
|
67
|
+
),
|
|
68
|
+
}: any),
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
case "parse":
|
|
72
|
+
return Promise.resolve(
|
|
73
|
+
({
|
|
74
|
+
status: 200,
|
|
75
|
+
text: () => Promise.resolve("INVALID JSON"),
|
|
76
|
+
}: any),
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
case "abort":
|
|
80
|
+
const abortError = new Error("Mock request aborted");
|
|
81
|
+
abortError.name = "AbortError";
|
|
82
|
+
return Promise.reject(abortError);
|
|
83
|
+
|
|
84
|
+
case "status":
|
|
85
|
+
return Promise.resolve(
|
|
86
|
+
({
|
|
87
|
+
status: response.statusCode,
|
|
88
|
+
text: () => Promise.resolve(JSON.stringify({})),
|
|
89
|
+
}: any),
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
case "invalid":
|
|
93
|
+
return Promise.resolve(
|
|
94
|
+
({
|
|
95
|
+
status: 200,
|
|
96
|
+
text: () =>
|
|
97
|
+
Promise.resolve(
|
|
98
|
+
JSON.stringify({
|
|
99
|
+
valid: "json",
|
|
100
|
+
that: "is not a valid graphql response",
|
|
101
|
+
}),
|
|
102
|
+
),
|
|
103
|
+
}: any),
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
case "graphql":
|
|
107
|
+
return Promise.resolve(
|
|
108
|
+
({
|
|
109
|
+
status: 200,
|
|
110
|
+
text: () =>
|
|
111
|
+
Promise.resolve(
|
|
112
|
+
JSON.stringify({
|
|
113
|
+
errors: response.errors.map((e) => ({
|
|
114
|
+
message: e,
|
|
115
|
+
})),
|
|
116
|
+
}),
|
|
117
|
+
),
|
|
118
|
+
}: any),
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
default:
|
|
122
|
+
throw new Error(`Unknown response type: ${response.type}`);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import type {GqlContext} from "@khanacademy/wonder-blocks-data";
|
|
3
|
+
import {gqlRequestMatchesMock} from "./gql-request-matches-mock.js";
|
|
4
|
+
import {makeGqlMockResponse} from "./make-gql-mock-response.js";
|
|
5
|
+
import type {GqlMockResponse} from "./make-gql-mock-response.js";
|
|
6
|
+
import type {GqlMock, GqlMockOperation, GqlFetchMockFn} from "./types.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A mock for the fetch function passed to GqlRouter.
|
|
10
|
+
*/
|
|
11
|
+
export const mockGqlFetch = (): GqlFetchMockFn => {
|
|
12
|
+
// We want this to work in jest and in fixtures to make life easy for folks.
|
|
13
|
+
// This is the array of mocked operations that we will traverse and
|
|
14
|
+
// manipulate.
|
|
15
|
+
const mocks: Array<GqlMock> = [];
|
|
16
|
+
|
|
17
|
+
// What we return has to be a drop in for the fetch function that is
|
|
18
|
+
// provided to `GqlRouter` which is how folks will then use this mock.
|
|
19
|
+
const gqlFetchMock: GqlFetchMockFn = (
|
|
20
|
+
operation,
|
|
21
|
+
variables,
|
|
22
|
+
context,
|
|
23
|
+
): Promise<Response> => {
|
|
24
|
+
// Iterate our mocked operations and find the first one that matches.
|
|
25
|
+
for (const mock of mocks) {
|
|
26
|
+
if (mock.onceOnly && mock.used) {
|
|
27
|
+
// This is a once-only mock and it has been used, so skip it.
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (
|
|
31
|
+
gqlRequestMatchesMock(
|
|
32
|
+
mock.operation,
|
|
33
|
+
operation,
|
|
34
|
+
variables,
|
|
35
|
+
context,
|
|
36
|
+
)
|
|
37
|
+
) {
|
|
38
|
+
mock.used = true;
|
|
39
|
+
return mock.response();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Default is to reject with some helpful info on what request
|
|
44
|
+
// we rejected.
|
|
45
|
+
return Promise.reject(
|
|
46
|
+
new Error(`No matching GraphQL mock response found for request:
|
|
47
|
+
Operation: ${operation.type} ${operation.id}
|
|
48
|
+
Variables: ${
|
|
49
|
+
variables == null ? "None" : JSON.stringify(variables, null, 2)
|
|
50
|
+
}
|
|
51
|
+
Context: ${JSON.stringify(context, null, 2)}`),
|
|
52
|
+
);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const addMockedOperation = <
|
|
56
|
+
TType,
|
|
57
|
+
TData,
|
|
58
|
+
TVariables: {...},
|
|
59
|
+
TContext: GqlContext,
|
|
60
|
+
>(
|
|
61
|
+
operation: GqlMockOperation<TType, TData, TVariables, TContext>,
|
|
62
|
+
response: GqlMockResponse<TData>,
|
|
63
|
+
onceOnly: boolean,
|
|
64
|
+
): GqlFetchMockFn => {
|
|
65
|
+
const mockResponse = () => makeGqlMockResponse(response);
|
|
66
|
+
mocks.push({
|
|
67
|
+
operation,
|
|
68
|
+
response: mockResponse,
|
|
69
|
+
onceOnly,
|
|
70
|
+
used: false,
|
|
71
|
+
});
|
|
72
|
+
return gqlFetchMock;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
gqlFetchMock.mockOperation = <
|
|
76
|
+
TType,
|
|
77
|
+
TData,
|
|
78
|
+
TVariables: {...},
|
|
79
|
+
TContext: GqlContext,
|
|
80
|
+
>(
|
|
81
|
+
operation: GqlMockOperation<TType, TData, TVariables, TContext>,
|
|
82
|
+
response: GqlMockResponse<TData>,
|
|
83
|
+
): GqlFetchMockFn => addMockedOperation(operation, response, false);
|
|
84
|
+
|
|
85
|
+
gqlFetchMock.mockOperationOnce = <
|
|
86
|
+
TType,
|
|
87
|
+
TData,
|
|
88
|
+
TVariables: {...},
|
|
89
|
+
TContext: GqlContext,
|
|
90
|
+
>(
|
|
91
|
+
operation: GqlMockOperation<TType, TData, TVariables, TContext>,
|
|
92
|
+
response: GqlMockResponse<TData>,
|
|
93
|
+
): GqlFetchMockFn => addMockedOperation(operation, response, true);
|
|
94
|
+
|
|
95
|
+
return gqlFetchMock;
|
|
96
|
+
};
|
package/src/gql/types.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
//@flow
|
|
2
|
+
import type {GqlOperation, GqlContext} from "@khanacademy/wonder-blocks-data";
|
|
3
|
+
import type {GqlMockResponse} from "./make-gql-mock-response.js";
|
|
4
|
+
|
|
5
|
+
export type GqlMockOperation<
|
|
6
|
+
TType,
|
|
7
|
+
TData,
|
|
8
|
+
TVariables: {...},
|
|
9
|
+
TContext: GqlContext,
|
|
10
|
+
> = {|
|
|
11
|
+
operation: GqlOperation<TType, TData, TVariables>,
|
|
12
|
+
variables?: TVariables,
|
|
13
|
+
context?: TContext,
|
|
14
|
+
|};
|
|
15
|
+
|
|
16
|
+
type GqlMockOperationFn = <
|
|
17
|
+
TType,
|
|
18
|
+
TData,
|
|
19
|
+
TVariables: {...},
|
|
20
|
+
TContext: GqlContext,
|
|
21
|
+
>(
|
|
22
|
+
operation: GqlMockOperation<TType, TData, TVariables, TContext>,
|
|
23
|
+
response: GqlMockResponse<TData>,
|
|
24
|
+
) => GqlFetchMockFn;
|
|
25
|
+
|
|
26
|
+
export type GqlFetchMockFn = {|
|
|
27
|
+
(
|
|
28
|
+
operation: GqlOperation<any, any, any>,
|
|
29
|
+
variables: ?{...},
|
|
30
|
+
context: GqlContext,
|
|
31
|
+
): Promise<Response>,
|
|
32
|
+
mockOperation: GqlMockOperationFn,
|
|
33
|
+
mockOperationOnce: GqlMockOperationFn,
|
|
34
|
+
|};
|
|
35
|
+
|
|
36
|
+
export type GqlMock = {|
|
|
37
|
+
operation: GqlMockOperation<any, any, any, any>,
|
|
38
|
+
onceOnly: boolean,
|
|
39
|
+
used: boolean,
|
|
40
|
+
response: () => Promise<Response>,
|
|
41
|
+
|};
|