@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.
@@ -16,19 +16,23 @@ interface RespondWith {
16
16
  /**
17
17
  * Rejects with an AbortError to simulate an aborted request.
18
18
  */
19
- abortedRequest: () => MockResponse<any>;
19
+ abortedRequest: (signal: ?SettleSignal = null) => MockResponse<any>;
20
20
 
21
21
  /**
22
22
  * A non-200 status code with empty text body.
23
23
  * Equivalent to calling `ResponseWith.text("", statusCode)`.
24
24
  */
25
- errorStatusCode: (statusCode: number) => MockResponse<any>;
25
+ errorStatusCode: (
26
+ statusCode: number,
27
+ signal: ?SettleSignal = null,
28
+ ) => MockResponse<any>;
26
29
 
27
30
  /**
28
31
  * Response with GraphQL data JSON body and status code 200.
29
32
  */
30
33
  graphQLData: <TData: {...}>(
31
34
  data: TData,
35
+ signal: ?SettleSignal = null,
32
36
  ) => MockResponse<GraphQLJson<TData>>;
33
37
 
34
38
  /**
@@ -36,22 +40,26 @@ interface RespondWith {
36
40
  */
37
41
  graphQLErrors: (
38
42
  errorMessages: $ReadOnlyArray<string>,
43
+ signal: ?SettleSignal = null,
39
44
  ) => MockResponse<any>;
40
45
 
41
46
  /**
42
47
  * Response with JSON body and status code 200.
43
48
  */
44
- json: <TJson: {...}>(json: TJson): MockResponse<TJson>;
49
+ json: <TJson: {...}>(
50
+ json: TJson,
51
+ signal: ?SettleSignal = null,
52
+ ): MockResponse<TJson>;
45
53
 
46
54
  /**
47
55
  * Response body that is valid JSON but not a valid GraphQL response.
48
56
  */
49
- nonGraphQLBody: () => MockResponse<any>;
57
+ nonGraphQLBody: (signal: ?SettleSignal = null) => MockResponse<any>;
50
58
 
51
59
  /**
52
60
  * Rejects with the given error.
53
61
  */
54
- reject: (error: Error) => MockResponse<any>;
62
+ reject: (error: Error, signal: ?SettleSignal = null) => MockResponse<any>;
55
63
 
56
64
  /**
57
65
  * Response with text body and status code.
@@ -60,14 +68,17 @@ interface RespondWith {
60
68
  text: <TData = string>(
61
69
  text: string,
62
70
  statusCode: number = 200,
71
+ signal: ?SettleSignal = null,
63
72
  ) => MockResponse<TData>;
64
73
 
65
74
  /**
66
75
  * Response with body that will not parse as JSON and status code 200.
67
76
  */
68
- unparseableBody: () => MockResponse<any>;
77
+ unparseableBody: (signal: ?SettleSignal = null) => MockResponse<any>;
69
78
  });
70
79
  ```
71
80
 
72
81
  The `RespondWith` object is a helper for defining mock responses to use with
73
82
  mock request methods such as [`mockGqlFetch`](/docs/testing-mocking-exports-mockgqlfetch--page).
83
+
84
+ Each call takes an optional `signal` that can be used to control when the promise generated from the call resolves. See [`SettleController`](/docs/testing-mocking-exports-settlecontroller--page) for related information.
@@ -0,0 +1,32 @@
1
+ import {Meta} from "@storybook/addon-docs";
2
+
3
+ <Meta
4
+ title="Testing / Mocking / Exports / SettleController"
5
+ parameters={{
6
+ chromatic: {
7
+ disableSnapshot: true,
8
+ },
9
+ }}
10
+ />
11
+
12
+ # SettleController
13
+
14
+ ```ts
15
+ class SettleController {
16
+ /**
17
+ * The signal to pass to the `RespondWith` API.
18
+ */
19
+ get signal(): SettleSignal;
20
+
21
+ /**
22
+ * Settle the signal and therefore any associated responses.
23
+ *
24
+ * @throws {Error} if the signal has already been settled.
25
+ */
26
+ settle(): void;
27
+ }
28
+ ```
29
+
30
+ The `SettleController` is used to control the settling of a signal. This is specifically created to work with the [`RespondWith`](/docs/testing-mocking-exports-respondwith--page) API. The `signal` property it exposes can be passed to `RespondWith` methods and then the `settle` method can be invoked to settle the signal, causing the related responses to either reject or resolve as appropriate.
31
+
32
+ This can be useful for tests where the order of operations needs to be controlled in order to verify the expected behaviour of the system under test.
@@ -12,7 +12,11 @@ import {Meta} from "@storybook/addon-docs";
12
12
  # MockResponse&lt;&gt;
13
13
 
14
14
  ```ts
