@khanacademy/wonder-blocks-testing 3.0.1 → 4.0.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 +27 -0
- package/dist/es/index.js +144 -323
- package/dist/index.js +259 -138
- package/package.json +4 -3
- 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/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
|
@@ -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>;
|
|
@@ -1,124 +0,0 @@
|
|
|
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
|
-
};
|