[apollo-mock-client] Rename and support query, mutation, and subscription operations

This commit is contained in:
Eloy Durán 2021-04-19 01:02:30 +02:00
Родитель 809be86745
Коммит e3082322c6
12 изменённых файлов: 1678 добавлений и 1392 удалений

Просмотреть файл

@ -0,0 +1,3 @@
# ApolloMockClient
An Apollo Client that allows mocking of payloads in response to operations, rather providing them all upfront. It is API-wise a port of [Relays RelayMocEnvironment](https://relay.dev/docs/guides/testing-relay-components/#relaymockenvironment-api-overview).

Просмотреть файл

@ -1,5 +1,5 @@
{
"name": "@graphitation/apollo-mock-provider",
"name": "@graphitation/apollo-mock-client",
"version": "0.1.0",
"main": "./src/index.ts",
"scripts": {
@ -11,10 +11,15 @@
},
"devDependencies": {
"@apollo/client": "^3.3.15",
"@graphitation/graphql-js-tag": "^0.1.0",
"@graphitation/graphql-js-operation-payload-generator": "^0.1.0",
"@types/jest": "^26.0.22",
"@types/react": "^17.0.3",
"@types/react-test-renderer": "^17.0.1",
"graphql": "^15.5.0",
"monorepo-scripts": "*"
"monorepo-scripts": "*",
"react": "^17.0.2",
"react-test-renderer": "^17.0.2"
},
"publishConfig": {
"main": "./lib/index.js",

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -0,0 +1,213 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ReactRelayTestMocker with Containers Basic Resolve/Reject Operations should resolve query 1`] = `"My id <mock-id-1> and name is <mock-value-for-field-\\"name\\">"`;
exports[`ReactRelayTestMocker with Containers Multiple Query Renderers should resolve both queries 1`] = `
Array [
<div
testID="user"
>
Alice
</div>,
<div
testID="page"
>
My Page
</div>,
]
`;
exports[`ReactRelayTestMocker with Containers Subscription Tests should resolve subscription 1`] = `
<div>
Feedback:
&lt;mock-value-for-field-"text"&gt;
<span
reactionType="Viewer does not like it"
testID="reaction"
/>
</div>
`;
exports[`ReactRelayTestMocker with Containers Test Mutations should reject mutation: Should render error message 1`] = `
<div>
<span
testID="errorMessage"
>
Uh-oh
</span>
Feedback:
&lt;mock-value-for-field-"text"&gt;
<button
disabled={false}
onClick={[Function]}
testID="likeButton"
>
Like
</button>
</div>
`;
exports[`ReactRelayTestMocker with Containers Test Mutations should resolve mutation: Button should be enabled. Text should be "Like". 1`] = `
<div>
Feedback:
&lt;mock-value-for-field-"text"&gt;
<button
disabled={false}
onClick={[Function]}
testID="likeButton"
>
Like
</button>
</div>
`;
exports[`ReactRelayTestMocker with Containers Test Mutations should resolve mutation: Should apply optimistic update. Button should says "Unlike". And it should be disabled 1`] = `
<div>
Feedback:
&lt;mock-value-for-field-"text"&gt;
<button
disabled={true}
onClick={[Function]}
testID="likeButton"
>
Unlike
</button>
</div>
`;
exports[`ReactRelayTestMocker with Containers Test Mutations should resolve mutation: Should render response from the server. Button should be enabled. And text still "Unlike" 1`] = `
<div>
Feedback:
&lt;mock-value-for-field-"text"&gt;
<button
disabled={false}
onClick={[Function]}
testID="likeButton"
>
Unlike
</button>
</div>
`;
exports[`ReactRelayTestMocker with Containers Test Query Renderer with Fragment Container should render data 1`] = `
<div>
My id $
&lt;mock-id-1&gt;
and name is $
&lt;mock-value-for-field-"name"&gt;
.
<hr />
<img
alt="<mock-value-for-field-\\"name\\">"
src="<mock-value-for-field-\\"uri\\">"
testID="profile_picture"
/>
</div>
`;
exports[`ReactRelayTestMocker with Containers Test Query Renderer with Pagination Container should load more data for pagination container: It should render a list of users with Alice and Bob, button "loadMore" should be disabled 1`] = `
<div>
My id $
my-pagination-test-user-id
and name is $
&lt;mock-value-for-field-"name"&gt;
.
<hr />
<ul
testID="list"
>
<li>
Friend:
Alice
<img
alt="Alice"
src="<mock-value-for-field-\\"uri\\">"
/>
</li>
<li>
Friend:
Bob
<img
alt="Bob"
src="<mock-value-for-field-\\"uri\\">"
/>
</li>
</ul>
<button
disabled={true}
onClick={[Function]}
testID="loadMore"
/>
</div>
`;
exports[`ReactRelayTestMocker with Containers Test Query Renderer with Pagination Container should render data: It should render list of users with just Alice and \`button\` loadMore should be enabled. 1`] = `
<div>
My id $
my-pagination-test-user-id
and name is $
&lt;mock-value-for-field-"name"&gt;
.
<hr />
<ul
testID="list"
>
<li>
Friend:
Alice
<img
alt="Alice"
src="<mock-value-for-field-\\"uri\\">"
/>
</li>
</ul>
<button
disabled={false}
onClick={[Function]}
testID="loadMore"
/>
</div>
`;
exports[`ReactRelayTestMocker with Containers Test Query Renderer with Refetch Container should refetch query: Should render hometown with SFO 1`] = `
<div>
My id $
&lt;mock-id-1&gt;
and name is $
&lt;mock-value-for-field-"name"&gt;
.
<hr />
<div
testID="hometown"
>
SFO
</div>
<div>
Websites:
&lt;mock-value-for-field-"websites"&gt;
</div>
<button
disabled={false}
onClick={[Function]}
testID="refetch"
>
Refetch
</button>
</div>
`;
exports[`ReactRelayTestMocker with Containers resolve/reject next with components should reject next operation: should render component with the error 1`] = `
<div
testID="error"
>
Uh-oh
</div>
`;
exports[`ReactRelayTestMocker with Containers resolve/reject next with components should resolve next operation: should render component with the data 1`] = `
<div
testID="user"
>
&lt;mock-value-for-field-"name"&gt;
</div>
`;

Просмотреть файл

@ -0,0 +1,230 @@
import * as React from "react";
import {
ApolloLink,
Observable,
Operation,
FetchResult,
ApolloClient,
InMemoryCache,
ApolloProvider,
NormalizedCacheObject,
} from "@apollo/client";
import invariant from "invariant";
import { assertType, GraphQLSchema, isAbstractType } from "graphql";
type MockData = Record<string, unknown>;
interface MockFunctions {
getAllOperations(): Operation[];
getMostRecentOperation(): Operation;
findOperation(findFn: (operation: Operation) => boolean): Operation;
nextValue(operation: Operation, data: MockData): void;
complete(operation: Operation): void;
resolve(operation: Operation, data: MockData): void;
reject(operation: Operation, error: Error): void;
resolveMostRecentOperation(
resolver: (operation: Operation) => MockData
): void;
rejectMostRecentOperation(
error: Error | ((operation: Operation) => Error)
): void;
}
interface ApolloClientExtension {
mock: MockFunctions;
mockClear: () => void;
}
export interface ApolloMockClient
extends ApolloClient<NormalizedCacheObject>,
ApolloClientExtension {}
class MockLink extends ApolloLink {
public schema: GraphQLSchema;
public mock: _MockEnvironment;
constructor(schema: GraphQLSchema) {
super();
this.schema = schema;
this.mock = new _MockEnvironment();
}
public mockClear() {
this.mock = new _MockEnvironment();
}
public request(operation: Operation): Observable<FetchResult> | null {
operation.setContext({ schema: this.schema });
return new Observable<FetchResult>((observer) => {
this.mock.addOperation(operation, observer);
});
}
}
class _MockEnvironment implements MockFunctions {
private operations: [
operation: Operation,
observer: ZenObservable.SubscriptionObserver<FetchResult>
][];
constructor() {
this.operations = [];
}
// TODO: This should remain file private
public addOperation(
operation: Operation,
observer: ZenObservable.SubscriptionObserver<FetchResult>
) {
this.operations.push([operation, observer]);
}
// ---
public getAllOperations(): Operation[] {
return this.operations.map(([op, _]) => op);
}
public getMostRecentOperation(): Operation {
invariant(
this.operations.length > 0,
"Expected at least one operation to have been started"
);
const [op, _] = this.operations[this.operations.length - 1];
return op;
}
// ---
public nextValue(operation: Operation, data: MockData): void {
const [_, observer] = this.operations[this.findOperationIndex(operation)];
observer.next(data);
}
public complete(operation: Operation): void {
const index = this.findOperationIndex(operation);
const [_, observer] = this.operations[index];
observer.complete();
this.operations.splice(index, 1);
}
public resolve(operation: Operation, data: MockData): void {
this.nextValue(operation, data);
this.complete(operation);
}
public reject(operation: Operation, error: Error): void {
const [_, observer] = this.operations[this.findOperationIndex(operation)];
observer.error(error);
this.complete(operation);
}
public resolveMostRecentOperation(
resolver: (operation: Operation) => MockData
): void {
const operation = this.getMostRecentOperation();
this.resolve(operation, resolver(operation));
}
public rejectMostRecentOperation(
error: Error | ((operation: Operation) => Error)
): void {
const operation = this.getMostRecentOperation();
this.reject(
operation,
typeof error === "function" ? error(operation) : error
);
}
public findOperation(findFn: (operation: Operation) => boolean): Operation {
const operation = this.operations.find(([op, _]) => findFn(op));
invariant(
operation,
"Operation was not found in the list of pending operations"
);
return operation[0];
}
// ----
private findOperationIndex(operation: Operation) {
const index = this.operations.findIndex(([op, _]) => op === operation);
invariant(index >= 0, "Expected to find operation");
return index;
}
}
export function createMockClient(schema: GraphQLSchema): ApolloMockClient {
// Build a list of abstract types and their possible types.
// TODO: Cache this on the schema?
const possibleTypes: Record<string, string[]> = {};
Object.keys(schema.getTypeMap()).forEach((typeName) => {
const type = schema.getType(typeName);
assertType(type);
if (isAbstractType(type)) {
possibleTypes[typeName] = schema
.getPossibleTypes(type)
.map((possibleType) => possibleType.name);
}
});
const link = new MockLink(schema);
const ext: ApolloClientExtension = {
get mock() {
return link.mock;
},
mockClear() {
link.mockClear();
},
};
const client = new ApolloClient({
cache: new InMemoryCache({
possibleTypes,
addTypename: false,
}),
link,
}) as ApolloMockClient;
// Object.defineProperties(client, {
// mock: {
// get() {
// return link.mock;
// },
// },
// mockClear: {
// value: () => link.mockClear(),
// },
// });
return Object.assign(client, ext);
}
// export const ApolloMockProvider: React.FC<{
// environment: MockEnvironment;
// }> = ({ children, environment }) => {
// // Build a list of abstract types and their possible types.
// // TODO: Cache this on the schema?
// const schema = (environment as _MockEnvironment).schema;
// const possibleTypes: Record<string, string[]> = {};
// Object.keys(schema.getTypeMap()).forEach((typeName) => {
// const type = schema.getType(typeName);
// assertType(type);
// if (isAbstractType(type)) {
// possibleTypes[typeName] = schema
// .getPossibleTypes(type)
// .map((possibleType) => possibleType.name);
// }
// });
// const client = new ApolloClient({
// cache: new InMemoryCache({
// possibleTypes,
// addTypename: false,
// }),
// link: new MockLink(environment as _MockEnvironment),
// });
// return <ApolloProvider client={client}>{children}</ApolloProvider>;
// };

Просмотреть файл

@ -8,5 +8,8 @@
"jsx": "react"
},
"include": ["src"],
"references": []
"references": [
{ "path": "../graphql-js-tag" },
{ "path": "../graphql-js-operation-payload-generator" }
]
}