15
- opaque type MockResponse<TJson>;
15
+ type MockResponse<TJson> = {|
16
+ toPromise: () => Promise<Response>,
17
+ |};
16
18
  ```
17
19
 
18
- This opaque type specifies a mock response. Values of this type are generated by the [`RespondWith`](/docs/testing-mocking-exports-respondwith--page) API.
20
+ This type specifies a mock response. Values of this type are generated by the [`RespondWith`](/docs/testing-mocking-exports-respondwith--page) API. The type parameter is included to allow uses to enforce if they only support `MockResponses` that resolve to a specific JSON pattern.
21
+
22
+ The `toPromise` method can be used to generate a new promise from the mocked response definition. Note that `toPromise` will always generate a new promise and any promise created will settle according to any signal passed to the corresponding `RespondWith` API call.
@@ -1,5 +1,5 @@
1
1
  // @flow
2
- import {RespondWith} from "../make-mock-response.js";
2
+ import {RespondWith} from "../respond-with.js";
3
3
  import {mockRequester} from "../mock-requester.js";
4
4
 
5
5
  describe("#mockRequester", () => {
@@ -0,0 +1,525 @@
1
+ // @flow
2
+ import {SettleController} from "../settle-controller.js";
3
+ import {RespondWith} from "../respond-with.js";
4
+
5
+ describe("RespondWith", () => {
6
+ describe("#text.toPromise", () => {
7
+ it("should respond with the given text", async () => {
8
+ // Arrange
9
+
10
+ // Act
11
+ const response = await RespondWith.text("SOME TEXT").toPromise();
12
+ const result = await response.text();
13
+
14
+ // Assert
15
+ expect(result).toBe("SOME TEXT");
16
+ });
17
+
18
+ it("should respond with the given status code", async () => {
19
+ // Arrange
20
+
21
+ // Act
22
+ const result = await RespondWith.text("SOME TEXT", 204).toPromise();
23
+
24
+ // Assert
25
+ expect(result).toHaveProperty("status", 204);
26
+ });
27
+
28
+ it("should not settle if the signal is not raised", async () => {
29
+ // Arrange
30
+ const settleController = new SettleController();
31
+ const settleableResponse = RespondWith.text(
32
+ "SIGNALLED",
33
+ 200,
34
+ settleController.signal,
35
+ ).toPromise();
36
+ const otherResponse = RespondWith.text("NO SIGNAL").toPromise();
37
+
38
+ // Act
39
+ const firstResponse = await Promise.race([
40
+ settleableResponse,
41
+ otherResponse,
42
+ ]);
43
+ const result = await firstResponse.text();
44
+
45
+ // Assert
46
+ expect(result).toBe("NO SIGNAL");
47
+ });
48
+
49
+ it("should settle if the signal is raised", async () => {
50
+ // Arrange
51
+ const settleController = new SettleController();
52
+ const settleableResponse = RespondWith.text(
53
+ "SIGNALLED",
54
+ 200,
55
+ settleController.signal,
56
+ ).toPromise();
57
+ const otherResponse = RespondWith.text("NO SIGNAL").toPromise();
58
+
59
+ // Act
60
+ settleController.settle();
61
+ const firstResponse = await Promise.race([
62
+ settleableResponse,
63
+ otherResponse,
64
+ ]);
65
+ const result = await firstResponse.text();
66
+
67
+ // Assert
68
+ expect(result).toBe("SIGNALLED");
69
+ });
70
+ });
71
+
72
+ describe("#json.toPromise", () => {
73
+ it("should respond with the given json", async () => {
74
+ // Arrange
75
+
76
+ // Act
77
+ const response = await RespondWith.json({
78
+ some: "json",
79
+ }).toPromise();
80
+ const result = await response.json();
81
+
82
+ // Assert
83
+ expect(result).toStrictEqual({some: "json"});
84
+ });
85
+
86
+ it("should not settle if the signal is not raised", async () => {
87
+ // Arrange
88
+ const settleController = new SettleController();
89
+ const settleableResponse = RespondWith.json(
90
+ {result: "SIGNALLED"},
91
+ settleController.signal,
92
+ ).toPromise();
93
+ const otherResponse = RespondWith.text("NO SIGNAL").toPromise();
94
+
95
+ // Act
96
+ const firstResponse = await Promise.race([
97
+ settleableResponse,
98
+ otherResponse,
99
+ ]);
100
+ const result = await firstResponse.text();
101
+
102
+ // Assert
103
+ expect(result).toBe("NO SIGNAL");
104
+ });
105
+
106
+ it("should settle if the signal is raised", async () => {
107
+ // Arrange
108
+ const settleController = new SettleController();
109
+ const settleableResponse = RespondWith.json(
110
+ {result: "SIGNALLED"},
111
+ settleController.signal,
112
+ ).toPromise();
113
+ const otherResponse = RespondWith.text("NO SIGNAL").toPromise();
114
+
115
+ // Act
116
+ settleController.settle();
117
+ const firstResponse = await Promise.race([
118
+ settleableResponse,
119
+ otherResponse,
120
+ ]);
121
+ const result = await firstResponse.json();
122
+
123
+ // Assert
124
+ expect(result).toStrictEqual({result: "SIGNALLED"});
125
+ });
126
+ });
127
+
128
+ describe("#graphQLData", () => {
129
+ it("should respond with the given GraphQL data", async () => {
130
+ // Arrange
131
+
132
+ // Act
133
+ const response = await RespondWith.graphQLData({
134
+ some: "json",
135
+ }).toPromise();
136
+ const result = await response.json();
137
+
138
+ // Assert
139
+ expect(result).toStrictEqual({data: {some: "json"}});
140
+ });
141
+
142
+ it("should not settle if the signal is not raised", async () => {
143
+ // Arrange
144
+ const settleController = new SettleController();
145
+ const settleableResponse = RespondWith.graphQLData(
146
+ {result: "SIGNALLED"},
147
+ settleController.signal,
148
+ ).toPromise();
149
+ const otherResponse = RespondWith.text("NO SIGNAL").toPromise();
150
+
151
+ // Act
152
+ const firstResponse = await Promise.race([
153
+ settleableResponse,
154
+ otherResponse,
155
+ ]);
156
+ const result = await firstResponse.text();
157
+
158
+ // Assert
159
+ expect(result).toBe("NO SIGNAL");
160
+ });
161
+
162
+ it("should settle if the signal is raised", async () => {
163
+ // Arrange
164
+ const settleController = new SettleController();
165
+ const settleableResponse = RespondWith.graphQLData(
166
+ {result: "SIGNALLED"},
167
+ settleController.signal,
168
+ ).toPromise();
169
+ const otherResponse = RespondWith.text("NO SIGNAL").toPromise();
170
+
171
+ // Act
172
+ settleController.settle();
173
+ const firstResponse = await Promise.race([
174
+ settleableResponse,
175
+ otherResponse,
176
+ ]);
177
+ const result = await firstResponse.json();
178
+
179
+ // Assert
180
+ expect(result).toStrictEqual({data: {result: "SIGNALLED"}});
181
+ });
182
+ });
183
+
184
+ describe("#unparseableBody", () => {
185
+ it("should reject JSON as unparseable", async () => {
186
+ // Arrange
187
+
188
+ // Act
189
+ const response = await RespondWith.unparseableBody().toPromise();
190
+ const act = response.json();
191
+
192
+ // Assert
193
+ await expect(act).rejects.toThrowErrorMatchingInlineSnapshot(
194
+ `"invalid json response body at reason: Unexpected token I in JSON at position 0"`,
195
+ );
196
+ });
197
+
198
+ it("should not settle if the signal is not raised", async () => {
199
+ // Arrange
200
+ const settleController = new SettleController();
201
+ const settleableResponse = RespondWith.unparseableBody(
202
+ settleController.signal,
203
+ ).toPromise();
204
+ const otherResponse = RespondWith.text("NO SIGNAL").toPromise();
205
+
206
+ // Act
207
+ const firstResponse = await Promise.race([
208
+ settleableResponse,
209
+ otherResponse,
210
+ ]);
211
+ const result = await firstResponse.text();
212
+
213
+ // Assert
214
+ expect(result).toBe("NO SIGNAL");
215
+ });
216
+
217
+ it("should settle if the signal is raised", async () => {
218
+ // Arrange
219
+ const settleController = new SettleController();
220
+ const settleableResponse = RespondWith.unparseableBody(
221
+ settleController.signal,
222
+ ).toPromise();
223
+ const otherResponse = RespondWith.text("NO SIGNAL").toPromise();
224
+
225
+ // Act
226
+ settleController.settle();
227
+ const firstResponse = await Promise.race([
228
+ settleableResponse,
229
+ otherResponse,
230
+ ]);
231
+ const act = firstResponse.json();
232
+
233
+ // Assert
234
+ await expect(act).rejects.toThrowError();
235
+ });
236
+ });
237
+
238
+ describe("#abortedRequest", () => {
239
+ it("should reject with AbortError", async () => {
240
+ // Arrange
241
+
242
+ // Act
243
+ const act = RespondWith.abortedRequest().toPromise();
244
+
245
+ // Assert
246
+ await expect(act).rejects.toThrowErrorMatchingInlineSnapshot(
247
+ `"Mock request aborted"`,
248
+ );
249
+ });
250
+
251
+ it("should not settle if the signal is not raised", async () => {
252
+ // Arrange
253
+ const settleController = new SettleController();
254
+ const settleableResponse = RespondWith.abortedRequest(
255
+ settleController.signal,
256
+ ).toPromise();
257
+ const otherResponse = RespondWith.text("NO SIGNAL").toPromise();
258
+
259
+ // Act
260
+ const act = Promise.race([settleableResponse, otherResponse]);
261
+
262
+ // Assert
263
+ await expect(act).resolves;
264
+ });
265
+
266
+ it("should settle if the signal is raised", async () => {
267
+ // Arrange
268
+ const settleController = new SettleController();
269
+ const settleableResponse = RespondWith.abortedRequest(
270
+ settleController.signal,
271
+ ).toPromise();
272
+ const otherResponse = RespondWith.text("NO SIGNAL").toPromise();
273
+
274
+ // Act
275
+ settleController.settle();
276
+ const act = Promise.race([settleableResponse, otherResponse]);
277
+
278
+ // Assert
279
+ await expect(act).rejects.toThrowError();
280
+ });
281
+ });
282
+
283
+ describe("#reject", () => {
284
+ it("should reject with AbortError", async () => {
285
+ // Arrange
286
+
287
+ // Act
288
+ const act = RespondWith.reject(new Error("BOOM!")).toPromise();
289
+
290
+ // Assert
291
+ await expect(act).rejects.toThrowErrorMatchingInlineSnapshot(
292
+ `"BOOM!"`,
293
+ );
294
+ });
295
+
296
+ it("should not settle if the signal is not raised", async () => {
297
+ // Arrange
298
+ const settleController = new SettleController();
299
+ const settleableResponse = RespondWith.reject(
300
+ new Error("BOOM!"),
301
+ settleController.signal,
302
+ ).toPromise();
303
+ const otherResponse = RespondWith.text("NO SIGNAL").toPromise();
304
+
305
+ // Act
306
+ const act = Promise.race([settleableResponse, otherResponse]);
307
+
308
+ // Assert
309
+ await expect(act).resolves;
310
+ });
311
+
312
+ it("should settle if the signal is raised", async () => {
313
+ // Arrange
314
+ const settleController = new SettleController();
315
+ const settleableResponse = RespondWith.reject(
316
+ new Error("BOOM!"),
317
+ settleController.signal,
318
+ ).toPromise();
319
+ const otherResponse = RespondWith.text("NO SIGNAL").toPromise();
320
+
321
+ // Act
322
+ settleController.settle();
323
+ const act = Promise.race([settleableResponse, otherResponse]);
324
+
325
+ // Assert
326
+ await expect(act).rejects.toThrowError();
327
+ });
328
+ });
329
+
330
+ describe("#errorStatusCode", () => {
331
+ it("should throw if the status code represents success", () => {
332
+ // Arrange
333
+
334
+ // Act
335
+ const result = () => RespondWith.errorStatusCode(200);
336
+
337
+ // Assert
338
+ expect(result).toThrowErrorMatchingInlineSnapshot(
339
+ `"200 is not a valid error status code"`,
340
+ );
341
+ });
342
+
343
+ it("should respond with the given status code", async () => {
344
+ // Arrange
345
+
346
+ // Act
347
+ const result = await RespondWith.errorStatusCode(400).toPromise();
348
+
349
+ // Assert
350
+ expect(result).toHaveProperty("status", 400);
351
+ });
352
+
353
+ it("should not settle if the signal is not raised", async () => {
354
+ // Arrange
355
+ const settleController = new SettleController();
356
+ const settleableResponse = RespondWith.errorStatusCode(
357
+ 500,
358
+ settleController.signal,
359
+ ).toPromise();
360
+ const otherResponse = RespondWith.text("NO SIGNAL").toPromise();
361
+
362
+ // Act
363
+ const firstResponse = await Promise.race([
364
+ settleableResponse,
365
+ otherResponse,
366
+ ]);
367
+ const result = await firstResponse.text();
368
+
369
+ // Assert
370
+ expect(result).toBe("NO SIGNAL");
371
+ });
372
+
373
+ it("should settle if the signal is raised", async () => {
374
+ // Arrange
375
+ const settleController = new SettleController();
376
+ const settleableResponse = RespondWith.errorStatusCode(
377
+ 500,
378
+ settleController.signal,
379
+ ).toPromise();
380
+ const otherResponse = RespondWith.text("NO SIGNAL").toPromise();
381
+
382
+ // Act
383
+ settleController.settle();
384
+ const result = await Promise.race([
385
+ settleableResponse,
386
+ otherResponse,
387
+ ]);
388
+
389
+ // Assert
390
+ expect(result).toHaveProperty("status", 500);
391
+ });
392
+ });
393
+
394
+ describe("#nonGraphQLBody", () => {
395
+ it("should respond with valid json", async () => {
396
+ // Arrange
397
+
398
+ // Act
399
+ const response = await RespondWith.nonGraphQLBody().toPromise();
400
+ const act = response.json();
401
+
402
+ // Assert
403
+ await expect(act).resolves.not.toThrow();
404
+ });
405
+
406
+ it("should respond with JSON that is not a valid GraphQL response", async () => {
407
+ // Arrange
408
+
409
+ // Act
410
+ const response = await RespondWith.nonGraphQLBody().toPromise();
411
+ const result = await response.json();
412
+
413
+ // Assert
414
+ expect(result).toMatchInlineSnapshot(`
415
+ Object {
416
+ "that": "is not a valid graphql response",
417
+ "valid": "json",
418
+ }
419
+ `);
420
+ });
421
+
422
+ it("should not settle if the signal is not raised", async () => {
423
+ // Arrange
424
+ const settleController = new SettleController();
425
+ const settleableResponse = RespondWith.nonGraphQLBody(
426
+ settleController.signal,
427
+ ).toPromise();
428
+ const otherResponse = RespondWith.text("NO SIGNAL").toPromise();
429
+
430
+ // Act
431
+ const firstResponse = await Promise.race([
432
+ settleableResponse,
433
+ otherResponse,
434
+ ]);
435
+ const result = await firstResponse.text();
436
+
437
+ // Assert
438
+ expect(result).toBe("NO SIGNAL");
439
+ });
440
+
441
+ it("should settle if the signal is raised", async () => {
442
+ // Arrange
443
+ const settleController = new SettleController();
444
+ const settleableResponse = RespondWith.nonGraphQLBody(
445
+ settleController.signal,
446
+ ).toPromise();
447
+ const otherResponse = RespondWith.text("NO SIGNAL").toPromise();
448
+
449
+ // Act
450
+ settleController.settle();
451
+ const firstResponse = await Promise.race([
452
+ settleableResponse,
453
+ otherResponse,
454
+ ]);
455
+ const result = await firstResponse.json();
456
+
457
+ // Assert
458
+ expect(result).toStrictEqual({
459
+ valid: "json",
460
+ that: "is not a valid graphql response",
461
+ });
462
+ });
463
+ });
464
+
465
+ describe("#graphQLErrors", () => {
466
+ it("should respond with the given GraphQL errors", async () => {
467
+ // Arrange
468
+
469
+ // Act
470
+ const response = await RespondWith.graphQLErrors([
471
+ "BOOM!",
472
+ "BANG!",
473
+ ]).toPromise();
474
+ const result = await response.json();
475
+
476
+ // Assert
477
+ expect(result).toStrictEqual({
478
+ errors: [{message: "BOOM!"}, {message: "BANG!"}],
479
+ });
480
+ });
481
+
482
+ it("should not settle if the signal is not raised", async () => {
483
+ // Arrange
484
+ const settleController = new SettleController();
485
+ const settleableResponse = RespondWith.graphQLErrors(
486
+ ["BOOM!", "BANG!"],
487
+ settleController.signal,
488
+ ).toPromise();
489
+ const otherResponse = RespondWith.text("NO SIGNAL").toPromise();
490
+
491
+ // Act
492
+ const firstResponse = await Promise.race([
493
+ settleableResponse,
494
+ otherResponse,
495
+ ]);
496
+ const result = await firstResponse.text();
497
+
498
+ // Assert
499
+ expect(result).toBe("NO SIGNAL");
500
+ });
501
+
502
+ it("should settle if the signal is raised", async () => {
503
+ // Arrange
504
+ const settleController = new SettleController();
505
+ const settleableResponse = RespondWith.graphQLErrors(
506
+ ["SIGNALLED"],
507
+ settleController.signal,
508
+ ).toPromise();
509
+ const otherResponse = RespondWith.text("NO SIGNAL").toPromise();
510
+
511
+ // Act
512
+ settleController.settle();
513
+ const firstResponse = await Promise.race([
514
+ settleableResponse,
515
+ otherResponse,
516
+ ]);
517
+ const result = await firstResponse.json();
518
+
519
+ // Assert
520
+ expect(result).toStrictEqual({
521
+ errors: [{message: "SIGNALLED"}],
522
+ });
523
+ });
524
+ });
525
+ });
@@ -0,0 +1,29 @@
1
+ // @flow
2
+ import {SettleController} from "../settle-controller.js";
3
+ import {SettleSignal} from "../settle-signal.js";
4
+
5
+ describe("SettleController", () => {
6
+ it("should have a signal", () => {
7
+ // Arrange
8
+
9
+ // Act
10
+ const result = new SettleController();
11
+
12
+ // Assert
13
+ expect(result).toHaveProperty("signal", expect.any(SettleSignal));
14
+ });
15
+
16
+ describe("#settle", () => {
17
+ it("should settle the signal", () => {
18
+ // Arrange
19
+ const controller = new SettleController();
20
+ const signal = controller.signal;
21
+
22
+ // Act
23
+ controller.settle();
24
+
25
+ // Assert
26
+ expect(signal.settled).toBe(true);
27
+ });
28
+ });
29
+ });