@khanacademy/wonder-blocks-testing 12.0.0 → 13.0.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.
@@ -1,269 +0,0 @@
1
- import * as React from "react";
2
- import {render, screen, waitFor} from "@testing-library/react";
3
-
4
- import {GqlRouter, useGql} from "@khanacademy/wonder-blocks-data";
5
- import {RespondWith} from "@khanacademy/wonder-blocks-testing-core";
6
- import {mockGqlFetch} from "../mock-gql-fetch";
7
-
8
- describe("integrating mockGqlFetch, RespondWith, GqlRouter and useGql", () => {
9
- it("should reject with error indicating there are no mocks", async () => {
10
- // Arrange
11
- const mockFetch = mockGqlFetch();
12
- const RenderError = () => {
13
- const [result, setResult] = React.useState<any>(null);
14
- const gqlFetch = useGql();
15
- React.useEffect(() => {
16
- gqlFetch({
17
- type: "query",
18
- id: "getMyStuff",
19
- }).catch((e: any) => {
20
- setResult(e.message);
21
- });
22
- }, [gqlFetch]);
23
-
24
- return <div data-testid="result">{result}</div>;
25
- };
26
-
27
- // Act
28
- render(
29
- <GqlRouter defaultContext={{}} fetch={mockFetch}>
30
- <RenderError />
31
- </GqlRouter>,
32
- );
33
- const result = screen.getByTestId("result");
34
-
35
- // Assert
36
- await waitFor(() =>
37
- expect(result).toHaveTextContent(
38
- "No matching mock response found for request",
39
- ),
40
- );
41
- });
42
-
43
- it("should resolve with data for RespondWith.graphQLData", async () => {
44
- // Arrange
45
- const mockFetch = mockGqlFetch();
46
- const query = {
47
- type: "query",
48
- id: "getMyStuff",
49
- } as const;
50
- const data = {myStuff: "stuff"} as const;
51
- const RenderData = () => {
52
- const [result, setResult] = React.useState<any>(null);
53
- const gqlFetch = useGql();
54
- React.useEffect(() => {
55
- // eslint-disable-next-line promise/catch-or-return
56
- gqlFetch(query).then((r: any) => {
57
- setResult(JSON.stringify(r ?? "(null)"));
58
- return;
59
- });
60
- }, [gqlFetch]);
61
-
62
- return <div data-testid="result">{result}</div>;
63
- };
64
-
65
- // Act
66
- mockFetch.mockOperation(
67
- {operation: query},
68
- RespondWith.graphQLData(data),
69
- );
70
- render(
71
- <GqlRouter defaultContext={{}} fetch={mockFetch}>
72
- <RenderData />
73
- </GqlRouter>,
74
- );
75
- const result = screen.getByTestId("result");
76
-
77
- // Assert
78
- await waitFor(() =>
79
- expect(result).toHaveTextContent(JSON.stringify(data)),
80
- );
81
- });
82
-
83
- it("should reject with AbortError for RespondWith.abortedRequest", async () => {
84
- // Arrange
85
- const mockFetch = mockGqlFetch();
86
- const query = {
87
- type: "query",
88
- id: "getMyStuff",
89
- } as const;
90
- const RenderError = () => {
91
- const [result, setResult] = React.useState<any>(null);
92
- const gqlFetch = useGql();
93
- React.useEffect(() => {
94
- // eslint-disable-next-line promise/catch-or-return
95
- gqlFetch(query).catch((e: any) => {
96
- setResult(e.message);
97
- return;
98
- });
99
- }, [gqlFetch]);
100
-
101
- return <div data-testid="result">{result}</div>;
102
- };
103
-
104
- // Act
105
- mockFetch.mockOperation(
106
- {operation: query},
107
- RespondWith.abortedRequest(),
108
- );
109
- render(
110
- <GqlRouter defaultContext={{}} fetch={mockFetch}>
111
- <RenderError />
112
- </GqlRouter>,
113
- );
114
- const result = screen.getByTestId("result");
115
-
116
- // Assert
117
- await waitFor(() => expect(result).toHaveTextContent("aborted"));
118
- });
119
-
120
- it("should reject with unsuccessful response error for RespondWith.errorStatusCode", async () => {
121
- // Arrange
122
- const mockFetch = mockGqlFetch();
123
- const query = {
124
- type: "query",
125
- id: "getMyStuff",
126
- } as const;
127
- const RenderError = () => {
128
- const [result, setResult] = React.useState<any>(null);
129
- const gqlFetch = useGql();
130
- React.useEffect(() => {
131
- // eslint-disable-next-line promise/catch-or-return
132
- gqlFetch(query).catch((e: any) => {
133
- setResult(e.message);
134
- });
135
- }, [gqlFetch]);
136
-
137
- return <div data-testid="result">{result}</div>;
138
- };
139
-
140
- // Act
141
- mockFetch.mockOperation(
142
- {operation: query},
143
- RespondWith.errorStatusCode(404),
144
- );
145
- render(
146
- <GqlRouter defaultContext={{}} fetch={mockFetch}>
147
- <RenderError />
148
- </GqlRouter>,
149
- );
150
- const result = screen.getByTestId("result");
151
-
152
- // Assert
153
- await waitFor(() =>
154
- expect(result).toHaveTextContent("Response unsuccessful"),
155
- );
156
- });
157
-
158
- it("should reject with parse error for RespondWith.unparseableBody", async () => {
159
- // Arrange
160
- const mockFetch = mockGqlFetch();
161
- const query = {
162
- type: "query",
163
- id: "getMyStuff",
164
- } as const;
165
- const RenderError = () => {
166
- const [result, setResult] = React.useState<any>(null);
167
- const gqlFetch = useGql();
168
- React.useEffect(() => {
169
- // eslint-disable-next-line promise/catch-or-return
170
- gqlFetch(query).catch((e: any) => {
171
- setResult(e.message);
172
- });
173
- }, [gqlFetch]);
174
-
175
- return <div data-testid="result">{result}</div>;
176
- };
177
-
178
- // Act
179
- mockFetch.mockOperation(
180
- {operation: query},
181
- RespondWith.unparseableBody(),
182
- );
183
- render(
184
- <GqlRouter defaultContext={{}} fetch={mockFetch}>
185
- <RenderError />
186
- </GqlRouter>,
187
- );
188
- const result = screen.getByTestId("result");
189
-
190
- // Assert
191
- await waitFor(() =>
192
- expect(result).toHaveTextContent("Failed to parse response"),
193
- );
194
- });
195
-
196
- it("should reject with missing response error for RespondWith.nonGraphQLBody", async () => {
197
- // Arrange
198
- const mockFetch = mockGqlFetch();
199
- const query = {
200
- type: "query",
201
- id: "getMyStuff",
202
- } as const;
203
- const RenderError = () => {
204
- const [result, setResult] = React.useState<any>(null);
205
- const gqlFetch = useGql();
206
- React.useEffect(() => {
207
- // eslint-disable-next-line promise/catch-or-return
208
- gqlFetch(query).catch((e: any) => {
209
- setResult(e.message);
210
- });
211
- }, [gqlFetch]);
212
-
213
- return <div data-testid="result">{result}</div>;
214
- };
215
-
216
- // Act
217
- mockFetch.mockOperation(
218
- {operation: query},
219
- RespondWith.nonGraphQLBody(),
220
- );
221
- render(
222
- <GqlRouter defaultContext={{}} fetch={mockFetch}>
223
- <RenderError />
224
- </GqlRouter>,
225
- );
226
- const result = screen.getByTestId("result");
227
-
228
- // Assert
229
- await waitFor(() =>
230
- expect(result).toHaveTextContent("Server response missing"),
231
- );
232
- });
233
-
234
- it("should reject with GraphQL error for RespondWith.graphQLErrors", async () => {
235
- // Arrange
236
- const mockFetch = mockGqlFetch();
237
- const query = {
238
- type: "query",
239
- id: "getMyStuff",
240
- } as const;
241
- const RenderError = () => {
242
- const [result, setResult] = React.useState<any>(null);
243
- const gqlFetch = useGql();
244
- React.useEffect(() => {
245
- // eslint-disable-next-line promise/catch-or-return
246
- gqlFetch(query).catch((e: any) => {
247
- setResult(e.message);
248
- });
249
- }, [gqlFetch]);
250
-
251
- return <div data-testid="result">{result}</div>;
252
- };
253
-
254
- // Act
255
- mockFetch.mockOperation(
256
- {operation: query},
257
- RespondWith.graphQLErrors(["error 1", "error 2"]),
258
- );
259
- render(
260
- <GqlRouter defaultContext={{}} fetch={mockFetch}>
261
- <RenderError />
262
- </GqlRouter>,
263
- );
264
- const result = screen.getByTestId("result");
265
-
266
- // Assert
267
- await waitFor(() => expect(result).toHaveTextContent("GraphQL errors"));
268
- });
269
- });
@@ -1,71 +0,0 @@
1
- import type {GqlOperation, GqlContext} from "@khanacademy/wonder-blocks-data";
2
- import type {GqlMockOperation} from "./types";
3
-
4
- const safeHasOwnProperty = (obj: any, prop: string): boolean =>
5
- Object.prototype.hasOwnProperty.call(obj, prop);
6
-
7
- // TODO(somewhatabstract, FEI-4268): use a third-party library to do this and
8
- // possibly make it also support the jest `jest.objectContaining` type matching
9
- // to simplify mock declaration (note that it would need to work in regular
10
- // tests and stories/fixtures).
11
- const areObjectsEqual = (a: any, b: any): boolean => {
12
- if (a === b) {
13
- return true;
14
- }
15
- if (a == null || b == null) {
16
- return false;
17
- }
18
- if (typeof a !== "object" || typeof b !== "object") {
19
- return false;
20
- }
21
- const aKeys = Object.keys(a);
22
- const bKeys = Object.keys(b);
23
- if (aKeys.length !== bKeys.length) {
24
- return false;
25
- }
26
- for (let i = 0; i < aKeys.length; i++) {
27
- const key = aKeys[i];
28
- if (!safeHasOwnProperty(b, key) || !areObjectsEqual(a[key], b[key])) {
29
- return false;
30
- }
31
- }
32
- return true;
33
- };
34
-
35
- export const gqlRequestMatchesMock = (
36
- mock: GqlMockOperation<any, any, any>,
37
- operation: GqlOperation<any, any>,
38
- variables: Record<any, any> | null | undefined,
39
- context: GqlContext,
40
- ): boolean => {
41
- // If they don't represent the same operation, then they can't match.
42
- // NOTE: Operations can include more fields than id and type, but we only
43
- // care about id and type. The rest is ignored.
44
- if (
45
- mock.operation.id !== operation.id ||
46
- mock.operation.type !== operation.type
47
- ) {
48
- return false;
49
- }
50
-
51
- // We do a loose match, so if the lhs doesn't define variables,
52
- // we just assume it matches everything.
53
- if (mock.variables != null) {
54
- // Variables have to match.
55
- if (!areObjectsEqual(mock.variables, variables)) {
56
- return false;
57
- }
58
- }
59
-
60
- // We do a loose match, so if the lhs doesn't define context,
61
- // we just assume it matches everything.
62
- if (mock.context != null) {
63
- // Context has to match.
64
- if (!areObjectsEqual(mock.context, context)) {
65
- return false;
66
- }
67
- }
68
-
69
- // If we get here, we have a match.
70
- return true;
71
- };
@@ -1,20 +0,0 @@
1
- import {mockRequester} from "@khanacademy/wonder-blocks-testing-core";
2
- import {gqlRequestMatchesMock} from "./gql-request-matches-mock";
3
- import type {GqlFetchMockFn, GqlMockOperation} from "./types";
4
-
5
- /**
6
- * A mock for the fetch function passed to GqlRouter.
7
- */
8
- export const mockGqlFetch = (): GqlFetchMockFn =>
9
- mockRequester<GqlMockOperation<any, any, any>>(
10
- gqlRequestMatchesMock,
11
- // Note that the identation at the start of each line is important.
12
- // TODO(somewhatabstract): Make a stringify that indents each line of
13
- // the output properly too.
14
- (operation, variables, context) =>
15
- `Operation: ${operation.type} ${operation.id}
16
- Variables: ${
17
- variables == null ? "None" : JSON.stringify(variables, null, 2)
18
- }
19
- Context: ${JSON.stringify(context, null, 2)}`,
20
- );
package/src/gql/types.ts DELETED
@@ -1,35 +0,0 @@
1
- import type {GqlOperation, GqlContext} from "@khanacademy/wonder-blocks-data";
2
- import type {
3
- GraphQLJson,
4
- MockResponse,
5
- } from "@khanacademy/wonder-blocks-testing-core";
6
-
7
- export type GqlMockOperation<
8
- TData extends Record<any, any>,
9
- TVariables extends Record<any, any>,
10
- TContext extends GqlContext,
11
- > = {
12
- operation: GqlOperation<TData, TVariables>;
13
- variables?: TVariables;
14
- context?: TContext;
15
- };
16
-
17
- type GqlMockOperationFn = <
18
- TData extends Record<any, any>,
19
- TVariables extends Record<any, any>,
20
- TContext extends GqlContext,
21
- TResponseData extends GraphQLJson<TData>,
22
- >(
23
- operation: GqlMockOperation<TData, TVariables, TContext>,
24
- response: MockResponse<TResponseData>,
25
- ) => GqlFetchMockFn;
26
-
27
- export type GqlFetchMockFn = {
28
- (
29
- operation: GqlOperation<any, any>,
30
- variables: Record<any, any> | null | undefined,
31
- context: GqlContext,
32
- ): Promise<Response>;
33
- mockOperation: GqlMockOperationFn;
34
- mockOperationOnce: GqlMockOperationFn;
35
- };
@@ -1,7 +0,0 @@
1
- // Jest Snapshot v1, https://goo.gl/fbAQLP
2
-
3
- exports[`SSR.adapter should throw on bad configuration ({"thisConfig": "isNotValid"}) 1`] = `"Unexpected configuration: set config to null to turn this adapter off"`;
4
-
5
- exports[`SSR.adapter should throw on bad configuration (false) 1`] = `"Unexpected configuration: set config to null to turn this adapter off"`;
6
-
7
- exports[`SSR.adapter should throw on bad configuration (string) 1`] = `"Unexpected configuration: set config to null to turn this adapter off"`;
@@ -1,75 +0,0 @@
1
- import * as React from "react";
2
- import {render, screen, waitFor} from "@testing-library/react";
3
- import {useCachedEffect} from "@khanacademy/wonder-blocks-data";
4
- import * as Data from "../data";
5
-
6
- describe("WonderBlocksData.adapter", () => {
7
- it("should render children when configuration arrays are empty", () => {
8
- // Arrange
9
- const children = <div>CONTENT</div>;
10
-
11
- // Act
12
- render(Data.adapter(children, []));
13
-
14
- // Assert
15
- expect(screen.getByText("CONTENT")).toBeInTheDocument();
16
- });
17
-
18
- it("should support request interception via configured dataIntercepts", async () => {
19
- // Arrange
20
-
21
- const TestFixture = () => {
22
- const [result] = useCachedEffect("ID", jest.fn());
23
-
24
- return (
25
- <div>
26
- CONTENT:{" "}
27
- {result.status === "success" ? result.data : undefined}
28
- </div>
29
- );
30
- };
31
-
32
- // Act
33
- const {container} = render(
34
- Data.adapter(<TestFixture />, () =>
35
- Promise.resolve("INTERCEPTED!" as any),
36
- ),
37
- );
38
-
39
- // Assert
40
- await waitFor(() => expect(container).toContainHTML("INTERCEPTED!"));
41
- });
42
-
43
- it("should render like we expect", () => {
44
- // Snapshot test is handy to visualize what's going on and help debug
45
- // test failures of the other cases. The other cases assert specifics.
46
- // Arrange
47
- const TestFixture = () => {
48
- const [result] = useCachedEffect("ID", jest.fn());
49
-
50
- return (
51
- <div>
52
- CONTENT:
53
- {result.status === "success" ? result.data : undefined}
54
- </div>
55
- );
56
- };
57
-
58
- // Act
59
- const {container} = render(
60
- Data.adapter(<TestFixture />, () =>
61
- Promise.resolve("INTERCEPTED!" as any),
62
- ),
63
- );
64
-
65
- // Assert
66
- expect(container).toMatchInlineSnapshot(`
67
- <div>
68
- <div>
69
- CONTENT:
70
- INTERCEPTED!
71
- </div>
72
- </div>
73
- `);
74
- });
75
- });
@@ -1,86 +0,0 @@
1
- import * as React from "react";
2
- import {render, screen} from "@testing-library/react";
3
- import * as WBCore from "@khanacademy/wonder-blocks-core";
4
- import {makeTestHarness} from "@khanacademy/wonder-blocks-testing-core";
5
-
6
- import * as RenderState from "../render-state";
7
-
8
- jest.mock("@khanacademy/wonder-stuff-core", () => {
9
- const actualCore = jest.requireActual("@khanacademy/wonder-stuff-core");
10
- return {
11
- ...actualCore,
12
- RenderStateRoot: (props: any) => (
13
- <actualCore.RenderStateRoot {...props} />
14
- ),
15
- };
16
- });
17
-
18
- describe("SSR.adapter", () => {
19
- it("should render the RenderStateRoot with throwIfNested set to false", () => {
20
- // Arrange
21
- const children = <div>CHILDREN!</div>;
22
- const renderStateRootSpy = jest.spyOn(WBCore, "RenderStateRoot");
23
-
24
- // Act
25
- render(RenderState.adapter(children, true));
26
-
27
- // Assert
28
- expect(renderStateRootSpy).toHaveBeenCalledWith(
29
- {
30
- children,
31
- throwIfNested: false,
32
- },
33
- {},
34
- );
35
- });
36
-
37
- it("should render the children correctly", () => {
38
- // Arrange
39
- const children = <div>CHILDREN!</div>;
40
-
41
- // Act
42
- render(RenderState.adapter(children, true));
43
-
44
- // Assert
45
- expect(screen.getByText("CHILDREN!")).toBeInTheDocument();
46
- });
47
-
48
- it("should enable harnessing of components that require RenderStateRoot", () => {
49
- // Arrange
50
- const ComponentNeedsSsr = (props: any) => {
51
- const idf = WBCore.useUniqueIdWithoutMock();
52
- return <div>{idf?.get("my-id")}</div>;
53
- };
54
- const testHarness = makeTestHarness(
55
- {
56
- renderState: RenderState.adapter,
57
- },
58
- {
59
- renderState: true,
60
- },
61
- );
62
- const Harnessed = testHarness(ComponentNeedsSsr);
63
-
64
- // Act
65
- const underTest = () => render(<Harnessed />);
66
-
67
- // Assert
68
- expect(underTest).not.toThrowError();
69
- });
70
-
71
- it.each`
72
- config
73
- ${false}
74
- ${"string"}
75
- ${{thisConfig: "isNotValid"}}
76
- `("should throw on bad configuration ($config)", ({config}) => {
77
- // Arrange
78
- const children = <div>CHILDREN!</div>;
79
-
80
- // Act
81
- const underTest = () => render(RenderState.adapter(children, config));
82
-
83
- // Assert
84
- expect(underTest).toThrowErrorMatchingSnapshot();
85
- });
86
- });
@@ -1,48 +0,0 @@
1
- import * as React from "react";
2
- import {InterceptRequests} from "@khanacademy/wonder-blocks-data";
3
- import type {TestHarnessAdapter} from "@khanacademy/wonder-blocks-testing-core";
4
-
5
- type Interceptor = JSX.LibraryManagedAttributes<
6
- typeof InterceptRequests,
7
- React.ComponentProps<typeof InterceptRequests>
8
- >["interceptor"];
9
-
10
- type Config = Interceptor | Array<Interceptor>;
11
-
12
- /**
13
- * Default configuration for the Wonder Blocks Data adapter.
14
- */
15
- export const defaultConfig = [] as Array<Interceptor>;
16
-
17
- /**
18
- * Test harness adapter to mock Wonder Blocks Data usage.
19
- *
20
- * NOTE: Consumers are responsible for properly defining their intercepts.
21
- * This component does not validate the configuration to ensure interceptors
22
- * are not overriding one another.
23
- */
24
- export const adapter: TestHarnessAdapter<Config> = (
25
- children: React.ReactNode,
26
- config: Config,
27
- ): React.ReactElement<any> => {
28
- // First we render the cache intercepts.
29
- let currentChildren = children;
30
-
31
- const interceptors = Array.isArray(config) ? config : [config];
32
-
33
- // Then we render the data intercepts.
34
- for (const interceptor of interceptors) {
35
- currentChildren = (
36
- <InterceptRequests interceptor={interceptor}>
37
- {currentChildren}
38
- </InterceptRequests>
39
- );
40
- }
41
-
42
- /**
43
- * `currentChildren` is a `React.Node` but we need it to be a
44
- * `React.Element<>`. Return it rendered in a fragment allows us to do
45
- * that.
46
- */
47
- return <>{currentChildren}</>;
48
- };
@@ -1,34 +0,0 @@
1
- import {harnessAdapters} from "@khanacademy/wonder-blocks-testing-core";
2
- import type {TestHarnessConfigs} from "@khanacademy/wonder-blocks-testing-core";
3
- import * as data from "./data";
4
- import * as renderState from "./render-state";
5
-
6
- /**
7
- * NOTE: We do not type `DefaultAdapters` with `Adapters` here because we want
8
- * the individual config types of each adapter to remain intact rather than
9
- * getting changed to `any`.
10
- */
11
-
12
- /**
13
- * The default adapters provided by Wonder Blocks.
14
- */
15
- export const DefaultAdapters = {
16
- // The error boundary is as close to the component under test as possible,
17
- // so that other adapters don't soil it with their own errors, if that
18
- // should happen.
19
- boundary: harnessAdapters.DefaultAdapters.boundary,
20
- css: harnessAdapters.DefaultAdapters.css,
21
- data: data.adapter,
22
- portal: harnessAdapters.DefaultAdapters.portal,
23
- router: harnessAdapters.DefaultAdapters.router,
24
- renderState: renderState.adapter,
25
- } as const;
26
-
27
- /**
28
- * The default configurations to use with the `DefaultAdapters`.
29
- */
30
- export const DefaultConfigs: TestHarnessConfigs<typeof DefaultAdapters> = {
31
- ...harnessAdapters.DefaultConfigs,
32
- data: data.defaultConfig,
33
- renderState: renderState.defaultConfig,
34
- } as const;
@@ -1,41 +0,0 @@
1
- import * as React from "react";
2
- import {KindError, Errors} from "@khanacademy/wonder-stuff-core";
3
- import {RenderStateRoot} from "@khanacademy/wonder-blocks-core";
4
-
5
- import type {TestHarnessAdapter} from "@khanacademy/wonder-blocks-testing-core";
6
-
7
- //
8
- type Config = true;
9
-
10
- // The default configuration is off since this will likely cause state changes
11
- // and add Testing Library act/waitFor calls in tests using the harness when
12
- // its enabled.
13
- export const defaultConfig: Config | null = null;
14
-
15
- /**
16
- * Test harness adapter for supporting render state-based hooks and components.
17
- *
18
- * Some components and hooks utilize the render state context to manage what
19
- * they render and when. In order for this to work, a `RenderStateRoot`
20
- * component must be present to track the current render state.
21
- *
22
- * This adapter wraps the children in a `RenderStateRoot` component to enable
23
- * the render state context. This adapter should be used when testing components
24
- * that rely on the render state.
25
- */
26
- export const adapter: TestHarnessAdapter<Config> = (
27
- children: React.ReactNode,
28
- config: Config,
29
- ): React.ReactElement<any> => {
30
- if (config !== true) {
31
- throw new KindError(
32
- "Unexpected configuration: set config to null to turn this adapter off",
33
- Errors.InvalidInput,
34
- {
35
- metadata: {config},
36
- },
37
- );
38
- }
39
- // We never want to throw if the test harness is nested.
40
- return <RenderStateRoot throwIfNested={false}>{children}</RenderStateRoot>;
41
- };