@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.
@@ -0,0 +1,99 @@
1
+ // @flow
2
+ import {Request} from "node-fetch";
3
+ import {fetchRequestMatchesMock} from "../fetch-request-matches-mock.js";
4
+
5
+ const TEST_URL = "http://example.com/foo?querya=1&queryb=elephants#fragment";
6
+
7
+ describe("#fetchRequestMatchesMock", () => {
8
+ it("should throw if mock is not valid", () => {
9
+ // Arrange
10
+ const mock = {
11
+ operation: {
12
+ id: "foo",
13
+ type: "query",
14
+ },
15
+ };
16
+
17
+ // Act
18
+ const underTest = () =>
19
+ fetchRequestMatchesMock((mock: any), TEST_URL, null);
20
+
21
+ // Assert
22
+ expect(underTest).toThrowErrorMatchingInlineSnapshot(
23
+ `"Unsupported mock operation: {\\"operation\\":{\\"id\\":\\"foo\\",\\"type\\":\\"query\\"}}"`,
24
+ );
25
+ });
26
+
27
+ it("should throw if input is not valid", () => {
28
+ // Arrange
29
+ const mock = "http://example.com/foo";
30
+
31
+ // Act
32
+ const underTest = () =>
33
+ fetchRequestMatchesMock(
34
+ mock,
35
+ ({not: "a valid request"}: any),
36
+ null,
37
+ );
38
+
39
+ // Assert
40
+ expect(underTest).toThrowErrorMatchingInlineSnapshot(
41
+ `"Unsupported input type"`,
42
+ );
43
+ });
44
+
45
+ describe.each([TEST_URL, new URL(TEST_URL), new Request(TEST_URL)])(
46
+ "for valid inputs",
47
+ (input) => {
48
+ it("should return false if mock is a string and it does not match the fetched URL", () => {
49
+ // Arrange
50
+ const mock = "http://example.com/bar";
51
+
52
+ // Act
53
+ const result = fetchRequestMatchesMock(mock, input, null);
54
+
55
+ // Assert
56
+ expect(result).toBe(false);
57
+ });
58
+
59
+ it("should return false if the mock is a regular expression and it doesn't match the fetched URL", () => {
60
+ // Arrange
61
+ const mock = /\/bar/;
62
+
63
+ // Act
64
+ const result = fetchRequestMatchesMock(mock, input, null);
65
+
66
+ // Assert
67
+ expect(result).toBe(false);
68
+ });
69
+
70
+ it("should return true if the mock is a string and matches the fetched URL", () => {
71
+ // Arrange
72
+ const mock = TEST_URL;
73
+
74
+ // Act
75
+ const result = fetchRequestMatchesMock(mock, input, null);
76
+
77
+ // Assert
78
+ expect(result).toBe(true);
79
+ });
80
+
81
+ it.each([
82
+ /http:\/\/example.com\/foo/,
83
+ /queryb=elephants/,
84
+ /^.*#fragment$/,
85
+ ])(
86
+ "should return true if the mock is a %s and matches the fetched URL",
87
+ (regex) => {
88
+ // Arrange
89
+
90
+ // Act
91
+ const result = fetchRequestMatchesMock(regex, input, null);
92
+
93
+ // Assert
94
+ expect(result).toBe(true);
95
+ },
96
+ );
97
+ },
98
+ );
99
+ });
@@ -0,0 +1,84 @@
1
+ // @flow
2
+ import {Request} from "node-fetch";
3
+ import {RespondWith} from "../../make-mock-response.js";
4
+ import {mockFetch} from "../mock-fetch.js";
5
+
6
+ describe("#mockFetch", () => {
7
+ it.each`
8
+ input | init
9
+ ${"http://example.com/foo"} | ${undefined}
10
+ ${new URL("http://example.com/foo")} | ${{method: "GET"}}
11
+ ${new Request("http://example.com/foo")} | ${{method: "POST"}}
12
+ `(
13
+ "should reject with a useful error when there are no matching mocks for %s",
14
+ async ({input, init}) => {
15
+ // Arrange
16
+ const mockFn = mockFetch();
17
+
18
+ // Act
19
+ const result = mockFn(input, init);
20
+
21
+ // Assert
22
+ await expect(result).rejects.toThrowErrorMatchingSnapshot();
23
+ },
24
+ );
25
+
26
+ describe("mockOperation", () => {
27
+ it("should match a similar operation", async () => {
28
+ // Arrange
29
+ const mockFn = mockFetch();
30
+ const operation = "http://example.com/foo";
31
+
32
+ // Act
33
+ mockFn.mockOperation(operation, RespondWith.text("TADA!"));
34
+ const result = mockFn(operation, {method: "GET"});
35
+
36
+ // Assert
37
+ await expect(result).resolves.toBeDefined();
38
+ });
39
+
40
+ it("should not match a different operation", async () => {
41
+ // Arrange
42
+ const mockFn = mockFetch();
43
+ const operation = "http://example.com/foo";
44
+
45
+ // Act
46
+ mockFn.mockOperation(operation, RespondWith.text("TADA!"));
47
+ const result = mockFn("http://example.com/bar", {method: "GET"});
48
+
49
+ // Assert
50
+ await expect(result).rejects.toThrowError();
51
+ });
52
+ });
53
+
54
+ describe("mockOperationOnce", () => {
55
+ it("should match once", async () => {
56
+ // Arrange
57
+ const mockFn = mockFetch();
58
+ const operation = "http://example.com/foo";
59
+
60
+ // Act
61
+ mockFn.mockOperationOnce(operation, RespondWith.text("TADA!"));
62
+ const result = mockFn(operation, {method: "GET"});
63
+
64
+ // Assert
65
+ await expect(result).resolves.toBeDefined();
66
+ });
67
+
68
+ it("should only match once", async () => {
69
+ // Arrange
70
+ const mockFn = mockFetch();
71
+ const operation = "http://example.com/foo";
72
+
73
+ // Act
74
+ mockFn.mockOperationOnce(operation, RespondWith.text("TADA!"));
75
+ const result = Promise.all([
76
+ mockFn(operation, {method: "GET"}),
77
+ mockFn(operation, {method: "POST"}),
78
+ ]);
79
+
80
+ // Assert
81
+ await expect(result).rejects.toThrowError();
82
+ });
83
+ });
84
+ });
@@ -0,0 +1,43 @@
1
+ // @flow
2
+ import type {FetchMockOperation} from "./types.js";
3
+
4
+ /**
5
+ * Get the URL from the given RequestInfo.
6
+ *
7
+ * Since we could be running in Node or in JSDOM, we don't check instance
8
+ * types, but just use a heuristic so that this works without knowing what
9
+ * was polyfilling things.
10
+ */
11
+ const getHref = (input: RequestInfo): string => {
12
+ if (typeof input === "string") {
13
+ return input;
14
+ } else if (typeof input.url === "string") {
15
+ return input.url;
16
+ } else if (typeof input.href === "string") {
17
+ return input.href;
18
+ } else {
19
+ throw new Error(`Unsupported input type`);
20
+ }
21
+ };
22
+
23
+ /**
24
+ * Determines if a given fetch invocation matches the given mock.
25
+ */
26
+ export const fetchRequestMatchesMock = (
27
+ mock: FetchMockOperation,
28
+ input: RequestInfo,
29
+ init: ?RequestOptions,
30
+ ): boolean => {
31
+ // Currently, we only match on the input portion.
32
+ // This can be a Request, a URL, or a string.
33
+ const href = getHref(input);
34
+
35
+ // Our mock operation is either a string for an exact match, or a regex.
36
+ if (typeof mock === "string") {
37
+ return href === mock;
38
+ } else if (mock instanceof RegExp) {
39
+ return mock.test(href);
40
+ } else {
41
+ throw new Error(`Unsupported mock operation: ${JSON.stringify(mock)}`);
42
+ }
43
+ };
@@ -0,0 +1,19 @@
1
+ // @flow
2
+ import {fetchRequestMatchesMock} from "./fetch-request-matches-mock.js";
3
+ import {mockRequester} from "../mock-requester.js";
4
+ import type {FetchMockFn, FetchMockOperation} from "./types.js";
5
+
6
+ /**
7
+ * A mock for the fetch function passed to GqlRouter.
8
+ */
9
+ export const mockFetch = (): FetchMockFn =>
10
+ mockRequester<FetchMockOperation, _>(
11
+ fetchRequestMatchesMock,
12
+ (input, init) =>
13
+ `Input: ${
14
+ typeof input === "string"
15
+ ? input
16
+ : JSON.stringify(input, null, 2)
17
+ }
18
+ Options: ${init == null ? "None" : JSON.stringify(init, null, 2)}`,
19
+ );
@@ -0,0 +1,18 @@
1
+ //@flow
2
+ import type {OperationMock} from "../types.js";
3
+ import type {MockResponse} from "../make-mock-response.js";
4
+
5
+ export type FetchMockOperation = RegExp | string;
6
+
7
+ type FetchMockOperationFn = (
8
+ operation: FetchMockOperation,
9
+ response: MockResponse<any>,
10
+ ) => FetchMockFn;
11
+
12
+ export type FetchMockFn = {|
13
+ (input: RequestInfo, init?: RequestOptions): Promise<Response>,
14
+ mockOperation: FetchMockOperationFn,
15
+ mockOperationOnce: FetchMockOperationFn,
16
+ |};
17
+
18
+ export type FetchMock = OperationMock<FetchMockOperation>;
@@ -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-gql-mock-response.js";
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 GraphQL mock response found for request",
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({operation: query}, RespondWith.data(data));
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 GraphQL mock response found for request:
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.data(data));
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.data(data));
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.data(data),
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.data(data),
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.data(data),
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.data(data),
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.data(data),
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.data(data),
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.data(data));
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({operation}, RespondWith.data(data));
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({operation}, RespondWith.data(data));
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 "../make-gql-mock-response.js";
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 GraphQL mock response found for request",
39
+ "No matching mock response found for request",
40
40
  ),
41
41
  );
42
42
  });
43
43
 
44
- it("should resolve with data for RespondWith.data", async () => {
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({operation: query}, RespondWith.data(data));
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 {makeGqlMockResponse} from "./make-gql-mock-response.js";
5
- import type {GqlMockResponse} from "./make-gql-mock-response.js";
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
- // We want this to work in jest and in fixtures to make life easy for folks.
13
- // This is the array of mocked operations that we will traverse and
14
- // manipulate.
15
- const mocks: Array<GqlMock> = [];
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 {GqlMockResponse} from "./make-gql-mock-response.js";
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 = <TData, TVariables: {...}, TContext: GqlContext>(
16
+ type GqlMockOperationFn = <
17
+ TData: {...},
18
+ TVariables: {...},
19
+ TContext: GqlContext,
20
+ TResponseData: GraphQLJson<TData>,
21
+ >(
16
22
  operation: GqlMockOperation<TData, TVariables, TContext>,
17
- response: GqlMockResponse<TData>,
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
- // GraphQL framework
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";