@khanacademy/wonder-blocks-testing 2.0.8 → 4.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 +22 -0
- package/dist/es/index.js +255 -140
- package/dist/index.js +304 -145
- package/package.json +2 -1
- package/src/{gql/__tests__/make-gql-mock-response.test.js → __tests__/make-mock-response.test.js} +196 -34
- package/src/__tests__/mock-requester.test.js +213 -0
- package/src/__tests__/response-impl.test.js +47 -0
- package/src/fetch/__tests__/__snapshots__/mock-fetch.test.js.snap +29 -0
- package/src/fetch/__tests__/fetch-request-matches-mock.test.js +99 -0
- package/src/fetch/__tests__/mock-fetch.test.js +84 -0
- package/src/fetch/fetch-request-matches-mock.js +43 -0
- package/src/fetch/mock-fetch.js +19 -0
- package/src/fetch/types.js +18 -0
- package/src/fixtures/__tests__/fixtures.test.js +66 -22
- package/src/fixtures/adapters/__tests__/adapter-group.test.js +24 -0
- package/src/fixtures/adapters/__tests__/adapter.test.js +6 -0
- package/src/fixtures/adapters/storybook.js +4 -1
- package/src/fixtures/fixtures.basic.stories.js +1 -1
- package/src/fixtures/fixtures.defaultwrapper.stories.js +1 -1
- package/src/fixtures/fixtures.js +47 -3
- package/src/fixtures/types.js +10 -1
- package/src/gql/__tests__/mock-gql-fetch.test.js +24 -15
- package/src/gql/__tests__/wb-data-integration.test.js +7 -4
- package/src/gql/mock-gql-fetch.js +9 -80
- package/src/gql/types.js +11 -10
- package/src/index.js +9 -3
- package/src/make-mock-response.js +150 -0
- package/src/mock-requester.js +75 -0
- package/src/response-impl.js +9 -0
- package/src/types.js +39 -0
- package/src/gql/make-gql-mock-response.js +0 -124
|
@@ -3,7 +3,7 @@ import * as React from "react";
|
|
|
3
3
|
import {render, screen, waitFor} from "@testing-library/react";
|
|
4
4
|
|
|
5
5
|
import {GqlRouter, useGql} from "@khanacademy/wonder-blocks-data";
|
|
6
|
-
import {RespondWith} from "
|
|
6
|
+
import {RespondWith} from "../../make-mock-response.js";
|
|
7
7
|
import {mockGqlFetch} from "../mock-gql-fetch.js";
|
|
8
8
|
|
|
9
9
|
describe("#mockGqlFetch", () => {
|
|
@@ -37,7 +37,7 @@ describe("#mockGqlFetch", () => {
|
|
|
37
37
|
// Assert
|
|
38
38
|
await waitFor(() =>
|
|
39
39
|
expect(result).toHaveTextContent(
|
|
40
|
-
"No matching
|
|
40
|
+
"No matching mock response found for request",
|
|
41
41
|
),
|
|
42
42
|
);
|
|
43
43
|
});
|
|
@@ -65,7 +65,10 @@ describe("#mockGqlFetch", () => {
|
|
|
65
65
|
};
|
|
66
66
|
|
|
67
67
|
// Act
|
|
68
|
-
mockFetch.mockOperation(
|
|
68
|
+
mockFetch.mockOperation(
|
|
69
|
+
{operation: query},
|
|
70
|
+
RespondWith.graphQLData(data),
|
|
71
|
+
);
|
|
69
72
|
render(
|
|
70
73
|
<GqlRouter defaultContext={{}} fetch={mockFetch}>
|
|
71
74
|
<RenderData />
|
|
@@ -171,7 +174,7 @@ describe("#mockGqlFetch", () => {
|
|
|
171
174
|
|
|
172
175
|
// Assert
|
|
173
176
|
await expect(result).rejects.toThrowErrorMatchingInlineSnapshot(`
|
|
174
|
-
"No matching
|
|
177
|
+
"No matching mock response found for request:
|
|
175
178
|
Operation: query getMyStuff
|
|
176
179
|
Variables: {
|
|
177
180
|
\\"a\\": \\"variable\\"
|
|
@@ -195,7 +198,7 @@ describe("#mockGqlFetch", () => {
|
|
|
195
198
|
};
|
|
196
199
|
|
|
197
200
|
// Act
|
|
198
|
-
mockFetch.mockOperation({operation}, RespondWith.
|
|
201
|
+
mockFetch.mockOperation({operation}, RespondWith.graphQLData(data));
|
|
199
202
|
const result = mockFetch(
|
|
200
203
|
operation,
|
|
201
204
|
{a: "variable"},
|
|
@@ -218,7 +221,7 @@ describe("#mockGqlFetch", () => {
|
|
|
218
221
|
};
|
|
219
222
|
|
|
220
223
|
// Act
|
|
221
|
-
mockFetch.mockOperation({operation}, RespondWith.
|
|
224
|
+
mockFetch.mockOperation({operation}, RespondWith.graphQLData(data));
|
|
222
225
|
const result = mockFetch(
|
|
223
226
|
{type: "mutation", id: "putMyStuff"},
|
|
224
227
|
{a: "variable"},
|
|
@@ -246,7 +249,7 @@ describe("#mockGqlFetch", () => {
|
|
|
246
249
|
// Act
|
|
247
250
|
mockFetch.mockOperation(
|
|
248
251
|
{operation, variables},
|
|
249
|
-
RespondWith.
|
|
252
|
+
RespondWith.graphQLData(data),
|
|
250
253
|
);
|
|
251
254
|
const result = mockFetch(operation, variables, {my: "context"});
|
|
252
255
|
|
|
@@ -271,7 +274,7 @@ describe("#mockGqlFetch", () => {
|
|
|
271
274
|
// Act
|
|
272
275
|
mockFetch.mockOperation(
|
|
273
276
|
{operation, variables},
|
|
274
|
-
RespondWith.
|
|
277
|
+
RespondWith.graphQLData(data),
|
|
275
278
|
);
|
|
276
279
|
const result = mockFetch(
|
|
277
280
|
operation,
|
|
@@ -300,7 +303,7 @@ describe("#mockGqlFetch", () => {
|
|
|
300
303
|
// Act
|
|
301
304
|
mockFetch.mockOperation(
|
|
302
305
|
{operation, context},
|
|
303
|
-
RespondWith.
|
|
306
|
+
RespondWith.graphQLData(data),
|
|
304
307
|
);
|
|
305
308
|
const result = mockFetch(operation, {a: "variable"}, context);
|
|
306
309
|
|
|
@@ -325,7 +328,7 @@ describe("#mockGqlFetch", () => {
|
|
|
325
328
|
// Act
|
|
326
329
|
mockFetch.mockOperation(
|
|
327
330
|
{operation, context},
|
|
328
|
-
RespondWith.
|
|
331
|
+
RespondWith.graphQLData(data),
|
|
329
332
|
);
|
|
330
333
|
const result = mockFetch(
|
|
331
334
|
operation,
|
|
@@ -357,7 +360,7 @@ describe("#mockGqlFetch", () => {
|
|
|
357
360
|
// Act
|
|
358
361
|
mockFetch.mockOperation(
|
|
359
362
|
{operation, variables, context},
|
|
360
|
-
RespondWith.
|
|
363
|
+
RespondWith.graphQLData(data),
|
|
361
364
|
);
|
|
362
365
|
const result = mockFetch(operation, variables, context);
|
|
363
366
|
|
|
@@ -385,7 +388,7 @@ describe("#mockGqlFetch", () => {
|
|
|
385
388
|
// Act
|
|
386
389
|
mockFetch.mockOperation(
|
|
387
390
|
{operation, variables, context},
|
|
388
|
-
RespondWith.
|
|
391
|
+
RespondWith.graphQLData(data),
|
|
389
392
|
);
|
|
390
393
|
const response = await mockFetch(operation, variables, context);
|
|
391
394
|
const result = await response.text();
|
|
@@ -406,7 +409,7 @@ describe("#mockGqlFetch", () => {
|
|
|
406
409
|
};
|
|
407
410
|
|
|
408
411
|
// Act
|
|
409
|
-
mockFetch.mockOperation({operation}, RespondWith.
|
|
412
|
+
mockFetch.mockOperation({operation}, RespondWith.graphQLData(data));
|
|
410
413
|
const result = Promise.all([
|
|
411
414
|
mockFetch(operation, {a: "variable"}, {my: "context"}),
|
|
412
415
|
mockFetch(operation, {b: "variable"}, {another: "context"}),
|
|
@@ -433,7 +436,10 @@ describe("#mockGqlFetch", () => {
|
|
|
433
436
|
};
|
|
434
437
|
|
|
435
438
|
// Act
|
|
436
|
-
mockFetch.mockOperationOnce(
|
|
439
|
+
mockFetch.mockOperationOnce(
|
|
440
|
+
{operation},
|
|
441
|
+
RespondWith.graphQLData(data),
|
|
442
|
+
);
|
|
437
443
|
const result = mockFetch(
|
|
438
444
|
operation,
|
|
439
445
|
{a: "variable"},
|
|
@@ -456,7 +462,10 @@ describe("#mockGqlFetch", () => {
|
|
|
456
462
|
};
|
|
457
463
|
|
|
458
464
|
// Act
|
|
459
|
-
mockFetch.mockOperationOnce(
|
|
465
|
+
mockFetch.mockOperationOnce(
|
|
466
|
+
{operation},
|
|
467
|
+
RespondWith.graphQLData(data),
|
|
468
|
+
);
|
|
460
469
|
const result = Promise.all([
|
|
461
470
|
mockFetch(operation, {a: "variable"}, {my: "context"}),
|
|
462
471
|
mockFetch(operation, {b: "variable"}, {another: "context"}),
|
|
@@ -3,7 +3,7 @@ import * as React from "react";
|
|
|
3
3
|
import {render, screen, waitFor} from "@testing-library/react";
|
|
4
4
|
|
|
5
5
|
import {GqlRouter, useGql} from "@khanacademy/wonder-blocks-data";
|
|
6
|
-
import {RespondWith} from "
|
|
6
|
+
import {RespondWith} from "../../make-mock-response.js";
|
|
7
7
|
import {mockGqlFetch} from "../mock-gql-fetch.js";
|
|
8
8
|
|
|
9
9
|
describe("integrating mockGqlFetch, RespondWith, GqlRouter and useGql", () => {
|
|
@@ -36,12 +36,12 @@ describe("integrating mockGqlFetch, RespondWith, GqlRouter and useGql", () => {
|
|
|
36
36
|
// Assert
|
|
37
37
|
await waitFor(() =>
|
|
38
38
|
expect(result).toHaveTextContent(
|
|
39
|
-
"No matching
|
|
39
|
+
"No matching mock response found for request",
|
|
40
40
|
),
|
|
41
41
|
);
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
-
it("should resolve with data for RespondWith.
|
|
44
|
+
it("should resolve with data for RespondWith.graphQLData", async () => {
|
|
45
45
|
// Arrange
|
|
46
46
|
const mockFetch = mockGqlFetch();
|
|
47
47
|
const query = {
|
|
@@ -64,7 +64,10 @@ describe("integrating mockGqlFetch, RespondWith, GqlRouter and useGql", () => {
|
|
|
64
64
|
};
|
|
65
65
|
|
|
66
66
|
// Act
|
|
67
|
-
mockFetch.mockOperation(
|
|
67
|
+
mockFetch.mockOperation(
|
|
68
|
+
{operation: query},
|
|
69
|
+
RespondWith.graphQLData(data),
|
|
70
|
+
);
|
|
68
71
|
render(
|
|
69
72
|
<GqlRouter defaultContext={{}} fetch={mockFetch}>
|
|
70
73
|
<RenderData />
|
|
@@ -1,89 +1,18 @@
|
|
|
1
1
|
// @flow
|
|
2
|
-
import type {GqlContext} from "@khanacademy/wonder-blocks-data";
|
|
3
2
|
import {gqlRequestMatchesMock} from "./gql-request-matches-mock.js";
|
|
4
|
-
import {
|
|
5
|
-
import type {
|
|
6
|
-
import type {GqlMock, GqlMockOperation, GqlFetchMockFn} from "./types.js";
|
|
3
|
+
import {mockRequester} from "../mock-requester.js";
|
|
4
|
+
import type {GqlFetchMockFn, GqlMockOperation} from "./types.js";
|
|
7
5
|
|
|
8
6
|
/**
|
|
9
7
|
* A mock for the fetch function passed to GqlRouter.
|
|
10
8
|
*/
|
|
11
|
-
export const mockGqlFetch = (): GqlFetchMockFn =>
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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}
|
|
9
|
+
export const mockGqlFetch = (): GqlFetchMockFn =>
|
|
10
|
+
mockRequester<GqlMockOperation<any, any, any>, _>(
|
|
11
|
+
gqlRequestMatchesMock,
|
|
12
|
+
(operation, variables, context) =>
|
|
13
|
+
`Operation: ${operation.type} ${operation.id}
|
|
48
14
|
Variables: ${
|
|
49
15
|
variables == null ? "None" : JSON.stringify(variables, null, 2)
|
|
50
16
|
}
|
|
51
|
-
Context: ${JSON.stringify(context, null, 2)}
|
|
52
|
-
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
const addMockedOperation = <TData, TVariables: {...}, TContext: GqlContext>(
|
|
56
|
-
operation: GqlMockOperation<TData, TVariables, TContext>,
|
|
57
|
-
response: GqlMockResponse<TData>,
|
|
58
|
-
onceOnly: boolean,
|
|
59
|
-
): GqlFetchMockFn => {
|
|
60
|
-
const mockResponse = () => makeGqlMockResponse(response);
|
|
61
|
-
mocks.push({
|
|
62
|
-
operation,
|
|
63
|
-
response: mockResponse,
|
|
64
|
-
onceOnly,
|
|
65
|
-
used: false,
|
|
66
|
-
});
|
|
67
|
-
return gqlFetchMock;
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
gqlFetchMock.mockOperation = <
|
|
71
|
-
TData,
|
|
72
|
-
TVariables: {...},
|
|
73
|
-
TContext: GqlContext,
|
|
74
|
-
>(
|
|
75
|
-
operation: GqlMockOperation<TData, TVariables, TContext>,
|
|
76
|
-
response: GqlMockResponse<TData>,
|
|
77
|
-
): GqlFetchMockFn => addMockedOperation(operation, response, false);
|
|
78
|
-
|
|
79
|
-
gqlFetchMock.mockOperationOnce = <
|
|
80
|
-
TData,
|
|
81
|
-
TVariables: {...},
|
|
82
|
-
TContext: GqlContext,
|
|
83
|
-
>(
|
|
84
|
-
operation: GqlMockOperation<TData, TVariables, TContext>,
|
|
85
|
-
response: GqlMockResponse<TData>,
|
|
86
|
-
): GqlFetchMockFn => addMockedOperation(operation, response, true);
|
|
87
|
-
|
|
88
|
-
return gqlFetchMock;
|
|
89
|
-
};
|
|
17
|
+
Context: ${JSON.stringify(context, null, 2)}`,
|
|
18
|
+
);
|
package/src/gql/types.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
//@flow
|
|
2
2
|
import type {GqlOperation, GqlContext} from "@khanacademy/wonder-blocks-data";
|
|
3
|
-
import type {
|
|
3
|
+
import type {OperationMock, GraphQLJson} from "../types.js";
|
|
4
|
+
import type {MockResponse} from "../make-mock-response.js";
|
|
4
5
|
|
|
5
6
|
export type GqlMockOperation<
|
|
6
|
-
TData,
|
|
7
|
+
TData: {...},
|
|
7
8
|
TVariables: {...},
|
|
8
9
|
TContext: GqlContext,
|
|
9
10
|
> = {|
|
|
@@ -12,9 +13,14 @@ export type GqlMockOperation<
|
|
|
12
13
|
context?: TContext,
|
|
13
14
|
|};
|
|
14
15
|
|
|
15
|
-
type GqlMockOperationFn = <
|
|
16
|
+
type GqlMockOperationFn = <
|
|
17
|
+
TData: {...},
|
|
18
|
+
TVariables: {...},
|
|
19
|
+
TContext: GqlContext,
|
|
20
|
+
TResponseData: GraphQLJson<TData>,
|
|
21
|
+
>(
|
|
16
22
|
operation: GqlMockOperation<TData, TVariables, TContext>,
|
|
17
|
-
response:
|
|
23
|
+
response: MockResponse<TResponseData>,
|
|
18
24
|
) => GqlFetchMockFn;
|
|
19
25
|
|
|
20
26
|
export type GqlFetchMockFn = {|
|
|
@@ -27,9 +33,4 @@ export type GqlFetchMockFn = {|
|
|
|
27
33
|
mockOperationOnce: GqlMockOperationFn,
|
|
28
34
|
|};
|
|
29
35
|
|
|
30
|
-
export type GqlMock =
|
|
31
|
-
operation: GqlMockOperation<any, any, any>,
|
|
32
|
-
onceOnly: boolean,
|
|
33
|
-
used: boolean,
|
|
34
|
-
response: () => Promise<Response>,
|
|
35
|
-
|};
|
|
36
|
+
export type GqlMock = OperationMock<GqlMockOperation<any, any, any>>;
|
package/src/index.js
CHANGED
|
@@ -17,8 +17,14 @@ export type {
|
|
|
17
17
|
FixturesOptions,
|
|
18
18
|
} from "./fixtures/types.js";
|
|
19
19
|
|
|
20
|
-
//
|
|
20
|
+
// Fetch mocking framework
|
|
21
|
+
export type {MockResponse} from "./make-mock-response.js";
|
|
22
|
+
export {RespondWith} from "./make-mock-response.js";
|
|
23
|
+
export {mockFetch} from "./fetch/mock-fetch.js";
|
|
24
|
+
export type {
|
|
25
|
+
FetchMockFn,
|
|
26
|
+
FetchMock,
|
|
27
|
+
FetchMockOperation,
|
|
28
|
+
} from "./fetch/types.js";
|
|
21
29
|
export {mockGqlFetch} from "./gql/mock-gql-fetch.js";
|
|
22
|
-
export type {GqlMockResponse} from "./gql/make-gql-mock-response.js";
|
|
23
|
-
export {RespondWith} from "./gql/make-gql-mock-response.js";
|
|
24
30
|
export type {GqlFetchMockFn, GqlMock, GqlMockOperation} from "./gql/types.js";
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import {ResponseImpl} from "./response-impl.js";
|
|
3
|
+
import type {GraphQLJson} from "./types.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Describes a mock response to a fetch request.
|
|
7
|
+
*/
|
|
8
|
+
export opaque type MockResponse<TJson> =
|
|
9
|
+
| {|
|
|
10
|
+
type: "text",
|
|
11
|
+
text: string | (() => string),
|
|
12
|
+
statusCode: number,
|
|
13
|
+
|}
|
|
14
|
+
| {|
|
|
15
|
+
type: "reject",
|
|
16
|
+
error: Error | (() => Error),
|
|
17
|
+
|};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Helper for creating a text-based mock response.
|
|
21
|
+
*/
|
|
22
|
+
const textResponse = <TData>(
|
|
23
|
+
text: string | (() => string),
|
|
24
|
+
statusCode: number = 200,
|
|
25
|
+
): MockResponse<TData> => ({
|
|
26
|
+
type: "text",
|
|
27
|
+
text,
|
|
28
|
+
statusCode,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Helper for creating a rejected mock response.
|
|
33
|
+
*/
|
|
34
|
+
const rejectResponse = (error: Error | (() => Error)): MockResponse<empty> => ({
|
|
35
|
+
type: "reject",
|
|
36
|
+
error,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Helpers to define mock responses for mocked requests.
|
|
41
|
+
*/
|
|
42
|
+
export const RespondWith = Object.freeze({
|
|
43
|
+
/**
|
|
44
|
+
* Response with text body and status code.
|
|
45
|
+
* Status code defaults to 200.
|
|
46
|
+
*/
|
|
47
|
+
text: <TData = string>(
|
|
48
|
+
text: string,
|
|
49
|
+
statusCode: number = 200,
|
|
50
|
+
): MockResponse<TData> => textResponse<TData>(text, statusCode),
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Response with JSON body and status code 200.
|
|
54
|
+
*/
|
|
55
|
+
json: <TJson: {...}>(json: TJson): MockResponse<TJson> =>
|
|
56
|
+
textResponse<TJson>(() => JSON.stringify(json)),
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Response with GraphQL data JSON body and status code 200.
|
|
60
|
+
*/
|
|
61
|
+
graphQLData: <TData: {...}>(
|
|
62
|
+
data: TData,
|
|
63
|
+
): MockResponse<GraphQLJson<TData>> =>
|
|
64
|
+
textResponse<GraphQLJson<TData>>(() => JSON.stringify({data})),
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Response with body that will not parse as JSON and status code 200.
|
|
68
|
+
*/
|
|
69
|
+
unparseableBody: (): MockResponse<any> => textResponse("INVALID JSON"),
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Rejects with an AbortError to simulate an aborted request.
|
|
73
|
+
*/
|
|
74
|
+
abortedRequest: (): MockResponse<any> =>
|
|
75
|
+
rejectResponse(() => {
|
|
76
|
+
const abortError = new Error("Mock request aborted");
|
|
77
|
+
abortError.name = "AbortError";
|
|
78
|
+
return abortError;
|
|
79
|
+
}),
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Rejects with the given error.
|
|
83
|
+
*/
|
|
84
|
+
reject: (error: Error): MockResponse<any> => rejectResponse(error),
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* A non-200 status code with empty text body.
|
|
88
|
+
* Equivalent to calling `ResponseWith.text("", statusCode)`.
|
|
89
|
+
*/
|
|
90
|
+
errorStatusCode: (statusCode: number): MockResponse<any> => {
|
|
91
|
+
if (statusCode < 300) {
|
|
92
|
+
throw new Error(`${statusCode} is not a valid error status code`);
|
|
93
|
+
}
|
|
94
|
+
return textResponse("{}", statusCode);
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Response body that is valid JSON but not a valid GraphQL response.
|
|
99
|
+
*/
|
|
100
|
+
nonGraphQLBody: (): MockResponse<any> =>
|
|
101
|
+
textResponse(() =>
|
|
102
|
+
JSON.stringify({
|
|
103
|
+
valid: "json",
|
|
104
|
+
that: "is not a valid graphql response",
|
|
105
|
+
}),
|
|
106
|
+
),
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Response that is a GraphQL errors response with status code 200.
|
|
110
|
+
*/
|
|
111
|
+
graphQLErrors: (
|
|
112
|
+
errorMessages: $ReadOnlyArray<string>,
|
|
113
|
+
): MockResponse<GraphQLJson<any>> =>
|
|
114
|
+
textResponse<GraphQLJson<any>>(() =>
|
|
115
|
+
JSON.stringify({
|
|
116
|
+
errors: errorMessages.map((e) => ({
|
|
117
|
+
message: e,
|
|
118
|
+
})),
|
|
119
|
+
}),
|
|
120
|
+
),
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Turns a MockResponse value to an actual Response that represents the mock.
|
|
125
|
+
*/
|
|
126
|
+
export const makeMockResponse = (
|
|
127
|
+
response: MockResponse<any>,
|
|
128
|
+
): Promise<Response> => {
|
|
129
|
+
switch (response.type) {
|
|
130
|
+
case "text":
|
|
131
|
+
const text =
|
|
132
|
+
typeof response.text === "function"
|
|
133
|
+
? response.text()
|
|
134
|
+
: response.text;
|
|
135
|
+
|
|
136
|
+
return Promise.resolve(
|
|
137
|
+
new ResponseImpl(text, {status: response.statusCode}),
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
case "reject":
|
|
141
|
+
const error =
|
|
142
|
+
response.error instanceof Error
|
|
143
|
+
? response.error
|
|
144
|
+
: response.error();
|
|
145
|
+
return Promise.reject(error);
|
|
146
|
+
|
|
147
|
+
default:
|
|
148
|
+
throw new Error(`Unknown response type: ${response.type}`);
|
|
149
|
+
}
|
|
150
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import {makeMockResponse} from "./make-mock-response.js";
|
|
3
|
+
import type {MockResponse} from "./make-mock-response.js";
|
|
4
|
+
import type {OperationMock, OperationMatcher, MockFn} from "./types.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A generic mock request function for using when mocking fetch or gqlFetch.
|
|
8
|
+
*/
|
|
9
|
+
export const mockRequester = <
|
|
10
|
+
TOperationType,
|
|
11
|
+
TOperationMock: OperationMock<TOperationType>,
|
|
12
|
+
>(
|
|
13
|
+
operationMatcher: OperationMatcher<any>,
|
|
14
|
+
operationToString: (
|
|
15
|
+
operationMock: TOperationMock,
|
|
16
|
+
...args: Array<any>
|
|
17
|
+
) => string,
|
|
18
|
+
): MockFn<TOperationType> => {
|
|
19
|
+
// We want this to work in jest and in fixtures to make life easy for folks.
|
|
20
|
+
// This is the array of mocked operations that we will traverse and
|
|
21
|
+
// manipulate.
|
|
22
|
+
const mocks: Array<OperationMock<any>> = [];
|
|
23
|
+
|
|
24
|
+
// What we return has to be a drop in for the fetch function that is
|
|
25
|
+
// provided to `GqlRouter` which is how folks will then use this mock.
|
|
26
|
+
const mockFn: MockFn<TOperationType> = (
|
|
27
|
+
...args: Array<any>
|
|
28
|
+
): Promise<Response> => {
|
|
29
|
+
// Iterate our mocked operations and find the first one that matches.
|
|
30
|
+
for (const mock of mocks) {
|
|
31
|
+
if (mock.onceOnly && mock.used) {
|
|
32
|
+
// This is a once-only mock and it has been used, so skip it.
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (operationMatcher(mock.operation, ...args)) {
|
|
36
|
+
mock.used = true;
|
|
37
|
+
return mock.response();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Default is to reject with some helpful info on what request
|
|
42
|
+
// we rejected.
|
|
43
|
+
return Promise.reject(
|
|
44
|
+
new Error(`No matching mock response found for request:
|
|
45
|
+
${operationToString(...args)}`),
|
|
46
|
+
);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const addMockedOperation = <TOperation>(
|
|
50
|
+
operation: TOperation,
|
|
51
|
+
response: MockResponse<any>,
|
|
52
|
+
onceOnly: boolean,
|
|
53
|
+
): MockFn<TOperationType> => {
|
|
54
|
+
const mockResponse = () => makeMockResponse(response);
|
|
55
|
+
mocks.push({
|
|
56
|
+
operation,
|
|
57
|
+
response: mockResponse,
|
|
58
|
+
onceOnly,
|
|
59
|
+
used: false,
|
|
60
|
+
});
|
|
61
|
+
return mockFn;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
mockFn.mockOperation = <TOperation>(
|
|
65
|
+
operation: TOperation,
|
|
66
|
+
response: MockResponse<any>,
|
|
67
|
+
): MockFn<TOperationType> => addMockedOperation(operation, response, false);
|
|
68
|
+
|
|
69
|
+
mockFn.mockOperationOnce = <TOperation>(
|
|
70
|
+
operation: TOperation,
|
|
71
|
+
response: MockResponse<any>,
|
|
72
|
+
): MockFn<TOperationType> => addMockedOperation(operation, response, true);
|
|
73
|
+
|
|
74
|
+
return mockFn;
|
|
75
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
|
|
3
|
+
// We need a version of Response. When we're in Jest JSDOM environment or a
|
|
4
|
+
// version of Node that supports the fetch API (17 and up, possibly with
|
|
5
|
+
// --experimental-fetch flag), then we're good, but otherwise we need an
|
|
6
|
+
// implementation, so this uses node-fetch as a peer dependency and uses that
|
|
7
|
+
// to provide the implementation if we don't already have one.
|
|
8
|
+
export const ResponseImpl: typeof Response =
|
|
9
|
+
typeof Response === "undefined" ? require("node-fetch").Response : Response;
|
package/src/types.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import type {MockResponse} from "./make-mock-response.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A valid GraphQL response as supported by our mocking framework.
|
|
6
|
+
* Note that we don't currently support both data and errors being set.
|
|
7
|
+
*/
|
|
8
|
+
export type GraphQLJson<TData: {...}> =
|
|
9
|
+
| {|
|
|
10
|
+
data: TData,
|
|
11
|
+
|}
|
|
12
|
+
| {|
|
|
13
|
+
errors: Array<{|
|
|
14
|
+
message: string,
|
|
15
|
+
|}>,
|
|
16
|
+
|};
|
|
17
|
+
|
|
18
|
+
export type MockFn<TOperationType> = {|
|
|
19
|
+
(...args: Array<any>): Promise<Response>,
|
|
20
|
+
mockOperation: MockOperationFn<TOperationType>,
|
|
21
|
+
mockOperationOnce: MockOperationFn<TOperationType>,
|
|
22
|
+
|};
|
|
23
|
+
|
|
24
|
+
export type OperationMock<TOperation> = {|
|
|
25
|
+
operation: TOperation,
|
|
26
|
+
onceOnly: boolean,
|
|
27
|
+
used: boolean,
|
|
28
|
+
response: () => Promise<Response>,
|
|
29
|
+
|};
|
|
30
|
+
|
|
31
|
+
export type OperationMatcher<TOperation> = (
|
|
32
|
+
operation: TOperation,
|
|
33
|
+
...args: Array<any>
|
|
34
|
+
) => boolean;
|
|
35
|
+
|
|
36
|
+
export type MockOperationFn<TOperationType> = <TOperation: TOperationType>(
|
|
37
|
+
operation: TOperation,
|
|
38
|
+
response: MockResponse<any>,
|
|
39
|
+
) => MockFn<TOperationType>;
|