@khanacademy/wonder-blocks-testing 7.0.3 → 7.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 +25 -0
- package/dist/es/index.js +160 -62
- package/dist/index.js +326 -167
- package/package.json +2 -2
- package/src/__docs__/exports.respond-with.stories.mdx +17 -6
- package/src/__docs__/exports.settle-controller.stories.mdx +32 -0
- package/src/__docs__/types.mock-response.stories.mdx +6 -2
- package/src/__tests__/mock-requester.test.js +1 -1
- package/src/__tests__/respond-with.test.js +525 -0
- package/src/__tests__/settle-controller.test.js +29 -0
- package/src/__tests__/settle-signal.test.js +105 -0
- package/src/fetch/__tests__/mock-fetch.test.js +1 -1
- package/src/fetch/types.js +1 -1
- package/src/gql/__tests__/mock-gql-fetch.test.js +1 -1
- package/src/gql/__tests__/wb-data-integration.test.js +1 -1
- package/src/gql/types.js +1 -1
- package/src/index.js +3 -2
- package/src/mock-requester.js +2 -3
- package/src/respond-with.js +236 -0
- package/src/settle-controller.js +35 -0
- package/src/settle-signal.js +41 -0
- package/src/types.js +1 -1
- package/src/__tests__/make-mock-response.test.js +0 -460
- package/src/make-mock-response.js +0 -150
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import {SettleSignal} from "../settle-signal.js";
|
|
3
|
+
|
|
4
|
+
describe("SettleSignal", () => {
|
|
5
|
+
it("should extend EventTarget", () => {
|
|
6
|
+
// Arrange
|
|
7
|
+
|
|
8
|
+
// Act
|
|
9
|
+
const result = new SettleSignal();
|
|
10
|
+
|
|
11
|
+
// Assert
|
|
12
|
+
expect(result).toBeInstanceOf(EventTarget);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("should start with settled = false", () => {
|
|
16
|
+
// Arrange
|
|
17
|
+
|
|
18
|
+
// Act
|
|
19
|
+
const result = new SettleSignal();
|
|
20
|
+
|
|
21
|
+
// Assert
|
|
22
|
+
expect(result).toHaveProperty("settled", false);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("should invoke the passed function with a function", () => {
|
|
26
|
+
// Arrange
|
|
27
|
+
const setSettleFn = jest.fn();
|
|
28
|
+
|
|
29
|
+
// Act
|
|
30
|
+
// eslint-disable-next-line no-new
|
|
31
|
+
new SettleSignal(setSettleFn);
|
|
32
|
+
|
|
33
|
+
// Assert
|
|
34
|
+
expect(setSettleFn).toHaveBeenCalledWith(expect.any(Function));
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe("setSettleFn argument", () => {
|
|
38
|
+
it("should set settled to true", () => {
|
|
39
|
+
// Arrange
|
|
40
|
+
const setSettleFn = jest.fn();
|
|
41
|
+
const signal = new SettleSignal(setSettleFn);
|
|
42
|
+
const settle = setSettleFn.mock.calls[0][0];
|
|
43
|
+
|
|
44
|
+
// Act
|
|
45
|
+
settle();
|
|
46
|
+
|
|
47
|
+
// Assert
|
|
48
|
+
expect(signal.settled).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("should raise the settled event", () => {
|
|
52
|
+
// Arrange
|
|
53
|
+
const setSettleFn = jest.fn();
|
|
54
|
+
const signal = new SettleSignal(setSettleFn);
|
|
55
|
+
const settle = setSettleFn.mock.calls[0][0];
|
|
56
|
+
const handler = jest.fn();
|
|
57
|
+
signal.addEventListener("settled", handler);
|
|
58
|
+
|
|
59
|
+
// Act
|
|
60
|
+
settle();
|
|
61
|
+
|
|
62
|
+
// Assert
|
|
63
|
+
expect(handler).toHaveBeenCalled();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("should throw if the signal has already been settled", () => {
|
|
67
|
+
// Arrange
|
|
68
|
+
const setSettleFn = jest.fn();
|
|
69
|
+
// eslint-disable-next-line no-new
|
|
70
|
+
new SettleSignal(setSettleFn);
|
|
71
|
+
const settle = setSettleFn.mock.calls[0][0];
|
|
72
|
+
settle();
|
|
73
|
+
|
|
74
|
+
// Act
|
|
75
|
+
const result = () => settle();
|
|
76
|
+
|
|
77
|
+
// Assert
|
|
78
|
+
expect(result).toThrowErrorMatchingInlineSnapshot(
|
|
79
|
+
`"SettleSignal already settled"`,
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("#SettleSignal.settle", () => {
|
|
85
|
+
it("should return a SettleSignal", () => {
|
|
86
|
+
// Arrange
|
|
87
|
+
|
|
88
|
+
// Act
|
|
89
|
+
const result = SettleSignal.settle();
|
|
90
|
+
|
|
91
|
+
// Assert
|
|
92
|
+
expect(result).toBeInstanceOf(SettleSignal);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("should return a SettleSignal that is already settled", () => {
|
|
96
|
+
// Arrange
|
|
97
|
+
|
|
98
|
+
// Act
|
|
99
|
+
const result = SettleSignal.settle();
|
|
100
|
+
|
|
101
|
+
// Assert
|
|
102
|
+
expect(result).toHaveProperty("settled", true);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
});
|
package/src/fetch/types.js
CHANGED
|
@@ -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 "../../respond-with.js";
|
|
7
7
|
import {mockGqlFetch} from "../mock-gql-fetch.js";
|
|
8
8
|
|
|
9
9
|
describe("#mockGqlFetch", () => {
|
|
@@ -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 "../../respond-with.js";
|
|
7
7
|
import {mockGqlFetch} from "../mock-gql-fetch.js";
|
|
8
8
|
|
|
9
9
|
describe("integrating mockGqlFetch, RespondWith, GqlRouter and useGql", () => {
|
package/src/gql/types.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
//@flow
|
|
2
2
|
import type {GqlOperation, GqlContext} from "@khanacademy/wonder-blocks-data";
|
|
3
3
|
import type {GraphQLJson} from "../types.js";
|
|
4
|
-
import type {MockResponse} from "../
|
|
4
|
+
import type {MockResponse} from "../respond-with.js";
|
|
5
5
|
|
|
6
6
|
export type GqlMockOperation<
|
|
7
7
|
TData: {...},
|
package/src/index.js
CHANGED
|
@@ -11,8 +11,9 @@ export type {
|
|
|
11
11
|
// Fetch mocking framework
|
|
12
12
|
export {mockFetch} from "./fetch/mock-fetch.js";
|
|
13
13
|
export {mockGqlFetch} from "./gql/mock-gql-fetch.js";
|
|
14
|
-
export {RespondWith} from "./
|
|
15
|
-
export
|
|
14
|
+
export {RespondWith} from "./respond-with.js";
|
|
15
|
+
export {SettleController} from "./settle-controller.js";
|
|
16
|
+
export type {MockResponse} from "./respond-with.js";
|
|
16
17
|
export type {FetchMockFn, FetchMockOperation} from "./fetch/types.js";
|
|
17
18
|
export type {GqlFetchMockFn, GqlMockOperation} from "./gql/types.js";
|
|
18
19
|
|
package/src/mock-requester.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
// @flow
|
|
2
|
-
import {
|
|
3
|
-
import type {MockResponse} from "./make-mock-response.js";
|
|
2
|
+
import type {MockResponse} from "./respond-with.js";
|
|
4
3
|
import type {OperationMock, OperationMatcher, MockFn} from "./types.js";
|
|
5
4
|
|
|
6
5
|
/**
|
|
@@ -51,7 +50,7 @@ export const mockRequester = <
|
|
|
51
50
|
response: MockResponse<any>,
|
|
52
51
|
onceOnly: boolean,
|
|
53
52
|
): MockFn<TOperationType> => {
|
|
54
|
-
const mockResponse = () =>
|
|
53
|
+
const mockResponse = () => response.toPromise();
|
|
55
54
|
mocks.push({
|
|
56
55
|
operation,
|
|
57
56
|
response: mockResponse,
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import {SettleSignal} from "./settle-signal.js";
|
|
3
|
+
import {ResponseImpl} from "./response-impl.js";
|
|
4
|
+
import type {GraphQLJson} from "./types.js";
|
|
5
|
+
|
|
6
|
+
// We want the parameterization here so that folks can assert a response is
|
|
7
|
+
// of a specific type if passing between various functions. For example,
|
|
8
|
+
// the graphql mocking framework might want to assert a response is returning
|
|
9
|
+
// the expected data structure. We could use `opaque` but that would then
|
|
10
|
+
// hide the `toPromise` call we want to provide.
|
|
11
|
+
/* eslint-disable no-unused-vars */
|
|
12
|
+
/**
|
|
13
|
+
* Describes a mock response to a fetch request.
|
|
14
|
+
*/
|
|
15
|
+
export type MockResponse<TData> = {|
|
|
16
|
+
/**
|
|
17
|
+
* Create a promise from the mocked response.
|
|
18
|
+
*
|
|
19
|
+
* If a signal was provided when the mock response was created, the promise
|
|
20
|
+
* will only settle to resolution or rejection if the signal is raised.
|
|
21
|
+
*/
|
|
22
|
+
+toPromise: () => Promise<Response>,
|
|
23
|
+
|};
|
|
24
|
+
/* eslint-enable no-unused-vars */
|
|
25
|
+
|
|
26
|
+
type InternalMockResponse =
|
|
27
|
+
| {|
|
|
28
|
+
+type: "text",
|
|
29
|
+
+text: string | (() => string),
|
|
30
|
+
+statusCode: number,
|
|
31
|
+
+signal: ?SettleSignal,
|
|
32
|
+
|}
|
|
33
|
+
| {|
|
|
34
|
+
+type: "reject",
|
|
35
|
+
+error: Error | (() => Error),
|
|
36
|
+
+signal: ?SettleSignal,
|
|
37
|
+
|};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Helper for creating a text-based mock response.
|
|
41
|
+
*/
|
|
42
|
+
const textResponse = <TData>(
|
|
43
|
+
text: string | (() => string),
|
|
44
|
+
statusCode: number,
|
|
45
|
+
signal: ?SettleSignal,
|
|
46
|
+
): MockResponse<TData> => ({
|
|
47
|
+
toPromise: () =>
|
|
48
|
+
makeMockResponse({
|
|
49
|
+
type: "text",
|
|
50
|
+
text,
|
|
51
|
+
statusCode,
|
|
52
|
+
signal,
|
|
53
|
+
}),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Helper for creating a rejected mock response.
|
|
58
|
+
*/
|
|
59
|
+
const rejectResponse = (
|
|
60
|
+
error: Error | (() => Error),
|
|
61
|
+
signal: ?SettleSignal,
|
|
62
|
+
): MockResponse<empty> => ({
|
|
63
|
+
toPromise: () =>
|
|
64
|
+
makeMockResponse({
|
|
65
|
+
type: "reject",
|
|
66
|
+
error,
|
|
67
|
+
signal,
|
|
68
|
+
}),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Helpers to define mock responses for mocked requests.
|
|
73
|
+
*/
|
|
74
|
+
export const RespondWith = Object.freeze({
|
|
75
|
+
/**
|
|
76
|
+
* Response with text body and status code.
|
|
77
|
+
* Status code defaults to 200.
|
|
78
|
+
*/
|
|
79
|
+
text: <TData = string>(
|
|
80
|
+
text: string,
|
|
81
|
+
statusCode: number = 200,
|
|
82
|
+
signal: ?SettleSignal = null,
|
|
83
|
+
): MockResponse<TData> => textResponse<TData>(text, statusCode, signal),
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Response with JSON body and status code 200.
|
|
87
|
+
*/
|
|
88
|
+
json: <TJson: {...}>(
|
|
89
|
+
json: TJson,
|
|
90
|
+
signal: ?SettleSignal = null,
|
|
91
|
+
): MockResponse<TJson> =>
|
|
92
|
+
textResponse<TJson>(() => JSON.stringify(json), 200, signal),
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Response with GraphQL data JSON body and status code 200.
|
|
96
|
+
*/
|
|
97
|
+
graphQLData: <TData: {...}>(
|
|
98
|
+
data: TData,
|
|
99
|
+
signal: ?SettleSignal = null,
|
|
100
|
+
): MockResponse<GraphQLJson<TData>> =>
|
|
101
|
+
textResponse<GraphQLJson<TData>>(
|
|
102
|
+
() => JSON.stringify({data}),
|
|
103
|
+
200,
|
|
104
|
+
signal,
|
|
105
|
+
),
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Response with body that will not parse as JSON and status code 200.
|
|
109
|
+
*/
|
|
110
|
+
unparseableBody: (signal: ?SettleSignal = null): MockResponse<any> =>
|
|
111
|
+
textResponse("INVALID JSON", 200, signal),
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Rejects with an AbortError to simulate an aborted request.
|
|
115
|
+
*/
|
|
116
|
+
abortedRequest: (signal: ?SettleSignal = null): MockResponse<any> =>
|
|
117
|
+
rejectResponse(() => {
|
|
118
|
+
const abortError = new Error("Mock request aborted");
|
|
119
|
+
abortError.name = "AbortError";
|
|
120
|
+
return abortError;
|
|
121
|
+
}, signal),
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Rejects with the given error.
|
|
125
|
+
*/
|
|
126
|
+
reject: (error: Error, signal: ?SettleSignal = null): MockResponse<any> =>
|
|
127
|
+
rejectResponse(error, signal),
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* A non-200 status code with empty text body.
|
|
131
|
+
* Equivalent to calling `ResponseWith.text("", statusCode)`.
|
|
132
|
+
*/
|
|
133
|
+
errorStatusCode: (
|
|
134
|
+
statusCode: number,
|
|
135
|
+
signal: ?SettleSignal = null,
|
|
136
|
+
): MockResponse<any> => {
|
|
137
|
+
if (statusCode < 300) {
|
|
138
|
+
throw new Error(`${statusCode} is not a valid error status code`);
|
|
139
|
+
}
|
|
140
|
+
return textResponse("{}", statusCode, signal);
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Response body that is valid JSON but not a valid GraphQL response.
|
|
145
|
+
*/
|
|
146
|
+
nonGraphQLBody: (signal: ?SettleSignal = null): MockResponse<any> =>
|
|
147
|
+
textResponse(
|
|
148
|
+
() =>
|
|
149
|
+
JSON.stringify({
|
|
150
|
+
valid: "json",
|
|
151
|
+
that: "is not a valid graphql response",
|
|
152
|
+
}),
|
|
153
|
+
200,
|
|
154
|
+
signal,
|
|
155
|
+
),
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Response that is a GraphQL errors response with status code 200.
|
|
159
|
+
*/
|
|
160
|
+
graphQLErrors: (
|
|
161
|
+
errorMessages: $ReadOnlyArray<string>,
|
|
162
|
+
signal: ?SettleSignal = null,
|
|
163
|
+
): MockResponse<GraphQLJson<any>> =>
|
|
164
|
+
textResponse<GraphQLJson<any>>(
|
|
165
|
+
() =>
|
|
166
|
+
JSON.stringify({
|
|
167
|
+
errors: errorMessages.map((e) => ({
|
|
168
|
+
message: e,
|
|
169
|
+
})),
|
|
170
|
+
}),
|
|
171
|
+
200,
|
|
172
|
+
signal,
|
|
173
|
+
),
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const callOnSettled = (signal: ?SettleSignal, fn: () => void): void => {
|
|
177
|
+
if (signal == null || signal.settled) {
|
|
178
|
+
fn();
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const onSettled = () => {
|
|
183
|
+
signal.removeEventListener("settled", onSettled);
|
|
184
|
+
fn();
|
|
185
|
+
};
|
|
186
|
+
signal.addEventListener("settled", onSettled);
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Turns a MockResponse value to an actual Response that represents the mock.
|
|
191
|
+
*/
|
|
192
|
+
const makeMockResponse = (
|
|
193
|
+
response: InternalMockResponse,
|
|
194
|
+
): Promise<Response> => {
|
|
195
|
+
const {signal} = response;
|
|
196
|
+
|
|
197
|
+
switch (response.type) {
|
|
198
|
+
case "text":
|
|
199
|
+
return new Promise((resolve, reject) => {
|
|
200
|
+
callOnSettled(signal, () => {
|
|
201
|
+
const text =
|
|
202
|
+
typeof response.text === "function"
|
|
203
|
+
? response.text()
|
|
204
|
+
: response.text;
|
|
205
|
+
resolve(
|
|
206
|
+
new ResponseImpl(text, {status: response.statusCode}),
|
|
207
|
+
);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
case "reject":
|
|
212
|
+
return new Promise((resolve, reject) => {
|
|
213
|
+
callOnSettled(signal, () =>
|
|
214
|
+
reject(
|
|
215
|
+
response.error instanceof Error
|
|
216
|
+
? response.error
|
|
217
|
+
: response.error(),
|
|
218
|
+
),
|
|
219
|
+
);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
/* istanbul ignore next */
|
|
223
|
+
default:
|
|
224
|
+
if (process.env.NODE_ENV !== "production") {
|
|
225
|
+
// If we're not in production, give an immediate signal that the
|
|
226
|
+
// dev forgot to support this new type.
|
|
227
|
+
throw new Error(`Unknown response type: ${response.type}`);
|
|
228
|
+
}
|
|
229
|
+
// Production; assume a rejection.
|
|
230
|
+
return makeMockResponse({
|
|
231
|
+
type: "reject",
|
|
232
|
+
error: new Error("Unknown response type"),
|
|
233
|
+
signal,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import {SettleSignal} from "./settle-signal.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A controller for the `RespondWith` API to control response settlement.
|
|
6
|
+
*/
|
|
7
|
+
export class SettleController {
|
|
8
|
+
#settleFn: () => void;
|
|
9
|
+
#signal: SettleSignal;
|
|
10
|
+
|
|
11
|
+
constructor() {
|
|
12
|
+
// Create our signal.
|
|
13
|
+
// We pass in a method to capture it's settle function so that
|
|
14
|
+
// only we can call it.
|
|
15
|
+
this.#signal = new SettleSignal(
|
|
16
|
+
(settleFn) => (this.#settleFn = settleFn),
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The signal to pass to the `RespondWith` API.
|
|
22
|
+
*/
|
|
23
|
+
get signal(): SettleSignal {
|
|
24
|
+
return this.#signal;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Settle the signal and therefore any associated responses.
|
|
29
|
+
*
|
|
30
|
+
* @throws {Error} if the signal has already been settled.
|
|
31
|
+
*/
|
|
32
|
+
settle(): void {
|
|
33
|
+
this.#settleFn();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
/**
|
|
3
|
+
* A signal for controlling the `RespondWith` API responses.
|
|
4
|
+
*
|
|
5
|
+
* This provide finely-grained control over the promise lifecycle to support
|
|
6
|
+
* complex test scenarios.
|
|
7
|
+
*/
|
|
8
|
+
export class SettleSignal extends EventTarget {
|
|
9
|
+
#settled: boolean = false;
|
|
10
|
+
|
|
11
|
+
constructor(setSettleFn: ?(settleFn: () => void) => mixed = null) {
|
|
12
|
+
super();
|
|
13
|
+
|
|
14
|
+
// If we were given a function, we call it with a method that will
|
|
15
|
+
// settle ourselves. This allows the appropriate SettleController
|
|
16
|
+
// to be in charge of settling this instance.
|
|
17
|
+
setSettleFn?.(() => {
|
|
18
|
+
if (this.#settled) {
|
|
19
|
+
throw new Error("SettleSignal already settled");
|
|
20
|
+
}
|
|
21
|
+
this.#settled = true;
|
|
22
|
+
this.dispatchEvent(new Event("settled"));
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* An already settled signal.
|
|
28
|
+
*/
|
|
29
|
+
static settle(): SettleSignal {
|
|
30
|
+
const signal = new SettleSignal();
|
|
31
|
+
signal.#settled = true;
|
|
32
|
+
return signal;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Has this signal been settled yet?
|
|
37
|
+
*/
|
|
38
|
+
get settled(): boolean {
|
|
39
|
+
return this.#settled;
|
|
40
|
+
}
|
|
41
|
+
}
|
package/src/types.js
CHANGED