Просмотреть файл

@ -1,3 +0,0 @@
# ApolloMockProvider
An Apollo Client provider that allows mocking of payloads in response to operations, rather providing them all upfront. It is API-wise a port of [Relays RelayMocEnvironment](https://relay.dev/docs/guides/testing-relay-components/#relaymockenvironment-api-overview).

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -1,7 +0,0 @@
import { bar } from "..";
describe("bar", () => {
it("returns a string", () => {
expect(typeof bar("test")).toBe("string");
});
});

Просмотреть файл

@ -1,146 +0,0 @@
/* eslint-disable max-classes-per-file */
import * as React from "react";
import {
ApolloLink,
Observable,
Operation,
NextLink,
FetchResult,
ApolloClient,
InMemoryCache,
ApolloProvider,
} from "@apollo/client";
import invariant from "invariant";
import { assertType, GraphQLSchema, isAbstractType } from "graphql";
class MockLink extends ApolloLink {
constructor(private environment: MockEnvironment) {
super();
}
public request(operation: Operation): Observable<FetchResult> | null {
operation.setContext({ schema: this.environment.schema });
return new Observable<FetchResult>((observer) => {
this.environment.addOperation(operation, observer);
});
}
}
// interface MockOperation {
// resolve();
// }
// An opaque type that internally holds an Apollo Client Operation, but that should not be exposed to the user.
type OperationDescriptor = { __brand: "OperationDescriptor" };
type MockData = Record<string, unknown>;
class MockEnvironment {
private operations: [
operation: Operation,
observer: ZenObservable.SubscriptionObserver<FetchResult>
][];
// TODO: This should remain file private
public schema: GraphQLSchema;
constructor(schema: GraphQLSchema) {
this.operations = [];
this.schema = schema;
}
// TODO: This should remain file private
public addOperation(
operation: Operation,
observer: ZenObservable.SubscriptionObserver<FetchResult>
) {
this.operations.push([operation, observer]);
}
// ---
public getMostRecentOperation(): OperationDescriptor {
invariant(
this.operations.length > 0,
"Expected at least one operation to have been started"
);
const [op, _] = this.operations[this.operations.length - 1];
return (op as unknown) as OperationDescriptor;
}
// ---
public nextValue(operation: OperationDescriptor, data: MockData): void {
const [_, observer] = this.operations[this.findOperationIndex(operation)];
observer.next(data);
}
// TODO: Does this need to do more work, such as finish the observable?
public complete(operation: OperationDescriptor): void {
const index = this.findOperationIndex(operation);
const [_, observer] = this.operations[index];
observer.complete();
this.operations.splice(index, 1);
}
public resolve(operation: OperationDescriptor, data: MockData): void {
this.nextValue(operation, data);
this.complete(operation);
}
public reject(operation: OperationDescriptor, error: Error): void {}
public resolveMostRecentOperation(
resolver: (operation: OperationDescriptor) => MockData
): void {
const operation = this.getMostRecentOperation();
this.resolve(operation, resolver(operation));
}
public rejectMostRecentOperation(
resolve: (operation: OperationDescriptor) => Error
): void {}
// ----
private findOperationIndex(operation: OperationDescriptor) {
const index = this.operations.findIndex(
([op, _]) => op === ((operation as unknown) as Operation)
);
invariant(index >= 0, "Expected to find operation");
return index;
}
}
export function createMockEnvironment(schema: GraphQLSchema) {
const env = new MockEnvironment(schema);
return env;
}
export const ApolloMockProvider: React.FC<{
environment: MockEnvironment;
}> = ({ children, environment }) => {
// Build a list of abstract types and their possible types.
// TODO: Cache this on the schema?
const schema = environment.schema;
const possibleTypes: Record<string, string[]> = {};
Object.keys(schema.getTypeMap()).forEach((typeName) => {
const type = schema.getType(typeName);
assertType(type);
if (isAbstractType(type)) {
possibleTypes[typeName] = schema
.getPossibleTypes(type)
.map((possibleType) => possibleType.name);
}
});
const client = new ApolloClient({
cache: new InMemoryCache({
possibleTypes,
addTypename: false,
}),
link: new MockLink(environment),
});
return <ApolloProvider client={client}>{children}</ApolloProvider>;
};

Просмотреть файл

@ -1176,7 +1176,14 @@
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7"
integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==
"@types/react@^17.0.3":
"@types/react-test-renderer@^17.0.1":
version "17.0.1"
resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-17.0.1.tgz#3120f7d1c157fba9df0118dae20cb0297ee0e06b"
integrity sha512-3Fi2O6Zzq/f3QR9dRnlnHso9bMl7weKCviFmfF6B4LS1Uat6Hkm15k0ZAQuDz+UBq6B3+g+NM6IT2nr5QgPzCw==
dependencies:
"@types/react" "*"
"@types/react@*", "@types/react@^17.0.3":
version "17.0.3"
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.3.tgz#ba6e215368501ac3826951eef2904574c262cc79"
integrity sha512-wYOUxIgs2HZZ0ACNiIayItyluADNbONl7kt8lkLjVK8IitMH5QMyAh75Fwhmo37r1m7L2JaFj03sIfxBVDvRAg==
@ -4523,7 +4530,7 @@ lodash@4.x, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.21, lodash@^4.7.0:
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
loose-envify@^1.0.0, loose-envify@^1.4.0:
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
@ -5308,15 +5315,41 @@ ramda@^0.27.1:
resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.27.1.tgz#66fc2df3ef873874ffc2da6aa8984658abacf5c9"
integrity sha512-PgIdVpn5y5Yns8vqb8FzBUEYn98V3xcPgawAkkgj0YJ0qDsnHCiNmZYfOGMgOvoB0eWFLpYbhxUR3mxfDIMvpw==
"react-is@^16.12.0 || ^17.0.0", react-is@^17.0.1, react-is@^17.0.2:
version "17.0.2"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
react-is@^16.7.0, react-is@^16.8.1:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
react-is@^17.0.1:
react-shallow-renderer@^16.13.1:
version "16.14.1"
resolved "https://registry.yarnpkg.com/react-shallow-renderer/-/react-shallow-renderer-16.14.1.tgz#bf0d02df8a519a558fd9b8215442efa5c840e124"
integrity sha512-rkIMcQi01/+kxiTE9D3fdS959U1g7gs+/rborw++42m1O9FAQiNI/UNRZExVUoAOprn4umcXf+pFRou8i4zuBg==
dependencies:
object-assign "^4.1.1"
react-is "^16.12.0 || ^17.0.0"
react-test-renderer@^17.0.2:
version "17.0.2"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-17.0.2.tgz#4cd4ae5ef1ad5670fc0ef776e8cc7e1231d9866c"
integrity sha512-yaQ9cB89c17PUb0x6UfWRs7kQCorVdHlutU1boVPEsB8IDZH6n9tHxMacc3y0JoXOJUsZb/t/Mb8FUWMKaM7iQ==
dependencies:
object-assign "^4.1.1"
react-is "^17.0.2"
react-shallow-renderer "^16.13.1"
scheduler "^0.20.2"
react@^17.0.2:
version "17.0.2"
resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
read-pkg-up@^7.0.1:
version "7.0.1"
@ -5654,6 +5687,14 @@ saxes@^5.0.1:
dependencies:
xmlchars "^2.2.0"
scheduler@^0.20.2:
version "0.20.2"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91"
integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
"semver@2 || 3 || 4 || 5", semver@^5.5.0:
version "5.7.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"