Skip to content

Commit

Permalink
feat(SubscriptionOperation): only complete Queries and Mutations
Browse files Browse the repository at this point in the history
BREAKING CHANGE: subscription observables must be manually completed by the `complete()` method.
  • Loading branch information
Frozen-byte committed Nov 4, 2024
1 parent cafb23a commit 96dadc5
Show file tree
Hide file tree
Showing 4 changed files with 139 additions and 6 deletions.
7 changes: 7 additions & 0 deletions .changeset/perfect-buckets-drum.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'apollo-angular': major
---

added a `complete()` method for `TestOperation` object to cancel subscriptions after `flush()`

BREAKING CHANGE: subscription observables must be manually completed by the `complete()` method.
21 changes: 16 additions & 5 deletions packages/apollo-angular/testing/src/operation.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { ExecutionResult, GraphQLError } from 'graphql';
import { ExecutionResult, GraphQLError, Kind, OperationTypeNode } from 'graphql';
import { Observer } from 'rxjs';
import { ApolloError, FetchResult, Operation as LinkOperation } from '@apollo/client/core';
import { getMainDefinition } from '@apollo/client/utilities';

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

export type Operation = LinkOperation & {
clientName: string;
Expand All @@ -22,10 +21,22 @@ export class TestOperation<T = { [key: string]: any }> {
} else {
const fetchResult = result ? { ...result } : result;
this.observer.next(fetchResult);
this.observer.complete();

const definition = getMainDefinition(this.operation.query);

if (
definition.kind === Kind.OPERATION_DEFINITION &&
definition.operation !== OperationTypeNode.SUBSCRIPTION
) {
this.complete();
}
}
}

public complete() {
this.observer.complete();
}

public flushData(data: T | null): void {
this.flush({
data,
Expand Down
116 changes: 115 additions & 1 deletion packages/apollo-angular/testing/tests/operation.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ApolloLink, execute, gql } from '@apollo/client/core';
import { ApolloLink, execute, FetchResult, gql } from '@apollo/client/core';
import { ApolloTestingBackend } from '../src/backend';
import { buildOperationForLink } from './utils';

Expand All @@ -9,6 +9,20 @@ const testQuery = gql`
}
}
`;
const testSubscription = gql`
subscription newHeroes {
heroes {
name
}
}
`;
const testMutation = gql`
mutation addHero($hero: String!) {
addHero(hero: $hero) {
name
}
}
`;

describe('TestOperation', () => {
let mock: ApolloTestingBackend;
Expand Down Expand Up @@ -52,4 +66,104 @@ describe('TestOperation', () => {
heroes: [],
});
});

test('should leave the operation open for a subscription', done => {
const operation = buildOperationForLink(testSubscription, {});
const emittedResults: FetchResult[] = [];

execute(link, operation).subscribe({
next(result) {
emittedResults.push(result);
},
complete() {
expect(emittedResults).toEqual([
{
data: {
heroes: ['first Hero'],
},
},
{
data: {
heroes: ['second Hero'],
},
},
]);
done();
},
});

const testOperation = mock.expectOne(testSubscription);

testOperation.flushData({
heroes: ['first Hero'],
});

testOperation.flushData({
heroes: ['second Hero'],
});

testOperation.complete();
});

test('should close the operation after a query', done => {
const operation = buildOperationForLink(testQuery, {});
const emittedResults: FetchResult[] = [];

execute(link, operation).subscribe({
next(result) {
emittedResults.push(result);
},
complete() {
expect(emittedResults).toEqual([
{
data: {
heroes: ['first Hero'],
},
},
]);
done();
},
});

const testOperation = mock.expectOne(testQuery);

testOperation.flushData({
heroes: ['first Hero'],
});

testOperation.flushData({
heroes: ['second Hero'],
});
});

test('should close the operation after a mutation', done => {
const operation = buildOperationForLink(testMutation, { hero: 'firstHero' });
const emittedResults: FetchResult[] = [];

execute(link, operation).subscribe({
next(result) {
emittedResults.push(result);
},
complete() {
expect(emittedResults).toEqual([
{
data: {
heroes: ['first Hero'],
},
},
]);
done();
},
});

const testOperation = mock.expectOne(testMutation);

testOperation.flushData({
heroes: ['first Hero'],
});

testOperation.flushData({
heroes: ['second Hero'],
});
});
});
1 change: 1 addition & 0 deletions website/src/pages/docs/development-and-testing/testing.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ It's an object returned by `expectOne` and `match` methods.
ApolloError instance
- `networkError(error: Error): void{:ts}` - to flush an operation with a network error
- `graphqlErrors(errors: GraphQLError[]): void{:ts}` - to flush an operation with graphql errors
- `complete(): void{:ts}` - manually complete the connection, useful for subscription based testing

## Using Named Clients

Expand Down

0 comments on commit 96dadc5

Please sign in to comment.