graphql-data-generator 0.3.0-alpha.8 → 0.3.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/README.md +111 -78
- package/esm/cli.js +0 -1
- package/esm/index.js +1 -2
- package/esm/init.js +33 -10
- package/esm/jest.js +105 -42
- package/esm/plugin.cjs +1 -1
- package/esm/proxy.js +119 -58
- package/package.json +6 -2
- package/script/cli.js +0 -1
- package/script/index.js +3 -2
- package/script/init.js +56 -10
- package/script/jest.js +107 -42
- package/script/plugin.cjs +0 -2
- package/script/proxy.js +119 -58
- package/types/cli.d.ts +1 -1
- package/types/extendedTypes.d.ts +1 -1
- package/types/index.d.ts +2 -3
- package/types/init.d.ts +10 -0
- package/types/jest.d.ts +24 -5
- package/types/plugin.d.cts +1 -1
- package/types/proxy.d.ts +5 -0
- package/types/types.d.ts +1 -1
- package/esm/_dnt.polyfills.js +0 -1
- package/script/_dnt.polyfills.js +0 -2
- package/types/_dnt.polyfills.d.ts +0 -24
package/README.md
CHANGED
|
@@ -1,11 +1,34 @@
|
|
|
1
1
|
# graphql-data-generator
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
graphql-data-generator is a testing utility for generating GraphQL data objects
|
|
4
|
+
and operation mocks from your schema. It simplifies test setup by combining
|
|
5
|
+
generated types, patching mechanisms, and customizable **transforms** for common
|
|
6
|
+
object variations. It also includes helpers for asserting mock coverage in
|
|
7
|
+
tests.
|
|
5
8
|
|
|
6
|
-
|
|
9
|
+
- [Example](#example-setting-up-a-builder-and-building-objects)
|
|
10
|
+
- [CLI options](#cli-options)
|
|
11
|
+
- [Patches](#patches)
|
|
12
|
+
- [Transforms](#transforms)
|
|
13
|
+
- [Testing](#testing)
|
|
14
|
+
- [Extra properties](#extra)
|
|
7
15
|
|
|
8
|
-
|
|
16
|
+
### Key Concepts
|
|
17
|
+
|
|
18
|
+
- **Patch:** Like a `DeepPartial`, but with functions and array helpers.
|
|
19
|
+
- **Transform:** Predefined reusable patches.
|
|
20
|
+
- **MockedProvider:** A helper that wraps `@apollo/client`'s `MockedProvider`
|
|
21
|
+
with built-in assertions and better stack traces.
|
|
22
|
+
|
|
23
|
+
## Example: Setting up a builder and building objects
|
|
24
|
+
|
|
25
|
+
### Step 1: Generating types
|
|
26
|
+
|
|
27
|
+
To use `graphql-data-generator` with TypeScript, you must pregenerated some a
|
|
28
|
+
types file. This file can optionally include your entire schema types, but at
|
|
29
|
+
minimal includes an index of types, inputs, and operations. You can generated
|
|
30
|
+
the types via the following CLI command or by plugging `@graphql-codegen` (see
|
|
31
|
+
docs).
|
|
9
32
|
|
|
10
33
|
```sh
|
|
11
34
|
npx graphql-data-generator --schema src/graphql/schema.graphql --outfile src/util/test/types.ts
|
|
@@ -86,31 +109,19 @@ const createPost = build.CreatePost({ data: { createPost: { id: "post-id" } } })
|
|
|
86
109
|
|
|
87
110
|
## CLI options
|
|
88
111
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
plugin, otherwise defaults to `keep`.
|
|
103
|
-
- `notypenames`: Toggle automatic inclusion of `__typename`.
|
|
104
|
-
- `operations=dir`: Restrict generation of operations to a specific directory.
|
|
105
|
-
Can be used multiple times for multiple directories.
|
|
106
|
-
- `outfile=file`: Switch output to right to `file` instead of stdout.
|
|
107
|
-
- `scalar=Scalar:Type`: Specify the type of an individual scalar. E.g.,
|
|
108
|
-
`type Scalar = Type;`
|
|
109
|
-
- `scalars=file.json`: Specify multiple scalars from a json file.
|
|
110
|
-
- `schema=file`: Specify the GraphQL schema file to use.
|
|
111
|
-
- `typesFile=file`: Specify file generated by
|
|
112
|
-
[`@graphql-codegen`](https://the-guild.dev/graphql/codegen) to import types
|
|
113
|
-
from.
|
|
112
|
+
| Option | Value | Description |
|
|
113
|
+
| ------------------ | --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
|
|
114
|
+
| `banner` | `<filepath>` or `<string>` | Places copy at the beginning of the generated output. |
|
|
115
|
+
| `enums` | `enums`, `literals`, `none`, or `import:<filepath>` | Controls how enums are generated. |
|
|
116
|
+
| `exports` | `operations` or `types` | Toggles exporting operations and/or types. |
|
|
117
|
+
| `namingConvention` | `NamingConvention` | Sets the [naming convention](https://the-guild.dev/graphql/codegen/docs/config-reference/naming-convention) for types. |
|
|
118
|
+
| `notypenames` | | Disables automatic inclusion of `__typename`. |
|
|
119
|
+
| `operations` | `<dir>` | Limits operation generation to a directory (repeatable). |
|
|
120
|
+
| `outfile` | `<file>` | Writes output to `file` instead of stdout. |
|
|
121
|
+
| `scalar` | `<Scalar>:<Type>` | Maps a scalar to a TypeScript type. |
|
|
122
|
+
| `scalars` | `<json filepath>` | Maps scalars from a JSON file. |
|
|
123
|
+
| `schema` | `<filepath>` | Specifies the schema file to use. |
|
|
124
|
+
| `typesFile` | `<filepath>` | Uses types generated by [`@graphql-codegen`](https://the-guild.dev/graphql/codegen) instead of generating them here. |
|
|
114
125
|
|
|
115
126
|
## @graphql-codegen plugin
|
|
116
127
|
|
|
@@ -158,7 +169,7 @@ file can then be consumed by a `build` script similar to the above example.
|
|
|
158
169
|
|
|
159
170
|
A `patch` is similar to a `DeepPartial` with a few extensions. First, functions
|
|
160
171
|
can be passed instead of literal properties. These functions will be invoked
|
|
161
|
-
during instantiation and will
|
|
172
|
+
during instantiation and will receive the previous host value as a property:
|
|
162
173
|
|
|
163
174
|
```ts
|
|
164
175
|
type Thing = { foo: string };
|
|
@@ -173,6 +184,8 @@ const patch3: ThingPatch = { foo: undefined };
|
|
|
173
184
|
const patch4: ThingPatch = { foo: (prev: Thing) => `${prev.foo}2` };
|
|
174
185
|
```
|
|
175
186
|
|
|
187
|
+
### Arrays
|
|
188
|
+
|
|
176
189
|
`Patch` also has added semantics for arrays, including an object notation:
|
|
177
190
|
|
|
178
191
|
```ts
|
|
@@ -191,38 +204,93 @@ const patch4: ContainerPatch = { values: { length: 0 } };
|
|
|
191
204
|
const patch5: ContainerPatch = { values: ["ok"] };
|
|
192
205
|
```
|
|
193
206
|
|
|
207
|
+
### `clear`
|
|
208
|
+
|
|
209
|
+
In rare circumstances, you may want to patch a nullable array input field back
|
|
210
|
+
to `undefined`. Since `undefined` is purposefully ignored as a patch value and
|
|
211
|
+
`null` is semantically different, the `clear` value exported from
|
|
212
|
+
`graphql-data-generator` can be used revert the field back to `undefined`.
|
|
213
|
+
|
|
194
214
|
## Transforms
|
|
195
215
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
transforms:
|
|
216
|
+
Transforms make it easy to define reusable **patches** for objects and
|
|
217
|
+
operations. For example, if you frequently need to build a User with a post, or
|
|
218
|
+
a `CreatePost` mutation with a pre-filled author, you can encode that logic into
|
|
219
|
+
a transform like `withPost` or `withAuthorId`. This keeps your test setup
|
|
220
|
+
concise and consistent. There are several built in transforms:
|
|
201
221
|
|
|
202
222
|
Objects:
|
|
203
223
|
|
|
204
224
|
- `default`: A special transform that is automatically called for all
|
|
205
|
-
|
|
225
|
+
instantiations
|
|
206
226
|
- `patch`: Accepts a list of patches
|
|
207
227
|
|
|
208
228
|
Operations:
|
|
209
229
|
|
|
210
230
|
- `patch`: Accepts a list of patches
|
|
211
|
-
- `variables`: Accepts an operation variable patch
|
|
212
|
-
|
|
231
|
+
- `variables`: Accepts an operation variable patch. Useful as an alternative to
|
|
232
|
+
`patch` if you don't need to specify `data`
|
|
233
|
+
- `data`: Accepts an operation data patch. Useful as an alternative to `patch`
|
|
234
|
+
if you don't need to specify `variables`
|
|
213
235
|
|
|
214
236
|
When defining custom transforms, the `default` transform has special meaning: it
|
|
215
|
-
will be automatically applied as the first
|
|
237
|
+
will be automatically applied as the first patch to all instances.
|
|
238
|
+
|
|
239
|
+
## Testing
|
|
240
|
+
|
|
241
|
+
`graphql-data-generator` also provides test utilities that work seamlessly with
|
|
242
|
+
`@apollo/client` and `@testing-library/react`. These helpers ensure all GraphQL
|
|
243
|
+
requests are properly mocked and all mocks are fully consumed.
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
import { MockedProvider, waitForMocks } from "graphql-data-generator";
|
|
247
|
+
import { render, screen } from "@testing-library/react";
|
|
248
|
+
import userEvent from "@testing-library/user-event";
|
|
249
|
+
|
|
250
|
+
import { build } from "util/tests/build.ts";
|
|
251
|
+
|
|
252
|
+
import Users from ".";
|
|
253
|
+
|
|
254
|
+
it("my test", async () => {
|
|
255
|
+
render(<Users />, {
|
|
256
|
+
wrapper: ({ children }) => (
|
|
257
|
+
<MockedProvider
|
|
258
|
+
mocks={[
|
|
259
|
+
build.users({ data: { users: [{ id: "user-0", name: "User 0" }] } }),
|
|
260
|
+
build.user({ variables: { id: "user-0" } }),
|
|
261
|
+
]}
|
|
262
|
+
stack={new Error("Render").stack} // Useful if render is wrapped
|
|
263
|
+
>
|
|
264
|
+
{children}
|
|
265
|
+
</MockedProvider>
|
|
266
|
+
),
|
|
267
|
+
});
|
|
268
|
+
await waitForMocks("users");
|
|
269
|
+
await userEvent.click(screen.getByText("User 0"));
|
|
270
|
+
});
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
The `stack` property of `MockedProvider` will be appended to errors in which
|
|
274
|
+
mocks are missing, uncalled, or have mismatched variables.
|
|
275
|
+
|
|
276
|
+
When transitioning to using `graphql-data-generator`'s `MockedProvider`, it may
|
|
277
|
+
be helpful to disable asserting all requests are mocked. A helper,
|
|
278
|
+
`allowMissingMocks`, exists to disable these assertions and can be called before
|
|
279
|
+
any tests.
|
|
280
|
+
|
|
281
|
+
If you are using an old version of `@apollo/client`, missing refetch requests
|
|
282
|
+
will emit warnings instead of errors. You can use `failRefetchWarnings` to
|
|
283
|
+
convert these warnings to errors.
|
|
216
284
|
|
|
217
285
|
## Extra
|
|
218
286
|
|
|
219
|
-
The `init` function supports a 6th optional generic parameter, Extra
|
|
287
|
+
The `init` function supports a 6th optional generic parameter, `Extra`, which
|
|
220
288
|
allows defining extra properties for operation mocks, passable in operation
|
|
221
289
|
patches. This is helpful to support extra Apollo-related properties or custom
|
|
222
290
|
logic. Extra properties will always be optional in patches and the final object
|
|
223
291
|
and will not be patched in but simply merged, such as by `Object.assign`.
|
|
224
292
|
|
|
225
|
-
### Example: Adding an extra
|
|
293
|
+
### Example: Adding an extra property for your own use
|
|
226
294
|
|
|
227
295
|
```ts
|
|
228
296
|
const build = init<
|
|
@@ -231,7 +299,7 @@ const build = init<
|
|
|
231
299
|
Subscription,
|
|
232
300
|
Types,
|
|
233
301
|
Inputs,
|
|
234
|
-
{
|
|
302
|
+
{ internal: boolean }
|
|
235
303
|
>(
|
|
236
304
|
schema,
|
|
237
305
|
queries,
|
|
@@ -242,40 +310,5 @@ const build = init<
|
|
|
242
310
|
scalars,
|
|
243
311
|
)(() => ({}));
|
|
244
312
|
|
|
245
|
-
build.CreatePost({
|
|
246
|
-
```
|
|
247
|
-
|
|
248
|
-
## finalizeOperation
|
|
249
|
-
|
|
250
|
-
The `init`'s final parmaeter, `options`, supports a `finalizeOperation` key.
|
|
251
|
-
This is used as final step when building an operation and acts as a generic
|
|
252
|
-
transform on the final mock itself, which can be useful to attach spies or when
|
|
253
|
-
building interactivity with other GQL tools.
|
|
254
|
-
|
|
255
|
-
### Exmaple: Enforcing a mock is used in Apollo & Jest
|
|
256
|
-
|
|
257
|
-
```ts
|
|
258
|
-
const build = init<Query, Mutation, Subscription, Types, Inputs>(
|
|
259
|
-
schema,
|
|
260
|
-
queries,
|
|
261
|
-
mutations,
|
|
262
|
-
subscriptions,
|
|
263
|
-
types,
|
|
264
|
-
inputs,
|
|
265
|
-
scalars,
|
|
266
|
-
{
|
|
267
|
-
finalizeOperation: (op) => {
|
|
268
|
-
const fn = Object.assign(
|
|
269
|
-
jest.fn(() => op.result),
|
|
270
|
-
op.result,
|
|
271
|
-
) as typeof op["result"];
|
|
272
|
-
op.result = fn;
|
|
273
|
-
return op;
|
|
274
|
-
},
|
|
275
|
-
},
|
|
276
|
-
)(() => ({}));
|
|
277
|
-
|
|
278
|
-
const createPost = build.CreatePost();
|
|
279
|
-
// ...
|
|
280
|
-
expect(createPost.result).toHaveBeenCalled();
|
|
313
|
+
build.CreatePost({ internal: true }).internal; // true
|
|
281
314
|
```
|
package/esm/cli.js
CHANGED
package/esm/index.js
CHANGED
package/esm/init.js
CHANGED
|
@@ -1,13 +1,24 @@
|
|
|
1
|
+
import * as dntShim from "./_dnt.shims.js";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
1
3
|
import { readFileSync } from "node:fs";
|
|
2
4
|
import { Kind, parse } from "graphql";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
4
5
|
import { gqlPluckFromCodeStringSync } from "@graphql-tools/graphql-tag-pluck";
|
|
5
6
|
import { operation, proxy, withGetDefaultPatch } from "./proxy.js";
|
|
6
7
|
import { toObject } from "./util.js";
|
|
8
|
+
import { dirname, resolve } from "node:path";
|
|
9
|
+
globalThis.require ??= createRequire(dntShim.Deno.cwd());
|
|
7
10
|
const files = {};
|
|
8
|
-
const loadFile = (path) =>
|
|
9
|
-
|
|
10
|
-
|
|
11
|
+
const loadFile = (path) => {
|
|
12
|
+
if (files[path])
|
|
13
|
+
return files[path];
|
|
14
|
+
const raw = files[path] = readFileSync(path, "utf-8");
|
|
15
|
+
const imports = Array.from(raw.matchAll(/#import "(.*)"/gm), ([, importPath]) => loadFile(require.resolve(importPath.startsWith(".")
|
|
16
|
+
? resolve(dntShim.Deno.cwd(), dirname(path), importPath)
|
|
17
|
+
: importPath, { paths: [dntShim.Deno.cwd()] })));
|
|
18
|
+
if (!imports.length)
|
|
19
|
+
return raw;
|
|
20
|
+
return [raw, ...imports].join("\n\n");
|
|
21
|
+
};
|
|
11
22
|
const getOperationContentMap = {};
|
|
12
23
|
const getOperationContent = (path, operationName) => {
|
|
13
24
|
const existing = getOperationContentMap[path];
|
|
@@ -23,7 +34,7 @@ const getOperationContent = (path, operationName) => {
|
|
|
23
34
|
const document = parse(s);
|
|
24
35
|
const firstOp = document.definitions.find((d) => d.kind === Kind.OPERATION_DEFINITION);
|
|
25
36
|
if (!firstOp)
|
|
26
|
-
throw new Error(`
|
|
37
|
+
throw new Error(`Could not find an operation in ${path}`);
|
|
27
38
|
return [firstOp.name?.value, document];
|
|
28
39
|
}));
|
|
29
40
|
}
|
|
@@ -32,7 +43,16 @@ const getOperationContent = (path, operationName) => {
|
|
|
32
43
|
}
|
|
33
44
|
return getOperationContent(path, operationName);
|
|
34
45
|
};
|
|
35
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Initialize the data builder.
|
|
48
|
+
* @param schema The plain text of your schema.
|
|
49
|
+
* @param queries List of queries exported from generated types.
|
|
50
|
+
* @param mutations List of mutations exported from generated types.
|
|
51
|
+
* @param subscriptions List of subscriptions exported from generated types.
|
|
52
|
+
* @param types List of types exported from generated types.
|
|
53
|
+
* @param inputs List of types exported from generated types.
|
|
54
|
+
* @param scalars A mapping to generate scalar values. Function values will be invoked with their `__typename`.
|
|
55
|
+
*/
|
|
36
56
|
export const init = (schema, queries, mutations, subscriptions, types, inputs, scalars, options) => (fn) => {
|
|
37
57
|
const doc = parse(schema);
|
|
38
58
|
const build = {};
|
|
@@ -157,10 +177,13 @@ export const init = (schema, queries, mutations, subscriptions, types, inputs, s
|
|
|
157
177
|
if (transforms[name] && "default" in transforms[name]) {
|
|
158
178
|
patches = [transforms[name].default, ...patches];
|
|
159
179
|
}
|
|
160
|
-
const {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
180
|
+
const { mock, parsedQuery } = withGetDefaultPatch((type) => transforms[type]?.default, () => {
|
|
181
|
+
const { request: { query: parsedQuery, ...request }, ...raw } = operation(doc.definitions, scalars, query, ...patches);
|
|
182
|
+
const mock = toObject({
|
|
183
|
+
request: { ...request },
|
|
184
|
+
...raw,
|
|
185
|
+
});
|
|
186
|
+
return { mock, parsedQuery };
|
|
164
187
|
});
|
|
165
188
|
mock.request.query = parsedQuery;
|
|
166
189
|
Error.captureStackTrace(mock, builder);
|
package/esm/jest.js
CHANGED
|
@@ -1,30 +1,52 @@
|
|
|
1
|
-
import "./_dnt.polyfills.js";
|
|
2
1
|
import * as dntShim from "./_dnt.shims.js";
|
|
3
2
|
import React, { useMemo } from "react";
|
|
4
3
|
import { ApolloLink, useApolloClient, } from "@apollo/client";
|
|
5
4
|
import { onError } from "@apollo/client/link/error";
|
|
6
|
-
import { MockedProvider, MockLink, } from "@apollo/client/testing";
|
|
5
|
+
import { MockedProvider as ApolloMockedProvider, MockLink, } from "@apollo/client/testing";
|
|
7
6
|
import { waitFor } from "@testing-library/dom";
|
|
8
7
|
import { Kind, print } from "graphql";
|
|
9
8
|
import { diff as jestDiff } from "jest-diff";
|
|
9
|
+
import "@testing-library/react/dont-cleanup-after-each";
|
|
10
|
+
import { cleanup } from "@testing-library/react";
|
|
10
11
|
let currentSpecResult;
|
|
11
12
|
jasmine.getEnv().addReporter({
|
|
12
13
|
specStarted: (result) => currentSpecResult = result,
|
|
13
14
|
});
|
|
15
|
+
let _skipCleanupAfterEach = false;
|
|
16
|
+
/**
|
|
17
|
+
* `@testing-library/react` automatically unmounts React trees that were mounted
|
|
18
|
+
* with render after each test, which `graphql-data-generator` hijacks to ensure
|
|
19
|
+
* cleanup is done after all mocks are consumed. Invoke this functions to
|
|
20
|
+
* disable automatic cleanup.
|
|
21
|
+
* @param value
|
|
22
|
+
*/
|
|
23
|
+
export const skipCleanupAfterEach = (value = false) => {
|
|
24
|
+
_skipCleanupAfterEach = value;
|
|
25
|
+
};
|
|
14
26
|
const afterTest = [];
|
|
15
27
|
afterEach(async () => {
|
|
16
28
|
const hooks = afterTest.splice(0);
|
|
17
29
|
for (const hook of hooks)
|
|
18
30
|
await hook();
|
|
31
|
+
if (!_skipCleanupAfterEach)
|
|
32
|
+
cleanup();
|
|
19
33
|
});
|
|
20
34
|
const diff = (a, b) => jestDiff(a, b, { omitAnnotationLines: true })
|
|
21
35
|
?.replace(/\w+ \{/g, "{") // Remove class names
|
|
22
36
|
.replace(/\w+ \[/g, "["); // Remove array class names
|
|
37
|
+
const getOperationDefinition = (document) => document.definitions.find((d) => d.kind === Kind.OPERATION_DEFINITION);
|
|
38
|
+
const getOperationType = (operation) => getOperationDefinition(operation.query)?.operation ??
|
|
39
|
+
"<unknown operation type>";
|
|
40
|
+
const getOperationName = (document) => getOperationDefinition(document)?.name?.value;
|
|
41
|
+
const getOperationInfo = (document) => {
|
|
42
|
+
const def = getOperationDefinition(document);
|
|
43
|
+
return {
|
|
44
|
+
name: def?.name?.value ?? "<unknown operation>",
|
|
45
|
+
operationType: def?.operation ?? "<unknown operation type>",
|
|
46
|
+
};
|
|
47
|
+
};
|
|
23
48
|
const getErrorMessage = (operation, mockLink) => {
|
|
24
|
-
const
|
|
25
|
-
const operationType = definition.kind === "OperationDefinition"
|
|
26
|
-
? definition.operation
|
|
27
|
-
: "<unknown operation type>";
|
|
49
|
+
const operationType = getOperationType(operation);
|
|
28
50
|
const key = JSON.stringify({
|
|
29
51
|
query: print(operation.query),
|
|
30
52
|
});
|
|
@@ -79,51 +101,89 @@ const AutoWatch = ({ mocks }) => {
|
|
|
79
101
|
client.watchQuery(mock.request);
|
|
80
102
|
return null;
|
|
81
103
|
};
|
|
104
|
+
let lastMocks = [];
|
|
105
|
+
// deno-lint-ignore ban-types
|
|
106
|
+
const getStack = (to) => {
|
|
107
|
+
const obj = {};
|
|
108
|
+
Error.captureStackTrace(obj, to);
|
|
109
|
+
return obj.stack;
|
|
110
|
+
};
|
|
111
|
+
const _waitForMocks = async (mocks, cause) => {
|
|
112
|
+
for (const mock of mocks) {
|
|
113
|
+
if (mock.optional || mock.error)
|
|
114
|
+
continue;
|
|
115
|
+
await waitFor(() => {
|
|
116
|
+
if (currentSpecResult.failedExpectations.length)
|
|
117
|
+
return;
|
|
118
|
+
if (mock.result.mock.calls.length === 0) {
|
|
119
|
+
const { name, operationType } = getOperationInfo(mock.request.query);
|
|
120
|
+
const err = new Error(`Expected to have used ${operationType} ${name}${mock.request.variables
|
|
121
|
+
? ` with variables ${dntShim.Deno.inspect(mock.request.variables, {
|
|
122
|
+
depth: Infinity,
|
|
123
|
+
colors: true,
|
|
124
|
+
})}`
|
|
125
|
+
: ""}`);
|
|
126
|
+
if (mock.stack) {
|
|
127
|
+
err.stack = `${mock.stack}${cause ? `\nCaused by: ${cause}` : ""}`;
|
|
128
|
+
}
|
|
129
|
+
else if (cause)
|
|
130
|
+
err.stack = cause;
|
|
131
|
+
throw err;
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
/**
|
|
137
|
+
* Wait for mocks to have been used.
|
|
138
|
+
* @param mock If `undefined`, waits for all mocks. If a number, waits fort he first `mocks` mocks. If a string, waits for all mocks up until and including that mock.
|
|
139
|
+
* @param offset If `mocks` is a string, grabs the `offset`th mock of that name (e.g., the third `getReport` mock)
|
|
140
|
+
*/
|
|
141
|
+
export const waitForMocks = async (mock = lastMocks.length, offset = 0) => {
|
|
142
|
+
if (typeof mock === "string") {
|
|
143
|
+
const matches = lastMocks.map((m, i) => [m, i])
|
|
144
|
+
.filter(([m]) => getOperationName(m.request.query) === mock);
|
|
145
|
+
if (matches.length <= offset) {
|
|
146
|
+
fail({
|
|
147
|
+
name: "Error",
|
|
148
|
+
message: `Expected mock ${mock} to have been mocked`,
|
|
149
|
+
stack: getStack(waitForMocks),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
expect(matches.length).toBeGreaterThan(offset);
|
|
153
|
+
mock = matches[offset][1] + 1;
|
|
154
|
+
}
|
|
155
|
+
await _waitForMocks(lastMocks.slice(0, mock), getStack(waitForMocks));
|
|
156
|
+
};
|
|
82
157
|
/**
|
|
83
158
|
* A wrapper for `@apollo/client/testing`, this component will assert all
|
|
84
159
|
* requests have matching mocks and all defined mocks are used unless marked
|
|
85
160
|
* `optional`.
|
|
86
161
|
*/
|
|
87
|
-
export const
|
|
162
|
+
export const MockedProvider = ({ mocks, stack: renderStack, children, link: passedLink, ...rest }) => {
|
|
88
163
|
const observableMocks = useMemo(() => {
|
|
89
164
|
const observableMocks = mocks.flatMap((m) => [
|
|
90
|
-
{
|
|
165
|
+
typeof m.result === "function" && "mock" in m.result ? m : {
|
|
91
166
|
...m,
|
|
92
167
|
stack: m.stack,
|
|
93
|
-
result: Object.assign(jest.fn(() => m.result), m.result),
|
|
168
|
+
result: Object.assign(jest.fn((vars) => typeof m.result === "function" ? m.result(vars) : m.result), m.result),
|
|
94
169
|
},
|
|
95
170
|
...(m.watch
|
|
96
171
|
? [{
|
|
97
172
|
...m,
|
|
98
173
|
stack: m.stack,
|
|
99
|
-
result:
|
|
174
|
+
result: typeof m.result === "function" && "mock" in m.result
|
|
175
|
+
? m.result
|
|
176
|
+
: Object.assign(jest.fn((vars) => typeof m.result === "function" ? m.result(vars) : m.result), m.result),
|
|
100
177
|
watch: false,
|
|
178
|
+
// TODO: this might be dependent on Apollo version or refetch method,
|
|
179
|
+
// ideally should be asserted when we can (maybe when
|
|
180
|
+
// _failRefetchWarnings is false?)
|
|
181
|
+
optional: true,
|
|
101
182
|
}]
|
|
102
183
|
: []),
|
|
103
184
|
]);
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
return;
|
|
107
|
-
for (const mock of observableMocks) {
|
|
108
|
-
if (mock.optional || mock.error)
|
|
109
|
-
continue;
|
|
110
|
-
await waitFor(() => {
|
|
111
|
-
if (currentSpecResult.failedExpectations.length)
|
|
112
|
-
return;
|
|
113
|
-
if (mock.result.mock.calls.length === 0) {
|
|
114
|
-
const operation = mock.request.query.definitions.find((d) => d.kind === Kind.OPERATION_DEFINITION);
|
|
115
|
-
const err = new Error(`Expected to have used ${operation?.operation} ${operation?.name?.value}${mock.request.variables
|
|
116
|
-
? ` with variables ${dntShim.Deno.inspect(mock.request.variables, {
|
|
117
|
-
depth: Infinity,
|
|
118
|
-
colors: true,
|
|
119
|
-
})}`
|
|
120
|
-
: ""}`);
|
|
121
|
-
err.stack = mock.stack ?? renderStack ?? err.stack;
|
|
122
|
-
throw err;
|
|
123
|
-
}
|
|
124
|
-
}, { onTimeout: (e) => e });
|
|
125
|
-
}
|
|
126
|
-
});
|
|
185
|
+
lastMocks = observableMocks;
|
|
186
|
+
afterTest.push(() => _waitForMocks(lastMocks, renderStack));
|
|
127
187
|
return observableMocks;
|
|
128
188
|
}, [mocks]);
|
|
129
189
|
const link = useMemo(() => {
|
|
@@ -131,14 +191,18 @@ export const MockProvider = ({ mocks, stack: renderStack, children, link: passed
|
|
|
131
191
|
mockLink.showWarnings = false;
|
|
132
192
|
const errorLoggingLink = onError(({ networkError, operation }) => {
|
|
133
193
|
if (_allowMissingMocks ||
|
|
134
|
-
!networkError?.message
|
|
194
|
+
!networkError?.message?.includes("No more mocked responses"))
|
|
135
195
|
return;
|
|
136
196
|
const { message, stack: altStack } = getErrorMessage(operation, mockLink);
|
|
137
197
|
try {
|
|
138
198
|
networkError.message = message;
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
199
|
+
if (altStack) {
|
|
200
|
+
networkError.stack = renderStack
|
|
201
|
+
? `${altStack}\nCaused By: ${renderStack}`
|
|
202
|
+
: altStack;
|
|
203
|
+
}
|
|
204
|
+
else if (renderStack)
|
|
205
|
+
networkError.stack = renderStack;
|
|
142
206
|
fail({ name: "Error", message, stack: networkError.stack });
|
|
143
207
|
}
|
|
144
208
|
catch {
|
|
@@ -154,14 +218,13 @@ export const MockProvider = ({ mocks, stack: renderStack, children, link: passed
|
|
|
154
218
|
if (_failRefetchWarnings) {
|
|
155
219
|
const oldWarn = console.warn.bind(console.warn);
|
|
156
220
|
console.warn = (message, operation, ...etc) => {
|
|
157
|
-
if (message !==
|
|
158
|
-
|
|
221
|
+
if (typeof message !== "string" ||
|
|
222
|
+
!message.match(/Unknown query named.*refetchQueries/))
|
|
159
223
|
return oldWarn(message, operation, ...etc);
|
|
160
|
-
}
|
|
161
224
|
try {
|
|
162
225
|
fail({
|
|
163
226
|
name: "Error",
|
|
164
|
-
message: `Expected query ${operation} requested in refetchQueries options.include array to have
|
|
227
|
+
message: `Expected query ${operation} requested in refetchQueries options.include array to have been mocked`,
|
|
165
228
|
stack: renderStack,
|
|
166
229
|
});
|
|
167
230
|
}
|
|
@@ -173,7 +236,7 @@ export const MockProvider = ({ mocks, stack: renderStack, children, link: passed
|
|
|
173
236
|
console.warn = oldWarn;
|
|
174
237
|
});
|
|
175
238
|
}
|
|
176
|
-
return (React.createElement(
|
|
239
|
+
return (React.createElement(ApolloMockedProvider, { ...rest, link: link },
|
|
177
240
|
React.createElement(React.Fragment, null,
|
|
178
241
|
React.createElement(AutoWatch, { mocks: observableMocks }),
|
|
179
242
|
children)));
|
package/esm/plugin.cjs
CHANGED