Skip to content

Commit 8c75368

Browse files
Frozen-bytePowerKiKi
authored andcommitted
feat(SubscriptionOperation): only complete Queries and Mutations
BREAKING CHANGE: subscription observables must be manually completed by the `complete()` method.
1 parent cafb23a commit 8c75368

File tree

4 files changed

+139
-6
lines changed

4 files changed

+139
-6
lines changed

.changeset/perfect-buckets-drum.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'apollo-angular': major
3+
---
4+
5+
added a `complete()` method for `TestOperation` object to cancel subscriptions after `flush()`
6+
7+
BREAKING CHANGE: subscription observables must be manually completed by the `complete()` method.

packages/apollo-angular/testing/src/operation.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
import { ExecutionResult, GraphQLError } from 'graphql';
1+
import { ExecutionResult, GraphQLError, Kind, OperationTypeNode } from 'graphql';
22
import { Observer } from 'rxjs';
33
import { ApolloError, FetchResult, Operation as LinkOperation } from '@apollo/client/core';
4+
import { getMainDefinition } from '@apollo/client/utilities';
45

5-
function isApolloError(error: unknown): error is ApolloError {
6-
return !!error && error.hasOwnProperty('graphQLErrors');
7-
}
6+
const isApolloError = (err: any): err is ApolloError => err && err.hasOwnProperty('graphQLErrors');
87

98
export type Operation = LinkOperation & {
109
clientName: string;
@@ -22,10 +21,22 @@ export class TestOperation<T = { [key: string]: any }> {
2221
} else {
2322
const fetchResult = result ? { ...result } : result;
2423
this.observer.next(fetchResult);
25-
this.observer.complete();
24+
25+
const definition = getMainDefinition(this.operation.query);
26+
27+
if (
28+
definition.kind === Kind.OPERATION_DEFINITION &&
29+
definition.operation !== OperationTypeNode.SUBSCRIPTION
30+
) {
31+
this.complete();
32+
}
2633
}
2734
}
2835

36+
public complete() {
37+
this.observer.complete();
38+
}
39+
2940
public flushData(data: T | null): void {
3041
this.flush({
3142
data,

packages/apollo-angular/testing/tests/operation.spec.ts

Lines changed: 115 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ApolloLink, execute, gql } from '@apollo/client/core';
1+
import { ApolloLink, execute, FetchResult, gql } from '@apollo/client/core';
22
import { ApolloTestingBackend } from '../src/backend';
33
import { buildOperationForLink } from './utils';
44

@@ -9,6 +9,20 @@ const testQuery = gql`
99
}
1010
}
1111
`;
12+
const testSubscription = gql`
13+
subscription newHeroes {
14+
heroes {
15+
name
16+
}
17+
}
18+
`;
19+
const testMutation = gql`
20+
mutation addHero($hero: String!) {
21+
addHero(hero: $hero) {
22+
name
23+
}
24+
}
25+
`;
1226

1327
describe('TestOperation', () => {
1428
let mock: ApolloTestingBackend;
@@ -52,4 +66,104 @@ describe('TestOperation', () => {
5266
heroes: [],
5367
});
5468
});
69+
70+
test('should leave the operation open for a subscription', done => {
71+
const operation = buildOperationForLink(testSubscription, {});
72+
const emittedResults: FetchResult[] = [];
73+
74+
execute(link, operation).subscribe({
75+
next(result) {
76+
emittedResults.push(result);
77+
},
78+
complete() {
79+
expect(emittedResults).toEqual([
80+
{
81+
data: {
82+
heroes: ['first Hero'],
83+
},
84+
},
85+
{
86+
data: {
87+
heroes: ['second Hero'],
88+
},
89+
},
90+
]);
91+
done();
92+
},
93+
});
94+
95+
const testOperation = mock.expectOne(testSubscription);
96+
97+
testOperation.flushData({
98+
heroes: ['first Hero'],
99+
});
100+
101+
testOperation.flushData({
102+
heroes: ['second Hero'],
103+
});
104+
105+
testOperation.complete();
106+
});
107+
108+
test('should close the operation after a query', done => {
109+
const operation = buildOperationForLink(testQuery, {});
110+
const emittedResults: FetchResult[] = [];
111+
112+
execute(link, operation).subscribe({
113+
next(result) {
114+
emittedResults.push(result);
115+
},
116+
complete() {
117+
expect(emittedResults).toEqual([
118+
{
119+
data: {
120+
heroes: ['first Hero'],
121+
},
122+
},
123+
]);
124+
done();
125+
},
126+
});
127+
128+
const testOperation = mock.expectOne(testQuery);
129+
130+
testOperation.flushData({
131+
heroes: ['first Hero'],
132+
});
133+
134+
testOperation.flushData({
135+
heroes: ['second Hero'],
136+
});
137+
});
138+
139+
test('should close the operation after a mutation', done => {
140+
const operation = buildOperationForLink(testMutation, { hero: 'firstHero' });
141+
const emittedResults: FetchResult[] = [];
142+
143+
execute(link, operation).subscribe({
144+
next(result) {
145+
emittedResults.push(result);
146+
},
147+
complete() {
148+
expect(emittedResults).toEqual([
149+
{
150+
data: {
151+
heroes: ['first Hero'],
152+
},
153+
},
154+
]);
155+
done();
156+
},
157+
});
158+
159+
const testOperation = mock.expectOne(testMutation);
160+
161+
testOperation.flushData({
162+
heroes: ['first Hero'],
163+
});
164+
165+
testOperation.flushData({
166+
heroes: ['second Hero'],
167+
});
168+
});
55169
});

website/src/pages/docs/development-and-testing/testing.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@ It's an object returned by `expectOne` and `match` methods.
230230
ApolloError instance
231231
- `networkError(error: Error): void{:ts}` - to flush an operation with a network error
232232
- `graphqlErrors(errors: GraphQLError[]): void{:ts}` - to flush an operation with graphql errors
233+
- `complete(): void{:ts}` - manually complete the connection, useful for subscription based testing
233234

234235
## Using Named Clients
235236

0 commit comments

Comments
 (0)