@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.
@@ -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
+ });
@@ -1,6 +1,6 @@
1
1
  // @flow
2
2
  import {Request} from "node-fetch";
3
- import {RespondWith} from "../../make-mock-response.js";
3
+ import {RespondWith} from "../../respond-with.js";
4
4
  import {mockFetch} from "../mock-fetch.js";
5
5
 
6
6
  describe("#mockFetch", () => {
@@ -1,5 +1,5 @@
1
1
  //@flow
2
- import type {MockResponse} from "../make-mock-response.js";
2
+ import type {MockResponse} from "../respond-with.js";
3
3
 
4
4
  export type FetchMockOperation = RegExp | string;
5
5
 
@@ -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 "../../make-mock-response.js";
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 "../../make-mock-response.js";
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 "../make-mock-response.js";
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 "./make-mock-response.js";
15
- export type {MockResponse} from "./make-mock-response.js";
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
 
@@ -1,6 +1,5 @@
1
1
  // @flow
2
- import {makeMockResponse} from "./make-mock-response.js";
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 = () => makeMockResponse(response);
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
@@ -1,5 +1,5 @@
1
1
  // @flow
2
- import type {MockResponse} from "./make-mock-response.js";
2
+ import type {MockResponse} from "./respond-with.js";
3
3
 
4
4
  /**
5
5
  * A valid GraphQL response as supported by our mocking framework.