diff --git a/src/__tests__/ApolloClient.ts b/src/__tests__/ApolloClient.ts index e147de233a4..b7c1e8eb39a 100644 --- a/src/__tests__/ApolloClient.ts +++ b/src/__tests__/ApolloClient.ts @@ -15,7 +15,6 @@ import { Observable } from "../utilities"; import { ApolloLink, FetchResult } from "../link/core"; import { HttpLink } from "../link/http"; import { createFragmentRegistry, InMemoryCache } from "../cache"; -import { itAsync } from "../testing"; import { ObservableStream, spyOnConsole } from "../testing/internal"; import { TypedDocumentNode } from "@graphql-typed-document-node/core"; import { invariant } from "../utilities/globals"; @@ -1205,235 +1204,181 @@ describe("ApolloClient", () => { result.data?.people.friends[0].id; }); - itAsync( - "with a replacement of nested array (wq)", - (resolve, reject) => { - let count = 0; - const client = newClient(); - const observable = client.watchQuery({ query }); - const subscription = observable.subscribe({ - next(nextResult) { - ++count; - if (count === 1) { - expect(nextResult.data).toEqual(data); - expect(observable.getCurrentResult().data).toEqual(data); - - const readData = client.readQuery({ query }); - expect(readData).toEqual(data); - - // modify readData and writeQuery - const bestFriends = readData!.people.friends.filter( - (x) => x.type === "best" - ); - // this should re call next - client.writeQuery({ - query, - data: { - people: { - id: 1, - friends: bestFriends, - __typename: "Person", - }, - }, - }); - } else if (count === 2) { - const expectation = { - people: { - id: 1, - friends: [bestFriend], - __typename: "Person", - }, - }; - expect(nextResult.data).toEqual(expectation); - expect(client.readQuery({ query })).toEqual( - expectation - ); - subscription.unsubscribe(); - resolve(); - } + it("with a replacement of nested array (wq)", async () => { + const client = newClient(); + const observable = client.watchQuery({ query }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitMatchedValue({ data }); + expect(observable.getCurrentResult().data).toEqual(data); + + const readData = client.readQuery({ query }); + expect(readData).toEqual(data); + + // modify readData and writeQuery + const bestFriends = readData!.people.friends.filter( + (x) => x.type === "best" + ); + // this should re call next + client.writeQuery({ + query, + data: { + people: { + id: 1, + friends: bestFriends, + __typename: "Person", }, - }); - } - ); + }, + }); - itAsync( - "with a value change inside a nested array (wq)", - (resolve, reject) => { - let count = 0; - const client = newClient(); - const observable = client.watchQuery({ query }); - observable.subscribe({ - next: (nextResult) => { - count++; - if (count === 1) { - expect(nextResult.data).toEqual(data); - expect(observable.getCurrentResult().data).toEqual(data); - - const readData = client.readQuery({ query }); - expect(readData).toEqual(data); - - // modify readData and writeQuery - const friends = readData!.people.friends.slice(); - friends[0] = { ...friends[0], type: "okayest" }; - friends[1] = { ...friends[1], type: "okayest" }; - - // this should re call next - client.writeQuery({ - query, - data: { - people: { - id: 1, - friends, - __typename: "Person", - }, - }, - }); - - setTimeout(() => { - if (count === 1) - reject( - new Error( - "writeFragment did not re-call observable with next value" - ) - ); - }, 250); - } + const expectation = { + people: { + id: 1, + friends: [bestFriend], + __typename: "Person", + }, + }; - if (count === 2) { - const expectation0 = { - ...bestFriend, - type: "okayest", - }; - const expectation1 = { - ...badFriend, - type: "okayest", - }; - const nextFriends = nextResult.data!.people.friends; - expect(nextFriends[0]).toEqual(expectation0); - expect(nextFriends[1]).toEqual(expectation1); - - const readFriends = client.readQuery({ query })!.people - .friends; - expect(readFriends[0]).toEqual(expectation0); - expect(readFriends[1]).toEqual(expectation1); - resolve(); - } + await expect(stream).toEmitMatchedValue({ data: expectation }); + expect(client.readQuery({ query })).toEqual(expectation); + }); + + it("with a value change inside a nested array (wq)", async () => { + const client = newClient(); + const observable = client.watchQuery({ query }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitMatchedValue({ data }); + + expect(observable.getCurrentResult().data).toEqual(data); + + const readData = client.readQuery({ query }); + expect(readData).toEqual(data); + + // modify readData and writeQuery + const friends = readData!.people.friends.slice(); + friends[0] = { ...friends[0], type: "okayest" }; + friends[1] = { ...friends[1], type: "okayest" }; + + // this should re call next + client.writeQuery({ + query, + data: { + people: { + id: 1, + friends, + __typename: "Person", }, - }); - } - ); + }, + }); + + const expectation0 = { + ...bestFriend, + type: "okayest", + }; + const expectation1 = { + ...badFriend, + type: "okayest", + }; + + const nextResult = await stream.takeNext(); + const nextFriends = nextResult.data!.people.friends; + + expect(nextFriends[0]).toEqual(expectation0); + expect(nextFriends[1]).toEqual(expectation1); + + const readFriends = client.readQuery({ query })!.people.friends; + expect(readFriends[0]).toEqual(expectation0); + expect(readFriends[1]).toEqual(expectation1); + }); }); + describe("using writeFragment", () => { - itAsync( - "with a replacement of nested array (wf)", - (resolve, reject) => { - let count = 0; - const client = newClient(); - const observable = client.watchQuery({ query }); - observable.subscribe({ - next: (result) => { - count++; - if (count === 1) { - expect(result.data).toEqual(data); - expect(observable.getCurrentResult().data).toEqual(data); - const bestFriends = result.data!.people.friends.filter( - (x) => x.type === "best" - ); - // this should re call next - client.writeFragment({ - id: `Person${result.data!.people.id}`, - fragment: gql` - fragment bestFriends on Person { - friends { - id - } - } - `, - data: { - friends: bestFriends, - __typename: "Person", - }, - }); - - setTimeout(() => { - if (count === 1) - reject( - new Error( - "writeFragment did not re-call observable with next value" - ) - ); - }, 50); - } + it("with a replacement of nested array (wf)", async () => { + const client = newClient(); + const observable = client.watchQuery({ query }); + const stream = new ObservableStream(observable); - if (count === 2) { - expect(result.data!.people.friends).toEqual([bestFriend]); - resolve(); + { + const result = await stream.takeNext(); + + expect(result.data).toEqual(data); + expect(observable.getCurrentResult().data).toEqual(data); + + const bestFriends = result.data!.people.friends.filter( + (x) => x.type === "best" + ); + + // this should re call next + client.writeFragment({ + id: `Person${result.data!.people.id}`, + fragment: gql` + fragment bestFriends on Person { + friends { + id + } } + `, + data: { + friends: bestFriends, + __typename: "Person", }, }); } - ); - itAsync( - "with a value change inside a nested array (wf)", - (resolve, reject) => { - let count = 0; - const client = newClient(); - const observable = client.watchQuery({ query }); - observable.subscribe({ - next: (result) => { - count++; - if (count === 1) { - expect(result.data).toEqual(data); - expect(observable.getCurrentResult().data).toEqual(data); - const friends = result.data!.people.friends; - - // this should re call next - client.writeFragment({ - id: `Person${result.data!.people.id}`, - fragment: gql` - fragment bestFriends on Person { - friends { - id - type - } - } - `, - data: { - friends: [ - { ...friends[0], type: "okayest" }, - { ...friends[1], type: "okayest" }, - ], - __typename: "Person", - }, - }); - - setTimeout(() => { - if (count === 1) - reject( - new Error( - "writeFragment did not re-call observable with next value" - ) - ); - }, 50); - } + { + const result = await stream.takeNext(); + expect(result.data!.people.friends).toEqual([bestFriend]); + } + }); + + it("with a value change inside a nested array (wf)", async () => { + const client = newClient(); + const observable = client.watchQuery({ query }); + const stream = new ObservableStream(observable); - if (count === 2) { - const nextFriends = result.data!.people.friends; - expect(nextFriends[0]).toEqual({ - ...bestFriend, - type: "okayest", - }); - expect(nextFriends[1]).toEqual({ - ...badFriend, - type: "okayest", - }); - resolve(); + { + const result = await stream.takeNext(); + + expect(result.data).toEqual(data); + expect(observable.getCurrentResult().data).toEqual(data); + const friends = result.data!.people.friends; + + // this should re call next + client.writeFragment({ + id: `Person${result.data!.people.id}`, + fragment: gql` + fragment bestFriends on Person { + friends { + id + type + } } + `, + data: { + friends: [ + { ...friends[0], type: "okayest" }, + { ...friends[1], type: "okayest" }, + ], + __typename: "Person", }, }); } - ); + + { + const result = await stream.takeNext(); + const nextFriends = result.data!.people.friends; + + expect(nextFriends[0]).toEqual({ + ...bestFriend, + type: "okayest", + }); + expect(nextFriends[1]).toEqual({ + ...badFriend, + type: "okayest", + }); + } + }); }); }); }); @@ -2804,69 +2749,63 @@ describe("ApolloClient", () => { invariantDebugSpy.mockRestore(); }); - itAsync( - "should catch refetchQueries error when not caught explicitly", - (resolve, reject) => { - const linkFn = jest - .fn( - () => - new Observable((observer) => { - setTimeout(() => { - observer.error(new Error("refetch failed")); - }); - }) - ) - .mockImplementationOnce(() => { - setTimeout(refetchQueries); - return Observable.of(); - }); - - const client = new ApolloClient({ - link: new ApolloLink(linkFn), - cache: new InMemoryCache(), + it("should catch refetchQueries error when not caught explicitly", (done) => { + expect.assertions(2); + const linkFn = jest + .fn( + () => + new Observable((observer) => { + setTimeout(() => { + observer.error(new Error("refetch failed")); + }); + }) + ) + .mockImplementationOnce(() => { + setTimeout(refetchQueries); + return Observable.of(); }); - const query = gql` - query someData { - foo { - bar - } + const client = new ApolloClient({ + link: new ApolloLink(linkFn), + cache: new InMemoryCache(), + }); + + const query = gql` + query someData { + foo { + bar } - `; + } + `; - const observable = client.watchQuery({ - query, - fetchPolicy: "network-only", - }); + const observable = client.watchQuery({ + query, + fetchPolicy: "network-only", + }); - observable.subscribe({}); + observable.subscribe({}); - function refetchQueries() { - const result = client.refetchQueries({ - include: "all", - }); + function refetchQueries() { + const result = client.refetchQueries({ + include: "all", + }); - result.queries[0].subscribe({ - error() { - setTimeout(() => { - try { - expect(invariantDebugSpy).toHaveBeenCalledTimes(1); - expect(invariantDebugSpy).toHaveBeenCalledWith( - "In client.refetchQueries, Promise.all promise rejected with error %o", - new ApolloError({ - networkError: new Error("refetch failed"), - }) - ); - resolve(); - } catch (err) { - reject(err); - } - }); - }, - }); - } + result.queries[0].subscribe({ + error() { + setTimeout(() => { + expect(invariantDebugSpy).toHaveBeenCalledTimes(1); + expect(invariantDebugSpy).toHaveBeenCalledWith( + "In client.refetchQueries, Promise.all promise rejected with error %o", + new ApolloError({ + networkError: new Error("refetch failed"), + }) + ); + done(); + }); + }, + }); } - ); + }); }); describe.skip("type tests", () => { diff --git a/src/__tests__/client.ts b/src/__tests__/client.ts index 686d1c078ee..a7443e4b787 100644 --- a/src/__tests__/client.ts +++ b/src/__tests__/client.ts @@ -36,7 +36,7 @@ import { } from "../cache"; import { ApolloError } from "../errors"; -import { itAsync, mockSingleLink, MockLink, wait } from "../testing"; +import { mockSingleLink, MockLink, wait } from "../testing"; import { ObservableStream, spyOnConsole } from "../testing/internal"; import { waitFor } from "@testing-library/react"; @@ -106,36 +106,33 @@ describe("client", () => { ); }); - itAsync( - "should allow for a single query to take place", - (resolve, reject) => { - const query = gql` - query people { - allPeople(first: 1) { - people { - name - __typename - } + it("should allow for a single query to take place", async () => { + const query = gql` + query people { + allPeople(first: 1) { + people { + name __typename } + __typename } - `; + } + `; - const data = { - allPeople: { - people: [ - { - name: "Luke Skywalker", - __typename: "Person", - }, - ], - __typename: "People", - }, - }; + const data = { + allPeople: { + people: [ + { + name: "Luke Skywalker", + __typename: "Person", + }, + ], + __typename: "People", + }, + }; - return clientRoundtrip(resolve, reject, query, { data }); - } - ); + await clientRoundtrip(query, { data }); + }); it("should allow a single query with an apollo-link enabled network interface", async () => { const query = gql` @@ -176,137 +173,132 @@ describe("client", () => { expect(actualResult.data).toEqual(data); }); - itAsync( - "should allow for a single query with complex default variables to take place", - (resolve, reject) => { - const query = gql` - query stuff( - $test: Input = { key1: ["value", "value2"], key2: { key3: 4 } } - ) { - allStuff(test: $test) { - people { - name - } + it("should allow for a single query with complex default variables to take place", async () => { + const query = gql` + query stuff( + $test: Input = { key1: ["value", "value2"], key2: { key3: 4 } } + ) { + allStuff(test: $test) { + people { + name } } - `; + } + `; - const result = { - allStuff: { - people: [ - { - name: "Luke Skywalker", - }, - { - name: "Jabba The Hutt", - }, - ], - }, - }; + const result = { + allStuff: { + people: [ + { + name: "Luke Skywalker", + }, + { + name: "Jabba The Hutt", + }, + ], + }, + }; - const variables = { - test: { key1: ["value", "value2"], key2: { key3: 4 } }, - }; + const variables = { + test: { key1: ["value", "value2"], key2: { key3: 4 } }, + }; - const link = mockSingleLink({ - request: { query, variables }, - result: { data: result }, - }).setOnError(reject); + const link = mockSingleLink({ + request: { query, variables }, + result: { data: result }, + }); - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ addTypename: false }), - }); + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ addTypename: false }), + }); - const basic = client.query({ query, variables }).then((actualResult) => { - expect(actualResult.data).toEqual(result); - }); + { + const actualResult = await client.query({ query, variables }); - const withDefault = client.query({ query }).then((actualResult) => { - expect(actualResult.data).toEqual(result); - }); + expect(actualResult.data).toEqual(result); + } - return Promise.all([basic, withDefault]).then(resolve, reject); + { + const actualResult = await client.query({ query }); + + expect(actualResult.data).toEqual(result); } - ); + }); - itAsync( - "should allow for a single query with default values that get overridden with variables", - (resolve, reject) => { - const query = gql` - query people($first: Int = 1) { - allPeople(first: $first) { - people { - name - } + it("should allow for a single query with default values that get overridden with variables", async () => { + const query = gql` + query people($first: Int = 1) { + allPeople(first: $first) { + people { + name } } - `; + } + `; - const variables = { first: 1 }; - const override = { first: 2 }; + const variables = { first: 1 }; + const override = { first: 2 }; - const result = { - allPeople: { - people: [ - { - name: "Luke Skywalker", - }, - ], - }, - }; + const result = { + allPeople: { + people: [ + { + name: "Luke Skywalker", + }, + ], + }, + }; - const overriddenResult = { - allPeople: { - people: [ - { - name: "Luke Skywalker", - }, - { - name: "Jabba The Hutt", - }, - ], - }, - }; + const overriddenResult = { + allPeople: { + people: [ + { + name: "Luke Skywalker", + }, + { + name: "Jabba The Hutt", + }, + ], + }, + }; - const link = mockSingleLink( - { - request: { query, variables }, - result: { data: result }, - }, - { - request: { query, variables: override }, - result: { data: overriddenResult }, - } - ).setOnError(reject); + const link = mockSingleLink( + { + request: { query, variables }, + result: { data: result }, + }, + { + request: { query, variables: override }, + result: { data: overriddenResult }, + } + ); - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ addTypename: false }), - }); + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ addTypename: false }), + }); - const basic = client.query({ query, variables }).then((actualResult) => { - expect(actualResult.data).toEqual(result); - }); + { + const actualResult = await client.query({ query, variables }); - const withDefault = client.query({ query }).then((actualResult) => { - return expect(actualResult.data).toEqual(result); - }); + expect(actualResult.data).toEqual(result); + } - const withOverride = client - .query({ query, variables: override }) - .then((actualResult) => { - return expect(actualResult.data).toEqual(overriddenResult); - }); + { + const actualResult = await client.query({ query }); - return Promise.all([basic, withDefault, withOverride]).then( - resolve, - reject - ); + expect(actualResult.data).toEqual(result); + } + + { + const actualResult = await client.query({ query, variables: override }); + + expect(actualResult.data).toEqual(overriddenResult); } - ); + }); - itAsync("should allow fragments on root query", (resolve, reject) => { + it("should allow fragments on root query", async () => { const query = gql` query { ...QueryFragment @@ -330,42 +322,39 @@ describe("client", () => { __typename: "Query", }; - return clientRoundtrip(resolve, reject, query, { data }, null); + return clientRoundtrip(query, { data }, null); }); - itAsync( - "should allow fragments on root query with ifm", - (resolve, reject) => { - const query = gql` - query { - ...QueryFragment - } + it("should allow fragments on root query with ifm", async () => { + const query = gql` + query { + ...QueryFragment + } - fragment QueryFragment on Query { - records { - id - name - __typename - } + fragment QueryFragment on Query { + records { + id + name __typename } - `; + __typename + } + `; - const data = { - records: [ - { id: 1, name: "One", __typename: "Record" }, - { id: 2, name: "Two", __typename: "Record" }, - ], - __typename: "Query", - }; + const data = { + records: [ + { id: 1, name: "One", __typename: "Record" }, + { id: 2, name: "Two", __typename: "Record" }, + ], + __typename: "Query", + }; - return clientRoundtrip(resolve, reject, query, { data }, null, { - Query: ["Record"], - }); - } - ); + await clientRoundtrip(query, { data }, null, { + Query: ["Record"], + }); + }); - itAsync("should merge fragments on root query", (resolve, reject) => { + it("should merge fragments on root query", async () => { // The fragment should be used after the selected fields for the query. // Otherwise, the results aren't merged. // see: https://github.com/apollographql/apollo-client/issues/1479 @@ -395,12 +384,12 @@ describe("client", () => { __typename: "Query", }; - return clientRoundtrip(resolve, reject, query, { data }, null, { + await clientRoundtrip(query, { data }, null, { Query: ["Record"], }); }); - itAsync("store can be rehydrated from the server", (resolve, reject) => { + it("store can be rehydrated from the server", async () => { const query = gql` query people { allPeople(first: 1) { @@ -424,7 +413,7 @@ describe("client", () => { const link = mockSingleLink({ request: { query }, result: { data }, - }).setOnError(reject); + }); const initialState: any = { data: { @@ -450,269 +439,239 @@ describe("client", () => { ), }); - return client - .query({ query }) - .then((result) => { - expect(result.data).toEqual(data); - expect(finalState.data).toEqual( - (client.cache as InMemoryCache).extract() - ); - }) - .then(resolve, reject); + const result = await client.query({ query }); + + expect(result.data).toEqual(data); + expect(finalState.data).toEqual((client.cache as InMemoryCache).extract()); }); - itAsync( - "store can be rehydrated from the server using the shadow method", - (resolve, reject) => { - const query = gql` - query people { - allPeople(first: 1) { - people { - name - } + it("store can be rehydrated from the server using the shadow method", async () => { + const query = gql` + query people { + allPeople(first: 1) { + people { + name } } - `; + } + `; - const data = { - allPeople: { - people: [ - { - name: "Luke Skywalker", - }, - ], - }, - }; + const data = { + allPeople: { + people: [ + { + name: "Luke Skywalker", + }, + ], + }, + }; - const link = mockSingleLink({ - request: { query }, - result: { data }, - }).setOnError(reject); + const link = mockSingleLink({ + request: { query }, + result: { data }, + }); - const initialState: any = { - data: { - ROOT_QUERY: { - 'allPeople({"first":1})': { - people: [ - { - name: "Luke Skywalker", - }, - ], - }, + const initialState: any = { + data: { + ROOT_QUERY: { + 'allPeople({"first":1})': { + people: [ + { + name: "Luke Skywalker", + }, + ], }, - optimistic: [], }, - }; + optimistic: [], + }, + }; - const finalState = assign({}, initialState, {}); + const finalState = assign({}, initialState, {}); - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ addTypename: false }).restore( - initialState.data - ), - }); + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ addTypename: false }).restore( + initialState.data + ), + }); - return client - .query({ query }) - .then((result) => { - expect(result.data).toEqual(data); - expect(finalState.data).toEqual(client.extract()); - }) - .then(resolve, reject); - } - ); + const result = await client.query({ query }); - itAsync( - "stores shadow of restore returns the same result as accessing the method directly on the cache", - (resolve, reject) => { - const query = gql` - query people { - allPeople(first: 1) { - people { - name - } - } - } - `; + expect(result.data).toEqual(data); + expect(finalState.data).toEqual(client.extract()); + }); - const data = { - allPeople: { - people: [ - { - name: "Luke Skywalker", - }, - ], - }, - }; - - const link = mockSingleLink({ - request: { query }, - result: { data }, - }).setOnError(reject); + it("stores shadow of restore returns the same result as accessing the method directly on the cache", async () => { + const query = gql` + query people { + allPeople(first: 1) { + people { + name + } + } + } + `; - const initialState: any = { - data: { - 'ROOT_QUERY.allPeople({"first":"1"}).people.0': { + const data = { + allPeople: { + people: [ + { name: "Luke Skywalker", }, - 'ROOT_QUERY.allPeople({"first":1})': { - people: [ - { - type: "id", - generated: true, - id: 'ROOT_QUERY.allPeople({"first":"1"}).people.0', - }, - ], - }, - ROOT_QUERY: { - 'allPeople({"first":1})': { + ], + }, + }; + + const link = mockSingleLink({ + request: { query }, + result: { data }, + }); + + const initialState: any = { + data: { + 'ROOT_QUERY.allPeople({"first":"1"}).people.0': { + name: "Luke Skywalker", + }, + 'ROOT_QUERY.allPeople({"first":1})': { + people: [ + { type: "id", - id: 'ROOT_QUERY.allPeople({"first":1})', generated: true, + id: 'ROOT_QUERY.allPeople({"first":"1"}).people.0', }, + ], + }, + ROOT_QUERY: { + 'allPeople({"first":1})': { + type: "id", + id: 'ROOT_QUERY.allPeople({"first":1})', + generated: true, }, - optimistic: [], }, - }; - - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ addTypename: false }).restore( - initialState.data - ), - }); + optimistic: [], + }, + }; - expect(client.restore(initialState.data)).toEqual( - client.cache.restore(initialState.data) - ); + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ addTypename: false }).restore( + initialState.data + ), + }); - resolve(); - } - ); + expect(client.restore(initialState.data)).toEqual( + client.cache.restore(initialState.data) + ); + }); - itAsync( - "should return errors correctly for a single query", - (resolve, reject) => { - const query = gql` - query people { - allPeople(first: 1) { - people { - name - } + it("should return errors correctly for a single query", async () => { + const query = gql` + query people { + allPeople(first: 1) { + people { + name } } - `; + } + `; - const errors: GraphQLError[] = [ - new GraphQLError( - "Syntax Error GraphQL request (8:9) Expected Name, found EOF" - ), - ]; + const errors: GraphQLError[] = [ + new GraphQLError( + "Syntax Error GraphQL request (8:9) Expected Name, found EOF" + ), + ]; - const link = mockSingleLink({ - request: { query }, - result: { errors }, - }).setOnError(reject); + const link = mockSingleLink({ + request: { query }, + result: { errors }, + }); - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ addTypename: false }), - }); + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ addTypename: false }), + }); - return client - .query({ query }) - .catch((error: ApolloError) => { - expect(error.graphQLErrors).toEqual(errors); - }) - .then(resolve, reject); - } - ); + await expect(client.query({ query })).rejects.toEqual( + expect.objectContaining({ graphQLErrors: errors }) + ); + }); - itAsync( - "should return GraphQL errors correctly for a single query with an apollo-link enabled network interface", - (resolve, reject) => { - const query = gql` - query people { - allPeople(first: 1) { - people { - name - } + it("should return GraphQL errors correctly for a single query with an apollo-link enabled network interface", async () => { + const query = gql` + query people { + allPeople(first: 1) { + people { + name } } - `; + } + `; - const data = { - allPeople: { - people: [ - { - name: "Luke Skywalker", - }, - ], - }, - }; + const data = { + allPeople: { + people: [ + { + name: "Luke Skywalker", + }, + ], + }, + }; - const errors: GraphQLError[] = [ - new GraphQLError( - "Syntax Error GraphQL request (8:9) Expected Name, found EOF" - ), - ]; + const errors: GraphQLError[] = [ + new GraphQLError( + "Syntax Error GraphQL request (8:9) Expected Name, found EOF" + ), + ]; - const link = ApolloLink.from([ - () => { - return new Observable((observer) => { - observer.next({ data, errors }); - }); - }, - ]); + const link = ApolloLink.from([ + () => { + return new Observable((observer) => { + observer.next({ data, errors }); + }); + }, + ]); - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ addTypename: false }), - }); + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ addTypename: false }), + }); - client.query({ query }).catch((error: ApolloError) => { - expect(error.graphQLErrors).toEqual(errors); - resolve(); - }); - } - ); + await expect(client.query({ query })).rejects.toEqual( + expect.objectContaining({ graphQLErrors: errors }) + ); + }); - itAsync( - "should pass a network error correctly on a query with apollo-link network interface", - (resolve, reject) => { - const query = gql` - query people { - allPeople(first: 1) { - people { - name - } + it("should pass a network error correctly on a query with apollo-link network interface", async () => { + const query = gql` + query people { + allPeople(first: 1) { + people { + name } } - `; + } + `; - const networkError = new Error("Some kind of network error."); + const networkError = new Error("Some kind of network error."); - const link = ApolloLink.from([ - () => { - return new Observable((_) => { - throw networkError; - }); - }, - ]); + const link = ApolloLink.from([ + () => { + return new Observable((_) => { + throw networkError; + }); + }, + ]); - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ addTypename: false }), - }); + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ addTypename: false }), + }); - client.query({ query }).catch((error: ApolloError) => { - expect(error.networkError).toBeDefined(); - expect(error.networkError!.message).toEqual(networkError.message); - resolve(); - }); - } - ); + await client.query({ query }).catch((error: ApolloError) => { + expect(error.networkError).toBeDefined(); + expect(error.networkError!.message).toEqual(networkError.message); + }); + }); it("should not warn when receiving multiple results from apollo-link network interface", () => { const query = gql` @@ -747,117 +706,22 @@ describe("client", () => { }); }); - itAsync.skip( - "should surface errors in observer.next as uncaught", - (resolve, reject) => { - const expectedError = new Error("this error should not reach the store"); - const listeners = process.listeners("uncaughtException"); - const oldHandler = listeners[listeners.length - 1]; - const handleUncaught = (e: Error) => { - console.log(e); - process.removeListener("uncaughtException", handleUncaught); - if (typeof oldHandler === "function") - process.addListener("uncaughtException", oldHandler); - if (e === expectedError) { - resolve(); - } else { - reject(e); - } - }; - process.removeListener("uncaughtException", oldHandler); - process.addListener("uncaughtException", handleUncaught); - - const query = gql` - query people { - allPeople(first: 1) { - people { - name - } - } - } - `; - - const data = { - allPeople: { - people: [ - { - name: "Luke Skywalker", - }, - ], - }, - }; - - const link = mockSingleLink({ - request: { query }, - result: { data }, - }).setOnError(reject); - - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ addTypename: false }), - }); - - const handle = client.watchQuery({ query }); - - handle.subscribe({ - next() { - throw expectedError; - }, - }); - } - ); - - itAsync.skip( - "should surfaces errors in observer.error as uncaught", - (resolve, reject) => { - const expectedError = new Error("this error should not reach the store"); - const listeners = process.listeners("uncaughtException"); - const oldHandler = listeners[listeners.length - 1]; - const handleUncaught = (e: Error) => { - process.removeListener("uncaughtException", handleUncaught); + it.skip("should surface errors in observer.next as uncaught", async () => { + const expectedError = new Error("this error should not reach the store"); + const listeners = process.listeners("uncaughtException"); + const oldHandler = listeners[listeners.length - 1]; + const handleUncaught = (e: Error) => { + console.log(e); + process.removeListener("uncaughtException", handleUncaught); + if (typeof oldHandler === "function") process.addListener("uncaughtException", oldHandler); - if (e === expectedError) { - resolve(); - } else { - reject(e); - } - }; - process.removeListener("uncaughtException", oldHandler); - process.addListener("uncaughtException", handleUncaught); - - const query = gql` - query people { - allPeople(first: 1) { - people { - name - } - } - } - `; - - const link = mockSingleLink({ - request: { query }, - result: {}, - }).setOnError(reject); - - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ addTypename: false }), - }); - - const handle = client.watchQuery({ query }); - handle.subscribe({ - next() { - reject(new Error("did not expect next to be called")); - }, - error() { - throw expectedError; - }, - }); - } - ); + if (e !== expectedError) { + throw e; + } + }; + process.removeListener("uncaughtException", oldHandler); + process.addListener("uncaughtException", handleUncaught); - itAsync("should allow for subscribing to a request", (resolve, reject) => { const query = gql` query people { allPeople(first: 1) { @@ -881,7 +745,7 @@ describe("client", () => { const link = mockSingleLink({ request: { query }, result: { data }, - }).setOnError(reject); + }); const client = new ApolloClient({ link, @@ -891,37 +755,118 @@ describe("client", () => { const handle = client.watchQuery({ query }); handle.subscribe({ - next(result) { - expect(result.data).toEqual(data); - resolve(); + next() { + throw expectedError; }, }); }); - itAsync("should be able to transform queries", (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } + it.skip("should surfaces errors in observer.error as uncaught", async () => { + const expectedError = new Error("this error should not reach the store"); + const listeners = process.listeners("uncaughtException"); + const oldHandler = listeners[listeners.length - 1]; + const handleUncaught = (e: Error) => { + process.removeListener("uncaughtException", handleUncaught); + process.addListener("uncaughtException", oldHandler); + if (e !== expectedError) { + throw e; } - `; - const transformedQuery = gql` - query { - author { - firstName - lastName - __typename + }; + process.removeListener("uncaughtException", oldHandler); + process.addListener("uncaughtException", handleUncaught); + + const query = gql` + query people { + allPeople(first: 1) { + people { + name + } } } `; - const result = { - author: { - firstName: "John", - lastName: "Smith", - }, + const link = mockSingleLink({ + request: { query }, + result: {}, + }); + + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ addTypename: false }), + }); + + const handle = client.watchQuery({ query }); + handle.subscribe({ + next() { + throw new Error("did not expect next to be called"); + }, + error() { + throw expectedError; + }, + }); + }); + + it("should allow for subscribing to a request", async () => { + const query = gql` + query people { + allPeople(first: 1) { + people { + name + } + } + } + `; + + const data = { + allPeople: { + people: [ + { + name: "Luke Skywalker", + }, + ], + }, + }; + + const link = mockSingleLink({ + request: { query }, + result: { data }, + }); + + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ addTypename: false }), + }); + + const handle = client.watchQuery({ query }); + const stream = new ObservableStream(handle); + + await expect(stream).toEmitMatchedValue({ data }); + }); + + it("should be able to transform queries", async () => { + const query = gql` + query { + author { + firstName + lastName + } + } + `; + const transformedQuery = gql` + query { + author { + firstName + lastName + __typename + } + } + `; + + const result = { + author: { + firstName: "John", + lastName: "Smith", + }, }; const transformedResult = { author: { @@ -941,79 +886,73 @@ describe("client", () => { result: { data: transformedResult }, }, false - ).setOnError(reject); + ); const client = new ApolloClient({ link, cache: new InMemoryCache({ addTypename: true }), }); - return client - .query({ query }) - .then((actualResult) => { - expect(actualResult.data).toEqual(transformedResult); - }) - .then(resolve, reject); + const actualResult = await client.query({ query }); + + expect(actualResult.data).toEqual(transformedResult); }); - itAsync( - "should be able to transform queries on network-only fetches", - (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } + it("should be able to transform queries on network-only fetches", async () => { + const query = gql` + query { + author { + firstName + lastName } - `; - const transformedQuery = gql` - query { - author { - firstName - lastName - __typename - } + } + `; + const transformedQuery = gql` + query { + author { + firstName + lastName + __typename } - `; - const result = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; - const transformedResult = { - author: { - firstName: "John", - lastName: "Smith", - __typename: "Author", - }, - }; - const link = mockSingleLink( - { - request: { query }, - result: { data: result }, - }, - { - request: { query: transformedQuery }, - result: { data: transformedResult }, - }, - false - ).setOnError(reject); + } + `; + const result = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; + const transformedResult = { + author: { + firstName: "John", + lastName: "Smith", + __typename: "Author", + }, + }; + const link = mockSingleLink( + { + request: { query }, + result: { data: result }, + }, + { + request: { query: transformedQuery }, + result: { data: transformedResult }, + }, + false + ); - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ addTypename: true }), - }); + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ addTypename: true }), + }); - return client - .query({ fetchPolicy: "network-only", query }) - .then((actualResult) => { - expect(actualResult.data).toEqual(transformedResult); - }) - .then(resolve, reject); - } - ); + const actualResult = await client.query({ + fetchPolicy: "network-only", + query, + }); + + expect(actualResult.data).toEqual(transformedResult); + }); it("removes @client fields from the query before it reaches the link", async () => { const result: { current: Operation | undefined } = { @@ -1063,7 +1002,7 @@ describe("client", () => { expect(print(result.current!.query)).toEqual(print(transformedQuery)); }); - itAsync("should handle named fragments on mutations", (resolve, reject) => { + it("should handle named fragments on mutations", async () => { const mutation = gql` mutation { starAuthor(id: 12) { @@ -1091,117 +1030,64 @@ describe("client", () => { const link = mockSingleLink({ request: { query: mutation }, result: { data: result }, - }).setOnError(reject); + }); const client = new ApolloClient({ link, cache: new InMemoryCache({ addTypename: false }), }); - return client - .mutate({ mutation }) - .then((actualResult) => { - expect(actualResult.data).toEqual(result); - }) - .then(resolve, reject); - }); - - itAsync( - "should be able to handle named fragments on network-only queries", - (resolve, reject) => { - const query = gql` - fragment authorDetails on Author { - firstName - lastName - } - - query { - author { - __typename - ...authorDetails - } - } - `; - const result = { - author: { - __typename: "Author", - firstName: "John", - lastName: "Smith", - }, - }; - - const link = mockSingleLink({ - request: { query }, - result: { data: result }, - }).setOnError(reject); + const actualResult = await client.mutate({ mutation }); - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ addTypename: false }), - }); + expect(actualResult.data).toEqual(result); + }); - return client - .query({ fetchPolicy: "network-only", query }) - .then((actualResult) => { - expect(actualResult.data).toEqual(result); - }) - .then(resolve, reject); - } - ); + it("should be able to handle named fragments on network-only queries", async () => { + const query = gql` + fragment authorDetails on Author { + firstName + lastName + } - itAsync( - "should be able to handle named fragments with multiple fragments", - (resolve, reject) => { - const query = gql` - query { - author { - __typename - ...authorDetails - ...moreDetails - } + query { + author { + __typename + ...authorDetails } + } + `; + const result = { + author: { + __typename: "Author", + firstName: "John", + lastName: "Smith", + }, + }; - fragment authorDetails on Author { - firstName - lastName - } + const link = mockSingleLink({ + request: { query }, + result: { data: result }, + }); - fragment moreDetails on Author { - address - } - `; - const result = { - author: { - __typename: "Author", - firstName: "John", - lastName: "Smith", - address: "1337 10th St.", - }, - }; + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ addTypename: false }), + }); - const link = mockSingleLink({ - request: { query }, - result: { data: result }, - }).setOnError(reject); - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ addTypename: false }), - }); + const actualResult = await client.query({ + fetchPolicy: "network-only", + query, + }); - return client - .query({ query }) - .then((actualResult) => { - expect(actualResult.data).toEqual(result); - }) - .then(resolve, reject); - } - ); + expect(actualResult.data).toEqual(result); + }); - itAsync("should be able to handle named fragments", (resolve, reject) => { + it("should be able to handle named fragments with multiple fragments", async () => { const query = gql` query { author { __typename ...authorDetails + ...moreDetails } } @@ -1209,240 +1095,251 @@ describe("client", () => { firstName lastName } + + fragment moreDetails on Author { + address + } `; const result = { author: { __typename: "Author", firstName: "John", lastName: "Smith", + address: "1337 10th St.", }, }; const link = mockSingleLink({ request: { query }, result: { data: result }, - }).setOnError(reject); + }); const client = new ApolloClient({ link, cache: new InMemoryCache({ addTypename: false }), }); - return client - .query({ query }) - .then((actualResult) => { - expect(actualResult.data).toEqual(result); - }) - .then(resolve, reject); - }); + const actualResult = await client.query({ query }); - itAsync( - "should be able to handle inlined fragments on an Interface type", - (resolve, reject) => { - const query = gql` - query items { - items { - ...ItemFragment - __typename - } - } + expect(actualResult.data).toEqual(result); + }); - fragment ItemFragment on Item { - id - __typename - ... on ColorItem { - color - __typename - } + it("should be able to handle named fragments", async () => { + const query = gql` + query { + author { + __typename + ...authorDetails } - `; - const result = { - items: [ - { - __typename: "ColorItem", - id: "27tlpoPeXm6odAxj3paGQP", - color: "red", - }, - { - __typename: "MonochromeItem", - id: "1t3iFLsHBm4c4RjOMdMgOO", - }, - ], - }; + } - const link = mockSingleLink({ - request: { query }, - result: { data: result }, - }).setOnError(reject); - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ - possibleTypes: { - Item: ["ColorItem", "MonochromeItem"], - }, - }), - }); - return client - .query({ query }) - .then((actualResult: any) => { - expect(actualResult.data).toEqual(result); - }) - .then(resolve, reject); - } - ); + fragment authorDetails on Author { + firstName + lastName + } + `; + const result = { + author: { + __typename: "Author", + firstName: "John", + lastName: "Smith", + }, + }; - itAsync( - "should be able to handle inlined fragments on an Interface type with introspection fragment matcher", - (resolve, reject) => { - const query = gql` - query items { - items { - ...ItemFragment - __typename - } - } + const link = mockSingleLink({ + request: { query }, + result: { data: result }, + }); + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ addTypename: false }), + }); - fragment ItemFragment on Item { - id - ... on ColorItem { - color - __typename - } + const actualResult = await client.query({ query }); + + expect(actualResult.data).toEqual(result); + }); + + it("should be able to handle inlined fragments on an Interface type", async () => { + const query = gql` + query items { + items { + ...ItemFragment __typename } - `; - const result = { - items: [ - { - __typename: "ColorItem", - id: "27tlpoPeXm6odAxj3paGQP", - color: "red", - }, - { - __typename: "MonochromeItem", - id: "1t3iFLsHBm4c4RjOMdMgOO", - }, - ], - }; + } - const link = mockSingleLink({ - request: { query }, - result: { data: result }, - }).setOnError(reject); + fragment ItemFragment on Item { + id + __typename + ... on ColorItem { + color + __typename + } + } + `; + const result = { + items: [ + { + __typename: "ColorItem", + id: "27tlpoPeXm6odAxj3paGQP", + color: "red", + }, + { + __typename: "MonochromeItem", + id: "1t3iFLsHBm4c4RjOMdMgOO", + }, + ], + }; - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ - possibleTypes: { - Item: ["ColorItem", "MonochromeItem"], - }, - }), - }); + const link = mockSingleLink({ + request: { query }, + result: { data: result }, + }); + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ + possibleTypes: { + Item: ["ColorItem", "MonochromeItem"], + }, + }), + }); + const actualResult = await client.query({ query }); - return client - .query({ query }) - .then((actualResult) => { - expect(actualResult.data).toEqual(result); - }) - .then(resolve, reject); - } - ); + expect(actualResult.data).toEqual(result); + }); - itAsync( - "should call updateQueries and update after mutation on query with inlined fragments on an Interface type", - (resolve, reject) => { - const query = gql` - query items { - items { - ...ItemFragment - __typename - } + it("should be able to handle inlined fragments on an Interface type with introspection fragment matcher", async () => { + const query = gql` + query items { + items { + ...ItemFragment + __typename } + } - fragment ItemFragment on Item { - id - ... on ColorItem { - color - __typename - } + fragment ItemFragment on Item { + id + ... on ColorItem { + color __typename } - `; - const result = { - items: [ - { - __typename: "ColorItem", - id: "27tlpoPeXm6odAxj3paGQP", - color: "red", - }, - { - __typename: "MonochromeItem", - id: "1t3iFLsHBm4c4RjOMdMgOO", - }, - ], - }; + __typename + } + `; + const result = { + items: [ + { + __typename: "ColorItem", + id: "27tlpoPeXm6odAxj3paGQP", + color: "red", + }, + { + __typename: "MonochromeItem", + id: "1t3iFLsHBm4c4RjOMdMgOO", + }, + ], + }; - const mutation = gql` - mutation myMutationName { - fortuneCookie + const link = mockSingleLink({ + request: { query }, + result: { data: result }, + }); + + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ + possibleTypes: { + Item: ["ColorItem", "MonochromeItem"], + }, + }), + }); + + const actualResult = await client.query({ query }); + + expect(actualResult.data).toEqual(result); + }); + + it("should call updateQueries and update after mutation on query with inlined fragments on an Interface type", async () => { + const query = gql` + query items { + items { + ...ItemFragment + __typename } - `; - const mutationResult = { - fortuneCookie: "The waiter spit in your food", - }; + } - const link = mockSingleLink( + fragment ItemFragment on Item { + id + ... on ColorItem { + color + __typename + } + __typename + } + `; + const result = { + items: [ { - request: { query }, - result: { data: result }, + __typename: "ColorItem", + id: "27tlpoPeXm6odAxj3paGQP", + color: "red", }, { - request: { query: mutation }, - result: { data: mutationResult }, - } - ).setOnError(reject); + __typename: "MonochromeItem", + id: "1t3iFLsHBm4c4RjOMdMgOO", + }, + ], + }; - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ - possibleTypes: { - Item: ["ColorItem", "MonochromeItem"], - }, - }), - }); + const mutation = gql` + mutation myMutationName { + fortuneCookie + } + `; + const mutationResult = { + fortuneCookie: "The waiter spit in your food", + }; - const queryUpdaterSpy = jest.fn(); - const queryUpdater = (prev: any) => { - queryUpdaterSpy(); - return prev; - }; - const updateQueries = { - items: queryUpdater, - }; + const link = mockSingleLink( + { + request: { query }, + result: { data: result }, + }, + { + request: { query: mutation }, + result: { data: mutationResult }, + } + ); - const updateSpy = jest.fn(); + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ + possibleTypes: { + Item: ["ColorItem", "MonochromeItem"], + }, + }), + }); - const obs = client.watchQuery({ query }); + const queryUpdaterSpy = jest.fn(); + const queryUpdater = (prev: any) => { + queryUpdaterSpy(); + return prev; + }; + const updateQueries = { + items: queryUpdater, + }; - const sub = obs.subscribe({ - next() { - client - .mutate({ mutation, updateQueries, update: updateSpy }) - .then(() => { - expect(queryUpdaterSpy).toBeCalled(); - expect(updateSpy).toBeCalled(); - sub.unsubscribe(); - resolve(); - }) - .catch((err) => { - reject(err); - }); - }, - error(err) { - reject(err); - }, - }); - } - ); + const updateSpy = jest.fn(); + + const obs = client.watchQuery({ query }); + const stream = new ObservableStream(obs); + + await expect(stream).toEmitNext(); + await client.mutate({ mutation, updateQueries, update: updateSpy }); + + expect(queryUpdaterSpy).toBeCalled(); + expect(updateSpy).toBeCalled(); + }); it("should send operationName along with the query to the server", () => { const query = gql` @@ -1494,61 +1391,7 @@ describe("client", () => { }); }); - itAsync( - "does not deduplicate queries if option is set to false", - (resolve, reject) => { - const queryDoc = gql` - query { - author { - name - } - } - `; - const data = { - author: { - name: "Jonas", - }, - }; - const data2 = { - author: { - name: "Dhaivat", - }, - }; - - // we have two responses for identical queries, and both should be requested. - // the second one should make it through to the network interface. - const link = mockSingleLink( - { - request: { query: queryDoc }, - result: { data }, - delay: 10, - }, - { - request: { query: queryDoc }, - result: { data: data2 }, - } - ).setOnError(reject); - - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ addTypename: false }), - queryDeduplication: false, - }); - - const q1 = client.query({ query: queryDoc }); - const q2 = client.query({ query: queryDoc }); - - // if deduplication happened, result2.data will equal data. - return Promise.all([q1, q2]) - .then(([result1, result2]) => { - expect(result1.data).toEqual(data); - expect(result2.data).toEqual(data2); - }) - .then(resolve, reject); - } - ); - - itAsync("deduplicates queries by default", (resolve, reject) => { + it("does not deduplicate queries if option is set to false", async () => { const queryDoc = gql` query { author { @@ -1567,8 +1410,8 @@ describe("client", () => { }, }; - // we have two responses for identical queries, but only the first should be requested. - // the second one should never make it through to the network interface. + // we have two responses for identical queries, and both should be requested. + // the second one should make it through to the network interface. const link = mockSingleLink( { request: { query: queryDoc }, @@ -1579,24 +1422,25 @@ describe("client", () => { request: { query: queryDoc }, result: { data: data2 }, } - ).setOnError(reject); + ); + const client = new ApolloClient({ link, cache: new InMemoryCache({ addTypename: false }), + queryDeduplication: false, }); const q1 = client.query({ query: queryDoc }); const q2 = client.query({ query: queryDoc }); - // if deduplication didn't happen, result.data will equal data2. - return Promise.all([q1, q2]) - .then(([result1, result2]) => { - expect(result1.data).toEqual(result2.data); - }) - .then(resolve, reject); + // if deduplication happened, result2.data will equal data. + const [result1, result2] = await Promise.all([q1, q2]); + + expect(result1.data).toEqual(data); + expect(result2.data).toEqual(data2); }); - it("deduplicates queries if query context.queryDeduplication is set to true", () => { + it("deduplicates queries by default", async () => { const queryDoc = gql` query { author { @@ -1631,24 +1475,70 @@ describe("client", () => { const client = new ApolloClient({ link, cache: new InMemoryCache({ addTypename: false }), - queryDeduplication: false, }); - // Both queries need to be deduplicated, otherwise only one gets tracked - const q1 = client.query({ - query: queryDoc, - context: { queryDeduplication: true }, - }); - const q2 = client.query({ - query: queryDoc, - context: { queryDeduplication: true }, - }); + const q1 = client.query({ query: queryDoc }); + const q2 = client.query({ query: queryDoc }); - // if deduplication happened, result2.data will equal data. - return Promise.all([q1, q2]).then(([result1, result2]) => { - expect(result1.data).toEqual(data); - expect(result2.data).toEqual(data); - }); + // if deduplication didn't happen, result.data will equal data2. + const [result1, result2] = await Promise.all([q1, q2]); + + expect(result1.data).toEqual(result2.data); + }); + + it("deduplicates queries if query context.queryDeduplication is set to true", () => { + const queryDoc = gql` + query { + author { + name + } + } + `; + const data = { + author: { + name: "Jonas", + }, + }; + const data2 = { + author: { + name: "Dhaivat", + }, + }; + + // we have two responses for identical queries, but only the first should be requested. + // the second one should never make it through to the network interface. + const link = mockSingleLink( + { + request: { query: queryDoc }, + result: { data }, + delay: 10, + }, + { + request: { query: queryDoc }, + result: { data: data2 }, + } + ); + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ addTypename: false }), + queryDeduplication: false, + }); + + // Both queries need to be deduplicated, otherwise only one gets tracked + const q1 = client.query({ + query: queryDoc, + context: { queryDeduplication: true }, + }); + const q2 = client.query({ + query: queryDoc, + context: { queryDeduplication: true }, + }); + + // if deduplication happened, result2.data will equal data. + return Promise.all([q1, q2]).then(([result1, result2]) => { + expect(result1.data).toEqual(data); + expect(result2.data).toEqual(data); + }); }); it("does not deduplicate queries if query context.queryDeduplication is set to false", () => { @@ -1702,53 +1592,53 @@ describe("client", () => { }); }); - itAsync( - "unsubscribes from deduplicated observables only once", - (resolve, reject) => { - const document: DocumentNode = gql` - query test1($x: String) { - test(x: $x) - } - `; + it("unsubscribes from deduplicated observables only once", async () => { + const document: DocumentNode = gql` + query test1($x: String) { + test(x: $x) + } + `; - const variables1 = { x: "Hello World" }; - const variables2 = { x: "Hello World" }; + const variables1 = { x: "Hello World" }; + const variables2 = { x: "Hello World" }; - let unsubscribed = false; + let unsubscribeCount = 0; - const client = new ApolloClient({ - link: new ApolloLink(() => { - return new Observable((observer) => { - observer.complete(); - return () => { - unsubscribed = true; - setTimeout(resolve, 0); - }; - }); - }), - cache: new InMemoryCache(), - }); + const client = new ApolloClient({ + link: new ApolloLink(() => { + return new Observable((observer) => { + observer.complete(); + return () => { + unsubscribeCount++; + }; + }); + }), + cache: new InMemoryCache(), + }); - const sub1 = client - .watchQuery({ - query: document, - variables: variables1, - }) - .subscribe({}); + const sub1 = client + .watchQuery({ + query: document, + variables: variables1, + }) + .subscribe({}); - const sub2 = client - .watchQuery({ - query: document, - variables: variables2, - }) - .subscribe({}); + const sub2 = client + .watchQuery({ + query: document, + variables: variables2, + }) + .subscribe({}); - sub1.unsubscribe(); - expect(unsubscribed).toBe(false); + sub1.unsubscribe(); + // cleanup happens async + expect(unsubscribeCount).toBe(0); - sub2.unsubscribe(); - } - ); + sub2.unsubscribe(); + + await wait(0); + expect(unsubscribeCount).toBe(1); + }); describe("deprecated options", () => { const query = gql` @@ -1801,11 +1691,11 @@ describe("client", () => { }, }; - itAsync("for internal store", (resolve, reject) => { + it("for internal store", async () => { const link = mockSingleLink({ request: { query }, result: { data }, - }).setOnError(reject); + }); const client = new ApolloClient({ link, @@ -1816,16 +1706,13 @@ describe("client", () => { }), }); - return client - .query({ query }) - .then((result) => { - expect(result.data).toEqual(data); - expect((client.cache as InMemoryCache).extract()["1"]).toEqual({ - id: "1", - name: "Luke Skywalker", - }); - }) - .then(resolve, reject); + const result = await client.query({ query }); + + expect(result.data).toEqual(data); + expect((client.cache as InMemoryCache).extract()["1"]).toEqual({ + id: "1", + name: "Luke Skywalker", + }); }); }); @@ -1950,7 +1837,7 @@ describe("client", () => { await expect(stream).not.toEmitAnything(); }); - itAsync("fails if network request fails", (resolve, reject) => { + it("fails if network request fails", async () => { const link = mockSingleLink(); // no queries = no replies. const client = new ApolloClient({ link, @@ -1961,59 +1848,42 @@ describe("client", () => { query, fetchPolicy: "cache-and-network", }); + const stream = new ObservableStream(obs); - obs.subscribe({ - error: (e) => { - if (!/No more mocked responses/.test(e.message)) { - reject(e); - } else { - resolve(); - } - }, - }); + const error = await stream.takeError(); + + expect(error.message).toMatch(/No more mocked responses/); }); - itAsync( - "fetches from cache first, then network and does not have an unhandled error", - (resolve, reject) => { - const link = mockSingleLink({ - request: { query }, - result: { errors: [{ message: "network failure" }] }, - }).setOnError(reject); + it("fetches from cache first, then network and does not have an unhandled error", async () => { + const link = mockSingleLink({ + request: { query }, + result: { errors: [{ message: "network failure" }] }, + }); - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ addTypename: false }), - }); + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ addTypename: false }), + }); - client.writeQuery({ query, data: initialData }); + client.writeQuery({ query, data: initialData }); - const obs = client.watchQuery({ - query, - fetchPolicy: "cache-and-network", - }); - let shouldFail = true; - process.once("unhandledRejection", (rejection) => { - if (shouldFail) reject("promise had an unhandledRejection"); - }); - let count = 0; - obs.subscribe({ - next: (result) => { - expect(result.data).toEqual(initialData); - expect(result.loading).toBe(true); - count++; - }, - error: (e) => { - expect(e.message).toMatch(/network failure/); - expect(count).toBe(1); // make sure next was called. - setTimeout(() => { - shouldFail = false; - resolve(); - }, 0); - }, - }); - } - ); + const obs = client.watchQuery({ + query, + fetchPolicy: "cache-and-network", + }); + const stream = new ObservableStream(obs); + + await expect(stream).toEmitValue({ + loading: true, + data: initialData, + networkStatus: 1, + }); + + const error = await stream.takeError(); + + expect(error.message).toMatch(/network failure/); + }); }); describe("standby queries", () => { @@ -2096,7 +1966,7 @@ describe("client", () => { }, }; - function makeLink(reject: (reason: any) => any) { + function makeLink() { return mockSingleLink( { request: { query }, @@ -2106,31 +1976,26 @@ describe("client", () => { request: { query }, result: { data: secondFetch }, } - ).setOnError(reject); + ); } - itAsync("forces the query to rerun", (resolve, reject) => { + it("forces the query to rerun", async () => { const client = new ApolloClient({ - link: makeLink(reject), + link: makeLink(), cache: new InMemoryCache({ addTypename: false }), }); // Run a query first to initialize the store - return ( - client - .query({ query }) - // then query for real - .then(() => client.query({ query, fetchPolicy: "network-only" })) - .then((result) => { - expect(result.data).toEqual({ myNumber: { n: 2 } }); - }) - .then(resolve, reject) - ); + await client.query({ query }); + // then query for real + const result = await client.query({ query, fetchPolicy: "network-only" }); + + expect(result.data).toEqual({ myNumber: { n: 2 } }); }); - itAsync("can be disabled with ssrMode", (resolve, reject) => { + it("can be disabled with ssrMode", async () => { const client = new ApolloClient({ - link: makeLink(reject), + link: makeLink(), ssrMode: true, cache: new InMemoryCache({ addTypename: false }), }); @@ -2138,185 +2003,82 @@ describe("client", () => { const options: QueryOptions = { query, fetchPolicy: "network-only" }; // Run a query first to initialize the store - return ( - client - .query({ query }) - // then query for real - .then(() => client.query(options)) - .then((result) => { - expect(result.data).toEqual({ myNumber: { n: 1 } }); - // Test that options weren't mutated, issue #339 - expect(options).toEqual({ - query, - fetchPolicy: "network-only", - }); - }) - .then(resolve, reject) - ); - }); - - itAsync( - "can temporarily be disabled with ssrForceFetchDelay", - (resolve, reject) => { - const client = new ApolloClient({ - link: makeLink(reject), - ssrForceFetchDelay: 100, - cache: new InMemoryCache({ addTypename: false }), - }); + await client.query({ query }); + // then query for real + const result = await client.query(options); - // Run a query first to initialize the store - return ( - client - .query({ query }) - // then query for real - .then(() => { - return client.query({ query, fetchPolicy: "network-only" }); - }) - .then(async (result) => { - expect(result.data).toEqual({ myNumber: { n: 1 } }); - await new Promise((resolve) => setTimeout(resolve, 100)); - return client.query({ query, fetchPolicy: "network-only" }); - }) - .then((result) => { - expect(result.data).toEqual({ myNumber: { n: 2 } }); - }) - .then(resolve, reject) - ); - } - ); - }); + expect(result.data).toEqual({ myNumber: { n: 1 } }); + // Test that options weren't mutated, issue #339 + expect(options).toEqual({ + query, + fetchPolicy: "network-only", + }); + }); - itAsync( - "should pass a network error correctly on a mutation", - (resolve, reject) => { - const mutation = gql` - mutation { - person { - firstName - lastName - } - } - `; - const data = { - person: { - firstName: "John", - lastName: "Smith", - }, - }; - const networkError = new Error("Some kind of network error."); + it("can temporarily be disabled with ssrForceFetchDelay", async () => { const client = new ApolloClient({ - link: mockSingleLink({ - request: { query: mutation }, - result: { data }, - error: networkError, - }), + link: makeLink(), + ssrForceFetchDelay: 100, cache: new InMemoryCache({ addTypename: false }), }); - client - .mutate({ mutation }) - .then((_) => { - reject(new Error("Returned a result when it should not have.")); - }) - .catch((error: ApolloError) => { - expect(error.networkError).toBeDefined(); - expect(error.networkError!.message).toBe(networkError.message); - resolve(); + // Run a query first to initialize the store + await client.query({ query }); + // then query for real + { + const result = await client.query({ + query, + fetchPolicy: "network-only", }); - } - ); - itAsync( - "should pass a GraphQL error correctly on a mutation", - (resolve, reject) => { - const mutation = gql` - mutation { - newPerson { - person { - firstName - lastName - } - } - } - `; - const data = { - person: { - firstName: "John", - lastName: "Smith", - }, - }; - const errors = [new Error("Some kind of GraphQL error.")]; - const client = new ApolloClient({ - link: mockSingleLink({ - request: { query: mutation }, - result: { data, errors }, - }).setOnError(reject), - cache: new InMemoryCache({ addTypename: false }), - }); - client - .mutate({ mutation }) - .then((_) => { - reject(new Error("Returned a result when it should not have.")); - }) - .catch((error: ApolloError) => { - expect(error.graphQLErrors).toBeDefined(); - expect(error.graphQLErrors.length).toBe(1); - expect(error.graphQLErrors[0].message).toBe(errors[0].message); - resolve(); - }); - } - ); + expect(result.data).toEqual({ myNumber: { n: 1 } }); + } - itAsync( - "should allow errors to be returned from a mutation", - (resolve, reject) => { - const mutation = gql` - mutation { - newPerson { - person { - firstName - lastName - } - } + await wait(100); + + const result = await client.query({ query, fetchPolicy: "network-only" }); + + expect(result.data).toEqual({ myNumber: { n: 2 } }); + }); + }); + + it("should pass a network error correctly on a mutation", async () => { + const mutation = gql` + mutation { + person { + firstName + lastName } - `; - const data = { - person: { - firstName: "John", - lastName: "Smith", - }, - }; - const errors = [new Error("Some kind of GraphQL error.")]; - const client = new ApolloClient({ - link: mockSingleLink({ - request: { query: mutation }, - result: { - errors, - data: { - newPerson: data, - }, - }, - }).setOnError(reject), - cache: new InMemoryCache({ addTypename: false }), - }); - client - .mutate({ mutation, errorPolicy: "all" }) - .then((result) => { - expect(result.errors).toBeDefined(); - expect(result.errors!.length).toBe(1); - expect(result.errors![0].message).toBe(errors[0].message); - expect(result.data).toEqual({ - newPerson: data, - }); - resolve(); - }) - .catch((error: ApolloError) => { - throw error; - }); + } + `; + const data = { + person: { + firstName: "John", + lastName: "Smith", + }, + }; + const networkError = new Error("Some kind of network error."); + const client = new ApolloClient({ + link: mockSingleLink({ + request: { query: mutation }, + result: { data }, + error: networkError, + }), + cache: new InMemoryCache({ addTypename: false }), + }); + + try { + await client.mutate({ mutation }); + throw new Error("Returned a result when it should not have."); + } catch (e) { + const error = e as ApolloError; + + expect(error.networkError).toBeDefined(); + expect(error.networkError!.message).toBe(networkError.message); } - ); + }); - itAsync("should strip errors on a mutation if ignored", (resolve, reject) => { + it("should pass a GraphQL error correctly on a mutation", async () => { const mutation = gql` mutation { newPerson { @@ -2328,11 +2090,9 @@ describe("client", () => { } `; const data = { - newPerson: { - person: { - firstName: "John", - lastName: "Smith", - }, + person: { + firstName: "John", + lastName: "Smith", }, }; const errors = [new Error("Some kind of GraphQL error.")]; @@ -2340,80 +2100,144 @@ describe("client", () => { link: mockSingleLink({ request: { query: mutation }, result: { data, errors }, - }).setOnError(reject), + }), cache: new InMemoryCache({ addTypename: false }), }); - client - .mutate({ mutation, errorPolicy: "ignore" }) - .then((result) => { - expect(result.errors).toBeUndefined(); - expect(result.data).toEqual(data); - resolve(); - }) - .catch((error: ApolloError) => { - throw error; - }); + + await expect(client.mutate({ mutation })).rejects.toEqual( + expect.objectContaining({ graphQLErrors: errors }) + ); }); - itAsync( - "should rollback optimistic after mutation got a GraphQL error", - (resolve, reject) => { - const mutation = gql` - mutation { - newPerson { - person { - firstName - lastName - } + it("should allow errors to be returned from a mutation", async () => { + const mutation = gql` + mutation { + newPerson { + person { + firstName + lastName } } - `; - const data = { - newPerson: { - person: { - firstName: "John", - lastName: "Smith", - }, - }, - }; - const errors = [new Error("Some kind of GraphQL error.")]; - const client = new ApolloClient({ - link: mockSingleLink({ - request: { query: mutation }, - result: { data, errors }, - }).setOnError(reject), - cache: new InMemoryCache({ addTypename: false }), - }); - const mutatePromise = client.mutate({ - mutation, - optimisticResponse: { - newPerson: { - person: { - firstName: "John*", - lastName: "Smith*", - }, + } + `; + const data = { + person: { + firstName: "John", + lastName: "Smith", + }, + }; + const errors = [new Error("Some kind of GraphQL error.")]; + const client = new ApolloClient({ + link: mockSingleLink({ + request: { query: mutation }, + result: { + errors, + data: { + newPerson: data, }, }, - }); + }), + cache: new InMemoryCache({ addTypename: false }), + }); - { - const { data, optimisticData } = client.cache as any; - expect(optimisticData).not.toBe(data); - expect(optimisticData.parent).toBe(data.stump); - expect(optimisticData.parent.parent).toBe(data); + const result = await client.mutate({ mutation, errorPolicy: "all" }); + + expect(result.errors).toBeDefined(); + expect(result.errors!.length).toBe(1); + expect(result.errors![0].message).toBe(errors[0].message); + expect(result.data).toEqual({ + newPerson: data, + }); + }); + + it("should strip errors on a mutation if ignored", async () => { + const mutation = gql` + mutation { + newPerson { + person { + firstName + lastName + } + } } + `; + const data = { + newPerson: { + person: { + firstName: "John", + lastName: "Smith", + }, + }, + }; + const errors = [new Error("Some kind of GraphQL error.")]; + const client = new ApolloClient({ + link: mockSingleLink({ + request: { query: mutation }, + result: { data, errors }, + }), + cache: new InMemoryCache({ addTypename: false }), + }); - mutatePromise - .then((_) => { - reject(new Error("Returned a result when it should not have.")); - }) - .catch((_: ApolloError) => { - const { data, optimisticData } = client.cache as any; - expect(optimisticData).toBe(data.stump); - resolve(); - }); + const result = await client.mutate({ mutation, errorPolicy: "ignore" }); + + expect(result.errors).toBeUndefined(); + expect(result.data).toEqual(data); + }); + + it("should rollback optimistic after mutation got a GraphQL error", async () => { + const mutation = gql` + mutation { + newPerson { + person { + firstName + lastName + } + } + } + `; + const data = { + newPerson: { + person: { + firstName: "John", + lastName: "Smith", + }, + }, + }; + const errors = [new Error("Some kind of GraphQL error.")]; + const client = new ApolloClient({ + link: mockSingleLink({ + request: { query: mutation }, + result: { data, errors }, + }), + cache: new InMemoryCache({ addTypename: false }), + }); + const mutatePromise = client.mutate({ + mutation, + optimisticResponse: { + newPerson: { + person: { + firstName: "John*", + lastName: "Smith*", + }, + }, + }, + }); + + { + const { data, optimisticData } = client.cache as any; + expect(optimisticData).not.toBe(data); + expect(optimisticData.parent).toBe(data.stump); + expect(optimisticData.parent.parent).toBe(data); } - ); + + await expect(mutatePromise).rejects.toThrow(); + + { + const { data, optimisticData } = client.cache as any; + + expect(optimisticData).toBe(data.stump); + } + }); it("has a clearStore method which calls QueryManager", async () => { const client = new ApolloClient({ @@ -2526,100 +2350,83 @@ describe("client", () => { expect(count).toEqual(2); }); - itAsync( - "invokes onResetStore callbacks before notifying queries during resetStore call", - async (resolve, reject) => { - const delay = (time: number) => new Promise((r) => setTimeout(r, time)); + it("invokes onResetStore callbacks before notifying queries during resetStore call", async () => { + const delay = (time: number) => new Promise((r) => setTimeout(r, time)); - const query = gql` - query { - author { - firstName - lastName - } + const query = gql` + query { + author { + firstName + lastName } - `; + } + `; - const data = { - author: { - __typename: "Author", - firstName: "John", - lastName: "Smith", - }, - }; + const data = { + author: { + __typename: "Author", + firstName: "John", + lastName: "Smith", + }, + }; - const data2 = { - author: { - __typename: "Author", - firstName: "Joe", - lastName: "Joe", - }, - }; + const data2 = { + author: { + __typename: "Author", + firstName: "Joe", + lastName: "Joe", + }, + }; - const link = ApolloLink.from([ - new ApolloLink( - () => - new Observable((observer) => { - observer.next({ data }); - observer.complete(); - return; - }) - ), - ]); + const link = ApolloLink.from([ + new ApolloLink( + () => + new Observable((observer) => { + observer.next({ data }); + observer.complete(); + return; + }) + ), + ]); - const client = new ApolloClient({ - link, - cache: new InMemoryCache(), - }); + const client = new ApolloClient({ + link, + cache: new InMemoryCache(), + }); - let count = 0; - const onResetStoreOne = jest.fn(async () => { - expect(count).toEqual(0); - await delay(10).then(() => count++); - expect(count).toEqual(1); - }); + let count = 0; + const onResetStoreOne = jest.fn(async () => { + expect(count).toEqual(0); + await delay(10).then(() => count++); + expect(count).toEqual(1); + }); - const onResetStoreTwo = jest.fn(async () => { - expect(count).toEqual(0); - await delay(11).then(() => count++); - expect(count).toEqual(2); - expect(client.readQuery({ query })).toBe(null); - client.cache.writeQuery({ query, data: data2 }); - }); + const onResetStoreTwo = jest.fn(async () => { + expect(count).toEqual(0); + await delay(11).then(() => count++); + expect(count).toEqual(2); + expect(client.readQuery({ query })).toBe(null); + client.cache.writeQuery({ query, data: data2 }); + }); - client.onResetStore(onResetStoreOne); - client.onResetStore(onResetStoreTwo); + client.onResetStore(onResetStoreOne); + client.onResetStore(onResetStoreTwo); - let called = false; - const next = jest.fn((d) => { - if (called) { - expect(onResetStoreOne).toHaveBeenCalled(); - } else { - expect(d.data).toEqual(data); - called = true; - } - }); + const observable = client.watchQuery({ + query, + notifyOnNetworkStatusChange: false, + }); + const stream = new ObservableStream(observable); - client - .watchQuery({ - query, - notifyOnNetworkStatusChange: false, - }) - .subscribe({ - next, - error: reject, - complete: reject, - }); + expect(count).toBe(0); + await client.resetStore(); + expect(count).toBe(2); - expect(count).toEqual(0); - await client.resetStore(); - expect(count).toEqual(2); - //watchQuery should only receive data twice - expect(next).toHaveBeenCalledTimes(2); + await expect(stream).toEmitMatchedValue({ data }); + await expect(stream).toEmitNext(); - resolve(); - } - ); + expect(onResetStoreOne).toHaveBeenCalled(); + }); it("has a reFetchObservableQueries method which calls QueryManager", async () => { const client = new ApolloClient({ @@ -2690,159 +2497,126 @@ describe("client", () => { expect(spy).toHaveBeenCalled(); }); - itAsync( - "should propagate errors from network interface to observers", - (resolve, reject) => { - const link = ApolloLink.from([ - () => - new Observable((x) => { - x.error(new Error("Uh oh!")); - return; - }), - ]); + it("should propagate errors from network interface to observers", async () => { + const link = ApolloLink.from([ + () => + new Observable((x) => { + x.error(new Error("Uh oh!")); + return; + }), + ]); - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ addTypename: false }), - }); + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ addTypename: false }), + }); - const handle = client.watchQuery({ - query: gql` - query { - a - b - c - } - `, - }); + const handle = client.watchQuery({ + query: gql` + query { + a + b + c + } + `, + }); - handle.subscribe({ - error(error) { - expect(error.message).toBe("Uh oh!"); - resolve(); - }, - }); - } - ); - - itAsync( - "should be able to refetch after there was a network error", - (resolve, reject) => { - const query: DocumentNode = gql` - query somethingelse { - allPeople(first: 1) { - people { - name - } + const stream = new ObservableStream(handle); + + const error = await stream.takeError(); + + expect(error.message).toBe("Uh oh!"); + }); + + it("should be able to refetch after there was a network error", async () => { + const query: DocumentNode = gql` + query somethingelse { + allPeople(first: 1) { + people { + name } } - `; + } + `; - const data = { allPeople: { people: [{ name: "Luke Skywalker" }] } }; - const dataTwo = { allPeople: { people: [{ name: "Princess Leia" }] } }; - const link = mockSingleLink( - { request: { query }, result: { data } }, - { request: { query }, error: new Error("This is an error!") }, - { request: { query }, result: { data: dataTwo } } - ); - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ addTypename: false }), - }); + const data = { allPeople: { people: [{ name: "Luke Skywalker" }] } }; + const dataTwo = { allPeople: { people: [{ name: "Princess Leia" }] } }; + const link = mockSingleLink( + { request: { query }, result: { data } }, + { request: { query }, error: new Error("This is an error!") }, + { request: { query }, result: { data: dataTwo } } + ); + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ addTypename: false }), + }); - let count = 0; - const noop = () => null; + const observable = client.watchQuery({ + query, + notifyOnNetworkStatusChange: true, + }); - const observable = client.watchQuery({ - query, - notifyOnNetworkStatusChange: true, - }); + let stream = new ObservableStream(observable); - let subscription: any = null; - - const observerOptions = { - next(result: any) { - try { - switch (count++) { - case 0: - if (!result.data!.allPeople) { - reject("Should have data by this point"); - break; - } - // First result is loaded, run a refetch to get the second result - // which is an error. - expect(result.loading).toBeFalsy(); - expect(result.networkStatus).toBe(7); - expect(result.data!.allPeople).toEqual(data.allPeople); - setTimeout(() => { - observable.refetch().then(() => { - reject("Expected error value on first refetch."); - }, noop); - }, 0); - break; - case 1: - // Waiting for the second result to load - expect(result.loading).toBeTruthy(); - expect(result.networkStatus).toBe(4); - break; - // case 2 is handled by the error callback - case 3: - expect(result.loading).toBeTruthy(); - expect(result.networkStatus).toBe(4); - expect(result.errors).toBeFalsy(); - break; - case 4: - // Third result's data is loaded - expect(result.loading).toBeFalsy(); - expect(result.networkStatus).toBe(7); - expect(result.errors).toBeFalsy(); - if (!result.data) { - reject("Should have data by this point"); - break; - } - expect(result.data.allPeople).toEqual(dataTwo.allPeople); - resolve(); - break; - default: - throw new Error("Unexpected fall through"); - } - } catch (e) { - reject(e); - } - }, - error(error: Error) { - expect(count++).toBe(2); - expect(error.message).toBe("This is an error!"); - - subscription.unsubscribe(); - - const lastError = observable.getLastError(); - expect(lastError).toBeInstanceOf(ApolloError); - expect(lastError!.networkError).toEqual((error as any).networkError); - - const lastResult = observable.getLastResult(); - expect(lastResult).toBeTruthy(); - expect(lastResult!.loading).toBe(false); - expect(lastResult!.networkStatus).toBe(8); - - observable.resetLastResults(); - subscription = observable.subscribe(observerOptions); - - // The error arrived, run a refetch to get the third result - // which should now contain valid data. - setTimeout(() => { - observable.refetch().catch(() => { - reject("Expected good data on second refetch."); - }); - }, 0); - }, - }; + await expect(stream).toEmitValue({ + loading: false, + networkStatus: NetworkStatus.ready, + data, + }); - subscription = observable.subscribe(observerOptions); - } - ); + await wait(0); + await expect(observable.refetch()).rejects.toThrow(); + + await expect(stream).toEmitValue({ + loading: true, + networkStatus: NetworkStatus.refetch, + data, + }); + + const error = await stream.takeError(); + + expect(error.message).toBe("This is an error!"); + + stream.unsubscribe(); + + const lastError = observable.getLastError(); + expect(lastError).toBeInstanceOf(ApolloError); + expect(lastError!.networkError).toEqual((error as any).networkError); + + const lastResult = observable.getLastResult(); + expect(lastResult).toBeTruthy(); + expect(lastResult!.loading).toBe(false); + expect(lastResult!.networkStatus).toBe(8); + + observable.resetLastResults(); + stream = new ObservableStream(observable); + + await expect(stream).toEmitValue({ + loading: false, + networkStatus: NetworkStatus.ready, + data, + }); - itAsync("should throw a GraphQL error", (resolve, reject) => { + await wait(0); + await expect(observable.refetch()).resolves.toBeTruthy(); + + await expect(stream).toEmitValue({ + loading: true, + networkStatus: NetworkStatus.refetch, + data, + }); + + await expect(stream).toEmitValue({ + loading: false, + networkStatus: NetworkStatus.ready, + errors: undefined, + data: dataTwo, + }); + + await expect(stream).not.toEmitAnything(); + }); + + it("should throw a GraphQL error", async () => { const query = gql` query { posts { @@ -2857,678 +2631,624 @@ describe("client", () => { const link = mockSingleLink({ request: { query }, result: { errors }, - }).setOnError(reject); + }); const client = new ApolloClient({ link, cache: new InMemoryCache(), }); - return client - .query({ query }) - .catch((err) => { - expect(err.message).toBe('Cannot query field "foo" on type "Post".'); - }) - .then(resolve, reject); + await expect(client.query({ query })).rejects.toThrow( + 'Cannot query field "foo" on type "Post".' + ); }); it("should warn if server returns wrong data", async () => { using _consoleSpies = spyOnConsole.takeSnapshots("error"); - await new Promise((resolve, reject) => { - const query = gql` - query { - todos { - id - name - description - __typename - } + const query = gql` + query { + todos { + id + name + description + __typename } - `; - const result = { - data: { - todos: [ - { - id: "1", - name: "Todo 1", - price: 100, - __typename: "Todo", - }, - ], - }, - }; - - const link = mockSingleLink({ - request: { query }, - result, - }).setOnError(reject); - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ - // Passing an empty map enables the warning: - possibleTypes: {}, - }), - }); + } + `; + const result = { + data: { + todos: [ + { + id: "1", + name: "Todo 1", + price: 100, + __typename: "Todo", + }, + ], + }, + }; - return client - .query({ query }) - .then(({ data }) => { - expect(data).toEqual(result.data); - }) - .then(resolve, reject); + const link = mockSingleLink({ + request: { query }, + result, + }); + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ + // Passing an empty map enables the warning: + possibleTypes: {}, + }), }); + + const { data } = await client.query({ query }); + + expect(data).toEqual(result.data); }); - itAsync( - "runs a query with the connection directive and writes it to the store key defined in the directive", - (resolve, reject) => { - const query = gql` - { - books(skip: 0, limit: 2) @connection(key: "abc") { - name - } + it("runs a query with the connection directive and writes it to the store key defined in the directive", async () => { + const query = gql` + { + books(skip: 0, limit: 2) @connection(key: "abc") { + name } - `; + } + `; - const transformedQuery = gql` - { - books(skip: 0, limit: 2) { - name - __typename - } + const transformedQuery = gql` + { + books(skip: 0, limit: 2) { + name + __typename } - `; + } + `; - const result = { - books: [ - { - name: "abcd", - __typename: "Book", - }, - ], - }; + const result = { + books: [ + { + name: "abcd", + __typename: "Book", + }, + ], + }; - const link = mockSingleLink({ - request: { query: transformedQuery }, - result: { data: result }, - }).setOnError(reject); + const link = mockSingleLink({ + request: { query: transformedQuery }, + result: { data: result }, + }); - const client = new ApolloClient({ - link, - cache: new InMemoryCache(), - }); + const client = new ApolloClient({ + link, + cache: new InMemoryCache(), + }); - return client - .query({ query }) - .then((actualResult) => { - expect(actualResult.data).toEqual(result); - }) - .then(resolve, reject); - } - ); + const actualResult = await client.query({ query }); - itAsync( - "runs query with cache field policy analogous to @connection", - (resolve, reject) => { - const query = gql` - { - books(skip: 0, limit: 2) { - name - } + expect(actualResult.data).toEqual(result); + }); + + it("runs query with cache field policy analogous to @connection", async () => { + const query = gql` + { + books(skip: 0, limit: 2) { + name } - `; + } + `; - const transformedQuery = gql` - { - books(skip: 0, limit: 2) { - name - __typename - } + const transformedQuery = gql` + { + books(skip: 0, limit: 2) { + name + __typename } - `; + } + `; - const result = { - books: [ - { - name: "abcd", - __typename: "Book", - }, - ], - }; + const result = { + books: [ + { + name: "abcd", + __typename: "Book", + }, + ], + }; - const link = mockSingleLink({ - request: { query: transformedQuery }, - result: { data: result }, - }).setOnError(reject); + const link = mockSingleLink({ + request: { query: transformedQuery }, + result: { data: result }, + }); - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ - typePolicies: { - Query: { - fields: { - books: { - keyArgs: () => "abc", - }, + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ + typePolicies: { + Query: { + fields: { + books: { + keyArgs: () => "abc", }, }, }, - }), - }); + }, + }), + }); - return client - .query({ query }) - .then((actualResult) => { - expect(actualResult.data).toEqual(result); - }) - .then(resolve, reject); - } - ); + const actualResult = await client.query({ query }); - itAsync( - "should remove the connection directive before the link is sent", - (resolve, reject) => { - const query = gql` - { - books(skip: 0, limit: 2) @connection(key: "books") { - name - } + expect(actualResult.data).toEqual(result); + }); + + it("should remove the connection directive before the link is sent", async () => { + const query = gql` + { + books(skip: 0, limit: 2) @connection(key: "books") { + name } - `; + } + `; - const transformedQuery = gql` - { - books(skip: 0, limit: 2) { - name - __typename - } + const transformedQuery = gql` + { + books(skip: 0, limit: 2) { + name + __typename } - `; + } + `; - const result = { - books: [ - { - name: "abcd", - __typename: "Book", - }, - ], - }; + const result = { + books: [ + { + name: "abcd", + __typename: "Book", + }, + ], + }; - const link = mockSingleLink({ - request: { query: transformedQuery }, - result: { data: result }, - }).setOnError(reject); + const link = mockSingleLink({ + request: { query: transformedQuery }, + result: { data: result }, + }); - const client = new ApolloClient({ - link, - cache: new InMemoryCache(), - }); + const client = new ApolloClient({ + link, + cache: new InMemoryCache(), + }); - return client - .query({ query }) - .then((actualResult) => { - expect(actualResult.data).toEqual(result); - }) - .then(resolve, reject); - } - ); + const actualResult = await client.query({ query }); + + expect(actualResult.data).toEqual(result); + }); }); describe("@connection", () => { - itAsync( - "should run a query with the @connection directive and write the result to the store key defined in the directive", - (resolve, reject) => { - const query = gql` - { - books(skip: 0, limit: 2) @connection(key: "abc") { - name - } + it("should run a query with the @connection directive and write the result to the store key defined in the directive", async () => { + const query = gql` + { + books(skip: 0, limit: 2) @connection(key: "abc") { + name } - `; + } + `; - const transformedQuery = gql` - { - books(skip: 0, limit: 2) { - name - __typename - } + const transformedQuery = gql` + { + books(skip: 0, limit: 2) { + name + __typename } - `; + } + `; - const result = { - books: [ - { - name: "abcd", - __typename: "Book", - }, - ], - }; + const result = { + books: [ + { + name: "abcd", + __typename: "Book", + }, + ], + }; - const link = mockSingleLink({ - request: { query: transformedQuery }, - result: { data: result }, - }).setOnError(reject); + const link = mockSingleLink({ + request: { query: transformedQuery }, + result: { data: result }, + }); - const client = new ApolloClient({ - link, - cache: new InMemoryCache(), - }); + const client = new ApolloClient({ + link, + cache: new InMemoryCache(), + }); - return client - .query({ query }) - .then((actualResult) => { - expect(actualResult.data).toEqual(result); - expect((client.cache as InMemoryCache).extract()).toMatchSnapshot(); - }) - .then(resolve, reject); - } - ); + const actualResult = await client.query({ query }); - itAsync( - "should run a query with the connection directive and filter arguments and write the result to the correct store key", - (resolve, reject) => { - const query = gql` - query books($order: string) { - books(skip: 0, limit: 2, order: $order) - @connection(key: "abc", filter: ["order"]) { - name - } + expect(actualResult.data).toEqual(result); + expect((client.cache as InMemoryCache).extract()).toMatchSnapshot(); + }); + + it("should run a query with the connection directive and filter arguments and write the result to the correct store key", async () => { + const query = gql` + query books($order: string) { + books(skip: 0, limit: 2, order: $order) + @connection(key: "abc", filter: ["order"]) { + name } - `; - const transformedQuery = gql` - query books($order: string) { - books(skip: 0, limit: 2, order: $order) { - name - __typename - } + } + `; + const transformedQuery = gql` + query books($order: string) { + books(skip: 0, limit: 2, order: $order) { + name + __typename } - `; + } + `; - const result = { - books: [ - { - name: "abcd", - __typename: "Book", - }, - ], - }; + const result = { + books: [ + { + name: "abcd", + __typename: "Book", + }, + ], + }; - const variables = { order: "popularity" }; + const variables = { order: "popularity" }; - const link = mockSingleLink({ - request: { query: transformedQuery, variables }, - result: { data: result }, - }).setOnError(reject); + const link = mockSingleLink({ + request: { query: transformedQuery, variables }, + result: { data: result }, + }); - const client = new ApolloClient({ - link, - cache: new InMemoryCache(), - }); + const client = new ApolloClient({ + link, + cache: new InMemoryCache(), + }); - return client - .query({ query, variables }) - .then((actualResult) => { - expect(actualResult.data).toEqual(result); - expect((client.cache as InMemoryCache).extract()).toMatchSnapshot(); - }) - .then(resolve, reject); - } - ); + const actualResult = await client.query({ query, variables }); - itAsync( - "should support cache field policies that filter key arguments", - (resolve, reject) => { - const query = gql` - query books($order: string) { - books(skip: 0, limit: 2, order: $order) { - name - } + expect(actualResult.data).toEqual(result); + expect((client.cache as InMemoryCache).extract()).toMatchSnapshot(); + }); + + it("should support cache field policies that filter key arguments", async () => { + const query = gql` + query books($order: string) { + books(skip: 0, limit: 2, order: $order) { + name } - `; - const transformedQuery = gql` - query books($order: string) { - books(skip: 0, limit: 2, order: $order) { - name - __typename - } + } + `; + const transformedQuery = gql` + query books($order: string) { + books(skip: 0, limit: 2, order: $order) { + name + __typename } - `; - - const result = { - books: [ - { - name: "abcd", - __typename: "Book", - }, - ], - }; + } + `; - const variables = { order: "popularity" }; + const result = { + books: [ + { + name: "abcd", + __typename: "Book", + }, + ], + }; - const link = mockSingleLink({ - request: { query: transformedQuery, variables }, - result: { data: result }, - }).setOnError(reject); + const variables = { order: "popularity" }; - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ - typePolicies: { - Query: { - fields: { - books: { - keyArgs: ["order"], - }, - }, - }, - }, - }), - }); + const link = mockSingleLink({ + request: { query: transformedQuery, variables }, + result: { data: result }, + }); - return client - .query({ query, variables }) - .then((actualResult) => { - expect(actualResult.data).toEqual(result); - expect((client.cache as InMemoryCache).extract()).toMatchSnapshot(); - }) - .then(resolve, reject); - } - ); - - itAsync( - "should broadcast changes for reactive variables", - async (resolve, reject) => { - const aVar = makeVar(123); - const bVar = makeVar("asdf"); - const cache: InMemoryCache = new InMemoryCache({ + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ typePolicies: { Query: { fields: { - a() { - return aVar(); - }, - b() { - return bVar(); + books: { + keyArgs: ["order"], }, }, }, }, - }); + }), + }); - const client = new ApolloClient({ cache }); + const actualResult = await client.query({ query, variables }); - const obsQueries = new Set>(); - const subs = new Set(); - function watch( - query: DocumentNode, - fetchPolicy: WatchQueryFetchPolicy = "cache-first" - ): any[] { - const results: any[] = []; - const obsQuery = client.watchQuery({ - query, - fetchPolicy, - }); - obsQueries.add(obsQuery); - subs.add( - obsQuery.subscribe({ - next(result) { - results.push(result.data); - }, - }) - ); - return results; - } + expect(actualResult.data).toEqual(result); + expect((client.cache as InMemoryCache).extract()).toMatchSnapshot(); + }); - const aResults = watch(gql` - { - a - } - `); - const bResults = watch(gql` - { - b - } - `); - const abResults = watch(gql` - { - a - b - } - `); + it("should broadcast changes for reactive variables", async () => { + const aVar = makeVar(123); + const bVar = makeVar("asdf"); + const cache: InMemoryCache = new InMemoryCache({ + typePolicies: { + Query: { + fields: { + a() { + return aVar(); + }, + b() { + return bVar(); + }, + }, + }, + }, + }); - await wait(); + const client = new ApolloClient({ cache }); - function checkLastResult( - results: any[], - expectedData: Record - ) { - const lastResult = results[results.length - 1]; - expect(lastResult).toEqual(expectedData); - return lastResult; - } - - checkLastResult(aResults, { a: 123 }); - const bAsdf = checkLastResult(bResults, { b: "asdf" }); - checkLastResult(abResults, { a: 123, b: "asdf" }); - - aVar(aVar() + 111); - await wait(); - - const a234 = checkLastResult(aResults, { a: 234 }); - expect(checkLastResult(bResults, { b: "asdf" })).toBe(bAsdf); - checkLastResult(abResults, { a: 234, b: "asdf" }); - - bVar(bVar().toUpperCase()); - await wait(); - - expect(checkLastResult(aResults, { a: 234 })).toBe(a234); - checkLastResult(bResults, { b: "ASDF" }); - checkLastResult(abResults, { a: 234, b: "ASDF" }); - - aVar(aVar() + 222); - bVar("oyez"); - await wait(); - - const a456 = checkLastResult(aResults, { a: 456 }); - const bOyez = checkLastResult(bResults, { b: "oyez" }); - const a456bOyez = checkLastResult(abResults, { a: 456, b: "oyez" }); - - // Since the ObservableQuery skips results that are the same as the - // previous result, and nothing is actually changing about the - // ROOT_QUERY.a field, clear previous results to give the invalidated - // results a chance to be delivered. - obsQueries.forEach((obsQuery) => obsQuery.resetLastResults()); - await wait(); - // Verify that resetting previous results did not trigger the delivery - // of any new results, by itself. - expect(checkLastResult(aResults, a456)).toBe(a456); - expect(checkLastResult(bResults, bOyez)).toBe(bOyez); - expect(checkLastResult(abResults, a456bOyez)).toBe(a456bOyez); - - // Now invalidate the ROOT_QUERY.a field. - client.cache.evict({ fieldName: "a" }); - await wait(); - - expect(checkLastResult(aResults, a456)).toBe(a456); - expect(checkLastResult(bResults, bOyez)).toBe(bOyez); - expect(checkLastResult(abResults, a456bOyez)).toBe(a456bOyez); - - const cQuery = gql` - { - c - } - `; - // Passing cache-only as the fetchPolicy allows the { c: "see" } - // result to be delivered even though networkStatus is still loading. - const cResults = watch(cQuery, "cache-only"); - - // Now try writing directly to the cache, rather than calling - // client.writeQuery. - client.cache.writeQuery({ - query: cQuery, - data: { - c: "see", - }, + const obsQueries = new Set>(); + const subs = new Set(); + function watch( + query: DocumentNode, + fetchPolicy: WatchQueryFetchPolicy = "cache-first" + ): any[] { + const results: any[] = []; + const obsQuery = client.watchQuery({ + query, + fetchPolicy, }); - await wait(); - - checkLastResult(aResults, a456); - checkLastResult(bResults, bOyez); - checkLastResult(abResults, a456bOyez); - checkLastResult(cResults, { c: "see" }); - - cache.modify({ - fields: { - c(value) { - expect(value).toBe("see"); - return "saw"; + obsQueries.add(obsQuery); + subs.add( + obsQuery.subscribe({ + next(result) { + results.push(result.data); }, - }, - }); - await wait(); + }) + ); + return results; + } - checkLastResult(aResults, a456); - checkLastResult(bResults, bOyez); - checkLastResult(abResults, a456bOyez); - checkLastResult(cResults, { c: "saw" }); + const aResults = watch(gql` + { + a + } + `); + const bResults = watch(gql` + { + b + } + `); + const abResults = watch(gql` + { + a + b + } + `); - client.cache.evict({ fieldName: "c" }); - await wait(); + await wait(); - checkLastResult(aResults, a456); - checkLastResult(bResults, bOyez); - checkLastResult(abResults, a456bOyez); - expect(checkLastResult(cResults, {})); + function checkLastResult( + results: any[], + expectedData: Record + ) { + const lastResult = results[results.length - 1]; + expect(lastResult).toEqual(expectedData); + return lastResult; + } - expect(aResults).toEqual([{ a: 123 }, { a: 234 }, { a: 456 }]); + checkLastResult(aResults, { a: 123 }); + const bAsdf = checkLastResult(bResults, { b: "asdf" }); + checkLastResult(abResults, { a: 123, b: "asdf" }); + + aVar(aVar() + 111); + await wait(); + + const a234 = checkLastResult(aResults, { a: 234 }); + expect(checkLastResult(bResults, { b: "asdf" })).toBe(bAsdf); + checkLastResult(abResults, { a: 234, b: "asdf" }); + + bVar(bVar().toUpperCase()); + await wait(); + + expect(checkLastResult(aResults, { a: 234 })).toBe(a234); + checkLastResult(bResults, { b: "ASDF" }); + checkLastResult(abResults, { a: 234, b: "ASDF" }); + + aVar(aVar() + 222); + bVar("oyez"); + await wait(); + + const a456 = checkLastResult(aResults, { a: 456 }); + const bOyez = checkLastResult(bResults, { b: "oyez" }); + const a456bOyez = checkLastResult(abResults, { a: 456, b: "oyez" }); + + // Since the ObservableQuery skips results that are the same as the + // previous result, and nothing is actually changing about the + // ROOT_QUERY.a field, clear previous results to give the invalidated + // results a chance to be delivered. + obsQueries.forEach((obsQuery) => obsQuery.resetLastResults()); + await wait(); + // Verify that resetting previous results did not trigger the delivery + // of any new results, by itself. + expect(checkLastResult(aResults, a456)).toBe(a456); + expect(checkLastResult(bResults, bOyez)).toBe(bOyez); + expect(checkLastResult(abResults, a456bOyez)).toBe(a456bOyez); + + // Now invalidate the ROOT_QUERY.a field. + client.cache.evict({ fieldName: "a" }); + await wait(); + + expect(checkLastResult(aResults, a456)).toBe(a456); + expect(checkLastResult(bResults, bOyez)).toBe(bOyez); + expect(checkLastResult(abResults, a456bOyez)).toBe(a456bOyez); + + const cQuery = gql` + { + c + } + `; + // Passing cache-only as the fetchPolicy allows the { c: "see" } + // result to be delivered even though networkStatus is still loading. + const cResults = watch(cQuery, "cache-only"); + + // Now try writing directly to the cache, rather than calling + // client.writeQuery. + client.cache.writeQuery({ + query: cQuery, + data: { + c: "see", + }, + }); + await wait(); - expect(bResults).toEqual([{ b: "asdf" }, { b: "ASDF" }, { b: "oyez" }]); + checkLastResult(aResults, a456); + checkLastResult(bResults, bOyez); + checkLastResult(abResults, a456bOyez); + checkLastResult(cResults, { c: "see" }); - expect(abResults).toEqual([ - { a: 123, b: "asdf" }, - { a: 234, b: "asdf" }, - { a: 234, b: "ASDF" }, - { a: 456, b: "oyez" }, - ]); + cache.modify({ + fields: { + c(value) { + expect(value).toBe("see"); + return "saw"; + }, + }, + }); + await wait(); - expect(cResults).toEqual([{}, { c: "see" }, { c: "saw" }, {}]); + checkLastResult(aResults, a456); + checkLastResult(bResults, bOyez); + checkLastResult(abResults, a456bOyez); + checkLastResult(cResults, { c: "saw" }); - subs.forEach((sub) => sub.unsubscribe()); + client.cache.evict({ fieldName: "c" }); + await wait(); - resolve(); - } - ); + checkLastResult(aResults, a456); + checkLastResult(bResults, bOyez); + checkLastResult(abResults, a456bOyez); + expect(checkLastResult(cResults, {})); + + expect(aResults).toEqual([{ a: 123 }, { a: 234 }, { a: 456 }]); + + expect(bResults).toEqual([{ b: "asdf" }, { b: "ASDF" }, { b: "oyez" }]); + + expect(abResults).toEqual([ + { a: 123, b: "asdf" }, + { a: 234, b: "asdf" }, + { a: 234, b: "ASDF" }, + { a: 456, b: "oyez" }, + ]); + + expect(cResults).toEqual([{}, { c: "see" }, { c: "saw" }, {}]); + + subs.forEach((sub) => sub.unsubscribe()); + }); function wait(time = 10) { return new Promise((resolve) => setTimeout(resolve, time)); } - itAsync( - "should call forgetCache for reactive vars when stopped", - async (resolve, reject) => { - const aVar = makeVar(123); - const bVar = makeVar("asdf"); - const aSpy = jest.spyOn(aVar, "forgetCache"); - const bSpy = jest.spyOn(bVar, "forgetCache"); - const cache: InMemoryCache = new InMemoryCache({ - typePolicies: { - Query: { - fields: { - a() { - return aVar(); - }, - b() { - return bVar(); - }, + it("should call forgetCache for reactive vars when stopped", async () => { + const aVar = makeVar(123); + const bVar = makeVar("asdf"); + const aSpy = jest.spyOn(aVar, "forgetCache"); + const bSpy = jest.spyOn(bVar, "forgetCache"); + const cache: InMemoryCache = new InMemoryCache({ + typePolicies: { + Query: { + fields: { + a() { + return aVar(); + }, + b() { + return bVar(); }, }, }, - }); - - const client = new ApolloClient({ cache }); + }, + }); - const obsQueries = new Set>(); - const subs = new Set(); - function watch( - query: DocumentNode, - fetchPolicy: WatchQueryFetchPolicy = "cache-first" - ): any[] { - const results: any[] = []; - const obsQuery = client.watchQuery({ - query, - fetchPolicy, - }); - obsQueries.add(obsQuery); - subs.add( - obsQuery.subscribe({ - next(result) { - results.push(result.data); - }, - }) - ); - return results; - } + const client = new ApolloClient({ cache }); - const aQuery = gql` - { - a - } - `; - const bQuery = gql` - { - b - } - `; - const abQuery = gql` - { - a - b - } - `; + const obsQueries = new Set>(); + const subs = new Set(); + function watch( + query: DocumentNode, + fetchPolicy: WatchQueryFetchPolicy = "cache-first" + ): any[] { + const results: any[] = []; + const obsQuery = client.watchQuery({ + query, + fetchPolicy, + }); + obsQueries.add(obsQuery); + subs.add( + obsQuery.subscribe({ + next(result) { + results.push(result.data); + }, + }) + ); + return results; + } - const aResults = watch(aQuery); - const bResults = watch(bQuery); + const aQuery = gql` + { + a + } + `; + const bQuery = gql` + { + b + } + `; + const abQuery = gql` + { + a + b + } + `; - expect(cache["watches"].size).toBe(2); + const aResults = watch(aQuery); + const bResults = watch(bQuery); - expect(aResults).toEqual([]); - expect(bResults).toEqual([]); + expect(cache["watches"].size).toBe(2); - expect(aSpy).not.toBeCalled(); - expect(bSpy).not.toBeCalled(); + expect(aResults).toEqual([]); + expect(bResults).toEqual([]); - subs.forEach((sub) => sub.unsubscribe()); + expect(aSpy).not.toBeCalled(); + expect(bSpy).not.toBeCalled(); - expect(aSpy).toBeCalledTimes(1); - expect(aSpy).toBeCalledWith(cache); - expect(bSpy).toBeCalledTimes(1); - expect(bSpy).toBeCalledWith(cache); + subs.forEach((sub) => sub.unsubscribe()); - expect(aResults).toEqual([]); - expect(bResults).toEqual([]); + expect(aSpy).toBeCalledTimes(1); + expect(aSpy).toBeCalledWith(cache); + expect(bSpy).toBeCalledTimes(1); + expect(bSpy).toBeCalledWith(cache); - expect(cache["watches"].size).toBe(0); - const abResults = watch(abQuery); - expect(abResults).toEqual([]); - expect(cache["watches"].size).toBe(1); + expect(aResults).toEqual([]); + expect(bResults).toEqual([]); - await wait(); + expect(cache["watches"].size).toBe(0); + const abResults = watch(abQuery); + expect(abResults).toEqual([]); + expect(cache["watches"].size).toBe(1); - expect(aResults).toEqual([]); - expect(bResults).toEqual([]); - expect(abResults).toEqual([{ a: 123, b: "asdf" }]); + await wait(); - client.stop(); + expect(aResults).toEqual([]); + expect(bResults).toEqual([]); + expect(abResults).toEqual([{ a: 123, b: "asdf" }]); - await wait(); + client.stop(); - expect(aSpy).toBeCalledTimes(2); - expect(aSpy).toBeCalledWith(cache); - expect(bSpy).toBeCalledTimes(2); - expect(bSpy).toBeCalledWith(cache); + await wait(); - resolve(); - } - ); + expect(aSpy).toBeCalledTimes(2); + expect(aSpy).toBeCalledWith(cache); + expect(bSpy).toBeCalledTimes(2); + expect(bSpy).toBeCalledWith(cache); + }); describe("default settings", () => { const query = gql` @@ -3777,12 +3497,12 @@ describe("@connection", () => { await expect(stream).not.toEmitAnything(); }); - itAsync("allows setting default options for query", (resolve, reject) => { + it("allows setting default options for query", async () => { const errors = [{ message: "failure", name: "failure" }]; const link = mockSingleLink({ request: { query }, result: { errors }, - }).setOnError(reject); + }); const client = new ApolloClient({ link, cache: new InMemoryCache({ addTypename: false }), @@ -3791,55 +3511,46 @@ describe("@connection", () => { }, }); - return client - .query({ query }) - .then((result) => { - expect(result.errors).toEqual(errors); - }) - .then(resolve, reject); + const result = await client.query({ query }); + + expect(result.errors).toEqual(errors); }); - itAsync( - "allows setting default options for mutation", - (resolve, reject) => { - const mutation = gql` - mutation upVote($id: ID!) { - upvote(id: $id) { - success - } + it("allows setting default options for mutation", async () => { + const mutation = gql` + mutation upVote($id: ID!) { + upvote(id: $id) { + success } - `; - - const data = { - upvote: { success: true }, - }; - - const link = mockSingleLink({ - request: { query: mutation, variables: { id: 1 } }, - result: { data }, - }).setOnError(reject); - - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ addTypename: false }), - defaultOptions: { - mutate: { variables: { id: 1 } }, - }, - }); + } + `; - return client - .mutate({ - mutation, - // This undefined value should be ignored in favor of - // defaultOptions.mutate.variables. - variables: void 0, - }) - .then((result) => { - expect(result.data).toEqual(data); - }) - .then(resolve, reject); - } - ); + const data = { + upvote: { success: true }, + }; + + const link = mockSingleLink({ + request: { query: mutation, variables: { id: 1 } }, + result: { data }, + }); + + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ addTypename: false }), + defaultOptions: { + mutate: { variables: { id: 1 } }, + }, + }); + + const result = await client.mutate({ + mutation, + // This undefined value should be ignored in favor of + // defaultOptions.mutate.variables. + variables: void 0, + }); + + expect(result.data).toEqual(data); + }); }); }); @@ -6351,8 +6062,6 @@ describe("custom document transforms", () => { }); function clientRoundtrip( - resolve: (result: any) => any, - reject: (reason: any) => any, query: DocumentNode, data: FormattedExecutionResult, variables?: any, @@ -6361,7 +6070,7 @@ function clientRoundtrip( const link = mockSingleLink({ request: { query: cloneDeep(query) }, result: data, - }).setOnError(reject); + }); const client = new ApolloClient({ link, @@ -6370,10 +6079,7 @@ function clientRoundtrip( }), }); - return client - .query({ query, variables }) - .then((result) => { - expect(result.data).toEqual(data.data); - }) - .then(resolve, reject); + return client.query({ query, variables }).then((result) => { + expect(result.data).toEqual(data.data); + }); } diff --git a/src/__tests__/graphqlSubscriptions.ts b/src/__tests__/graphqlSubscriptions.ts index d666ed22e65..ca32fe6a18a 100644 --- a/src/__tests__/graphqlSubscriptions.ts +++ b/src/__tests__/graphqlSubscriptions.ts @@ -4,9 +4,9 @@ import { ApolloClient, FetchResult } from "../core"; import { InMemoryCache } from "../cache"; import { ApolloError, PROTOCOL_ERRORS_SYMBOL } from "../errors"; import { QueryManager } from "../core/QueryManager"; -import { itAsync, mockObservableLink } from "../testing"; +import { mockObservableLink } from "../testing"; import { GraphQLError } from "graphql"; -import { spyOnConsole } from "../testing/internal"; +import { ObservableStream, spyOnConsole } from "../testing/internal"; import { getDefaultOptionsForQueryManagerTests } from "../testing/core/mocking/mockQueryManager"; describe("GraphQL Subscriptions", () => { @@ -47,36 +47,25 @@ describe("GraphQL Subscriptions", () => { }; }); - itAsync( - "should start a subscription on network interface and unsubscribe", - (resolve, reject) => { - const link = mockObservableLink(); - // This test calls directly through Apollo Client - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ addTypename: false }), - }); + it("should start a subscription on network interface and unsubscribe", async () => { + const link = mockObservableLink(); + // This test calls directly through Apollo Client + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ addTypename: false }), + }); - let count = 0; - const sub = client.subscribe(defaultOptions).subscribe({ - next(result) { - count++; - expect(result).toEqual(results[0].result); + const stream = new ObservableStream(client.subscribe(defaultOptions)); + link.simulateResult(results[0]); - // Test unsubscribing - if (count > 1) { - throw new Error("next fired after unsubscribing"); - } - sub.unsubscribe(); - resolve(); - }, - }); + await expect(stream).toEmitValue(results[0].result); - link.simulateResult(results[0]); - } - ); + stream.unsubscribe(); + + await expect(stream).not.toEmitAnything(); + }); - itAsync("should subscribe with default values", (resolve, reject) => { + it("should subscribe with default values", async () => { const link = mockObservableLink(); // This test calls directly through Apollo Client const client = new ApolloClient({ @@ -84,25 +73,18 @@ describe("GraphQL Subscriptions", () => { cache: new InMemoryCache({ addTypename: false }), }); - let count = 0; - const sub = client.subscribe(options).subscribe({ - next(result) { - expect(result).toEqual(results[0].result); + const stream = new ObservableStream(client.subscribe(options)); - // Test unsubscribing - if (count > 1) { - throw new Error("next fired after unsubscribing"); - } - sub.unsubscribe(); + link.simulateResult(results[0]); - resolve(); - }, - }); + await expect(stream).toEmitValue(results[0].result); - link.simulateResult(results[0]); + stream.unsubscribe(); + + await expect(stream).not.toEmitAnything(); }); - itAsync("should multiplex subscriptions", (resolve, reject) => { + it("should multiplex subscriptions", async () => { const link = mockObservableLink(); const queryManager = new QueryManager( getDefaultOptionsForQueryManagerTests({ @@ -112,88 +94,57 @@ describe("GraphQL Subscriptions", () => { ); const obs = queryManager.startGraphQLSubscription(options); - - let counter = 0; - - // tslint:disable-next-line - obs.subscribe({ - next(result) { - expect(result).toEqual(results[0].result); - counter++; - if (counter === 2) { - resolve(); - } - }, - }) as any; - - // Subscribe again. Should also receive the same result. - // tslint:disable-next-line - obs.subscribe({ - next(result) { - expect(result).toEqual(results[0].result); - counter++; - if (counter === 2) { - resolve(); - } - }, - }) as any; + const stream1 = new ObservableStream(obs); + const stream2 = new ObservableStream(obs); link.simulateResult(results[0]); + + await expect(stream1).toEmitValue(results[0].result); + await expect(stream2).toEmitValue(results[0].result); }); - itAsync( - "should receive multiple results for a subscription", - (resolve, reject) => { - const link = mockObservableLink(); - let numResults = 0; - const queryManager = new QueryManager( - getDefaultOptionsForQueryManagerTests({ - link, - cache: new InMemoryCache({ addTypename: false }), - }) - ); - - // tslint:disable-next-line - queryManager.startGraphQLSubscription(options).subscribe({ - next(result) { - expect(result).toEqual(results[numResults].result); - numResults++; - if (numResults === 4) { - resolve(); - } - }, - }) as any; + it("should receive multiple results for a subscription", async () => { + const link = mockObservableLink(); + const queryManager = new QueryManager( + getDefaultOptionsForQueryManagerTests({ + link, + cache: new InMemoryCache({ addTypename: false }), + }) + ); - for (let i = 0; i < 4; i++) { - link.simulateResult(results[i]); - } + const stream = new ObservableStream( + queryManager.startGraphQLSubscription(options) + ); + + for (let i = 0; i < 4; i++) { + link.simulateResult(results[i]); } - ); - - itAsync( - "should not cache subscription data if a `no-cache` fetch policy is used", - (resolve, reject) => { - const link = mockObservableLink(); - const cache = new InMemoryCache({ addTypename: false }); - const client = new ApolloClient({ - link, - cache, - }); - expect(cache.extract()).toEqual({}); + await expect(stream).toEmitValue(results[0].result); + await expect(stream).toEmitValue(results[1].result); + await expect(stream).toEmitValue(results[2].result); + await expect(stream).toEmitValue(results[3].result); + await expect(stream).not.toEmitAnything(); + }); - options.fetchPolicy = "no-cache"; - const sub = client.subscribe(options).subscribe({ - next() { - expect(cache.extract()).toEqual({}); - sub.unsubscribe(); - resolve(); - }, - }); + it("should not cache subscription data if a `no-cache` fetch policy is used", async () => { + const link = mockObservableLink(); + const cache = new InMemoryCache({ addTypename: false }); + const client = new ApolloClient({ + link, + cache, + }); - link.simulateResult(results[0]); - } - ); + expect(cache.extract()).toEqual({}); + + options.fetchPolicy = "no-cache"; + const stream = new ObservableStream(client.subscribe(options)); + + link.simulateResult(results[0]); + + await expect(stream).toEmitNext(); + expect(cache.extract()).toEqual({}); + }); it("should throw an error if the result has errors on it", () => { const link = mockObservableLink(); @@ -492,27 +443,22 @@ describe("GraphQL Subscriptions", () => { }); }); - itAsync( - "should pass a context object through the link execution chain", - (resolve, reject) => { - const link = mockObservableLink(); - const client = new ApolloClient({ - cache: new InMemoryCache(), - link, - }); + it("should pass a context object through the link execution chain", async () => { + const link = mockObservableLink(); + const client = new ApolloClient({ + cache: new InMemoryCache(), + link, + }); - client.subscribe(options).subscribe({ - next() { - expect(link.operation?.getContext().someVar).toEqual( - options.context.someVar - ); - resolve(); - }, - }); + const stream = new ObservableStream(client.subscribe(options)); - link.simulateResult(results[0]); - } - ); + link.simulateResult(results[0]); + + await expect(stream).toEmitNext(); + expect(link.operation?.getContext().someVar).toEqual( + options.context.someVar + ); + }); it("should throw an error if the result has protocolErrors on it", async () => { const link = mockObservableLink(); diff --git a/src/__tests__/local-state/export.ts b/src/__tests__/local-state/export.ts index ea3fb15ae5b..f0e5daf4d60 100644 --- a/src/__tests__/local-state/export.ts +++ b/src/__tests__/local-state/export.ts @@ -2,183 +2,164 @@ import gql from "graphql-tag"; import { print } from "graphql"; import { Observable } from "../../utilities"; -import { itAsync } from "../../testing"; import { ApolloLink } from "../../link/core"; import { ApolloClient } from "../../core"; import { InMemoryCache } from "../../cache"; -import { spyOnConsole } from "../../testing/internal"; +import { ObservableStream, spyOnConsole } from "../../testing/internal"; describe("@client @export tests", () => { - itAsync( - "should not break @client only queries when the @export directive is " + - "used", - (resolve, reject) => { - const query = gql` - { - field @client @export(as: "someVar") - } - `; + it("should not break @client only queries when the @export directive is used", async () => { + const query = gql` + { + field @client @export(as: "someVar") + } + `; - const cache = new InMemoryCache(); - const client = new ApolloClient({ - cache, - link: ApolloLink.empty(), - resolvers: {}, - }); + const cache = new InMemoryCache(); + const client = new ApolloClient({ + cache, + link: ApolloLink.empty(), + resolvers: {}, + }); - cache.writeQuery({ - query, - data: { field: 1 }, - }); + cache.writeQuery({ + query, + data: { field: 1 }, + }); - return client.query({ query }).then(({ data }: any) => { - expect({ ...data }).toMatchObject({ field: 1 }); - resolve(); - }); - } - ); + const { data } = await client.query({ query }); - itAsync( - "should not break @client only queries when the @export directive is " + - "used on nested fields", - (resolve, reject) => { - const query = gql` - { - car @client { - engine { - torque @export(as: "torque") - } + expect(data).toEqual({ field: 1 }); + }); + + it("should not break @client only queries when the @export directive is used on nested fields", async () => { + const query = gql` + { + car @client { + engine { + torque @export(as: "torque") } } - `; + } + `; - const cache = new InMemoryCache(); - const client = new ApolloClient({ - cache, - link: ApolloLink.empty(), - resolvers: {}, - }); + const cache = new InMemoryCache(); + const client = new ApolloClient({ + cache, + link: ApolloLink.empty(), + resolvers: {}, + }); - cache.writeQuery({ - query, - data: { - car: { - engine: { - cylinders: 8, - torque: 7200, - __typename: "Engine", - }, - __typename: "Car", + cache.writeQuery({ + query, + data: { + car: { + engine: { + cylinders: 8, + torque: 7200, + __typename: "Engine", }, + __typename: "Car", }, - }); + }, + }); - return client.query({ query }).then(({ data }: any) => { - expect({ ...data }).toMatchObject({ - car: { - engine: { - torque: 7200, - }, - }, - }); - resolve(); - }); - } - ); + const { data } = await client.query({ query }); - itAsync( - "should store the @client field value in the specified @export " + - "variable, and make it available to a subsequent resolver", - (resolve, reject) => { - const query = gql` - query currentAuthorPostCount($authorId: Int!) { - currentAuthorId @client @export(as: "authorId") - postCount(authorId: $authorId) @client - } - `; + expect(data).toEqual({ + car: { + __typename: "Car", + engine: { + __typename: "Engine", + torque: 7200, + }, + }, + }); + }); - const testAuthorId = 100; - const testPostCount = 200; + it("should store the @client field value in the specified @export variable, and make it available to a subsequent resolver", async () => { + const query = gql` + query currentAuthorPostCount($authorId: Int!) { + currentAuthorId @client @export(as: "authorId") + postCount(authorId: $authorId) @client + } + `; - const cache = new InMemoryCache(); - const client = new ApolloClient({ - cache, - resolvers: { - Query: { - postCount(_, { authorId }) { - return authorId === testAuthorId ? testPostCount : 0; - }, + const testAuthorId = 100; + const testPostCount = 200; + + const cache = new InMemoryCache(); + const client = new ApolloClient({ + cache, + resolvers: { + Query: { + postCount(_, { authorId }) { + return authorId === testAuthorId ? testPostCount : 0; }, }, - }); + }, + }); - cache.writeQuery({ - query, - data: { - currentAuthorId: testAuthorId, - }, - }); + cache.writeQuery({ + query, + data: { + currentAuthorId: testAuthorId, + }, + }); - return client.query({ query }).then(({ data }: any) => { - expect({ ...data }).toMatchObject({ - currentAuthorId: testAuthorId, - postCount: testPostCount, - }); - resolve(); - }); - } - ); + const { data } = await client.query({ query }); - itAsync( - "should store the @client nested field value in the specified @export " + - "variable, and make it avilable to a subsequent resolver", - (resolve, reject) => { - const query = gql` - query currentAuthorPostCount($authorId: Int!) { - currentAuthor @client { - name - authorId @export(as: "authorId") - } - postCount(authorId: $authorId) @client + expect(data).toEqual({ + currentAuthorId: testAuthorId, + postCount: testPostCount, + }); + }); + + it("should store the @client nested field value in the specified @export variable, and make it avilable to a subsequent resolver", async () => { + const query = gql` + query currentAuthorPostCount($authorId: Int!) { + currentAuthor @client { + name + authorId @export(as: "authorId") } - `; + postCount(authorId: $authorId) @client + } + `; - const testAuthor = { - name: "John Smith", - authorId: 100, - __typename: "Author", - }; + const testAuthor = { + name: "John Smith", + authorId: 100, + __typename: "Author", + }; - const testPostCount = 200; + const testPostCount = 200; - const cache = new InMemoryCache(); - const client = new ApolloClient({ - cache, - resolvers: { - Query: { - postCount(_, { authorId }) { - return authorId === testAuthor.authorId ? testPostCount : 0; - }, + const cache = new InMemoryCache(); + const client = new ApolloClient({ + cache, + resolvers: { + Query: { + postCount(_, { authorId }) { + return authorId === testAuthor.authorId ? testPostCount : 0; }, }, - }); + }, + }); - cache.writeQuery({ - query, - data: { - currentAuthor: testAuthor, - }, - }); + cache.writeQuery({ + query, + data: { + currentAuthor: testAuthor, + }, + }); - return client.query({ query }).then(({ data }: any) => { - expect({ ...data }).toMatchObject({ - currentAuthor: testAuthor, - postCount: testPostCount, - }); - resolve(); - }); - } - ); + const { data } = await client.query({ query }); + + expect({ ...data }).toMatchObject({ + currentAuthor: testAuthor, + postCount: testPostCount, + }); + }); it("should allow @client @export variables to be used with remote queries", async () => { using _consoleSpies = spyOnConsole.takeSnapshots("error"); @@ -233,160 +214,154 @@ describe("@client @export tests", () => { }); }); - itAsync( - "should support @client @export variables that are nested multiple " + - "levels deep", - (resolve, reject) => { - const query = gql` - query currentAuthorPostCount($authorId: Int!) { - appContainer @client { - systemDetails { - currentAuthor { - name - authorId @export(as: "authorId") - } + it("should support @client @export variables that are nested multiple levels deep", async () => { + const query = gql` + query currentAuthorPostCount($authorId: Int!) { + appContainer @client { + systemDetails { + currentAuthor { + name + authorId @export(as: "authorId") } } - postCount(authorId: $authorId) } - `; + postCount(authorId: $authorId) + } + `; - const appContainer = { - systemDetails: { - currentAuthor: { - name: "John Smith", - authorId: 100, - __typename: "Author", - }, - __typename: "SystemDetails", + const appContainer = { + systemDetails: { + currentAuthor: { + name: "John Smith", + authorId: 100, + __typename: "Author", }, - __typename: "AppContainer", - }; + __typename: "SystemDetails", + }, + __typename: "AppContainer", + }; - const testPostCount = 200; + const testPostCount = 200; - const link = new ApolloLink(() => - Observable.of({ - data: { - postCount: testPostCount, - }, - }) - ); + const link = new ApolloLink(() => + Observable.of({ + data: { + postCount: testPostCount, + }, + }) + ); - const cache = new InMemoryCache(); - const client = new ApolloClient({ - cache, - link, - resolvers: {}, - }); + const cache = new InMemoryCache(); + const client = new ApolloClient({ + cache, + link, + resolvers: {}, + }); + { + using _ = spyOnConsole("error"); cache.writeQuery({ query, data: { appContainer, }, }); - - return client.query({ query }).then(({ data }: any) => { - expect({ ...data }).toMatchObject({ - appContainer, - postCount: testPostCount, - }); - resolve(); - }); } - ); - itAsync( - "should ignore @export directives if not used with @client", - (resolve, reject) => { - const query = gql` - query currentAuthorPostCount($authorId: Int!) { - currentAuthor { - name - authorId @export(as: "authorId") - } - postCount(authorId: $authorId) - } - `; + const { data } = await client.query({ query }); - const testAuthor = { - name: "John Smith", - authorId: 100, - __typename: "Author", - }; - const testPostCount = 200; + expect(data).toEqual({ + appContainer, + postCount: testPostCount, + }); + }); - const link = new ApolloLink(() => - Observable.of({ - data: { - currentAuthor: testAuthor, - postCount: testPostCount, - }, - }) - ); + it("should ignore @export directives if not used with @client", async () => { + const query = gql` + query currentAuthorPostCount($authorId: Int!) { + currentAuthor { + name + authorId @export(as: "authorId") + } + postCount(authorId: $authorId) + } + `; - const client = new ApolloClient({ - cache: new InMemoryCache(), - link, - resolvers: {}, - }); + const testAuthor = { + name: "John Smith", + authorId: 100, + __typename: "Author", + }; + const testPostCount = 200; - return client.query({ query }).then(({ data }: any) => { - expect({ ...data }).toMatchObject({ + const link = new ApolloLink(() => + Observable.of({ + data: { currentAuthor: testAuthor, postCount: testPostCount, - }); - resolve(); - }); - } - ); + }, + }) + ); - itAsync( - "should support setting an @client @export variable, loaded from the " + - "cache, on a virtual field that is combined into a remote query.", - (resolve, reject) => { - const query = gql` - query postRequiringReview($reviewerId: Int!) { - postRequiringReview { - id - title - loggedInReviewerId @client @export(as: "reviewerId") - } - reviewerDetails(reviewerId: $reviewerId) { - name - } + const client = new ApolloClient({ + cache: new InMemoryCache(), + link, + resolvers: {}, + }); + + const { data } = await client.query({ query }); + + expect(data).toEqual({ + currentAuthor: testAuthor, + postCount: testPostCount, + }); + }); + + it("should support setting an @client @export variable, loaded from the cache, on a virtual field that is combined into a remote query.", async () => { + const query = gql` + query postRequiringReview($reviewerId: Int!) { + postRequiringReview { + id + title + loggedInReviewerId @client @export(as: "reviewerId") } - `; + reviewerDetails(reviewerId: $reviewerId) { + name + } + } + `; - const postRequiringReview = { - id: 10, - title: "The Local State Conundrum", - __typename: "Post", - }; - const reviewerDetails = { - name: "John Smith", - __typename: "Reviewer", - }; - const loggedInReviewerId = 100; + const postRequiringReview = { + id: 10, + title: "The Local State Conundrum", + __typename: "Post", + }; + const reviewerDetails = { + name: "John Smith", + __typename: "Reviewer", + }; + const loggedInReviewerId = 100; - const link = new ApolloLink(({ variables }) => { - expect(variables).toMatchObject({ reviewerId: loggedInReviewerId }); - return Observable.of({ - data: { - postRequiringReview, - reviewerDetails, - }, - }); - }).setOnError(reject); + const link = new ApolloLink(({ variables }) => { + expect(variables).toMatchObject({ reviewerId: loggedInReviewerId }); - const cache = new InMemoryCache(); - const client = new ApolloClient({ - cache, - link, - resolvers: {}, + return Observable.of({ + data: { + postRequiringReview, + reviewerDetails, + }, }); + }); + const cache = new InMemoryCache(); + const client = new ApolloClient({ + cache, + link, + resolvers: {}, + }); + + { + using _ = spyOnConsole("error"); cache.writeQuery({ query, data: { @@ -397,79 +372,76 @@ describe("@client @export tests", () => { }, }, }); - - return client - .query({ query }) - .then(({ data }: any) => { - expect({ ...data }).toMatchObject({ - postRequiringReview: { - id: postRequiringReview.id, - title: postRequiringReview.title, - loggedInReviewerId, - }, - reviewerDetails, - }); - }) - .then(resolve, reject); } - ); - itAsync( - "should support setting a @client @export variable, loaded via a " + - "local resolver, on a virtual field that is combined into a remote query.", - (resolve, reject) => { - const query = gql` - query postRequiringReview($reviewerId: Int!) { - postRequiringReview { - id - title - currentReviewer @client { - id @export(as: "reviewerId") - } - } - reviewerDetails(reviewerId: $reviewerId) { - name + const { data } = await client.query({ query }); + + expect(data).toEqual({ + postRequiringReview: { + __typename: "Post", + id: postRequiringReview.id, + title: postRequiringReview.title, + loggedInReviewerId, + }, + reviewerDetails, + }); + }); + + it("should support setting a @client @export variable, loaded via a local resolver, on a virtual field that is combined into a remote query.", async () => { + const query = gql` + query postRequiringReview($reviewerId: Int!) { + postRequiringReview { + id + title + currentReviewer @client { + id @export(as: "reviewerId") } } - `; + reviewerDetails(reviewerId: $reviewerId) { + name + } + } + `; - const postRequiringReview = { - id: 10, - title: "The Local State Conundrum", - __typename: "Post", - }; - const reviewerDetails = { - name: "John Smith", - __typename: "Reviewer", - }; - const currentReviewer = { - id: 100, - __typename: "CurrentReviewer", - }; + const postRequiringReview = { + id: 10, + title: "The Local State Conundrum", + __typename: "Post", + }; + const reviewerDetails = { + name: "John Smith", + __typename: "Reviewer", + }; + const currentReviewer = { + id: 100, + __typename: "CurrentReviewer", + }; - const link = new ApolloLink(({ variables }) => { - expect(variables).toMatchObject({ reviewerId: currentReviewer.id }); - return Observable.of({ - data: { - postRequiringReview, - reviewerDetails, - }, - }); + const link = new ApolloLink(({ variables }) => { + expect(variables).toMatchObject({ reviewerId: currentReviewer.id }); + return Observable.of({ + data: { + postRequiringReview, + reviewerDetails, + }, }); - - const cache = new InMemoryCache(); - const client = new ApolloClient({ - cache, - link, - resolvers: { - Post: { - currentReviewer() { - return currentReviewer; - }, + }); + + const cache = new InMemoryCache(); + const client = new ApolloClient({ + cache, + link, + resolvers: { + Post: { + currentReviewer() { + return currentReviewer; }, }, - }); + }, + }); + { + using _ = spyOnConsole("error"); cache.writeQuery({ query, data: { @@ -478,130 +450,120 @@ describe("@client @export tests", () => { }, }, }); - - return client.query({ query }).then(({ data }: any) => { - expect({ ...data }).toMatchObject({ - postRequiringReview: { - id: postRequiringReview.id, - title: postRequiringReview.title, - currentReviewer, - }, - reviewerDetails, - }); - resolve(); - }); } - ); - - itAsync( - "should support combining @client @export variables, calculated by a " + - "local resolver, with remote mutations", - (resolve, reject) => { - const mutation = gql` - mutation upvotePost($postId: Int!) { - topPost @client @export(as: "postId") - upvotePost(postId: $postId) { - title - votes - } + + const { data } = await client.query({ query }); + + expect(data).toMatchObject({ + postRequiringReview: { + id: postRequiringReview.id, + title: postRequiringReview.title, + currentReviewer, + }, + reviewerDetails, + }); + }); + + it("should support combining @client @export variables, calculated by a local resolver, with remote mutations", async () => { + const mutation = gql` + mutation upvotePost($postId: Int!) { + topPost @client @export(as: "postId") + upvotePost(postId: $postId) { + title + votes } - `; + } + `; - const testPostId = 100; - const testPost = { - title: "The Day of the Jackal", - votes: 10, - __typename: "post", - }; + const testPostId = 100; + const testPost = { + title: "The Day of the Jackal", + votes: 10, + __typename: "post", + }; - const link = new ApolloLink(({ variables }) => { - expect(variables).toMatchObject({ postId: testPostId }); - return Observable.of({ - data: { - upvotePost: testPost, - }, - }); + const link = new ApolloLink(({ variables }) => { + expect(variables).toMatchObject({ postId: testPostId }); + return Observable.of({ + data: { + upvotePost: testPost, + }, }); + }); - const client = new ApolloClient({ - cache: new InMemoryCache(), - link, - resolvers: { - Mutation: { - topPost() { - return testPostId; - }, + const client = new ApolloClient({ + cache: new InMemoryCache(), + link, + resolvers: { + Mutation: { + topPost() { + return testPostId; }, }, - }); + }, + }); - return client.mutate({ mutation }).then(({ data }: any) => { - expect({ ...data }).toMatchObject({ - upvotePost: testPost, - }); - resolve(); - }); - } - ); - - itAsync( - "should support combining @client @export variables, calculated by " + - "reading from the cache, with remote mutations", - (resolve, reject) => { - const mutation = gql` - mutation upvotePost($postId: Int!) { - topPost @client @export(as: "postId") - upvotePost(postId: $postId) { - title - votes - } - } - `; + const { data } = await client.mutate({ mutation }); - const testPostId = 100; - const testPost = { - title: "The Day of the Jackal", - votes: 10, - __typename: "post", - }; + expect(data).toEqual({ + topPost: 100, + upvotePost: testPost, + }); + }); - const link = new ApolloLink(({ variables }) => { - expect(variables).toMatchObject({ postId: testPostId }); - return Observable.of({ - data: { - upvotePost: testPost, - }, - }); - }); + it("should support combining @client @export variables, calculated by reading from the cache, with remote mutations", async () => { + const mutation = gql` + mutation upvotePost($postId: Int!) { + topPost @client @export(as: "postId") + upvotePost(postId: $postId) { + title + votes + } + } + `; - const cache = new InMemoryCache(); - const client = new ApolloClient({ - cache, - link, - resolvers: {}, - }); + const testPostId = 100; + const testPost = { + title: "The Day of the Jackal", + votes: 10, + __typename: "post", + }; - cache.writeQuery({ - query: gql` - { - topPost - } - `, + const link = new ApolloLink(({ variables }) => { + expect(variables).toMatchObject({ postId: testPostId }); + return Observable.of({ data: { - topPost: testPostId, + upvotePost: testPost, }, }); + }); - return client.mutate({ mutation }).then(({ data }: any) => { - expect({ ...data }).toMatchObject({ - upvotePost: testPost, - }); - resolve(); - }); - } - ); + const cache = new InMemoryCache(); + const client = new ApolloClient({ + cache, + link, + resolvers: {}, + }); + + cache.writeQuery({ + query: gql` + { + topPost + } + `, + data: { + topPost: testPostId, + }, + }); + + const { data } = await client.mutate({ mutation }); - it("should not add __typename to @export-ed objects (#4691)", () => { + expect(data).toEqual({ + upvotePost: testPost, + }); + }); + + it("should not add __typename to @export-ed objects (#4691)", async () => { const query = gql` query GetListItems($where: LessonFilter) { currentFilter @client @export(as: "where") { @@ -666,51 +628,50 @@ describe("@client @export tests", () => { }, }); - return client.query({ query }).then((result) => { - expect(result.data).toEqual({ - currentFilter, - ...data, - }); + const result = await client.query({ query }); + + expect(result.data).toEqual({ + currentFilter, + ...data, }); }); - itAsync( - "should use the value of the last @export variable defined, if multiple " + - "variables are defined with the same name", - (resolve, reject) => { - const query = gql` - query reviewerPost($reviewerId: Int!) { - primaryReviewerId @client @export(as: "reviewerId") - secondaryReviewerId @client @export(as: "reviewerId") - post(reviewerId: $reviewerId) { - title - } + it("should use the value of the last @export variable defined, if multiple variables are defined with the same name", async () => { + const query = gql` + query reviewerPost($reviewerId: Int!) { + primaryReviewerId @client @export(as: "reviewerId") + secondaryReviewerId @client @export(as: "reviewerId") + post(reviewerId: $reviewerId) { + title } - `; + } + `; - const post = { - title: "The One Post to Rule Them All", - __typename: "Post", - }; - const primaryReviewerId = 100; - const secondaryReviewerId = 200; + const post = { + title: "The One Post to Rule Them All", + __typename: "Post", + }; + const primaryReviewerId = 100; + const secondaryReviewerId = 200; - const link = new ApolloLink(({ variables }) => { - expect(variables).toMatchObject({ reviewerId: secondaryReviewerId }); - return Observable.of({ - data: { - post, - }, - }); + const link = new ApolloLink(({ variables }) => { + expect(variables).toMatchObject({ reviewerId: secondaryReviewerId }); + return Observable.of({ + data: { + post, + }, }); + }); - const cache = new InMemoryCache(); - const client = new ApolloClient({ - cache, - link, - resolvers: {}, - }); + const cache = new InMemoryCache(); + const client = new ApolloClient({ + cache, + link, + resolvers: {}, + }); + { + using _ = spyOnConsole("error"); cache.writeQuery({ query, data: { @@ -718,304 +679,268 @@ describe("@client @export tests", () => { secondaryReviewerId, }, }); - - return client.query({ query }).then(({ data }: any) => { - expect({ ...data }).toMatchObject({ - post, - }); - resolve(); - }); } - ); - - it( - "should refetch if an @export variable changes, the current fetch " + - "policy is not cache-only, and the query includes fields that need to " + - "be resolved remotely", - async () => { - using _consoleSpies = spyOnConsole.takeSnapshots("error"); - await new Promise((resolve, reject) => { - const query = gql` - query currentAuthorPostCount($authorId: Int!) { - currentAuthorId @client @export(as: "authorId") - postCount(authorId: $authorId) - } - `; - const testAuthorId1 = 100; - const testPostCount1 = 200; + const { data } = await client.query({ query }); + + expect(data).toEqual({ + post, + primaryReviewerId, + secondaryReviewerId, + }); + }); + + it("should refetch if an @export variable changes, the current fetch policy is not cache-only, and the query includes fields that need to be resolved remotely", async () => { + using _consoleSpies = spyOnConsole.takeSnapshots("error"); + const query = gql` + query currentAuthorPostCount($authorId: Int!) { + currentAuthorId @client @export(as: "authorId") + postCount(authorId: $authorId) + } + `; - const testAuthorId2 = 101; - const testPostCount2 = 201; + const testAuthorId1 = 100; + const testPostCount1 = 200; - let resultCount = 0; + const testAuthorId2 = 101; + const testPostCount2 = 201; - const link = new ApolloLink(() => - Observable.of({ - data: { - postCount: resultCount === 0 ? testPostCount1 : testPostCount2, - }, - }) - ); + let currentAuthorId = testAuthorId1; - const cache = new InMemoryCache(); - const client = new ApolloClient({ - cache, - link, - resolvers: {}, - }); + const link = new ApolloLink(() => + Observable.of({ + data: { + postCount: + currentAuthorId === testAuthorId1 ? testPostCount1 : testPostCount2, + }, + }) + ); - client.writeQuery({ - query, - data: { currentAuthorId: testAuthorId1 }, - }); + const cache = new InMemoryCache(); + const client = new ApolloClient({ + cache, + link, + resolvers: {}, + }); - const obs = client.watchQuery({ query }); - obs.subscribe({ - next({ data }) { - if (resultCount === 0) { - expect({ ...data }).toMatchObject({ - currentAuthorId: testAuthorId1, - postCount: testPostCount1, - }); - client.writeQuery({ - query, - data: { currentAuthorId: testAuthorId2 }, - }); - } else if (resultCount === 1) { - expect({ ...data }).toMatchObject({ - currentAuthorId: testAuthorId2, - postCount: testPostCount2, - }); - resolve(); - } - resultCount += 1; - }, - }); - }); - } - ); - - it( - "should NOT refetch if an @export variable has not changed, the " + - "current fetch policy is not cache-only, and the query includes fields " + - "that need to be resolved remotely", - async () => { - using _consoleSpies = spyOnConsole.takeSnapshots("error"); - await new Promise((resolve, reject) => { - const query = gql` - query currentAuthorPostCount($authorId: Int!) { - currentAuthorId @client @export(as: "authorId") - postCount(authorId: $authorId) - } - `; + client.writeQuery({ + query, + data: { currentAuthorId }, + }); - const testAuthorId1 = 100; - const testPostCount1 = 200; + const obs = client.watchQuery({ query }); + const stream = new ObservableStream(obs); - const testPostCount2 = 201; + await expect(stream).toEmitMatchedValue({ + data: { + currentAuthorId: testAuthorId1, + postCount: testPostCount1, + }, + }); - let resultCount = 0; + currentAuthorId = testAuthorId2; + client.writeQuery({ + query, + data: { currentAuthorId }, + }); - let fetchCount = 0; - const link = new ApolloLink(() => { - fetchCount += 1; - return Observable.of({ - data: { - postCount: testPostCount1, - }, - }); - }); + await expect(stream).toEmitMatchedValue({ + data: { + currentAuthorId: testAuthorId2, + postCount: testPostCount2, + }, + }); + }); - const cache = new InMemoryCache(); - const client = new ApolloClient({ - cache, - link, - resolvers: {}, - }); + it("should NOT refetch if an @export variable has not changed, the current fetch policy is not cache-only, and the query includes fields that need to be resolved remotely", async () => { + using _consoleSpies = spyOnConsole.takeSnapshots("error"); + const query = gql` + query currentAuthorPostCount($authorId: Int!) { + currentAuthorId @client @export(as: "authorId") + postCount(authorId: $authorId) + } + `; - client.writeQuery({ - query, - data: { currentAuthorId: testAuthorId1 }, - }); + const testAuthorId1 = 100; + const testPostCount1 = 200; - const obs = client.watchQuery({ query }); - obs.subscribe({ - next(result) { - if (resultCount === 0) { - expect(fetchCount).toBe(1); - expect(result.data).toMatchObject({ - currentAuthorId: testAuthorId1, - postCount: testPostCount1, - }); - - client.writeQuery({ - query, - variables: { authorId: testAuthorId1 }, - data: { postCount: testPostCount2 }, - }); - } else if (resultCount === 1) { - // Should not have refetched - expect(fetchCount).toBe(1); - resolve(); - } + const testPostCount2 = 201; - resultCount += 1; - }, - }); + let fetchCount = 0; + const link = new ApolloLink(() => { + fetchCount += 1; + return Observable.of({ + data: { + postCount: testPostCount1, + }, }); - } - ); - - itAsync( - "should NOT attempt to refetch over the network if an @export variable " + - "has changed, the current fetch policy is cache-first, and the remote " + - "part of the query (that leverages the @export variable) can be fully " + - "found in the cache.", - (resolve, reject) => { - const query = gql` - query currentAuthorPostCount($authorId: Int!) { - currentAuthorId @client @export(as: "authorId") - postCount(authorId: $authorId) - } - `; + }); - const testAuthorId1 = 1; - const testPostCount1 = 100; + const cache = new InMemoryCache(); + const client = new ApolloClient({ + cache, + link, + resolvers: {}, + }); - const testAuthorId2 = 2; - const testPostCount2 = 200; + client.writeQuery({ + query, + data: { currentAuthorId: testAuthorId1 }, + }); - let fetchCount = 0; - const link = new ApolloLink(() => { - fetchCount += 1; - return Observable.of({ - data: { - postCount: testPostCount1, - }, - }); - }); + const obs = client.watchQuery({ query }); + const stream = new ObservableStream(obs); - const cache = new InMemoryCache(); - const client = new ApolloClient({ - cache, - link, - resolvers: {}, - }); + await expect(stream).toEmitMatchedValue({ + data: { + currentAuthorId: testAuthorId1, + postCount: testPostCount1, + }, + }); + expect(fetchCount).toBe(1); - client.writeQuery({ - query: gql` - { - currentAuthorId - } - `, - data: { currentAuthorId: testAuthorId1 }, - }); + client.writeQuery({ + query, + variables: { authorId: testAuthorId1 }, + data: { postCount: testPostCount2 }, + }); - let resultCount = 0; - const obs = client.watchQuery({ query, fetchPolicy: "cache-first" }); - obs.subscribe({ - next(result) { - if (resultCount === 0) { - // The initial result is fetched over the network. - expect(fetchCount).toBe(1); - expect(result.data).toMatchObject({ - currentAuthorId: testAuthorId1, - postCount: testPostCount1, - }); - - client.writeQuery({ - query, - variables: { authorId: testAuthorId2 }, - data: { postCount: testPostCount2 }, - }); - client.writeQuery({ - query: gql` - { - currentAuthorId - } - `, - data: { currentAuthorId: testAuthorId2 }, - }); - } else if (resultCount === 1) { - // The updated result should not have been fetched over the - // network, as it can be found in the cache. - expect(fetchCount).toBe(1); - expect(result.data).toMatchObject({ - currentAuthorId: testAuthorId2, - postCount: testPostCount2, - }); - resolve(); - } + await expect(stream).toEmitNext(); + expect(fetchCount).toBe(1); + }); + + it("should NOT attempt to refetch over the network if an @export variable has changed, the current fetch policy is cache-first, and the remote part of the query (that leverages the @export variable) can be fully found in the cache.", async () => { + const query = gql` + query currentAuthorPostCount($authorId: Int!) { + currentAuthorId @client @export(as: "authorId") + postCount(authorId: $authorId) + } + `; + + const testAuthorId1 = 1; + const testPostCount1 = 100; - resultCount += 1; + const testAuthorId2 = 2; + const testPostCount2 = 200; + + let fetchCount = 0; + const link = new ApolloLink(() => { + fetchCount += 1; + return Observable.of({ + data: { + postCount: testPostCount1, }, }); - } - ); + }); - itAsync( - "should update @client @export variables on each broadcast if they've " + - "changed", - (resolve, reject) => { - const cache = new InMemoryCache(); + const cache = new InMemoryCache(); + const client = new ApolloClient({ + cache, + link, + resolvers: {}, + }); - const widgetCountQuery = gql` + client.writeQuery({ + query: gql` { - widgetCount @client + currentAuthorId } - `; - cache.writeQuery({ - query: widgetCountQuery, - data: { - widgetCount: 100, - }, - }); + `, + data: { currentAuthorId: testAuthorId1 }, + }); - const client = new ApolloClient({ - cache, - resolvers: { - Query: { - doubleWidgets(_, { widgetCount }) { - return widgetCount ? widgetCount * 2 : 0; - }, - }, - }, - }); + const obs = client.watchQuery({ query, fetchPolicy: "cache-first" }); + const stream = new ObservableStream(obs); - const doubleWidgetsQuery = gql` - query DoubleWidgets($widgetCount: Int!) { - widgetCount @client @export(as: "widgetCount") - doubleWidgets(widgetCount: $widgetCount) @client + await expect(stream).toEmitMatchedValue({ + data: { + currentAuthorId: testAuthorId1, + postCount: testPostCount1, + }, + }); + // The initial result is fetched over the network. + expect(fetchCount).toBe(1); + + client.writeQuery({ + query, + variables: { authorId: testAuthorId2 }, + data: { postCount: testPostCount2 }, + }); + client.writeQuery({ + query: gql` + { + currentAuthorId } - `; + `, + data: { currentAuthorId: testAuthorId2 }, + }); - let count = 0; - const obs = client.watchQuery({ query: doubleWidgetsQuery }); - obs.subscribe({ - next({ data }) { - switch (count) { - case 0: - expect(data.widgetCount).toEqual(100); - expect(data.doubleWidgets).toEqual(200); - - client.writeQuery({ - query: widgetCountQuery, - data: { - widgetCount: 500, - }, - }); - break; - case 1: - expect(data.widgetCount).toEqual(500); - expect(data.doubleWidgets).toEqual(1000); - resolve(); - break; - default: - } - count += 1; + await expect(stream).toEmitMatchedValue({ + data: { + currentAuthorId: testAuthorId2, + postCount: testPostCount2, + }, + }); + // The updated result should not have been fetched over the + // network, as it can be found in the cache. + expect(fetchCount).toBe(1); + }); + + it("should update @client @export variables on each broadcast if they've changed", async () => { + const cache = new InMemoryCache(); + + const widgetCountQuery = gql` + { + widgetCount @client + } + `; + cache.writeQuery({ + query: widgetCountQuery, + data: { + widgetCount: 100, + }, + }); + + const client = new ApolloClient({ + cache, + resolvers: { + Query: { + doubleWidgets(_, { widgetCount }) { + return widgetCount ? widgetCount * 2 : 0; + }, }, - }); - } - ); + }, + }); + + const doubleWidgetsQuery = gql` + query DoubleWidgets($widgetCount: Int!) { + widgetCount @client @export(as: "widgetCount") + doubleWidgets(widgetCount: $widgetCount) @client + } + `; + + const obs = client.watchQuery({ query: doubleWidgetsQuery }); + const stream = new ObservableStream(obs); + + await expect(stream).toEmitMatchedValue({ + data: { + widgetCount: 100, + doubleWidgets: 200, + }, + }); + + client.writeQuery({ + query: widgetCountQuery, + data: { + widgetCount: 500, + }, + }); + + await expect(stream).toEmitMatchedValue({ + data: { + widgetCount: 500, + doubleWidgets: 1000, + }, + }); + }); }); diff --git a/src/__tests__/local-state/general.ts b/src/__tests__/local-state/general.ts index 0d65993e821..c1f570e85ac 100644 --- a/src/__tests__/local-state/general.ts +++ b/src/__tests__/local-state/general.ts @@ -17,8 +17,7 @@ import { ApolloLink } from "../../link/core"; import { Operation } from "../../link/core"; import { ApolloClient } from "../../core"; import { ApolloCache, InMemoryCache } from "../../cache"; -import { itAsync } from "../../testing"; -import { spyOnConsole } from "../../testing/internal"; +import { ObservableStream, spyOnConsole } from "../../testing/internal"; describe("General functionality", () => { it("should not impact normal non-@client use", () => { @@ -279,57 +278,43 @@ describe("Cache manipulation", () => { }); }); - itAsync( - "should be able to write to the cache with a local mutation and have " + - "things rerender automatically", - (resolve, reject) => { - const query = gql` - { - field @client - } - `; + it("should be able to write to the cache with a local mutation and have things rerender automatically", async () => { + const query = gql` + { + field @client + } + `; - const mutation = gql` - mutation start { - start @client - } - `; + const mutation = gql` + mutation start { + start @client + } + `; - const resolvers = { - Query: { - field: () => 0, - }, - Mutation: { - start: (_1: any, _2: any, { cache }: { cache: InMemoryCache }) => { - cache.writeQuery({ query, data: { field: 1 } }); - return { start: true }; - }, + const resolvers = { + Query: { + field: () => 0, + }, + Mutation: { + start: (_1: any, _2: any, { cache }: { cache: InMemoryCache }) => { + cache.writeQuery({ query, data: { field: 1 } }); + return { start: true }; }, - }; + }, + }; - const client = new ApolloClient({ - cache: new InMemoryCache(), - link: ApolloLink.empty(), - resolvers, - }); + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: ApolloLink.empty(), + resolvers, + }); - let count = 0; - client.watchQuery({ query }).subscribe({ - next: ({ data }) => { - count++; - if (count === 1) { - expect({ ...data }).toMatchObject({ field: 0 }); - client.mutate({ mutation }); - } + const stream = new ObservableStream(client.watchQuery({ query })); - if (count === 2) { - expect({ ...data }).toMatchObject({ field: 1 }); - resolve(); - } - }, - }); - } - ); + await expect(stream).toEmitMatchedValue({ data: { field: 0 } }); + await client.mutate({ mutation }); + await expect(stream).toEmitMatchedValue({ data: { field: 1 } }); + }); it("should support writing to the cache with a local mutation using variables", () => { const query = gql` @@ -381,376 +366,352 @@ describe("Cache manipulation", () => { }); }); - itAsync( - "should read @client fields from cache on refetch (#4741)", - (resolve, reject) => { - const query = gql` - query FetchInitialData { - serverData { - id - title - } - selectedItemId @client + it("should read @client fields from cache on refetch (#4741)", async () => { + const query = gql` + query FetchInitialData { + serverData { + id + title } - `; + selectedItemId @client + } + `; - const mutation = gql` - mutation Select { - select(itemId: $id) @client - } - `; + const mutation = gql` + mutation Select { + select(itemId: $id) @client + } + `; - const serverData = { - __typename: "ServerData", - id: 123, - title: "Oyez and Onoz", - }; + const serverData = { + __typename: "ServerData", + id: 123, + title: "Oyez and Onoz", + }; - let selectedItemId = -1; - const client = new ApolloClient({ - cache: new InMemoryCache(), - link: new ApolloLink(() => Observable.of({ data: { serverData } })), - resolvers: { - Query: { - selectedItemId() { - return selectedItemId; - }, + let selectedItemId = -1; + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: new ApolloLink(() => Observable.of({ data: { serverData } })), + resolvers: { + Query: { + selectedItemId() { + return selectedItemId; }, - Mutation: { - select(_, { itemId }) { - selectedItemId = itemId; - }, + }, + Mutation: { + select(_, { itemId }) { + selectedItemId = itemId; }, }, - }); + }, + }); - client.watchQuery({ query }).subscribe({ - next(result) { - expect(result).toEqual({ - data: { - serverData, - selectedItemId, - }, - loading: false, - networkStatus: 7, - }); + const stream = new ObservableStream(client.watchQuery({ query })); - if (selectedItemId !== 123) { - client.mutate({ - mutation, - variables: { - id: 123, - }, - refetchQueries: ["FetchInitialData"], - }); - } else { - resolve(); - } - }, - }); - } - ); + await expect(stream).toEmitValue({ + data: { + serverData, + selectedItemId: -1, + }, + loading: false, + networkStatus: 7, + }); - itAsync( - "should rerun @client(always: true) fields on entity update", - (resolve, reject) => { - const query = gql` - query GetClientData($id: ID) { - clientEntity(id: $id) @client(always: true) { - id - title - titleLength @client(always: true) - } - } - `; + await client.mutate({ + mutation, + variables: { id: 123 }, + refetchQueries: ["FetchInitialData"], + }); - const mutation = gql` - mutation AddOrUpdate { - addOrUpdate(id: $id, title: $title) @client - } - `; + await expect(stream).toEmitValue({ + data: { + serverData, + selectedItemId: 123, + }, + loading: false, + networkStatus: 7, + }); + }); - const fragment = gql` - fragment ClientDataFragment on ClientData { + it("should rerun @client(always: true) fields on entity update", async () => { + const query = gql` + query GetClientData($id: ID) { + clientEntity(id: $id) @client(always: true) { id title + titleLength @client(always: true) } - `; - const client = new ApolloClient({ - cache: new InMemoryCache(), - link: new ApolloLink(() => Observable.of({ data: {} })), - resolvers: { - ClientData: { - titleLength(data) { - return data.title.length; - }, + } + `; + + const mutation = gql` + mutation AddOrUpdate { + addOrUpdate(id: $id, title: $title) @client + } + `; + + const fragment = gql` + fragment ClientDataFragment on ClientData { + id + title + } + `; + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: new ApolloLink(() => Observable.of({ data: {} })), + resolvers: { + ClientData: { + titleLength(data) { + return data.title.length; }, - Query: { - clientEntity(_root, { id }, { cache }) { - return cache.readFragment({ - id: cache.identify({ id, __typename: "ClientData" }), - fragment, - }); - }, + }, + Query: { + clientEntity(_root, { id }, { cache }) { + return cache.readFragment({ + id: cache.identify({ id, __typename: "ClientData" }), + fragment, + }); }, - Mutation: { - addOrUpdate(_root, { id, title }, { cache }) { - return cache.writeFragment({ - id: cache.identify({ id, __typename: "ClientData" }), - fragment, - data: { id, title, __typename: "ClientData" }, - }); - }, + }, + Mutation: { + addOrUpdate(_root, { id, title }, { cache }) { + return cache.writeFragment({ + id: cache.identify({ id, __typename: "ClientData" }), + fragment, + data: { id, title, __typename: "ClientData" }, + }); }, }, - }); + }, + }); - const entityId = 1; - const shortTitle = "Short"; - const longerTitle = "A little longer"; - client.mutate({ - mutation, - variables: { - id: entityId, - title: shortTitle, - }, + const entityId = 1; + const shortTitle = "Short"; + const longerTitle = "A little longer"; + await client.mutate({ + mutation, + variables: { + id: entityId, + title: shortTitle, + }, + }); + const stream = new ObservableStream( + client.watchQuery({ query, variables: { id: entityId } }) + ); + + { + const result = await stream.takeNext(); + + expect(result.data.clientEntity).toEqual({ + id: entityId, + title: shortTitle, + titleLength: shortTitle.length, + __typename: "ClientData", }); - let mutated = false; - client.watchQuery({ query, variables: { id: entityId } }).subscribe({ - next(result) { - if (!mutated) { - expect(result.data.clientEntity).toEqual({ - id: entityId, - title: shortTitle, - titleLength: shortTitle.length, - __typename: "ClientData", - }); - client.mutate({ - mutation, - variables: { - id: entityId, - title: longerTitle, - }, - }); - mutated = true; - } else if (mutated) { - expect(result.data.clientEntity).toEqual({ - id: entityId, - title: longerTitle, - titleLength: longerTitle.length, - __typename: "ClientData", - }); - resolve(); - } - }, + } + + await client.mutate({ + mutation, + variables: { + id: entityId, + title: longerTitle, + }, + }); + + { + const result = await stream.takeNext(); + + expect(result.data.clientEntity).toEqual({ + id: entityId, + title: longerTitle, + titleLength: longerTitle.length, + __typename: "ClientData", }); } - ); + + await expect(stream).not.toEmitAnything(); + }); }); describe("Sample apps", () => { - itAsync( - "should support a simple counter app using local state", - (resolve, reject) => { - const query = gql` - query GetCount { - count @client - lastCount # stored in db on server - } - `; + it("should support a simple counter app using local state", async () => { + const query = gql` + query GetCount { + count @client + lastCount # stored in db on server + } + `; - const increment = gql` - mutation Increment($amount: Int = 1) { - increment(amount: $amount) @client - } - `; + const increment = gql` + mutation Increment($amount: Int = 1) { + increment(amount: $amount) @client + } + `; - const decrement = gql` - mutation Decrement($amount: Int = 1) { - decrement(amount: $amount) @client - } - `; + const decrement = gql` + mutation Decrement($amount: Int = 1) { + decrement(amount: $amount) @client + } + `; - const link = new ApolloLink((operation) => { - expect(operation.operationName).toBe("GetCount"); - return Observable.of({ data: { lastCount: 1 } }); - }); + const link = new ApolloLink((operation) => { + expect(operation.operationName).toBe("GetCount"); + return Observable.of({ data: { lastCount: 1 } }); + }); - const client = new ApolloClient({ - link, - cache: new InMemoryCache(), - resolvers: {}, - }); + const client = new ApolloClient({ + link, + cache: new InMemoryCache(), + resolvers: {}, + }); - const update = ( - query: DocumentNode, - updater: (data: { count: number }, variables: { amount: number }) => any - ) => { - return ( - _result: {}, - variables: { amount: number }, - { cache }: { cache: ApolloCache } - ): null => { - const read = client.readQuery<{ count: number }>({ - query, - variables, - }); - if (read) { - const data = updater(read, variables); - cache.writeQuery({ query, variables, data }); - } else { - throw new Error("readQuery returned a falsy value"); - } - return null; - }; + const update = ( + query: DocumentNode, + updater: (data: { count: number }, variables: { amount: number }) => any + ) => { + return ( + _result: {}, + variables: { amount: number }, + { cache }: { cache: ApolloCache } + ): null => { + const read = client.readQuery<{ count: number }>({ + query, + variables, + }); + if (read) { + const data = updater(read, variables); + cache.writeQuery({ query, variables, data }); + } else { + throw new Error("readQuery returned a falsy value"); + } + return null; }; + }; - const resolvers = { - Query: { - count: () => 0, - }, - Mutation: { - increment: update(query, ({ count, ...rest }, { amount }) => ({ - ...rest, - count: count + amount, - })), - decrement: update(query, ({ count, ...rest }, { amount }) => ({ - ...rest, - count: count - amount, - })), - }, - }; + const resolvers = { + Query: { + count: () => 0, + }, + Mutation: { + increment: update(query, ({ count, ...rest }, { amount }) => ({ + ...rest, + count: count + amount, + })), + decrement: update(query, ({ count, ...rest }, { amount }) => ({ + ...rest, + count: count - amount, + })), + }, + }; - client.addResolvers(resolvers); - - let count = 0; - client.watchQuery({ query }).subscribe({ - next: ({ data }) => { - count++; - if (count === 1) { - try { - expect({ ...data }).toMatchObject({ count: 0, lastCount: 1 }); - } catch (e) { - reject(e); - } - client.mutate({ mutation: increment, variables: { amount: 2 } }); - } + client.addResolvers(resolvers); + const stream = new ObservableStream(client.watchQuery({ query })); - if (count === 2) { - try { - expect({ ...data }).toMatchObject({ count: 2, lastCount: 1 }); - } catch (e) { - reject(e); - } - client.mutate({ mutation: decrement, variables: { amount: 1 } }); - } - if (count === 3) { - try { - expect({ ...data }).toMatchObject({ count: 1, lastCount: 1 }); - } catch (e) { - reject(e); - } - resolve(); - } - }, - error: (e) => reject(e), - complete: reject, - }); - } - ); + await expect(stream).toEmitMatchedValue({ + data: { count: 0, lastCount: 1 }, + }); - itAsync( - "should support a simple todo app using local state", - (resolve, reject) => { - const query = gql` - query GetTasks { - todos @client { - message - title - } - } - `; + await client.mutate({ mutation: increment, variables: { amount: 2 } }); - const mutation = gql` - mutation AddTodo($message: String, $title: String) { - addTodo(message: $message, title: $title) @client - } - `; + await expect(stream).toEmitMatchedValue({ + data: { count: 2, lastCount: 1 }, + }); - const client = new ApolloClient({ - link: ApolloLink.empty(), - cache: new InMemoryCache(), - resolvers: {}, - }); + await client.mutate({ mutation: decrement, variables: { amount: 1 } }); + + await expect(stream).toEmitMatchedValue({ + data: { count: 1, lastCount: 1 }, + }); + }); - interface Todo { - title: string; - message: string; - __typename: string; + it("should support a simple todo app using local state", async () => { + const query = gql` + query GetTasks { + todos @client { + message + title + } } + `; - const update = ( - query: DocumentNode, - updater: (todos: any, variables: Todo) => any - ) => { - return ( - _result: {}, - variables: Todo, - { cache }: { cache: ApolloCache } - ): null => { - const data = updater( - client.readQuery({ query, variables }), - variables - ); - cache.writeQuery({ query, variables, data }); - return null; - }; - }; + const mutation = gql` + mutation AddTodo($message: String, $title: String) { + addTodo(message: $message, title: $title) @client + } + `; - const resolvers = { - Query: { - todos: () => [], - }, - Mutation: { - addTodo: update(query, ({ todos }, { title, message }: Todo) => ({ - todos: todos.concat([{ message, title, __typename: "Todo" }]), - })), - }, + const client = new ApolloClient({ + link: ApolloLink.empty(), + cache: new InMemoryCache(), + resolvers: {}, + }); + + interface Todo { + title: string; + message: string; + __typename: string; + } + + const update = ( + query: DocumentNode, + updater: (todos: any, variables: Todo) => any + ) => { + return ( + _result: {}, + variables: Todo, + { cache }: { cache: ApolloCache } + ): null => { + const data = updater(client.readQuery({ query, variables }), variables); + cache.writeQuery({ query, variables, data }); + return null; }; + }; - client.addResolvers(resolvers); - - let count = 0; - client.watchQuery({ query }).subscribe({ - next: ({ data }: any) => { - count++; - if (count === 1) { - expect({ ...data }).toMatchObject({ todos: [] }); - client.mutate({ - mutation, - variables: { - title: "Apollo Client 2.0", - message: "ship it", - }, - }); - } else if (count === 2) { - expect(data.todos.map((x: Todo) => ({ ...x }))).toMatchObject([ - { - title: "Apollo Client 2.0", - message: "ship it", - __typename: "Todo", - }, - ]); - resolve(); - } + const resolvers = { + Query: { + todos: () => [], + }, + Mutation: { + addTodo: update(query, ({ todos }, { title, message }: Todo) => ({ + todos: todos.concat([{ message, title, __typename: "Todo" }]), + })), + }, + }; + + client.addResolvers(resolvers); + const stream = new ObservableStream(client.watchQuery({ query })); + + { + const { data } = await stream.takeNext(); + + expect(data).toEqual({ todos: [] }); + } + + await client.mutate({ + mutation, + variables: { + title: "Apollo Client 2.0", + message: "ship it", + }, + }); + + { + const { data } = await stream.takeNext(); + + expect(data.todos).toEqual([ + { + title: "Apollo Client 2.0", + message: "ship it", + __typename: "Todo", }, - }); + ]); } - ); + }); }); describe("Combining client and server state/operations", () => { - itAsync("should merge remote and local state", (resolve, reject) => { + it("should merge remote and local state", async () => { const query = gql` query list { list(name: "my list") { @@ -815,462 +776,403 @@ describe("Combining client and server state/operations", () => { const observer = client.watchQuery({ query }); - let count = 0; - observer.subscribe({ - next: (response) => { - if (count === 0) { - const initial = { ...data }; - initial.list.items = initial.list.items.map((x) => ({ - ...x, - isSelected: false, - })); - expect(response.data).toMatchObject(initial); - } - if (count === 1) { - expect((response.data as any).list.items[0].isSelected).toBe(true); - expect((response.data as any).list.items[1].isSelected).toBe(false); - resolve(); + const stream = new ObservableStream(observer); + + { + const response = await stream.takeNext(); + const initial = { ...data }; + initial.list.items = initial.list.items.map((x) => ({ + ...x, + isSelected: false, + })); + + expect(response.data).toMatchObject(initial); + } + + await client.mutate({ + mutation: gql` + mutation SelectItem($id: Int!) { + toggleItem(id: $id) @client } - count++; - }, - error: reject, + `, + variables: { id: 1 }, }); - const variables = { id: 1 }; - const mutation = gql` - mutation SelectItem($id: Int!) { - toggleItem(id: $id) @client - } - `; - // After initial result, toggle the state of one of the items - setTimeout(() => { - client.mutate({ mutation, variables }); - }, 10); + + { + const response = await stream.takeNext(); + + expect((response.data as any).list.items[0].isSelected).toBe(true); + expect((response.data as any).list.items[1].isSelected).toBe(false); + } }); - itAsync( - "query resolves with loading: false if subsequent responses contain the same data", - (resolve, reject) => { - const request = { - query: gql` - query people($id: Int) { - people(id: $id) { - id - name - } + it("query resolves with loading: false if subsequent responses contain the same data", async () => { + const request = { + query: gql` + query people($id: Int) { + people(id: $id) { + id + name } - `, - variables: { - id: 1, - }, - notifyOnNetworkStatusChange: true, - }; + } + `, + variables: { + id: 1, + }, + notifyOnNetworkStatusChange: true, + }; - const PersonType = new GraphQLObjectType({ - name: "Person", - fields: { - id: { type: GraphQLID }, - name: { type: GraphQLString }, - }, - }); + const PersonType = new GraphQLObjectType({ + name: "Person", + fields: { + id: { type: GraphQLID }, + name: { type: GraphQLString }, + }, + }); - const peopleData = [ - { id: 1, name: "John Smith" }, - { id: 2, name: "Sara Smith" }, - { id: 3, name: "Budd Deey" }, - ]; - - const QueryType = new GraphQLObjectType({ - name: "Query", - fields: { - people: { - type: PersonType, - args: { - id: { - type: GraphQLInt, - }, - }, - resolve: (_, { id }) => { - return peopleData; + const peopleData = [ + { id: 1, name: "John Smith" }, + { id: 2, name: "Sara Smith" }, + { id: 3, name: "Budd Deey" }, + ]; + + const QueryType = new GraphQLObjectType({ + name: "Query", + fields: { + people: { + type: PersonType, + args: { + id: { + type: GraphQLInt, }, }, + resolve: (_, { id }) => { + return peopleData; + }, }, - }); + }, + }); - const schema = new GraphQLSchema({ query: QueryType }); - - const link = new ApolloLink((operation) => { - // @ts-ignore - return new Observable(async (observer) => { - const { query, operationName, variables } = operation; - try { - const result = await graphql({ - schema, - source: print(query), - variableValues: variables, - operationName, - }); - observer.next(result); - observer.complete(); - } catch (err) { - observer.error(err); - } - }); + const schema = new GraphQLSchema({ query: QueryType }); + + const link = new ApolloLink((operation) => { + // @ts-ignore + return new Observable(async (observer) => { + const { query, operationName, variables } = operation; + try { + const result = await graphql({ + schema, + source: print(query), + variableValues: variables, + operationName, + }); + observer.next(result); + observer.complete(); + } catch (err) { + observer.error(err); + } }); + }); - const client = new ApolloClient({ - cache: new InMemoryCache(), - link, - }); + const client = new ApolloClient({ + cache: new InMemoryCache(), + link, + }); - const observer = client.watchQuery(request); + const observable = client.watchQuery(request); + const stream = new ObservableStream(observable); - let count = 0; - observer.subscribe({ - next: ({ loading, data }) => { - if (count === 0) expect(loading).toBe(false); - if (count === 1) expect(loading).toBe(true); - if (count === 2) { - expect(loading).toBe(false); - resolve(); - } - count++; - }, - error: reject, - }); + await expect(stream).toEmitMatchedValue({ loading: false }); - setTimeout(() => { - observer.refetch({ - id: 2, - }); - }, 1); - } - ); + await observable.refetch({ id: 2 }); - itAsync( - "should correctly propagate an error from a client resolver", - async (resolve, reject) => { - const data = { - list: { - __typename: "List", - items: [ - { __typename: "ListItem", id: 1, name: "first", isDone: true }, - { __typename: "ListItem", id: 2, name: "second", isDone: false }, - ], - }, - }; + await expect(stream).toEmitMatchedValue({ loading: true }); + await expect(stream).toEmitMatchedValue({ loading: false }); + }); - const link = new ApolloLink(() => Observable.of({ data })); + it("should correctly propagate an error from a client resolver", async () => { + const data = { + list: { + __typename: "List", + items: [ + { __typename: "ListItem", id: 1, name: "first", isDone: true }, + { __typename: "ListItem", id: 2, name: "second", isDone: false }, + ], + }, + }; - const client = new ApolloClient({ - cache: new InMemoryCache(), - link, - resolvers: { - Query: { - hasBeenIllegallyTouched: (_, _v, _c) => { - throw new Error("Illegal Query Operation Occurred"); - }, - }, + const link = new ApolloLink(() => Observable.of({ data })); - Mutation: { - touchIllegally: (_, _v, _c) => { - throw new Error("Illegal Mutation Operation Occurred"); - }, + const client = new ApolloClient({ + cache: new InMemoryCache(), + link, + resolvers: { + Query: { + hasBeenIllegallyTouched: (_, _v, _c) => { + throw new Error("Illegal Query Operation Occurred"); }, }, - }); - const variables = { id: 1 }; - const query = gql` - query hasBeenIllegallyTouched($id: Int!) { - hasBeenIllegallyTouched(id: $id) @client - } - `; - const mutation = gql` - mutation SelectItem($id: Int!) { - touchIllegally(id: $id) @client - } - `; + Mutation: { + touchIllegally: (_, _v, _c) => { + throw new Error("Illegal Mutation Operation Occurred"); + }, + }, + }, + }); - try { - await client.query({ query, variables }); - reject("Should have thrown!"); - } catch (e) { - // Test Passed! - expect(() => { - throw e; - }).toThrowErrorMatchingSnapshot(); + const variables = { id: 1 }; + const query = gql` + query hasBeenIllegallyTouched($id: Int!) { + hasBeenIllegallyTouched(id: $id) @client } - - try { - await client.mutate({ mutation, variables }); - reject("Should have thrown!"); - } catch (e) { - // Test Passed! - expect(() => { - throw e; - }).toThrowErrorMatchingSnapshot(); + `; + const mutation = gql` + mutation SelectItem($id: Int!) { + touchIllegally(id: $id) @client } + `; - resolve(); - } - ); + await expect( + client.query({ query, variables }) + ).rejects.toThrowErrorMatchingSnapshot(); + + await expect( + client.mutate({ mutation, variables }) + ).rejects.toThrowErrorMatchingSnapshot(); + }); it("should handle a simple query with both server and client fields", async () => { using _consoleSpies = spyOnConsole.takeSnapshots("error"); - await new Promise((resolve, reject) => { - const query = gql` - query GetCount { - count @client - lastCount - } - `; - const cache = new InMemoryCache(); + const query = gql` + query GetCount { + count @client + lastCount + } + `; + const cache = new InMemoryCache(); - const link = new ApolloLink((operation) => { - expect(operation.operationName).toBe("GetCount"); - return Observable.of({ data: { lastCount: 1 } }); - }); + const link = new ApolloLink((operation) => { + expect(operation.operationName).toBe("GetCount"); + return Observable.of({ data: { lastCount: 1 } }); + }); - const client = new ApolloClient({ - cache, - link, - resolvers: {}, - }); + const client = new ApolloClient({ + cache, + link, + resolvers: {}, + }); - cache.writeQuery({ - query, - data: { - count: 0, - }, - }); + cache.writeQuery({ + query, + data: { + count: 0, + }, + }); - client.watchQuery({ query }).subscribe({ - next: ({ data }) => { - expect({ ...data }).toMatchObject({ count: 0, lastCount: 1 }); - resolve(); - }, - }); + const stream = new ObservableStream(client.watchQuery({ query })); + + await expect(stream).toEmitMatchedValue({ + data: { count: 0, lastCount: 1 }, }); }); it("should support nested querying of both server and client fields", async () => { using _consoleSpies = spyOnConsole.takeSnapshots("error"); - await new Promise((resolve, reject) => { - const query = gql` - query GetUser { - user { - firstName @client - lastName - } + const query = gql` + query GetUser { + user { + firstName @client + lastName } - `; - - const cache = new InMemoryCache(); - const link = new ApolloLink((operation) => { - expect(operation.operationName).toBe("GetUser"); - return Observable.of({ - data: { - user: { - __typename: "User", - // We need an id (or a keyFields policy) because, if the User - // object is not identifiable, the call to cache.writeQuery - // below will simply replace the existing data rather than - // merging the new data with the existing data. - id: 123, - lastName: "Doe", - }, - }, - }); - }); - - const client = new ApolloClient({ - cache, - link, - resolvers: {}, - }); + } + `; - cache.writeQuery({ - query, + const cache = new InMemoryCache(); + const link = new ApolloLink((operation) => { + expect(operation.operationName).toBe("GetUser"); + return Observable.of({ data: { user: { __typename: "User", + // We need an id (or a keyFields policy) because, if the User + // object is not identifiable, the call to cache.writeQuery + // below will simply replace the existing data rather than + // merging the new data with the existing data. id: 123, - firstName: "John", + lastName: "Doe", }, }, }); + }); - client.watchQuery({ query }).subscribe({ - next({ data }: any) { - const { user } = data; - try { - expect(user).toMatchObject({ - firstName: "John", - lastName: "Doe", - __typename: "User", - }); - } catch (e) { - reject(e); - } - resolve(); + const client = new ApolloClient({ + cache, + link, + resolvers: {}, + }); + + cache.writeQuery({ + query, + data: { + user: { + __typename: "User", + id: 123, + firstName: "John", }, - }); + }, + }); + + const stream = new ObservableStream(client.watchQuery({ query })); + + await expect(stream).toEmitMatchedValue({ + data: { + user: { + firstName: "John", + lastName: "Doe", + __typename: "User", + }, + }, }); }); - itAsync( - "should combine both server and client mutations", - (resolve, reject) => { - const query = gql` - query SampleQuery { - count @client - user { - firstName - } + it("should combine both server and client mutations", async () => { + const query = gql` + query SampleQuery { + count @client + user { + firstName } - `; + } + `; - const mutation = gql` - mutation SampleMutation { - incrementCount @client - updateUser(firstName: "Harry") { - firstName - } + const mutation = gql` + mutation SampleMutation { + incrementCount @client + updateUser(firstName: "Harry") { + firstName } - `; + } + `; - const counterQuery = gql` - { - count @client - } - `; + const counterQuery = gql` + { + count @client + } + `; - const userQuery = gql` - { - user { - firstName - } + const userQuery = gql` + { + user { + firstName } - `; + } + `; - let watchCount = 0; - const link = new ApolloLink((operation: Operation): Observable<{}> => { - if (operation.operationName === "SampleQuery") { - return Observable.of({ - data: { user: { __typename: "User", firstName: "John" } }, - }); - } - if (operation.operationName === "SampleMutation") { - return Observable.of({ - data: { updateUser: { __typename: "User", firstName: "Harry" } }, - }); - } + const link = new ApolloLink((operation: Operation): Observable<{}> => { + if (operation.operationName === "SampleQuery") { return Observable.of({ - errors: [new Error(`Unknown operation ${operation.operationName}`)], + data: { user: { __typename: "User", firstName: "John" } }, }); + } + if (operation.operationName === "SampleMutation") { + return Observable.of({ + data: { updateUser: { __typename: "User", firstName: "Harry" } }, + }); + } + return Observable.of({ + errors: [new Error(`Unknown operation ${operation.operationName}`)], }); + }); - const cache = new InMemoryCache(); - const client = new ApolloClient({ - cache, - link, - resolvers: { - Mutation: { - incrementCount: (_, __, { cache }) => { - const { count } = cache.readQuery({ query: counterQuery }); - const data = { count: count + 1 }; - cache.writeQuery({ - query: counterQuery, - data, - }); - return null; - }, + const cache = new InMemoryCache(); + const client = new ApolloClient({ + cache, + link, + resolvers: { + Mutation: { + incrementCount: (_, __, { cache }) => { + const { count } = cache.readQuery({ query: counterQuery }); + const data = { count: count + 1 }; + cache.writeQuery({ + query: counterQuery, + data, + }); + return null; }, }, - }); + }, + }); - cache.writeQuery({ - query: counterQuery, - data: { - count: 0, - }, - }); + cache.writeQuery({ + query: counterQuery, + data: { + count: 0, + }, + }); - client.watchQuery({ query }).subscribe({ - next: ({ data }: any) => { - if (watchCount === 0) { - expect(data.count).toEqual(0); - expect({ ...data.user }).toMatchObject({ - __typename: "User", - firstName: "John", - }); - watchCount += 1; - client.mutate({ - mutation, - update(proxy, { data: { updateUser } }) { - proxy.writeQuery({ - query: userQuery, - data: { - user: { ...updateUser }, - }, - }); - }, - }); - } else { - expect(data.count).toEqual(1); - expect({ ...data.user }).toMatchObject({ - __typename: "User", - firstName: "Harry", - }); - resolve(); - } - }, - }); - } - ); + const stream = new ObservableStream(client.watchQuery({ query })); - itAsync( - "handles server errors when root data property is null", - (resolve, reject) => { - const query = gql` - query GetUser { - user { - firstName @client - lastName - } - } - `; + await expect(stream).toEmitMatchedValue({ + data: { + count: 0, + user: { __typename: "User", firstName: "John" }, + }, + }); - const cache = new InMemoryCache(); - const link = new ApolloLink((operation) => { - return Observable.of({ - data: null, - errors: [ - new GraphQLError("something went wrong", { - extensions: { - code: "INTERNAL_SERVER_ERROR", - }, - path: ["user"], - }), - ], + await client.mutate({ + mutation, + update(proxy, { data: { updateUser } }) { + proxy.writeQuery({ + query: userQuery, + data: { + user: { ...updateUser }, + }, }); - }); + }, + }); - const client = new ApolloClient({ - cache, - link, - resolvers: {}, - }); + await expect(stream).toEmitMatchedValue({ + data: { + count: 1, + user: { __typename: "User", firstName: "Harry" }, + }, + }); + }); - client.watchQuery({ query }).subscribe({ - error(error) { - expect(error.message).toEqual("something went wrong"); - resolve(); - }, - next() { - reject(); - }, + it("handles server errors when root data property is null", async () => { + const query = gql` + query GetUser { + user { + firstName @client + lastName + } + } + `; + + const cache = new InMemoryCache(); + const link = new ApolloLink((operation) => { + return Observable.of({ + data: null, + errors: [ + new GraphQLError("something went wrong", { + extensions: { + code: "INTERNAL_SERVER_ERROR", + }, + path: ["user"], + }), + ], }); - } - ); + }); + + const client = new ApolloClient({ + cache, + link, + resolvers: {}, + }); + + const stream = new ObservableStream(client.watchQuery({ query })); + + await expect(stream).toEmitError("something went wrong"); + }); }); diff --git a/src/__tests__/local-state/resolvers.ts b/src/__tests__/local-state/resolvers.ts index b305a3ed7d8..b1941cd80c7 100644 --- a/src/__tests__/local-state/resolvers.ts +++ b/src/__tests__/local-state/resolvers.ts @@ -1,27 +1,19 @@ import gql from "graphql-tag"; import { DocumentNode, ExecutionResult } from "graphql"; -import { assign } from "lodash"; import { LocalState } from "../../core/LocalState"; -import { - ApolloClient, - ApolloQueryResult, - Resolvers, - WatchQueryOptions, -} from "../../core"; +import { ApolloClient, ApolloQueryResult, Resolvers } from "../../core"; import { InMemoryCache, isReference } from "../../cache"; -import { Observable, Observer } from "../../utilities"; +import { Observable } from "../../utilities"; import { ApolloLink } from "../../link/core"; -import { itAsync } from "../../testing"; import mockQueryManager from "../../testing/core/mocking/mockQueryManager"; -import wrap from "../../testing/core/wrap"; +import { ObservableStream } from "../../testing/internal"; // Helper method that sets up a mockQueryManager and then passes on the // results to an observer. -const assertWithObserver = ({ - reject, +const setupTestWithResolvers = ({ resolvers, query, serverQuery, @@ -30,10 +22,8 @@ const assertWithObserver = ({ serverResult, error, delay, - observer, }: { - reject: (reason: any) => any; - resolvers?: Resolvers; + resolvers: Resolvers; query: DocumentNode; serverQuery?: DocumentNode; variables?: object; @@ -41,7 +31,6 @@ const assertWithObserver = ({ error?: Error; serverResult?: ExecutionResult; delay?: number; - observer: Observer>; }) => { const queryManager = mockQueryManager({ request: { query: serverQuery || query, variables }, @@ -50,22 +39,15 @@ const assertWithObserver = ({ delay, }); - if (resolvers) { - queryManager.getLocalState().addResolvers(resolvers); - } - - const finalOptions = assign( - { query, variables }, - queryOptions - ) as WatchQueryOptions; - return queryManager.watchQuery(finalOptions).subscribe({ - next: wrap(reject, observer.next!), - error: observer.error, - }); + queryManager.getLocalState().addResolvers(resolvers); + + return new ObservableStream( + queryManager.watchQuery({ query, variables, ...queryOptions }) + ); }; describe("Basic resolver capabilities", () => { - itAsync("should run resolvers for @client queries", (resolve, reject) => { + it("should run resolvers for @client queries", async () => { const query = gql` query Test { foo @client { @@ -80,234 +62,183 @@ describe("Basic resolver capabilities", () => { }, }; - assertWithObserver({ - reject, + const stream = setupTestWithResolvers({ resolvers, query }); + + await expect(stream).toEmitMatchedValue({ data: { foo: { bar: true } } }); + }); + + it("should handle queries with a mix of @client and server fields", async () => { + const query = gql` + query Mixed { + foo @client { + bar + } + bar { + baz + } + } + `; + + const serverQuery = gql` + query Mixed { + bar { + baz + } + } + `; + + const resolvers = { + Query: { + foo: () => ({ bar: true }), + }, + }; + + const stream = setupTestWithResolvers({ resolvers, query, - observer: { - next({ data }) { - try { - expect(data).toEqual({ foo: { bar: true } }); - } catch (error) { - reject(error); - } - resolve(); - }, + serverQuery, + serverResult: { data: { bar: { baz: true } } }, + }); + + await expect(stream).toEmitMatchedValue({ + data: { + foo: { bar: true }, + bar: { baz: true }, }, }); }); - itAsync( - "should handle queries with a mix of @client and server fields", - (resolve, reject) => { - const query = gql` - query Mixed { - foo @client { - bar - } - bar { - baz - } - } - `; + it("should handle a mix of @client fields with fragments and server fields", async () => { + const query = gql` + fragment client on ClientData { + bar + __typename + } - const serverQuery = gql` - query Mixed { - bar { - baz - } + query Mixed { + foo @client { + ...client } - `; - - const resolvers = { - Query: { - foo: () => ({ bar: true }), - }, - }; - - assertWithObserver({ - reject, - resolvers, - query, - serverQuery, - serverResult: { data: { bar: { baz: true } } }, - observer: { - next({ data }) { - try { - expect(data).toEqual({ foo: { bar: true }, bar: { baz: true } }); - } catch (error) { - reject(error); - } - resolve(); - }, - }, - }); - } - ); - - itAsync( - "should handle a mix of @client fields with fragments and server fields", - (resolve, reject) => { - const query = gql` - fragment client on ClientData { - bar - __typename + bar { + baz } + } + `; - query Mixed { - foo @client { - ...client - } - bar { - baz - } + const serverQuery = gql` + query Mixed { + bar { + baz } - `; + } + `; - const serverQuery = gql` - query Mixed { - bar { - baz - } - } - `; + const resolvers = { + Query: { + foo: () => ({ bar: true, __typename: "ClientData" }), + }, + }; - const resolvers = { - Query: { - foo: () => ({ bar: true, __typename: "ClientData" }), - }, - }; - - assertWithObserver({ - reject, - resolvers, - query, - serverQuery, - serverResult: { data: { bar: { baz: true, __typename: "Bar" } } }, - observer: { - next({ data }) { - try { - expect(data).toEqual({ - foo: { bar: true, __typename: "ClientData" }, - bar: { baz: true }, - }); - } catch (error) { - reject(error); - } - resolve(); - }, - }, - }); - } - ); + const stream = setupTestWithResolvers({ + resolvers, + query, + serverQuery, + serverResult: { data: { bar: { baz: true, __typename: "Bar" } } }, + }); - itAsync( - "should handle @client fields inside fragments", - (resolve, reject) => { - const query = gql` - fragment Foo on Foo { - bar - ...Foo2 - } - fragment Foo2 on Foo { - __typename - baz @client + await expect(stream).toEmitMatchedValue({ + data: { + foo: { bar: true, __typename: "ClientData" }, + bar: { baz: true }, + }, + }); + }); + + it("should handle @client fields inside fragments", async () => { + const query = gql` + fragment Foo on Foo { + bar + ...Foo2 + } + fragment Foo2 on Foo { + __typename + baz @client + } + query Mixed { + foo { + ...Foo } - query Mixed { - foo { - ...Foo - } - bar { - baz - } + bar { + baz } - `; + } + `; - const serverQuery = gql` - fragment Foo on Foo { - bar + const serverQuery = gql` + fragment Foo on Foo { + bar + } + query Mixed { + foo { + ...Foo } - query Mixed { - foo { - ...Foo - } - bar { - baz - } + bar { + baz } - `; + } + `; - const resolvers = { - Foo: { - baz: () => false, - }, - }; - - assertWithObserver({ - reject, - resolvers, - query, - serverQuery, - serverResult: { - data: { foo: { bar: true, __typename: `Foo` }, bar: { baz: true } }, - }, - observer: { - next({ data }) { - try { - expect(data).toEqual({ - foo: { bar: true, baz: false, __typename: "Foo" }, - bar: { baz: true }, - }); - } catch (error) { - reject(error); - } - resolve(); - }, - }, - }); - } - ); + const resolvers = { + Foo: { + baz: () => false, + }, + }; - itAsync( - "should have access to query variables when running @client resolvers", - (resolve, reject) => { - const query = gql` - query WithVariables($id: ID!) { - foo @client { - bar(id: $id) - } + const stream = setupTestWithResolvers({ + resolvers, + query, + serverQuery, + serverResult: { + data: { foo: { bar: true, __typename: `Foo` }, bar: { baz: true } }, + }, + }); + + await expect(stream).toEmitMatchedValue({ + data: { + foo: { bar: true, baz: false, __typename: "Foo" }, + bar: { baz: true }, + }, + }); + }); + + it("should have access to query variables when running @client resolvers", async () => { + const query = gql` + query WithVariables($id: ID!) { + foo @client { + bar(id: $id) } - `; + } + `; - const resolvers = { - Query: { - foo: () => ({ __typename: "Foo" }), - }, - Foo: { - bar: (_data: any, { id }: { id: number }) => id, - }, - }; - - assertWithObserver({ - reject, - resolvers, - query, - variables: { id: 1 }, - observer: { - next({ data }) { - try { - expect(data).toEqual({ foo: { bar: 1 } }); - } catch (error) { - reject(error); - } - resolve(); - }, - }, - }); - } - ); + const resolvers = { + Query: { + foo: () => ({ __typename: "Foo" }), + }, + Foo: { + bar: (_data: any, { id }: { id: number }) => id, + }, + }; + + const stream = setupTestWithResolvers({ + resolvers, + query, + variables: { id: 1 }, + }); + + await expect(stream).toEmitMatchedValue({ data: { foo: { bar: 1 } } }); + }); - itAsync("should pass context to @client resolvers", (resolve, reject) => { + it("should pass context to @client resolvers", async () => { const query = gql` query WithContext { foo @client { @@ -325,127 +256,99 @@ describe("Basic resolver capabilities", () => { }, }; - assertWithObserver({ - reject, + const stream = setupTestWithResolvers({ resolvers, query, queryOptions: { context: { id: 1 } }, - observer: { - next({ data }) { - try { - expect(data).toEqual({ foo: { bar: 1 } }); - } catch (error) { - reject(error); - } - resolve(); - }, - }, }); + + await expect(stream).toEmitMatchedValue({ data: { foo: { bar: 1 } } }); }); - itAsync( - "should combine local @client resolver results with server results, for " + - "the same field", - (resolve, reject) => { - const query = gql` - query author { - author { - name - stats { - totalPosts - postsToday @client - } + it("should combine local @client resolver results with server results, for the same field", async () => { + const query = gql` + query author { + author { + name + stats { + totalPosts + postsToday @client } } - `; + } + `; - const serverQuery = gql` - query author { - author { - name - stats { - totalPosts - } + const serverQuery = gql` + query author { + author { + name + stats { + totalPosts } } - `; + } + `; - const resolvers = { - Stats: { - postsToday: () => 10, - }, - }; - - assertWithObserver({ - reject, - resolvers, - query, - serverQuery, - serverResult: { - data: { - author: { - name: "John Smith", - stats: { - totalPosts: 100, - __typename: "Stats", - }, - __typename: "Author", + const resolvers = { + Stats: { + postsToday: () => 10, + }, + }; + + const stream = setupTestWithResolvers({ + resolvers, + query, + serverQuery, + serverResult: { + data: { + author: { + name: "John Smith", + stats: { + totalPosts: 100, + __typename: "Stats", }, + __typename: "Author", }, }, - observer: { - next({ data }) { - try { - expect(data).toEqual({ - author: { - name: "John Smith", - stats: { - totalPosts: 100, - postsToday: 10, - }, - }, - }); - } catch (error) { - reject(error); - } - resolve(); + }, + }); + + await expect(stream).toEmitMatchedValue({ + data: { + author: { + name: "John Smith", + stats: { + totalPosts: 100, + postsToday: 10, }, }, - }); - } - ); + }, + }); + }); - itAsync( - "should handle resolvers that work with booleans properly", - (resolve, reject) => { - const query = gql` - query CartDetails { - isInCart @client - } - `; + it("should handle resolvers that work with booleans properly", async () => { + const query = gql` + query CartDetails { + isInCart @client + } + `; - const cache = new InMemoryCache(); - cache.writeQuery({ query, data: { isInCart: true } }); + const cache = new InMemoryCache(); + cache.writeQuery({ query, data: { isInCart: true } }); - const client = new ApolloClient({ - cache, - resolvers: { - Query: { - isInCart: () => false, - }, + const client = new ApolloClient({ + cache, + resolvers: { + Query: { + isInCart: () => false, }, - }); + }, + }); - return client - .query({ query, fetchPolicy: "network-only" }) - .then(({ data }: any) => { - expect({ ...data }).toMatchObject({ - isInCart: false, - }); - resolve(); - }); - } - ); + const { data } = await client.query({ query, fetchPolicy: "network-only" }); + + expect(data).toMatchObject({ isInCart: false }); + }); it("should handle nested asynchronous @client resolvers (issue #4841)", () => { const query = gql` @@ -569,57 +472,47 @@ describe("Basic resolver capabilities", () => { ]); }); - itAsync( - "should not run resolvers without @client directive (issue #9571)", - (resolve, reject) => { - const query = gql` - query Mixed { - foo @client { - bar - } - bar { - baz - } + it("should not run resolvers without @client directive (issue #9571)", async () => { + const query = gql` + query Mixed { + foo @client { + bar + } + bar { + baz } - `; + } + `; - const serverQuery = gql` - query Mixed { - bar { - baz - } + const serverQuery = gql` + query Mixed { + bar { + baz } - `; + } + `; - const barResolver = jest.fn(() => ({ __typename: `Bar`, baz: false })); + const barResolver = jest.fn(() => ({ __typename: `Bar`, baz: false })); - const resolvers = { - Query: { - foo: () => ({ __typename: `Foo`, bar: true }), - bar: barResolver, - }, - }; - - assertWithObserver({ - reject, - resolvers, - query, - serverQuery, - serverResult: { data: { bar: { baz: true } } }, - observer: { - next({ data }) { - try { - expect(data).toEqual({ foo: { bar: true }, bar: { baz: true } }); - expect(barResolver).not.toHaveBeenCalled(); - } catch (error) { - reject(error); - } - resolve(); - }, - }, - }); - } - ); + const resolvers = { + Query: { + foo: () => ({ __typename: `Foo`, bar: true }), + bar: barResolver, + }, + }; + + const stream = setupTestWithResolvers({ + resolvers, + query, + serverQuery, + serverResult: { data: { bar: { baz: true } } }, + }); + + await expect(stream).toEmitMatchedValue({ + data: { foo: { bar: true }, bar: { baz: true } }, + }); + expect(barResolver).not.toHaveBeenCalled(); + }); }); describe("Writing cache data from resolvers", () => { @@ -777,440 +670,394 @@ describe("Writing cache data from resolvers", () => { }); describe("Resolving field aliases", () => { - itAsync( - "should run resolvers for missing client queries with aliased field", - (resolve, reject) => { - // expect.assertions(1); - const query = gql` - query Aliased { - foo @client { - bar - } - baz: bar { - foo - } + it("should run resolvers for missing client queries with aliased field", async () => { + const query = gql` + query Aliased { + foo @client { + bar } - `; + baz: bar { + foo + } + } + `; - const link = new ApolloLink(() => - // Each link is responsible for implementing their own aliasing so it - // returns baz not bar - Observable.of({ data: { baz: { foo: true, __typename: "Baz" } } }) - ); + const link = new ApolloLink(() => + // Each link is responsible for implementing their own aliasing so it + // returns baz not bar + Observable.of({ data: { baz: { foo: true, __typename: "Baz" } } }) + ); - const client = new ApolloClient({ - cache: new InMemoryCache(), - link, - resolvers: { - Query: { - foo: () => ({ bar: true, __typename: "Foo" }), - }, + const client = new ApolloClient({ + cache: new InMemoryCache(), + link, + resolvers: { + Query: { + foo: () => ({ bar: true, __typename: "Foo" }), }, - }); + }, + }); - client.query({ query }).then(({ data }) => { - try { - expect(data).toEqual({ - foo: { bar: true, __typename: "Foo" }, - baz: { foo: true, __typename: "Baz" }, - }); - } catch (e) { - reject(e); - return; - } - resolve(); - }, reject); - } - ); + const { data } = await client.query({ query }); - itAsync( - "should run resolvers for client queries when aliases are in use on " + - "the @client-tagged node", - (resolve, reject) => { - const aliasedQuery = gql` - query Test { - fie: foo @client { - bar - } - } - `; - - const client = new ApolloClient({ - cache: new InMemoryCache(), - link: ApolloLink.empty(), - resolvers: { - Query: { - foo: () => ({ bar: true, __typename: "Foo" }), - fie: () => { - reject( - "Called the resolver using the alias' name, instead of " + - "the correct resolver name." - ); - }, - }, - }, - }); + expect(data).toEqual({ + foo: { bar: true, __typename: "Foo" }, + baz: { foo: true, __typename: "Baz" }, + }); + }); - client.query({ query: aliasedQuery }).then(({ data }) => { - expect(data).toEqual({ fie: { bar: true, __typename: "Foo" } }); - resolve(); - }, reject); - } - ); + it("should run resolvers for client queries when aliases are in use on the @client-tagged node", async () => { + const aliasedQuery = gql` + query Test { + fie: foo @client { + bar + } + } + `; - itAsync( - "should respect aliases for *nested fields* on the @client-tagged node", - (resolve, reject) => { - const aliasedQuery = gql` - query Test { - fie: foo @client { - fum: bar - } - baz: bar { - foo - } + const fie = jest.fn(); + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: ApolloLink.empty(), + resolvers: { + Query: { + foo: () => ({ bar: true, __typename: "Foo" }), + fie, + }, + }, + }); + + const { data } = await client.query({ query: aliasedQuery }); + + expect(data).toEqual({ fie: { bar: true, __typename: "Foo" } }); + expect(fie).not.toHaveBeenCalled(); + }); + + it("should respect aliases for *nested fields* on the @client-tagged node", async () => { + const aliasedQuery = gql` + query Test { + fie: foo @client { + fum: bar } - `; + baz: bar { + foo + } + } + `; - const link = new ApolloLink(() => - Observable.of({ data: { baz: { foo: true, __typename: "Baz" } } }) - ); + const link = new ApolloLink(() => + Observable.of({ data: { baz: { foo: true, __typename: "Baz" } } }) + ); - const client = new ApolloClient({ - cache: new InMemoryCache(), - link, - resolvers: { - Query: { - foo: () => ({ bar: true, __typename: "Foo" }), - fie: () => { - reject( - "Called the resolver using the alias' name, instead of " + - "the correct resolver name." - ); - }, - }, + const fie = jest.fn(); + const client = new ApolloClient({ + cache: new InMemoryCache(), + link, + resolvers: { + Query: { + foo: () => ({ bar: true, __typename: "Foo" }), + fie, }, - }); + }, + }); - client.query({ query: aliasedQuery }).then(({ data }) => { - expect(data).toEqual({ - fie: { fum: true, __typename: "Foo" }, - baz: { foo: true, __typename: "Baz" }, - }); - resolve(); - }, reject); - } - ); + const { data } = await client.query({ query: aliasedQuery }); + + expect(data).toEqual({ + fie: { fum: true, __typename: "Foo" }, + baz: { foo: true, __typename: "Baz" }, + }); + expect(fie).not.toHaveBeenCalled(); + }); - it( - "should pull initialized values for aliased fields tagged with @client " + - "from the cache", - () => { - const query = gql` + it("should pull initialized values for aliased fields tagged with @client from the cache", async () => { + const query = gql` + { + fie: foo @client { + bar + } + } + `; + + const cache = new InMemoryCache(); + const client = new ApolloClient({ + cache, + link: ApolloLink.empty(), + resolvers: {}, + }); + + cache.writeQuery({ + query: gql` { - fie: foo @client { + foo { bar } } - `; + `, + data: { + foo: { + bar: "yo", + __typename: "Foo", + }, + }, + }); - const cache = new InMemoryCache(); - const client = new ApolloClient({ - cache, - link: ApolloLink.empty(), - resolvers: {}, - }); + const { data } = await client.query({ query }); - cache.writeQuery({ - query: gql` - { - foo { - bar - } - } - `, + expect({ ...data }).toMatchObject({ + fie: { bar: "yo", __typename: "Foo" }, + }); + }); + + it("should resolve @client fields using local resolvers and not have their value overridden when a fragment is loaded", async () => { + const query = gql` + fragment LaunchDetails on Launch { + id + __typename + } + query Launch { + launch { + isInCart @client + ...LaunchDetails + } + } + `; + + const link = new ApolloLink(() => + Observable.of({ data: { - foo: { - bar: "yo", - __typename: "Foo", + launch: { + id: 1, + __typename: "Launch", }, }, - }); + }) + ); - return client.query({ query }).then(({ data }) => { - expect({ ...data }).toMatchObject({ - fie: { bar: "yo", __typename: "Foo" }, - }); - }); - } - ); + const client = new ApolloClient({ + cache: new InMemoryCache(), + link, + resolvers: { + Launch: { + isInCart() { + return true; + }, + }, + }, + }); - it( - "should resolve @client fields using local resolvers and not have " + - "their value overridden when a fragment is loaded", - () => { - const query = gql` - fragment LaunchDetails on Launch { - id - __typename - } - query Launch { + client.writeQuery({ + query: gql` + { launch { - isInCart @client - ...LaunchDetails + isInCart } } - `; - - const link = new ApolloLink(() => - Observable.of({ - data: { - launch: { - id: 1, - __typename: "Launch", - }, - }, - }) - ); - - const client = new ApolloClient({ - cache: new InMemoryCache(), - link, - resolvers: { - Launch: { - isInCart() { - return true; - }, - }, + `, + data: { + launch: { + isInCart: false, + __typename: "Launch", }, - }); + }, + }); - client.writeQuery({ - query: gql` - { - launch { - isInCart - } - } - `, - data: { - launch: { - isInCart: false, - __typename: "Launch", - }, - }, - }); + { + const { data } = await client.query({ query }); + // `isInCart` resolver is fired, returning `true` (which is then + // stored in the cache). + expect(data.launch.isInCart).toBe(true); + } - return client - .query({ query }) - .then(({ data }) => { - // `isInCart` resolver is fired, returning `true` (which is then - // stored in the cache). - expect(data.launch.isInCart).toBe(true); - }) - .then(() => { - client.query({ query }).then(({ data }) => { - // When the same query fires again, `isInCart` should be pulled from - // the cache and have a value of `true`. - expect(data.launch.isInCart).toBe(true); - }); - }); + { + const { data } = await client.query({ query }); + // When the same query fires again, `isInCart` should be pulled from + // the cache and have a value of `true`. + expect(data.launch.isInCart).toBe(true); } - ); + }); }); describe("Force local resolvers", () => { - it( - "should force the running of local resolvers marked with " + - "`@client(always: true)` when using `ApolloClient.query`", - async () => { - const query = gql` - query Author { - author { - name - isLoggedIn @client(always: true) - } + it("should force the running of local resolvers marked with `@client(always: true)` when using `ApolloClient.query`", async () => { + const query = gql` + query Author { + author { + name + isLoggedIn @client(always: true) } - `; + } + `; - const cache = new InMemoryCache(); - const client = new ApolloClient({ - cache, - link: ApolloLink.empty(), - resolvers: {}, - }); + const cache = new InMemoryCache(); + const client = new ApolloClient({ + cache, + link: ApolloLink.empty(), + resolvers: {}, + }); + + cache.writeQuery({ + query, + data: { + author: { + name: "John Smith", + isLoggedIn: false, + __typename: "Author", + }, + }, + }); + + // When the resolver isn't defined, there isn't anything to force, so + // make sure the query resolves from the cache properly. + const { data: data1 } = await client.query({ query }); + expect(data1.author.isLoggedIn).toEqual(false); + + client.addResolvers({ + Author: { + isLoggedIn() { + return true; + }, + }, + }); + + // A resolver is defined, so make sure it's forced, and the result + // resolves properly as a combination of cache and local resolver + // data. + const { data: data2 } = await client.query({ query }); + expect(data2.author.isLoggedIn).toEqual(true); + }); + + it("should avoid running forced resolvers a second time when loading results over the network (so not from the cache)", async () => { + const query = gql` + query Author { + author { + name + isLoggedIn @client(always: true) + } + } + `; - cache.writeQuery({ - query, + const link = new ApolloLink(() => + Observable.of({ data: { author: { name: "John Smith", - isLoggedIn: false, __typename: "Author", }, }, - }); - - // When the resolver isn't defined, there isn't anything to force, so - // make sure the query resolves from the cache properly. - const { data: data1 } = await client.query({ query }); - expect(data1.author.isLoggedIn).toEqual(false); + }) + ); - client.addResolvers({ + let count = 0; + const client = new ApolloClient({ + cache: new InMemoryCache(), + link, + resolvers: { Author: { isLoggedIn() { + count += 1; return true; }, }, - }); + }, + }); - // A resolver is defined, so make sure it's forced, and the result - // resolves properly as a combination of cache and local resolver - // data. - const { data: data2 } = await client.query({ query }); - expect(data2.author.isLoggedIn).toEqual(true); - } - ); + const { data } = await client.query({ query }); + expect(data.author.isLoggedIn).toEqual(true); + expect(count).toEqual(1); + }); - it( - "should avoid running forced resolvers a second time when " + - "loading results over the network (so not from the cache)", - async () => { - const query = gql` - query Author { - author { - name - isLoggedIn @client(always: true) - } - } - `; - - const link = new ApolloLink(() => - Observable.of({ - data: { - author: { - name: "John Smith", - __typename: "Author", - }, - }, - }) - ); + it("should only force resolvers for fields marked with `@client(always: true)`, not all `@client` fields", async () => { + const query = gql` + query UserDetails { + name @client + isLoggedIn @client(always: true) + } + `; - let count = 0; - const client = new ApolloClient({ - cache: new InMemoryCache(), - link, - resolvers: { - Author: { - isLoggedIn() { - count += 1; - return true; - }, + let nameCount = 0; + let isLoggedInCount = 0; + const client = new ApolloClient({ + cache: new InMemoryCache(), + resolvers: { + Query: { + name() { + nameCount += 1; + return "John Smith"; + }, + isLoggedIn() { + isLoggedInCount += 1; + return true; }, }, - }); + }, + }); - const { data } = await client.query({ query }); - expect(data.author.isLoggedIn).toEqual(true); - expect(count).toEqual(1); - } - ); + await client.query({ query }); + expect(nameCount).toEqual(1); + expect(isLoggedInCount).toEqual(1); - it( - "should only force resolvers for fields marked with " + - "`@client(always: true)`, not all `@client` fields", - async () => { - const query = gql` - query UserDetails { - name @client - isLoggedIn @client(always: true) - } - `; - - let nameCount = 0; - let isLoggedInCount = 0; - const client = new ApolloClient({ - cache: new InMemoryCache(), - resolvers: { - Query: { - name() { - nameCount += 1; - return "John Smith"; - }, - isLoggedIn() { - isLoggedInCount += 1; - return true; - }, + // On the next request, `name` will be loaded from the cache only, + // whereas `isLoggedIn` will be loaded from the cache then overwritten + // by running its forced local resolver. + await client.query({ query }); + expect(nameCount).toEqual(1); + expect(isLoggedInCount).toEqual(2); + }); + + it("should force the running of local resolvers marked with `@client(always: true)` when using `ApolloClient.watchQuery`", async () => { + const query = gql` + query IsUserLoggedIn { + isUserLoggedIn @client(always: true) + } + `; + + const queryNoForce = gql` + query IsUserLoggedIn { + isUserLoggedIn @client + } + `; + + let callCount = 0; + const client = new ApolloClient({ + cache: new InMemoryCache(), + resolvers: { + Query: { + isUserLoggedIn() { + callCount += 1; + return true; }, }, - }); + }, + }); - await client.query({ query }); - expect(nameCount).toEqual(1); - expect(isLoggedInCount).toEqual(1); + { + const stream = new ObservableStream(client.watchQuery({ query })); - // On the next request, `name` will be loaded from the cache only, - // whereas `isLoggedIn` will be loaded from the cache then overwritten - // by running its forced local resolver. - await client.query({ query }); - expect(nameCount).toEqual(1); - expect(isLoggedInCount).toEqual(2); + await expect(stream).toEmitNext(); + expect(callCount).toBe(1); } - ); - itAsync( - "should force the running of local resolvers marked with " + - "`@client(always: true)` when using `ApolloClient.watchQuery`", - (resolve, reject) => { - const query = gql` - query IsUserLoggedIn { - isUserLoggedIn @client(always: true) - } - `; - - const queryNoForce = gql` - query IsUserLoggedIn { - isUserLoggedIn @client - } - `; - - let callCount = 0; - const client = new ApolloClient({ - cache: new InMemoryCache(), - resolvers: { - Query: { - isUserLoggedIn() { - callCount += 1; - return true; - }, - }, - }, - }); + { + const stream = new ObservableStream(client.watchQuery({ query })); - client.watchQuery({ query }).subscribe({ - next() { - expect(callCount).toBe(1); + await expect(stream).toEmitNext(); + expect(callCount).toBe(2); + } - client.watchQuery({ query }).subscribe({ - next() { - expect(callCount).toBe(2); + { + const stream = new ObservableStream( + client.watchQuery({ query: queryNoForce }) + ); - client.watchQuery({ query: queryNoForce }).subscribe({ - next() { - // Result is loaded from the cache since the resolver - // isn't being forced. - expect(callCount).toBe(2); - resolve(); - }, - }); - }, - }); - }, - }); + await expect(stream).toEmitNext(); + // Result is loaded from the cache since the resolver + // isn't being forced. + expect(callCount).toBe(2); } - ); + }); - it("should allow client-only virtual resolvers (#4731)", function () { + it("should allow client-only virtual resolvers (#4731)", async () => { const query = gql` query UserData { userData @client { @@ -1241,21 +1088,21 @@ describe("Force local resolvers", () => { }, }); - return client.query({ query }).then((result) => { - expect(result.data).toEqual({ - userData: { - __typename: "User", - firstName: "Ben", - lastName: "Newman", - fullName: "Ben Newman", - }, - }); + const result = await client.query({ query }); + + expect(result.data).toEqual({ + userData: { + __typename: "User", + firstName: "Ben", + lastName: "Newman", + fullName: "Ben Newman", + }, }); }); }); describe("Async resolvers", () => { - itAsync("should support async @client resolvers", async (resolve, reject) => { + it("should support async @client resolvers", async () => { const query = gql` query Member { isLoggedIn @client @@ -1276,64 +1123,61 @@ describe("Async resolvers", () => { const { data: { isLoggedIn }, } = await client.query({ query })!; + expect(isLoggedIn).toBe(true); - return resolve(); }); - itAsync( - "should support async @client resolvers mixed with remotely resolved data", - async (resolve, reject) => { - const query = gql` - query Member { - member { - name - sessionCount @client - isLoggedIn @client - } + it("should support async @client resolvers mixed with remotely resolved data", async () => { + const query = gql` + query Member { + member { + name + sessionCount @client + isLoggedIn @client } - `; - - const testMember = { - name: "John Smithsonian", - isLoggedIn: true, - sessionCount: 10, - }; - - const link = new ApolloLink(() => - Observable.of({ - data: { - member: { - name: testMember.name, - __typename: "Member", - }, + } + `; + + const testMember = { + name: "John Smithsonian", + isLoggedIn: true, + sessionCount: 10, + }; + + const link = new ApolloLink(() => + Observable.of({ + data: { + member: { + name: testMember.name, + __typename: "Member", }, - }) - ); + }, + }) + ); - const client = new ApolloClient({ - cache: new InMemoryCache(), - link, - resolvers: { - Member: { - isLoggedIn() { - return Promise.resolve(testMember.isLoggedIn); - }, - sessionCount() { - return testMember.sessionCount; - }, + const client = new ApolloClient({ + cache: new InMemoryCache(), + link, + resolvers: { + Member: { + isLoggedIn() { + return Promise.resolve(testMember.isLoggedIn); + }, + sessionCount() { + return testMember.sessionCount; }, }, - }); + }, + }); - const { - data: { member }, - } = await client.query({ query })!; - expect(member.name).toBe(testMember.name); - expect(member.isLoggedIn).toBe(testMember.isLoggedIn); - expect(member.sessionCount).toBe(testMember.sessionCount); - return resolve(); - } - ); + const { + data: { member }, + } = await client.query({ query })!; + + expect(member.name).toBe(testMember.name); + expect(member.isLoggedIn).toBe(testMember.isLoggedIn); + expect(member.sessionCount).toBe(testMember.sessionCount); + }); }); describe("LocalState helpers", () => { diff --git a/src/__tests__/local-state/subscriptions.ts b/src/__tests__/local-state/subscriptions.ts index d331cd4fb42..9a3c94edd49 100644 --- a/src/__tests__/local-state/subscriptions.ts +++ b/src/__tests__/local-state/subscriptions.ts @@ -4,10 +4,10 @@ import { Observable } from "../../utilities"; import { ApolloLink } from "../../link/core"; import { ApolloClient } from "../../core"; import { InMemoryCache } from "../../cache"; -import { itAsync } from "../../testing"; +import { ObservableStream } from "../../testing/internal"; describe("Basic functionality", () => { - itAsync("should not break subscriptions", (resolve, reject) => { + it("should not break subscriptions", async () => { const query = gql` subscription { field @@ -28,65 +28,43 @@ describe("Basic functionality", () => { }, }); - let counter = 0; - expect.assertions(2); - client.subscribe({ query }).forEach((item) => { - expect(item).toMatchObject({ data: { field: ++counter } }); - if (counter === 2) { - resolve(); - } - }); + const stream = new ObservableStream(client.subscribe({ query })); + + await expect(stream).toEmitValue({ data: { field: 1 } }); + await expect(stream).toEmitValue({ data: { field: 2 } }); + await expect(stream).toComplete(); }); - itAsync( - "should be able to mix @client fields with subscription results", - (resolve, reject) => { - const query = gql` - subscription { - field - count @client - } - `; + it("should be able to mix @client fields with subscription results", async () => { + const query = gql` + subscription { + field + count @client + } + `; - const link = new ApolloLink(() => - Observable.of({ data: { field: 1 } }, { data: { field: 2 } }) - ); + const link = new ApolloLink(() => + Observable.of({ data: { field: 1 } }, { data: { field: 2 } }) + ); - let subCounter = 0; - const client = new ApolloClient({ - cache: new InMemoryCache(), - link, - resolvers: { - Subscription: { - count: () => { - subCounter += 1; - return subCounter; - }, + let subCounter = 0; + const client = new ApolloClient({ + cache: new InMemoryCache(), + link, + resolvers: { + Subscription: { + count: () => { + subCounter += 1; + return subCounter; }, }, - }); + }, + }); - expect.assertions(2); - const obs = client.subscribe({ query }); - let resultCounter = 1; - obs.subscribe({ - next(result) { - try { - expect(result).toMatchObject({ - data: { - field: resultCounter, - count: resultCounter, - }, - }); - } catch (error) { - reject(error); - } - resultCounter += 1; - }, - complete() { - resolve(); - }, - }); - } - ); + const stream = new ObservableStream(client.subscribe({ query })); + + await expect(stream).toEmitValue({ data: { field: 1, count: 1 } }); + await expect(stream).toEmitValue({ data: { field: 2, count: 2 } }); + await expect(stream).toComplete(); + }); }); diff --git a/src/__tests__/optimistic.ts b/src/__tests__/optimistic.ts index ed53dc8cf9c..d8d41511a6c 100644 --- a/src/__tests__/optimistic.ts +++ b/src/__tests__/optimistic.ts @@ -10,19 +10,17 @@ import { ApolloCache, MutationQueryReducersMap, TypedDocumentNode, + ApolloError, } from "../core"; import { QueryManager } from "../core/QueryManager"; import { Cache, InMemoryCache } from "../cache"; -import { - Observable, - ObservableSubscription as Subscription, - addTypenameToDocument, -} from "../utilities"; +import { Observable, addTypenameToDocument } from "../utilities"; -import { itAsync, mockSingleLink } from "../testing"; +import { MockedResponse, mockSingleLink } from "../testing"; +import { ObservableStream } from "../testing/internal"; describe("optimistic mutation results", () => { const query = gql` @@ -108,10 +106,7 @@ describe("optimistic mutation results", () => { }, }; - async function setup( - reject: (reason: any) => any, - ...mockedResponses: any[] - ) { + async function setup(...mockedResponses: MockedResponse[]) { const link = mockSingleLink( { request: { query }, @@ -213,223 +208,196 @@ describe("optimistic mutation results", () => { }, }; - itAsync( - "handles a single error for a single mutation", - async (resolve, reject) => { - expect.assertions(6); - const client = await setup(reject, { - request: { query: mutation }, - error: new Error("forbidden (test error)"), - }); - try { - const promise = client.mutate({ - mutation, - optimisticResponse, - updateQueries, - }); + it("handles a single error for a single mutation", async () => { + expect.assertions(5); + const client = await setup({ + request: { query: mutation }, + error: new Error("forbidden (test error)"), + }); + const promise = client.mutate({ + mutation, + optimisticResponse, + updateQueries, + }); - const dataInStore = (client.cache as InMemoryCache).extract(true); - expect((dataInStore["TodoList5"] as any).todos.length).toBe(4); - expect((dataInStore["Todo99"] as any).text).toBe( - "Optimistically generated" - ); - await promise; - } catch (err) { - expect(err).toBeInstanceOf(Error); - expect((err as Error).message).toBe("forbidden (test error)"); + { + const dataInStore = (client.cache as InMemoryCache).extract(true); + expect((dataInStore["TodoList5"] as any).todos.length).toBe(4); + expect((dataInStore["Todo99"] as any).text).toBe( + "Optimistically generated" + ); + } - const dataInStore = (client.cache as InMemoryCache).extract(true); - expect((dataInStore["TodoList5"] as any).todos.length).toBe(3); - expect(dataInStore).not.toHaveProperty("Todo99"); - } + await expect(promise).rejects.toThrow( + new ApolloError({ networkError: new Error("forbidden (test error)") }) + ); - resolve(); + { + const dataInStore = (client.cache as InMemoryCache).extract(true); + expect((dataInStore["TodoList5"] as any).todos.length).toBe(3); + expect(dataInStore).not.toHaveProperty("Todo99"); } - ); + }); - itAsync( - "handles errors produced by one mutation in a series", - async (resolve, reject) => { - expect.assertions(10); - let subscriptionHandle: Subscription; - const client = await setup( - reject, - { - request: { query: mutation }, - error: new Error("forbidden (test error)"), - }, - { - request: { query: mutation }, - result: mutationResult2, - } - ); + it("handles errors produced by one mutation in a series", async () => { + expect.assertions(12); + const client = await setup( + { + request: { query: mutation }, + error: new Error("forbidden (test error)"), + }, + { + request: { query: mutation }, + result: mutationResult2, + } + ); - // we have to actually subscribe to the query to be able to update it - await new Promise((resolve) => { - const handle = client.watchQuery({ query }); - subscriptionHandle = handle.subscribe({ - next(res: any) { - resolve(res); - }, - }); - }); + const stream = new ObservableStream(client.watchQuery({ query })); - const promise = client - .mutate({ - mutation, - optimisticResponse, - updateQueries, - }) - .catch((err: any) => { - // it is ok to fail here - expect(err).toBeInstanceOf(Error); - expect(err.message).toBe("forbidden (test error)"); - return null; - }); + await expect(stream).toEmitNext(); - const promise2 = client.mutate({ + const promise = client + .mutate({ mutation, - optimisticResponse: optimisticResponse2, + optimisticResponse, updateQueries, + }) + .catch((err: any) => { + // it is ok to fail here + expect(err).toBeInstanceOf(Error); + expect(err.message).toBe("forbidden (test error)"); + return null; }); + const promise2 = client.mutate({ + mutation, + optimisticResponse: optimisticResponse2, + updateQueries, + }); + + const dataInStore = (client.cache as InMemoryCache).extract(true); + expect((dataInStore["TodoList5"] as any).todos.length).toBe(5); + expect((dataInStore["Todo99"] as any).text).toBe( + "Optimistically generated" + ); + expect((dataInStore["Todo66"] as any).text).toBe( + "Optimistically generated 2" + ); + + await Promise.all([promise, promise2]); + + stream.unsubscribe(); + + { const dataInStore = (client.cache as InMemoryCache).extract(true); - expect((dataInStore["TodoList5"] as any).todos.length).toBe(5); - expect((dataInStore["Todo99"] as any).text).toBe( - "Optimistically generated" + expect((dataInStore["TodoList5"] as any).todos.length).toBe(4); + expect(dataInStore).not.toHaveProperty("Todo99"); + expect(dataInStore).toHaveProperty("Todo66"); + expect((dataInStore["TodoList5"] as any).todos).toContainEqual( + makeReference("Todo66") ); - expect((dataInStore["Todo66"] as any).text).toBe( - "Optimistically generated 2" + expect((dataInStore["TodoList5"] as any).todos).not.toContainEqual( + makeReference("Todo99") ); + } + }); - await Promise.all([promise, promise2]); - - subscriptionHandle!.unsubscribe(); + it("can run 2 mutations concurrently and handles all intermediate states well", async () => { + expect.assertions(36); + function checkBothMutationsAreApplied( + expectedText1: any, + expectedText2: any + ) { + const dataInStore = (client.cache as InMemoryCache).extract(true); + expect((dataInStore["TodoList5"] as any).todos.length).toBe(5); + expect(dataInStore).toHaveProperty("Todo99"); + expect(dataInStore).toHaveProperty("Todo66"); + // can be removed once @types/chai adds deepInclude + expect((dataInStore["TodoList5"] as any).todos).toContainEqual( + makeReference("Todo66") + ); + expect((dataInStore["TodoList5"] as any).todos).toContainEqual( + makeReference("Todo99") + ); + expect((dataInStore["Todo99"] as any).text).toBe(expectedText1); + expect((dataInStore["Todo66"] as any).text).toBe(expectedText2); + } + const client = await setup( { - const dataInStore = (client.cache as InMemoryCache).extract(true); - expect((dataInStore["TodoList5"] as any).todos.length).toBe(4); - expect(dataInStore).not.toHaveProperty("Todo99"); - expect(dataInStore).toHaveProperty("Todo66"); - expect((dataInStore["TodoList5"] as any).todos).toContainEqual( - makeReference("Todo66") - ); - expect((dataInStore["TodoList5"] as any).todos).not.toContainEqual( - makeReference("Todo99") - ); - resolve(); + request: { query: mutation }, + result: mutationResult, + }, + { + request: { query: mutation }, + result: mutationResult2, + // make sure it always happens later + delay: 100, } - } - ); + ); + const stream = new ObservableStream(client.watchQuery({ query })); - itAsync( - "can run 2 mutations concurrently and handles all intermediate states well", - async (resolve, reject) => { - expect.assertions(34); - function checkBothMutationsAreApplied( - expectedText1: any, - expectedText2: any - ) { - const dataInStore = (client.cache as InMemoryCache).extract(true); - expect((dataInStore["TodoList5"] as any).todos.length).toBe(5); - expect(dataInStore).toHaveProperty("Todo99"); - expect(dataInStore).toHaveProperty("Todo66"); - // can be removed once @types/chai adds deepInclude - expect((dataInStore["TodoList5"] as any).todos).toContainEqual( - makeReference("Todo66") - ); - expect((dataInStore["TodoList5"] as any).todos).toContainEqual( - makeReference("Todo99") + await expect(stream).toEmitNext(); + + const queryManager: QueryManager = (client as any).queryManager; + + const promise = client + .mutate({ + mutation, + optimisticResponse, + updateQueries, + }) + .then((res: any) => { + checkBothMutationsAreApplied( + "This one was created with a mutation.", + "Optimistically generated 2" ); - expect((dataInStore["Todo99"] as any).text).toBe(expectedText1); - expect((dataInStore["Todo66"] as any).text).toBe(expectedText2); - } - let subscriptionHandle: Subscription; - const client = await setup( - reject, - { - request: { query: mutation }, - result: mutationResult, - }, - { - request: { query: mutation }, - result: mutationResult2, - // make sure it always happens later - delay: 100, - } - ); - // we have to actually subscribe to the query to be able to update it - await new Promise((resolve) => { - const handle = client.watchQuery({ query }); - subscriptionHandle = handle.subscribe({ - next(res: any) { - resolve(res); - }, - }); - }); - const queryManager: QueryManager = (client as any).queryManager; + // @ts-ignore + const latestState = queryManager.mutationStore!; + expect(latestState[1].loading).toBe(false); + expect(latestState[2].loading).toBe(true); - const promise = client - .mutate({ - mutation, - optimisticResponse, - updateQueries, - }) - .then((res: any) => { - checkBothMutationsAreApplied( - "This one was created with a mutation.", - "Optimistically generated 2" - ); - - // @ts-ignore - const latestState = queryManager.mutationStore!; - expect(latestState[1].loading).toBe(false); - expect(latestState[2].loading).toBe(true); - - return res; - }); + return res; + }); - const promise2 = client - .mutate({ - mutation, - optimisticResponse: optimisticResponse2, - updateQueries, - }) - .then((res: any) => { - checkBothMutationsAreApplied( - "This one was created with a mutation.", - "Second mutation." - ); - - // @ts-ignore - const latestState = queryManager.mutationStore!; - expect(latestState[1].loading).toBe(false); - expect(latestState[2].loading).toBe(false); - - return res; - }); + const promise2 = client + .mutate({ + mutation, + optimisticResponse: optimisticResponse2, + updateQueries, + }) + .then((res: any) => { + checkBothMutationsAreApplied( + "This one was created with a mutation.", + "Second mutation." + ); - // @ts-ignore - const mutationsState = queryManager.mutationStore!; - expect(mutationsState[1].loading).toBe(true); - expect(mutationsState[2].loading).toBe(true); + // @ts-ignore + const latestState = queryManager.mutationStore!; + expect(latestState[1].loading).toBe(false); + expect(latestState[2].loading).toBe(false); - checkBothMutationsAreApplied( - "Optimistically generated", - "Optimistically generated 2" - ); + return res; + }); - await Promise.all([promise, promise2]); + // @ts-ignore + const mutationsState = queryManager.mutationStore!; + expect(mutationsState[1].loading).toBe(true); + expect(mutationsState[2].loading).toBe(true); - subscriptionHandle!.unsubscribe(); - checkBothMutationsAreApplied( - "This one was created with a mutation.", - "Second mutation." - ); + checkBothMutationsAreApplied( + "Optimistically generated", + "Optimistically generated 2" + ); - resolve(); - } - ); + await Promise.all([promise, promise2]); + + stream.unsubscribe(); + checkBothMutationsAreApplied( + "This one was created with a mutation.", + "Second mutation." + ); + }); }); describe("with `update`", () => { @@ -464,225 +432,195 @@ describe("optimistic mutation results", () => { }); }; - itAsync( - "handles a single error for a single mutation", - async (resolve, reject) => { - expect.assertions(6); - - const client = await setup(reject, { - request: { query: mutation }, - error: new Error("forbidden (test error)"), - }); + it("handles a single error for a single mutation", async () => { + expect.assertions(5); - try { - const promise = client.mutate({ - mutation, - optimisticResponse, - update, - }); + const client = await setup({ + request: { query: mutation }, + error: new Error("forbidden (test error)"), + }); - const dataInStore = (client.cache as InMemoryCache).extract(true); - expect((dataInStore["TodoList5"] as any).todos.length).toBe(4); - expect((dataInStore["Todo99"] as any).text).toBe( - "Optimistically generated" - ); + const promise = client.mutate({ + mutation, + optimisticResponse, + update, + }); - await promise; - } catch (err) { - expect(err).toBeInstanceOf(Error); - expect((err as Error).message).toBe("forbidden (test error)"); + { + const dataInStore = (client.cache as InMemoryCache).extract(true); + expect((dataInStore["TodoList5"] as any).todos.length).toBe(4); + expect((dataInStore["Todo99"] as any).text).toBe( + "Optimistically generated" + ); + } - const dataInStore = (client.cache as InMemoryCache).extract(true); - expect((dataInStore["TodoList5"] as any).todos.length).toBe(3); - expect(dataInStore).not.toHaveProperty("Todo99"); - } + await expect(promise).rejects.toThrow( + new ApolloError({ networkError: new Error("forbidden (test error)") }) + ); - resolve(); + { + const dataInStore = (client.cache as InMemoryCache).extract(true); + expect((dataInStore["TodoList5"] as any).todos.length).toBe(3); + expect(dataInStore).not.toHaveProperty("Todo99"); } - ); + }); - itAsync( - "handles errors produced by one mutation in a series", - async (resolve, reject) => { - expect.assertions(10); - let subscriptionHandle: Subscription; - const client = await setup( - reject, - { - request: { query: mutation }, - error: new Error("forbidden (test error)"), - }, - { - request: { query: mutation }, - result: mutationResult2, - } - ); + it("handles errors produced by one mutation in a series", async () => { + expect.assertions(12); + const client = await setup( + { + request: { query: mutation }, + error: new Error("forbidden (test error)"), + }, + { + request: { query: mutation }, + result: mutationResult2, + } + ); - // we have to actually subscribe to the query to be able to update it - await new Promise((resolve) => { - const handle = client.watchQuery({ query }); - subscriptionHandle = handle.subscribe({ - next(res: any) { - resolve(res); - }, - }); - }); + const stream = new ObservableStream(client.watchQuery({ query })); - const promise = client - .mutate({ - mutation, - optimisticResponse, - update, - }) - .catch((err: any) => { - // it is ok to fail here - expect(err).toBeInstanceOf(Error); - expect(err.message).toBe("forbidden (test error)"); - return null; - }); + await expect(stream).toEmitNext(); - const promise2 = client.mutate({ + const promise = client + .mutate({ mutation, - optimisticResponse: optimisticResponse2, + optimisticResponse, update, + }) + .catch((err: any) => { + // it is ok to fail here + expect(err).toBeInstanceOf(Error); + expect(err.message).toBe("forbidden (test error)"); + return null; }); + const promise2 = client.mutate({ + mutation, + optimisticResponse: optimisticResponse2, + update, + }); + + const dataInStore = (client.cache as InMemoryCache).extract(true); + expect((dataInStore["TodoList5"] as any).todos.length).toBe(5); + expect((dataInStore["Todo99"] as any).text).toBe( + "Optimistically generated" + ); + expect((dataInStore["Todo66"] as any).text).toBe( + "Optimistically generated 2" + ); + + await Promise.all([promise, promise2]); + + stream.unsubscribe(); + { const dataInStore = (client.cache as InMemoryCache).extract(true); - expect((dataInStore["TodoList5"] as any).todos.length).toBe(5); - expect((dataInStore["Todo99"] as any).text).toBe( - "Optimistically generated" + expect((dataInStore["TodoList5"] as any).todos.length).toBe(4); + expect(dataInStore).not.toHaveProperty("Todo99"); + expect(dataInStore).toHaveProperty("Todo66"); + expect((dataInStore["TodoList5"] as any).todos).toContainEqual( + makeReference("Todo66") ); - expect((dataInStore["Todo66"] as any).text).toBe( - "Optimistically generated 2" + expect((dataInStore["TodoList5"] as any).todos).not.toContainEqual( + makeReference("Todo99") ); + } + }); - await Promise.all([promise, promise2]); + it("can run 2 mutations concurrently and handles all intermediate states well", async () => { + expect.assertions(36); + function checkBothMutationsAreApplied( + expectedText1: any, + expectedText2: any + ) { + const dataInStore = (client.cache as InMemoryCache).extract(true); + expect((dataInStore["TodoList5"] as any).todos.length).toBe(5); + expect(dataInStore).toHaveProperty("Todo99"); + expect(dataInStore).toHaveProperty("Todo66"); + expect((dataInStore["TodoList5"] as any).todos).toContainEqual( + makeReference("Todo66") + ); + expect((dataInStore["TodoList5"] as any).todos).toContainEqual( + makeReference("Todo99") + ); + expect((dataInStore["Todo99"] as any).text).toBe(expectedText1); + expect((dataInStore["Todo66"] as any).text).toBe(expectedText2); + } - subscriptionHandle!.unsubscribe(); + const client = await setup( { - const dataInStore = (client.cache as InMemoryCache).extract(true); - expect((dataInStore["TodoList5"] as any).todos.length).toBe(4); - expect(dataInStore).not.toHaveProperty("Todo99"); - expect(dataInStore).toHaveProperty("Todo66"); - expect((dataInStore["TodoList5"] as any).todos).toContainEqual( - makeReference("Todo66") - ); - expect((dataInStore["TodoList5"] as any).todos).not.toContainEqual( - makeReference("Todo99") - ); - resolve(); + request: { query: mutation }, + result: mutationResult, + }, + { + request: { query: mutation }, + result: mutationResult2, + // make sure it always happens later + delay: 100, } - } - ); + ); + const stream = new ObservableStream(client.watchQuery({ query })); - itAsync( - "can run 2 mutations concurrently and handles all intermediate states well", - async (resolve, reject) => { - expect.assertions(34); - function checkBothMutationsAreApplied( - expectedText1: any, - expectedText2: any - ) { - const dataInStore = (client.cache as InMemoryCache).extract(true); - expect((dataInStore["TodoList5"] as any).todos.length).toBe(5); - expect(dataInStore).toHaveProperty("Todo99"); - expect(dataInStore).toHaveProperty("Todo66"); - expect((dataInStore["TodoList5"] as any).todos).toContainEqual( - makeReference("Todo66") - ); - expect((dataInStore["TodoList5"] as any).todos).toContainEqual( - makeReference("Todo99") + await expect(stream).toEmitNext(); + + const promise = client + .mutate({ + mutation, + optimisticResponse, + update, + }) + .then((res: any) => { + checkBothMutationsAreApplied( + "This one was created with a mutation.", + "Optimistically generated 2" ); - expect((dataInStore["Todo99"] as any).text).toBe(expectedText1); - expect((dataInStore["Todo66"] as any).text).toBe(expectedText2); - } - let subscriptionHandle: Subscription; - const client = await setup( - reject, - { - request: { query: mutation }, - result: mutationResult, - }, - { - request: { query: mutation }, - result: mutationResult2, - // make sure it always happens later - delay: 100, - } - ); + // @ts-ignore + const latestState = client.queryManager.mutationStore!; + expect(latestState[1].loading).toBe(false); + expect(latestState[2].loading).toBe(true); - // we have to actually subscribe to the query to be able to update it - await new Promise((resolve) => { - const handle = client.watchQuery({ query }); - subscriptionHandle = handle.subscribe({ - next(res: any) { - resolve(res); - }, - }); + return res; }); - const promise = client - .mutate({ - mutation, - optimisticResponse, - update, - }) - .then((res: any) => { - checkBothMutationsAreApplied( - "This one was created with a mutation.", - "Optimistically generated 2" - ); - - // @ts-ignore - const latestState = client.queryManager.mutationStore!; - expect(latestState[1].loading).toBe(false); - expect(latestState[2].loading).toBe(true); - - return res; - }); + const promise2 = client + .mutate({ + mutation, + optimisticResponse: optimisticResponse2, + update, + }) + .then((res: any) => { + checkBothMutationsAreApplied( + "This one was created with a mutation.", + "Second mutation." + ); - const promise2 = client - .mutate({ - mutation, - optimisticResponse: optimisticResponse2, - update, - }) - .then((res: any) => { - checkBothMutationsAreApplied( - "This one was created with a mutation.", - "Second mutation." - ); - - // @ts-ignore - const latestState = client.queryManager.mutationStore!; - expect(latestState[1].loading).toBe(false); - expect(latestState[2].loading).toBe(false); - - return res; - }); + // @ts-ignore + const latestState = client.queryManager.mutationStore!; + expect(latestState[1].loading).toBe(false); + expect(latestState[2].loading).toBe(false); - // @ts-ignore - const mutationsState = client.queryManager.mutationStore!; - expect(mutationsState[1].loading).toBe(true); - expect(mutationsState[2].loading).toBe(true); + return res; + }); - checkBothMutationsAreApplied( - "Optimistically generated", - "Optimistically generated 2" - ); + // @ts-ignore + const mutationsState = client.queryManager.mutationStore!; + expect(mutationsState[1].loading).toBe(true); + expect(mutationsState[2].loading).toBe(true); - await Promise.all([promise, promise2]); + checkBothMutationsAreApplied( + "Optimistically generated", + "Optimistically generated 2" + ); - subscriptionHandle!.unsubscribe(); - checkBothMutationsAreApplied( - "This one was created with a mutation.", - "Second mutation." - ); + await Promise.all([promise, promise2]); - resolve(); - } - ); + stream.unsubscribe(); + checkBothMutationsAreApplied( + "This one was created with a mutation.", + "Second mutation." + ); + }); }); }); @@ -752,39 +690,34 @@ describe("optimistic mutation results", () => { } `; - itAsync( - "client.readQuery should read the optimistic response of a mutation " + - "only when update function is called optimistically", - (resolve, reject) => { - return setup(reject, { - request: { query: todoListMutation }, - result: todoListMutationResult, - }) - .then((client) => { - let updateCount = 0; - return client.mutate({ - mutation: todoListMutation, - optimisticResponse: todoListOptimisticResponse, - update: (proxy: any, mResult: any) => { - ++updateCount; - const data = proxy.readQuery({ query: todoListQuery }); - const readText = data.todoList.todos[0].text; - if (updateCount === 1) { - const optimisticText = - todoListOptimisticResponse.createTodo.todos[0].text; - expect(readText).toEqual(optimisticText); - } else if (updateCount === 2) { - const incomingText = mResult.data.createTodo.todos[0].text; - expect(readText).toEqual(incomingText); - } else { - reject("too many update calls"); - } - }, - }); - }) - .then(resolve, reject); - } - ); + it("client.readQuery should read the optimistic response of a mutation only when update function is called optimistically", async () => { + expect.assertions(2); + const client = await setup({ + request: { query: todoListMutation }, + result: todoListMutationResult, + }); + + let updateCount = 0; + await client.mutate({ + mutation: todoListMutation, + optimisticResponse: todoListOptimisticResponse, + update: (proxy: any, mResult: any) => { + ++updateCount; + const data = proxy.readQuery({ query: todoListQuery }); + const readText = data.todoList.todos[0].text; + if (updateCount === 1) { + const optimisticText = + todoListOptimisticResponse.createTodo.todos[0].text; + expect(readText).toEqual(optimisticText); + } else if (updateCount === 2) { + const incomingText = mResult.data.createTodo.todos[0].text; + expect(readText).toEqual(incomingText); + } else { + throw new Error("too many update calls"); + } + }, + }); + }); const todoListFragment = gql` fragment todoList on TodoList { @@ -797,79 +730,67 @@ describe("optimistic mutation results", () => { } `; - itAsync( - "should read the optimistic response of a mutation when making an " + - "ApolloClient.readFragment() call, if the `optimistic` param is set " + - "to true", - (resolve, reject) => { - return setup(reject, { - request: { query: todoListMutation }, - result: todoListMutationResult, - }) - .then((client) => { - let updateCount = 0; - return client.mutate({ - mutation: todoListMutation, - optimisticResponse: todoListOptimisticResponse, - update: (proxy: any, mResult: any) => { - ++updateCount; - const data: any = proxy.readFragment( - { - id: "TodoList5", - fragment: todoListFragment, - }, - true - ); - if (updateCount === 1) { - expect(data.todos[0].text).toEqual( - todoListOptimisticResponse.createTodo.todos[0].text - ); - } else if (updateCount === 2) { - expect(data.todos[0].text).toEqual( - mResult.data.createTodo.todos[0].text - ); - expect(data.todos[0].text).toEqual( - todoListMutationResult.data.createTodo.todos[0].text - ); - } else { - reject("too many update calls"); - } - }, - }); - }) - .then(resolve, reject); - } - ); + it("should read the optimistic response of a mutation when making an ApolloClient.readFragment() call, if the `optimistic` param is set to true", async () => { + expect.assertions(3); + const client = await setup({ + request: { query: todoListMutation }, + result: todoListMutationResult, + }); - itAsync( - "should not read the optimistic response of a mutation when making " + - "an ApolloClient.readFragment() call, if the `optimistic` param is " + - "set to false", - (resolve, reject) => { - return setup(reject, { - request: { query: todoListMutation }, - result: todoListMutationResult, - }) - .then((client) => { - return client.mutate({ - mutation: todoListMutation, - optimisticResponse: todoListOptimisticResponse, - update: (proxy: any, mResult: any) => { - const incomingText = mResult.data.createTodo.todos[0].text; - const data: any = proxy.readFragment( - { - id: "TodoList5", - fragment: todoListFragment, - }, - false - ); - expect(data.todos[0].text).toEqual(incomingText); - }, - }); - }) - .then(resolve, reject); - } - ); + let updateCount = 0; + await client.mutate({ + mutation: todoListMutation, + optimisticResponse: todoListOptimisticResponse, + update: (proxy: any, mResult: any) => { + ++updateCount; + const data: any = proxy.readFragment( + { + id: "TodoList5", + fragment: todoListFragment, + }, + true + ); + if (updateCount === 1) { + expect(data.todos[0].text).toEqual( + todoListOptimisticResponse.createTodo.todos[0].text + ); + } else if (updateCount === 2) { + expect(data.todos[0].text).toEqual( + mResult.data.createTodo.todos[0].text + ); + expect(data.todos[0].text).toEqual( + todoListMutationResult.data.createTodo.todos[0].text + ); + } else { + throw new Error("too many update calls"); + } + }, + }); + }); + + it("should not read the optimistic response of a mutation when making an ApolloClient.readFragment() call, if the `optimistic` param is set to false", async () => { + expect.assertions(2); + const client = await setup({ + request: { query: todoListMutation }, + result: todoListMutationResult, + }); + + await client.mutate({ + mutation: todoListMutation, + optimisticResponse: todoListOptimisticResponse, + update: (proxy: any, mResult: any) => { + const incomingText = mResult.data.createTodo.todos[0].text; + const data: any = proxy.readFragment( + { + id: "TodoList5", + fragment: todoListFragment, + }, + false + ); + expect(data.todos[0].text).toEqual(incomingText); + }, + }); + }); }); describe("passing a function to optimisticResponse", () => { @@ -909,187 +830,157 @@ describe("optimistic mutation results", () => { }, }); - itAsync( - "will use a passed variable in optimisticResponse", - async (resolve, reject) => { - expect.assertions(6); - let subscriptionHandle: Subscription; - const client = await setup(reject, { - request: { query: mutation, variables }, - result: mutationResult, - }); - - // we have to actually subscribe to the query to be able to update it - await new Promise((resolve) => { - const handle = client.watchQuery({ query }); - subscriptionHandle = handle.subscribe({ - next(res: any) { - resolve(res); - }, - }); - }); - - const promise = client.mutate({ - mutation, - variables, - optimisticResponse, - update: (proxy: any, mResult: any) => { - expect(mResult.data.createTodo.id).toBe("99"); - - const id = "TodoList5"; - const fragment = gql` - fragment todoList on TodoList { - todos { - id - text - completed - __typename - } - } - `; - - const data: any = proxy.readFragment({ id, fragment }); + it("will use a passed variable in optimisticResponse", async () => { + expect.assertions(8); + const client = await setup({ + request: { query: mutation, variables }, + result: mutationResult, + }); + const stream = new ObservableStream(client.watchQuery({ query })); - proxy.writeFragment({ - data: { - ...data, - todos: [mResult.data.createTodo, ...data.todos], - }, - id, - fragment, - }); - }, - }); + await expect(stream).toEmitNext(); - const dataInStore = (client.cache as InMemoryCache).extract(true); - expect((dataInStore["TodoList5"] as any).todos.length).toEqual(4); - expect((dataInStore["Todo99"] as any).text).toEqual( - "Optimistically generated from variables" - ); + const promise = client.mutate({ + mutation, + variables, + optimisticResponse, + update: (proxy: any, mResult: any) => { + expect(mResult.data.createTodo.id).toBe("99"); - await promise; + const id = "TodoList5"; + const fragment = gql` + fragment todoList on TodoList { + todos { + id + text + completed + __typename + } + } + `; - const newResult: any = await client.query({ query }); + const data: any = proxy.readFragment({ id, fragment }); - subscriptionHandle!.unsubscribe(); - // There should be one more todo item than before - expect(newResult.data.todoList.todos.length).toEqual(4); + proxy.writeFragment({ + data: { + ...data, + todos: [mResult.data.createTodo, ...data.todos], + }, + id, + fragment, + }); + }, + }); - // Since we used `prepend` it should be at the front - expect(newResult.data.todoList.todos[0].text).toEqual( - "This one was created with a mutation." - ); + const dataInStore = (client.cache as InMemoryCache).extract(true); + expect((dataInStore["TodoList5"] as any).todos.length).toEqual(4); + expect((dataInStore["Todo99"] as any).text).toEqual( + "Optimistically generated from variables" + ); - resolve(); - } - ); + await promise; - itAsync( - "will not update optimistically if optimisticResponse returns IGNORE sentinel object", - async (resolve, reject) => { - expect.assertions(5); + const newResult: any = await client.query({ query }); - let subscriptionHandle: Subscription; + stream.unsubscribe(); + // There should be one more todo item than before + expect(newResult.data.todoList.todos.length).toEqual(4); - const client = await setup(reject, { - request: { query: mutation, variables }, - result: mutationResult, - }); + // Since we used `prepend` it should be at the front + expect(newResult.data.todoList.todos[0].text).toEqual( + "This one was created with a mutation." + ); + }); - // we have to actually subscribe to the query to be able to update it - await new Promise((resolve) => { - const handle = client.watchQuery({ query }); - subscriptionHandle = handle.subscribe({ - next(res: any) { - resolve(res); - }, - }); - }); + it("will not update optimistically if optimisticResponse returns IGNORE sentinel object", async () => { + expect.assertions(7); - const id = "TodoList5"; - const isTodoList = ( - list: unknown - ): list is { todos: { text: string }[] } => - typeof initialList === "object" && - initialList !== null && - "todos" in initialList && - Array.isArray(initialList.todos); + const client = await setup({ + request: { query: mutation, variables }, + result: mutationResult, + }); + const stream = new ObservableStream(client.watchQuery({ query })); - const initialList = client.cache.extract(true)[id]; + await expect(stream).toEmitNext(); - if (!isTodoList(initialList)) { - reject(new Error("Expected TodoList")); - return; - } + const id = "TodoList5"; + const isTodoList = ( + list: unknown + ): list is { todos: { text: string }[] } => + typeof initialList === "object" && + initialList !== null && + "todos" in initialList && + Array.isArray(initialList.todos); - expect(initialList.todos.length).toEqual(3); + const initialList = client.cache.extract(true)[id]; - const promise = client.mutate({ - mutation, - variables, - optimisticResponse: (vars, { IGNORE }) => { - return IGNORE; - }, - update: (proxy: any, mResult: any) => { - expect(mResult.data.createTodo.id).toBe("99"); - - const fragment = gql` - fragment todoList on TodoList { - todos { - id - text - completed - __typename - } - } - `; + if (!isTodoList(initialList)) { + throw new Error("Expected TodoList"); + } - const data: any = proxy.readFragment({ id, fragment }); + expect(initialList.todos.length).toEqual(3); - proxy.writeFragment({ - data: { - ...data, - todos: [mResult.data.createTodo, ...data.todos], - }, - id, - fragment, - }); - }, - }); + const promise = client.mutate({ + mutation, + variables, + optimisticResponse: (vars, { IGNORE }) => { + return IGNORE; + }, + update: (proxy: any, mResult: any) => { + expect(mResult.data.createTodo.id).toBe("99"); - const list = client.cache.extract(true)[id]; + const fragment = gql` + fragment todoList on TodoList { + todos { + id + text + completed + __typename + } + } + `; - if (!isTodoList(list)) { - reject(new Error("Expected TodoList")); - return; - } + const data: any = proxy.readFragment({ id, fragment }); - expect(list.todos.length).toEqual(3); + proxy.writeFragment({ + data: { + ...data, + todos: [mResult.data.createTodo, ...data.todos], + }, + id, + fragment, + }); + }, + }); - await promise; + const list = client.cache.extract(true)[id]; - const result = await client.query({ query }); + if (!isTodoList(list)) { + throw new Error("Expected TodoList"); + } - subscriptionHandle!.unsubscribe(); + expect(list.todos.length).toEqual(3); - const newList = result.data.todoList; + await promise; - if (!isTodoList(newList)) { - reject(new Error("Expected TodoList")); - return; - } + const result = await client.query({ query }); - // There should be one more todo item than before - expect(newList.todos.length).toEqual(4); + stream.unsubscribe(); - // Since we used `prepend` it should be at the front - expect(newList.todos[0].text).toBe( - "This one was created with a mutation." - ); + const newList = result.data.todoList; - resolve(); + if (!isTodoList(newList)) { + throw new Error("Expected TodoList"); } - ); + + // There should be one more todo item than before + expect(newList.todos.length).toEqual(4); + + // Since we used `prepend` it should be at the front + expect(newList.todos[0].text).toBe( + "This one was created with a mutation." + ); + }); it("allows IgnoreModifier as return value when inferring from a TypedDocumentNode mutation", () => { const mutation: TypedDocumentNode<{ bar: string }> = gql` @@ -1176,72 +1067,57 @@ describe("optimistic mutation results", () => { }, }; - itAsync( - "will insert a single itemAsync to the beginning", - async (resolve, reject) => { - expect.assertions(7); - let subscriptionHandle: Subscription; - const client = await setup(reject, { - request: { query: mutation }, - result: mutationResult, - }); + it("will insert a single itemAsync to the beginning", async () => { + expect.assertions(9); + const client = await setup({ + request: { query: mutation }, + result: mutationResult, + }); + const stream = new ObservableStream(client.watchQuery({ query })); - // we have to actually subscribe to the query to be able to update it - await new Promise((resolve) => { - const handle = client.watchQuery({ query }); - subscriptionHandle = handle.subscribe({ - next(res: any) { - resolve(res); - }, - }); - }); + await expect(stream).toEmitNext(); - const promise = client.mutate({ - mutation, - optimisticResponse, - updateQueries: { - todoList(prev: any, options: any) { - const mResult = options.mutationResult as any; - expect(mResult.data.createTodo.id).toEqual("99"); - return { - ...prev, - todoList: { - ...prev.todoList, - todos: [mResult.data.createTodo, ...prev.todoList.todos], - }, - }; - }, + const promise = client.mutate({ + mutation, + optimisticResponse, + updateQueries: { + todoList(prev: any, options: any) { + const mResult = options.mutationResult as any; + expect(mResult.data.createTodo.id).toEqual("99"); + return { + ...prev, + todoList: { + ...prev.todoList, + todos: [mResult.data.createTodo, ...prev.todoList.todos], + }, + }; }, - }); - - const dataInStore = (client.cache as InMemoryCache).extract(true); - expect((dataInStore["TodoList5"] as any).todos.length).toEqual(4); - expect((dataInStore["Todo99"] as any).text).toEqual( - "Optimistically generated" - ); + }, + }); - await promise; + const dataInStore = (client.cache as InMemoryCache).extract(true); + expect((dataInStore["TodoList5"] as any).todos.length).toEqual(4); + expect((dataInStore["Todo99"] as any).text).toEqual( + "Optimistically generated" + ); - const newResult: any = await client.query({ query }); + await promise; - subscriptionHandle!.unsubscribe(); - // There should be one more todo item than before - expect(newResult.data.todoList.todos.length).toEqual(4); + const newResult: any = await client.query({ query }); - // Since we used `prepend` it should be at the front - expect(newResult.data.todoList.todos[0].text).toEqual( - "This one was created with a mutation." - ); + stream.unsubscribe(); + // There should be one more todo item than before + expect(newResult.data.todoList.todos.length).toEqual(4); - resolve(); - } - ); + // Since we used `prepend` it should be at the front + expect(newResult.data.todoList.todos[0].text).toEqual( + "This one was created with a mutation." + ); + }); - itAsync("two array insert like mutations", async (resolve, reject) => { - expect.assertions(9); - let subscriptionHandle: Subscription; + it("two array insert like mutations", async () => { + expect.assertions(11); const client = await setup( - reject, { request: { query: mutation }, result: mutationResult, @@ -1252,16 +1128,9 @@ describe("optimistic mutation results", () => { delay: 50, } ); + const stream = new ObservableStream(client.watchQuery({ query })); - // we have to actually subscribe to the query to be able to update it - await new Promise((resolve) => { - const handle = client.watchQuery({ query }); - subscriptionHandle = handle.subscribe({ - next(res: any) { - resolve(res); - }, - }); - }); + await expect(stream).toEmitNext(); const updateQueries = { todoList: (prev, options) => { @@ -1317,7 +1186,7 @@ describe("optimistic mutation results", () => { const newResult: any = await client.query({ query }); - subscriptionHandle!.unsubscribe(); + stream.unsubscribe(); // There should be one more todo item than before expect(newResult.data.todoList.todos.length).toEqual(5); @@ -1326,15 +1195,11 @@ describe("optimistic mutation results", () => { expect(newResult.data.todoList.todos[1].text).toEqual( "This one was created with a mutation." ); - - resolve(); }); - itAsync("two mutations, one fails", async (resolve, reject) => { - expect.assertions(10); - let subscriptionHandle: Subscription; + it("two mutations, one fails", async () => { + expect.assertions(12); const client = await setup( - reject, { request: { query: mutation }, error: new Error("forbidden (test error)"), @@ -1351,16 +1216,9 @@ describe("optimistic mutation results", () => { // delay: 50, } ); + const stream = new ObservableStream(client.watchQuery({ query })); - // we have to actually subscribe to the query to be able to update it - await new Promise((resolve) => { - const handle = client.watchQuery({ query }); - subscriptionHandle = handle.subscribe({ - next(res: any) { - resolve(res); - }, - }); - }); + await expect(stream).toEmitNext(); const updateQueries = { todoList: (prev, options) => { @@ -1405,7 +1263,7 @@ describe("optimistic mutation results", () => { await Promise.all([promise, promise2]); - subscriptionHandle!.unsubscribe(); + stream.unsubscribe(); { const dataInStore = (client.cache as InMemoryCache).extract(true); expect((dataInStore["TodoList5"] as any).todos.length).toEqual(4); @@ -1417,11 +1275,10 @@ describe("optimistic mutation results", () => { expect((dataInStore["TodoList5"] as any).todos).not.toContainEqual( makeReference("Todo99") ); - resolve(); } }); - itAsync("will handle dependent updates", async (resolve, reject) => { + it("will handle dependent updates", async () => { expect.assertions(1); const link = mockSingleLink( { @@ -1438,7 +1295,7 @@ describe("optimistic mutation results", () => { result: mutationResult2, delay: 20, } - ).setOnError(reject); + ); const customOptimisticResponse1 = { __typename: "Mutation", @@ -1502,13 +1359,13 @@ describe("optimistic mutation results", () => { // https://github.com/apollographql/apollo-client/issues/3723 await new Promise((resolve) => setTimeout(resolve)); - client.mutate({ + void client.mutate({ mutation, optimisticResponse: customOptimisticResponse1, updateQueries, }); - client.mutate({ + void client.mutate({ mutation, optimisticResponse: customOptimisticResponse2, updateQueries, @@ -1536,8 +1393,6 @@ describe("optimistic mutation results", () => { ...defaultTodos, ], ]); - - resolve(); }); }); @@ -1596,82 +1451,67 @@ describe("optimistic mutation results", () => { }, }; - itAsync( - "will insert a single itemAsync to the beginning", - async (resolve, reject) => { - expect.assertions(6); - let subscriptionHandle: Subscription; - const client = await setup(reject, { - request: { query: mutation }, - delay: 300, - result: mutationResult, - }); + it("will insert a single itemAsync to the beginning", async () => { + expect.assertions(8); + const client = await setup({ + request: { query: mutation }, + delay: 300, + result: mutationResult, + }); + const stream = new ObservableStream(client.watchQuery({ query })); - // we have to actually subscribe to the query to be able to update it - await new Promise((resolve) => { - const handle = client.watchQuery({ query }); - subscriptionHandle = handle.subscribe({ - next(res: any) { - resolve(res); - }, - }); - }); + await expect(stream).toEmitNext(); - let firstTime = true; - let before = Date.now(); - const promise = client.mutate({ - mutation, - optimisticResponse, - update: (proxy: any, mResult: any) => { - const after = Date.now(); - const duration = after - before; - if (firstTime) { - expect(duration < 300).toBe(true); - firstTime = false; - } else { - expect(duration > 300).toBe(true); - } - let data = proxy.readQuery({ query }); + let firstTime = true; + let before = Date.now(); + const promise = client.mutate({ + mutation, + optimisticResponse, + update: (proxy: any, mResult: any) => { + const after = Date.now(); + const duration = after - before; + if (firstTime) { + expect(duration < 300).toBe(true); + firstTime = false; + } else { + expect(duration > 300).toBe(true); + } + let data = proxy.readQuery({ query }); - proxy.writeQuery({ - query, - data: { - ...data, - todoList: { - ...data.todoList, - todos: [mResult.data.createTodo, ...data.todoList.todos], - }, + proxy.writeQuery({ + query, + data: { + ...data, + todoList: { + ...data.todoList, + todos: [mResult.data.createTodo, ...data.todoList.todos], }, - }); - }, - }); + }, + }); + }, + }); - const dataInStore = (client.cache as InMemoryCache).extract(true); - expect((dataInStore["TodoList5"] as any).todos.length).toBe(4); - expect((dataInStore["Todo99"] as any).text).toBe( - "Optimistically generated" - ); - await promise; - await client.query({ query }).then((newResult: any) => { - subscriptionHandle!.unsubscribe(); - // There should be one more todo item than before - expect(newResult.data.todoList.todos.length).toBe(4); - - // Since we used `prepend` it should be at the front - expect(newResult.data.todoList.todos[0].text).toBe( - "This one was created with a mutation." - ); - }); + const dataInStore = (client.cache as InMemoryCache).extract(true); + expect((dataInStore["TodoList5"] as any).todos.length).toBe(4); + expect((dataInStore["Todo99"] as any).text).toBe( + "Optimistically generated" + ); + await promise; + const newResult = await client.query({ query }); - resolve(); - } - ); + stream.unsubscribe(); + // There should be one more todo item than before + expect(newResult.data.todoList.todos.length).toBe(4); - itAsync("two array insert like mutations", async (resolve, reject) => { - expect.assertions(9); - let subscriptionHandle: Subscription; + // Since we used `prepend` it should be at the front + expect(newResult.data.todoList.todos[0].text).toBe( + "This one was created with a mutation." + ); + }); + + it("two array insert like mutations", async () => { + expect.assertions(11); const client = await setup( - reject, { request: { query: mutation }, result: mutationResult, @@ -1682,16 +1522,9 @@ describe("optimistic mutation results", () => { delay: 50, } ); + const stream = new ObservableStream(client.watchQuery({ query })); - // we have to actually subscribe to the query to be able to update it - await new Promise((resolve) => { - const handle = client.watchQuery({ query }); - subscriptionHandle = handle.subscribe({ - next(res: any) { - resolve(res); - }, - }); - }); + await expect(stream).toEmitNext(); const update = (proxy: any, mResult: any) => { const data: any = proxy.readFragment({ @@ -1765,7 +1598,7 @@ describe("optimistic mutation results", () => { const newResult: any = await client.query({ query }); - subscriptionHandle!.unsubscribe(); + stream.unsubscribe(); // There should be one more todo item than before expect(newResult.data.todoList.todos.length).toBe(5); @@ -1774,15 +1607,11 @@ describe("optimistic mutation results", () => { expect(newResult.data.todoList.todos[1].text).toBe( "This one was created with a mutation." ); - - resolve(); }); - itAsync("two mutations, one fails", async (resolve, reject) => { - expect.assertions(10); - let subscriptionHandle: Subscription; + it("two mutations, one fails", async () => { + expect.assertions(12); const client = await setup( - reject, { request: { query: mutation }, error: new Error("forbidden (test error)"), @@ -1799,16 +1628,9 @@ describe("optimistic mutation results", () => { // delay: 50, } ); + const stream = new ObservableStream(client.watchQuery({ query })); - // we have to actually subscribe to the query to be able to update it - await new Promise((resolve) => { - const handle = client.watchQuery({ query }); - subscriptionHandle = handle.subscribe({ - next(res: any) { - resolve(res); - }, - }); - }); + await expect(stream).toEmitNext(); const update = (proxy: any, mResult: any) => { const data: any = proxy.readFragment({ @@ -1873,7 +1695,7 @@ describe("optimistic mutation results", () => { await Promise.all([promise, promise2]); - subscriptionHandle!.unsubscribe(); + stream.unsubscribe(); { const dataInStore = (client.cache as InMemoryCache).extract(true); expect((dataInStore["TodoList5"] as any).todos.length).toBe(4); @@ -1885,11 +1707,10 @@ describe("optimistic mutation results", () => { expect((dataInStore["TodoList5"] as any).todos).not.toContainEqual( makeReference("Todo99") ); - resolve(); } }); - itAsync("will handle dependent updates", async (resolve, reject) => { + it("will handle dependent updates", async () => { expect.assertions(1); const link = mockSingleLink( { @@ -1906,7 +1727,7 @@ describe("optimistic mutation results", () => { result: mutationResult2, delay: 20, } - ).setOnError(reject); + ); const customOptimisticResponse1 = { __typename: "Mutation", @@ -1983,13 +1804,13 @@ describe("optimistic mutation results", () => { await new Promise((resolve) => setTimeout(resolve)); - client.mutate({ + void client.mutate({ mutation, optimisticResponse: customOptimisticResponse1, update, }); - client.mutate({ + void client.mutate({ mutation, optimisticResponse: customOptimisticResponse2, update, @@ -2016,11 +1837,9 @@ describe("optimistic mutation results", () => { ...defaultTodos, ], ]); - - resolve(); }); - itAsync("final update ignores optimistic data", (resolve, reject) => { + it("final update ignores optimistic data", async () => { const cache = new InMemoryCache(); const client = new ApolloClient({ cache, @@ -2143,209 +1962,190 @@ describe("optimistic mutation results", () => { const optimisticItem = makeItem("optimistic"); const mutationItem = makeItem("mutation"); - const wrapReject = ( - fn: (...args: TArgs) => TResult - ): typeof fn => { - return function (this: unknown, ...args: TArgs) { - try { - return fn.apply(this, args); - } catch (e) { - reject(e); - throw e; - } - }; - }; + const result = await client.mutate({ + mutation, + optimisticResponse: { + addItem: optimisticItem, + }, + variables: { + item: mutationItem, + }, + update: (cache, mutationResult) => { + ++updateCount; + if (updateCount === 1) { + expect(mutationResult).toEqual({ + data: { + addItem: optimisticItem, + }, + }); - return client - .mutate({ - mutation, - optimisticResponse: { - addItem: optimisticItem, - }, - variables: { - item: mutationItem, - }, - update: wrapReject((cache, mutationResult) => { - ++updateCount; - if (updateCount === 1) { - expect(mutationResult).toEqual({ - data: { - addItem: optimisticItem, + append(cache, optimisticItem); + + const expected = { + ROOT_QUERY: { + __typename: "Query", + items: [manualItem1, manualItem2, optimisticItem], + }, + ROOT_MUTATION: { + __typename: "Mutation", + // Although ROOT_MUTATION field data gets removed immediately + // after the mutation finishes, it is still temporarily visible + // to the update function. + 'addItem({"item":{"__typename":"Item","text":"mutation 4"}})': { + __typename: "Item", + text: "optimistic 3", }, - }); + }, + }; + + // Since we're in an optimistic update function, reading + // non-optimistically still returns optimistic data. + expect(cache.extract(false)).toEqual(expected); + expect(cache.extract(true)).toEqual(expected); + } else if (updateCount === 2) { + expect(mutationResult).toEqual({ + data: { + addItem: mutationItem, + }, + }); - append(cache, optimisticItem); + append(cache, mutationItem); - const expected = { - ROOT_QUERY: { - __typename: "Query", - items: [manualItem1, manualItem2, optimisticItem], - }, - ROOT_MUTATION: { - __typename: "Mutation", - // Although ROOT_MUTATION field data gets removed immediately - // after the mutation finishes, it is still temporarily visible - // to the update function. - 'addItem({"item":{"__typename":"Item","text":"mutation 4"}})': - { - __typename: "Item", - text: "optimistic 3", - }, - }, - }; - - // Since we're in an optimistic update function, reading - // non-optimistically still returns optimistic data. - expect(cache.extract(false)).toEqual(expected); - expect(cache.extract(true)).toEqual(expected); - } else if (updateCount === 2) { - expect(mutationResult).toEqual({ - data: { - addItem: mutationItem, + const expected = { + ROOT_QUERY: { + __typename: "Query", + items: [mutationItem], + }, + ROOT_MUTATION: { + __typename: "Mutation", + 'addItem({"item":{"__typename":"Item","text":"mutation 4"}})': { + __typename: "Item", + text: "mutation 4", }, - }); + }, + }; + + // Since we're in the final (non-optimistic) update function, + // optimistic data is invisible, even if we try to read + // optimistically. + expect(cache.extract(false)).toEqual(expected); + expect(cache.extract(true)).toEqual(expected); + } else { + throw new Error("too many updates"); + } + }, + }); - append(cache, mutationItem); + expect(result).toEqual({ + data: { + addItem: mutationItem, + }, + }); - const expected = { - ROOT_QUERY: { - __typename: "Query", - items: [mutationItem], - }, - ROOT_MUTATION: { - __typename: "Mutation", - 'addItem({"item":{"__typename":"Item","text":"mutation 4"}})': - { - __typename: "Item", - text: "mutation 4", - }, - }, - }; - - // Since we're in the final (non-optimistic) update function, - // optimistic data is invisible, even if we try to read - // optimistically. - expect(cache.extract(false)).toEqual(expected); - expect(cache.extract(true)).toEqual(expected); - } else { - throw new Error("too many updates"); - } - }), - }) - .then((result) => { - expect(result).toEqual({ - data: { - addItem: mutationItem, - }, - }); + // Only the final update function ever touched non-optimistic + // cache data. + expect(cache.extract(false)).toEqual({ + ROOT_QUERY: { + __typename: "Query", + items: [mutationItem], + }, + ROOT_MUTATION: { + __typename: "Mutation", + }, + }); - // Only the final update function ever touched non-optimistic - // cache data. - expect(cache.extract(false)).toEqual({ - ROOT_QUERY: { - __typename: "Query", - items: [mutationItem], - }, - ROOT_MUTATION: { - __typename: "Mutation", - }, - }); + // Now that the mutation is finished, reading optimistically from + // the cache should return the manually added items again. + expect(cache.extract(true)).toEqual({ + ROOT_QUERY: { + __typename: "Query", + items: [ + // If we wanted to keep optimistic data as up-to-date as + // possible, we could rerun all optimistic transactions + // after writing to the root (non-optimistic) layer of the + // cache, which would result in mutationItem appearing in + // this list along with manualItem1 and manualItem2 + // (presumably in that order). However, rerunning those + // optimistic transactions would trigger additional + // broadcasts for optimistic query watches, with + // intermediate results that (re)combine optimistic and + // non-optimistic data. Since rerendering the UI tends to be + // expensive, we should prioritize broadcasting states that + // matter most, and in this case that means broadcasting the + // initial optimistic state (for perceived performance), + // followed by the final, authoritative, non-optimistic + // state. Other intermediate states are a distraction, as + // they will probably soon be superseded by another (more + // authoritative) update. This particular state is visible + // only because we haven't rolled back this manual Layer + // just yet (see cache.removeOptimistic below). + manualItem1, + manualItem2, + ], + }, + ROOT_MUTATION: { + __typename: "Mutation", + }, + }); - // Now that the mutation is finished, reading optimistically from - // the cache should return the manually added items again. - expect(cache.extract(true)).toEqual({ - ROOT_QUERY: { - __typename: "Query", - items: [ - // If we wanted to keep optimistic data as up-to-date as - // possible, we could rerun all optimistic transactions - // after writing to the root (non-optimistic) layer of the - // cache, which would result in mutationItem appearing in - // this list along with manualItem1 and manualItem2 - // (presumably in that order). However, rerunning those - // optimistic transactions would trigger additional - // broadcasts for optimistic query watches, with - // intermediate results that (re)combine optimistic and - // non-optimistic data. Since rerendering the UI tends to be - // expensive, we should prioritize broadcasting states that - // matter most, and in this case that means broadcasting the - // initial optimistic state (for perceived performance), - // followed by the final, authoritative, non-optimistic - // state. Other intermediate states are a distraction, as - // they will probably soon be superseded by another (more - // authoritative) update. This particular state is visible - // only because we haven't rolled back this manual Layer - // just yet (see cache.removeOptimistic below). - manualItem1, - manualItem2, - ], - }, - ROOT_MUTATION: { - __typename: "Mutation", - }, - }); + cache.removeOptimistic("manual"); - cache.removeOptimistic("manual"); + // After removing the manual optimistic layer, only the + // non-optimistic data remains. + expect(cache.extract(true)).toEqual({ + ROOT_QUERY: { + __typename: "Query", + items: [mutationItem], + }, + ROOT_MUTATION: { + __typename: "Mutation", + }, + }); - // After removing the manual optimistic layer, only the - // non-optimistic data remains. - expect(cache.extract(true)).toEqual({ - ROOT_QUERY: { - __typename: "Query", - items: [mutationItem], - }, - ROOT_MUTATION: { - __typename: "Mutation", - }, - }); - }) - .then(() => { - cancelFns.forEach((cancel) => cancel()); + cancelFns.forEach((cancel) => cancel()); - expect(optimisticDiffs).toEqual([ - { - complete: true, - fromOptimisticTransaction: true, - result: { - items: manualItems, - }, - }, - { - complete: true, - fromOptimisticTransaction: true, - result: { - items: [...manualItems, optimisticItem], - }, - }, - { - complete: true, - result: { - items: manualItems, - }, - }, - { - complete: true, - result: { - items: [mutationItem], - }, - }, - ]); + expect(optimisticDiffs).toEqual([ + { + complete: true, + fromOptimisticTransaction: true, + result: { + items: manualItems, + }, + }, + { + complete: true, + fromOptimisticTransaction: true, + result: { + items: [...manualItems, optimisticItem], + }, + }, + { + complete: true, + result: { + items: manualItems, + }, + }, + { + complete: true, + result: { + items: [mutationItem], + }, + }, + ]); - expect(realisticDiffs).toEqual([ - { - complete: false, - missing: [expect.anything()], - result: {}, - }, - { - complete: true, - result: { - items: [mutationItem], - }, - }, - ]); - }) - .then(resolve, reject); + expect(realisticDiffs).toEqual([ + { + complete: false, + missing: [expect.anything()], + result: {}, + }, + { + complete: true, + result: { + items: [mutationItem], + }, + }, + ]); }); }); }); @@ -2403,10 +2203,7 @@ describe("optimistic mutation - githunt comments", () => { }, }; - async function setup( - reject: (reason: any) => any, - ...mockedResponses: any[] - ) { + async function setup(...mockedResponses: MockedResponse[]) { const link = mockSingleLink( { request: { @@ -2423,7 +2220,7 @@ describe("optimistic mutation - githunt comments", () => { result, }, ...mockedResponses - ).setOnError(reject); + ); const client = new ApolloClient({ link, @@ -2501,31 +2298,25 @@ describe("optimistic mutation - githunt comments", () => { }, }; - itAsync("can post a new comment", async (resolve, reject) => { - expect.assertions(1); + it("can post a new comment", async () => { + expect.assertions(3); const mutationVariables = { repoFullName: "org/repo", commentContent: "New Comment", }; - let subscriptionHandle: Subscription; - const client = await setup(reject, { + const client = await setup({ request: { query: addTypenameToDocument(mutation), variables: mutationVariables, }, result: mutationResult, }); + const stream = new ObservableStream( + client.watchQuery({ query, variables }) + ); - // we have to actually subscribe to the query to be able to update it - await new Promise((resolve) => { - const handle = client.watchQuery({ query, variables }); - subscriptionHandle = handle.subscribe({ - next(res: any) { - resolve(res); - }, - }); - }); + await expect(stream).toEmitNext(); await client.mutate({ mutation, @@ -2536,9 +2327,7 @@ describe("optimistic mutation - githunt comments", () => { const newResult: any = await client.query({ query, variables }); - subscriptionHandle!.unsubscribe(); + stream.unsubscribe(); expect(newResult.data.entry.comments.length).toBe(2); - - resolve(); }); }); diff --git a/src/__tests__/refetchQueries.ts b/src/__tests__/refetchQueries.ts index 4b843c61165..87b7f93d734 100644 --- a/src/__tests__/refetchQueries.ts +++ b/src/__tests__/refetchQueries.ts @@ -1,6 +1,5 @@ import { Subscription } from "zen-observable-ts"; -import { itAsync } from "../testing"; import { ApolloClient, ApolloLink, @@ -10,29 +9,30 @@ import { TypedDocumentNode, ObservableQuery, } from "../core"; +import { ObservableStream } from "../testing/internal"; describe("client.refetchQueries", () => { - itAsync("is public and callable", (resolve, reject) => { + it("is public and callable", async () => { const client = new ApolloClient({ cache: new InMemoryCache(), }); expect(typeof client.refetchQueries).toBe("function"); + const onQueryUpdated = jest.fn(); const result = client.refetchQueries({ updateCache(cache) { expect(cache).toBe(client.cache); expect(cache.extract()).toEqual({}); }, - onQueryUpdated() { - reject("should not have called onQueryUpdated"); - return false; - }, + onQueryUpdated, }); expect(result.queries).toEqual([]); expect(result.results).toEqual([]); - result.then(resolve, reject); + await result; + + expect(onQueryUpdated).not.toHaveBeenCalled(); }); const aQuery: TypedDocumentNode<{ a: string }> = gql` @@ -113,917 +113,848 @@ describe("client.refetchQueries", () => { }); } - itAsync( - "includes watched queries affected by updateCache", - async (resolve, reject) => { - const client = makeClient(); - const [aObs, bObs, abObs] = await setup(client); + it("includes watched queries affected by updateCache", async () => { + const client = makeClient(); + const [aObs, bObs, abObs] = await setup(client); - const ayyResults = await client.refetchQueries({ - updateCache(cache) { - cache.writeQuery({ - query: aQuery, - data: { - a: "Ayy", - }, - }); - }, + const ayyResults = await client.refetchQueries({ + updateCache(cache) { + cache.writeQuery({ + query: aQuery, + data: { + a: "Ayy", + }, + }); + }, - onQueryUpdated(obs, diff) { - if (obs === aObs) { - expect(diff.result).toEqual({ a: "Ayy" }); - } else if (obs === bObs) { - reject("bQuery should not have been updated"); - } else if (obs === abObs) { - expect(diff.result).toEqual({ a: "Ayy", b: "B" }); - } else { - reject( - `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` - ); - } - return Promise.resolve(diff.result); - }, - }); + onQueryUpdated(obs, diff) { + if (obs === aObs) { + expect(diff.result).toEqual({ a: "Ayy" }); + } else if (obs === bObs) { + throw new Error("bQuery should not have been updated"); + } else if (obs === abObs) { + expect(diff.result).toEqual({ a: "Ayy", b: "B" }); + } else { + throw new Error( + `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` + ); + } + return Promise.resolve(diff.result); + }, + }); - sortObjects(ayyResults); + sortObjects(ayyResults); - expect(ayyResults).toEqual([ - { a: "Ayy" }, - { a: "Ayy", b: "B" }, - // Note that no bQuery result is included here. - ]); + expect(ayyResults).toEqual([ + { a: "Ayy" }, + { a: "Ayy", b: "B" }, + // Note that no bQuery result is included here. + ]); - const beeResults = await client.refetchQueries({ - updateCache(cache) { - cache.writeQuery({ - query: bQuery, - data: { - b: "Bee", - }, - }); - }, + const beeResults = await client.refetchQueries({ + updateCache(cache) { + cache.writeQuery({ + query: bQuery, + data: { + b: "Bee", + }, + }); + }, - onQueryUpdated(obs, diff) { - if (obs === aObs) { - reject("aQuery should not have been updated"); - } else if (obs === bObs) { - expect(diff.result).toEqual({ b: "Bee" }); - } else if (obs === abObs) { - expect(diff.result).toEqual({ a: "Ayy", b: "Bee" }); - } else { - reject( - `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` - ); - } - return diff.result; - }, - }); + onQueryUpdated(obs, diff) { + if (obs === aObs) { + throw new Error("aQuery should not have been updated"); + } else if (obs === bObs) { + expect(diff.result).toEqual({ b: "Bee" }); + } else if (obs === abObs) { + expect(diff.result).toEqual({ a: "Ayy", b: "Bee" }); + } else { + throw new Error( + `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` + ); + } + return diff.result; + }, + }); - sortObjects(beeResults); + sortObjects(beeResults); - expect(beeResults).toEqual([ - // Note that no aQuery result is included here. - { a: "Ayy", b: "Bee" }, - { b: "Bee" }, - ]); + expect(beeResults).toEqual([ + // Note that no aQuery result is included here. + { a: "Ayy", b: "Bee" }, + { b: "Bee" }, + ]); - unsubscribe(); - resolve(); - } - ); + unsubscribe(); + }); - itAsync( - "includes watched queries named in options.include", - async (resolve, reject) => { - const client = makeClient(); - const [aObs, bObs, abObs] = await setup(client); + it("includes watched queries named in options.include", async () => { + const client = makeClient(); + const [aObs, bObs, abObs] = await setup(client); - const ayyResults = await client.refetchQueries({ - updateCache(cache) { - cache.writeQuery({ - query: aQuery, - data: { - a: "Ayy", - }, - }); - }, + const ayyResults = await client.refetchQueries({ + updateCache(cache) { + cache.writeQuery({ + query: aQuery, + data: { + a: "Ayy", + }, + }); + }, - // This is the options.include array mentioned in the test description. - include: ["B"], + // This is the options.include array mentioned in the test description. + include: ["B"], + + onQueryUpdated(obs, diff) { + if (obs === aObs) { + expect(diff.result).toEqual({ a: "Ayy" }); + } else if (obs === bObs) { + expect(diff.result).toEqual({ b: "B" }); + } else if (obs === abObs) { + expect(diff.result).toEqual({ a: "Ayy", b: "B" }); + } else { + throw new Error( + `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` + ); + } + return Promise.resolve(diff.result); + }, + }); - onQueryUpdated(obs, diff) { - if (obs === aObs) { - expect(diff.result).toEqual({ a: "Ayy" }); - } else if (obs === bObs) { - expect(diff.result).toEqual({ b: "B" }); - } else if (obs === abObs) { - expect(diff.result).toEqual({ a: "Ayy", b: "B" }); - } else { - reject( - `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` - ); - } - return Promise.resolve(diff.result); - }, - }); + sortObjects(ayyResults); - sortObjects(ayyResults); + expect(ayyResults).toEqual([ + { a: "Ayy" }, + { a: "Ayy", b: "B" }, + // Included this time! + { b: "B" }, + ]); - expect(ayyResults).toEqual([ - { a: "Ayy" }, - { a: "Ayy", b: "B" }, - // Included this time! - { b: "B" }, - ]); + const beeResults = await client.refetchQueries({ + updateCache(cache) { + cache.writeQuery({ + query: bQuery, + data: { + b: "Bee", + }, + }); + }, - const beeResults = await client.refetchQueries({ - updateCache(cache) { - cache.writeQuery({ - query: bQuery, - data: { - b: "Bee", - }, - }); - }, + // The "A" here causes aObs to be included, but the "AB" should be + // redundant because that query is already included. + include: ["A", "AB"], + + onQueryUpdated(obs, diff) { + if (obs === aObs) { + expect(diff.result).toEqual({ a: "Ayy" }); + } else if (obs === bObs) { + expect(diff.result).toEqual({ b: "Bee" }); + } else if (obs === abObs) { + expect(diff.result).toEqual({ a: "Ayy", b: "Bee" }); + } else { + throw new Error( + `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` + ); + } + return diff.result; + }, + }); - // The "A" here causes aObs to be included, but the "AB" should be - // redundant because that query is already included. - include: ["A", "AB"], - - onQueryUpdated(obs, diff) { - if (obs === aObs) { - expect(diff.result).toEqual({ a: "Ayy" }); - } else if (obs === bObs) { - expect(diff.result).toEqual({ b: "Bee" }); - } else if (obs === abObs) { - expect(diff.result).toEqual({ a: "Ayy", b: "Bee" }); - } else { - reject( - `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` - ); - } - return diff.result; - }, - }); + sortObjects(beeResults); - sortObjects(beeResults); + expect(beeResults).toEqual([ + { a: "Ayy" }, // Included this time! + { a: "Ayy", b: "Bee" }, + { b: "Bee" }, + ]); - expect(beeResults).toEqual([ - { a: "Ayy" }, // Included this time! - { a: "Ayy", b: "Bee" }, - { b: "Bee" }, - ]); + unsubscribe(); + }); - unsubscribe(); - resolve(); - } - ); + it("includes query DocumentNode objects specified in options.include", async () => { + const client = makeClient(); + const [aObs, bObs, abObs] = await setup(client); - itAsync( - "includes query DocumentNode objects specified in options.include", - async (resolve, reject) => { - const client = makeClient(); - const [aObs, bObs, abObs] = await setup(client); + const ayyResults = await client.refetchQueries({ + updateCache(cache) { + cache.writeQuery({ + query: aQuery, + data: { + a: "Ayy", + }, + }); + }, - const ayyResults = await client.refetchQueries({ - updateCache(cache) { - cache.writeQuery({ - query: aQuery, - data: { - a: "Ayy", - }, - }); - }, + // Note that we're passing query DocumentNode objects instead of query + // name strings, in this test. + include: [bQuery, abQuery], + + onQueryUpdated(obs, diff) { + if (obs === aObs) { + expect(diff.result).toEqual({ a: "Ayy" }); + } else if (obs === bObs) { + expect(diff.result).toEqual({ b: "B" }); + } else if (obs === abObs) { + expect(diff.result).toEqual({ a: "Ayy", b: "B" }); + } else { + throw new Error( + `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` + ); + } + return Promise.resolve(diff.result); + }, + }); - // Note that we're passing query DocumentNode objects instead of query - // name strings, in this test. - include: [bQuery, abQuery], - - onQueryUpdated(obs, diff) { - if (obs === aObs) { - expect(diff.result).toEqual({ a: "Ayy" }); - } else if (obs === bObs) { - expect(diff.result).toEqual({ b: "B" }); - } else if (obs === abObs) { - expect(diff.result).toEqual({ a: "Ayy", b: "B" }); - } else { - reject( - `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` - ); - } - return Promise.resolve(diff.result); - }, - }); + sortObjects(ayyResults); - sortObjects(ayyResults); + expect(ayyResults).toEqual([ + { a: "Ayy" }, + { a: "Ayy", b: "B" }, + // Included this time! + { b: "B" }, + ]); - expect(ayyResults).toEqual([ - { a: "Ayy" }, - { a: "Ayy", b: "B" }, - // Included this time! - { b: "B" }, - ]); + const beeResults = await client.refetchQueries({ + updateCache(cache) { + cache.writeQuery({ + query: bQuery, + data: { + b: "Bee", + }, + }); + }, - const beeResults = await client.refetchQueries({ - updateCache(cache) { - cache.writeQuery({ - query: bQuery, - data: { - b: "Bee", - }, - }); - }, + // The abQuery and "AB" should be redundant, but the aQuery here is + // important for aObs to be included. + include: [abQuery, "AB", aQuery], + + onQueryUpdated(obs, diff) { + if (obs === aObs) { + expect(diff.result).toEqual({ a: "Ayy" }); + } else if (obs === bObs) { + expect(diff.result).toEqual({ b: "Bee" }); + } else if (obs === abObs) { + expect(diff.result).toEqual({ a: "Ayy", b: "Bee" }); + } else { + throw new Error( + `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` + ); + } + return diff.result; + }, + }); - // The abQuery and "AB" should be redundant, but the aQuery here is - // important for aObs to be included. - include: [abQuery, "AB", aQuery], - - onQueryUpdated(obs, diff) { - if (obs === aObs) { - expect(diff.result).toEqual({ a: "Ayy" }); - } else if (obs === bObs) { - expect(diff.result).toEqual({ b: "Bee" }); - } else if (obs === abObs) { - expect(diff.result).toEqual({ a: "Ayy", b: "Bee" }); - } else { - reject( - `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` - ); - } - return diff.result; - }, - }); + sortObjects(beeResults); - sortObjects(beeResults); + expect(beeResults).toEqual([ + { a: "Ayy" }, // Included this time! + { a: "Ayy", b: "Bee" }, + { b: "Bee" }, + ]); - expect(beeResults).toEqual([ - { a: "Ayy" }, // Included this time! - { a: "Ayy", b: "Bee" }, - { b: "Bee" }, - ]); + unsubscribe(); + }); - unsubscribe(); - resolve(); - } - ); + it('includes all queries when options.include === "all"', async () => { + const client = makeClient(); + const [aObs, bObs, abObs] = await setup(client); - itAsync( - 'includes all queries when options.include === "all"', - async (resolve, reject) => { - const client = makeClient(); - const [aObs, bObs, abObs] = await setup(client); + const ayyResults = await client.refetchQueries({ + include: "all", - const ayyResults = await client.refetchQueries({ - include: "all", + updateCache(cache) { + cache.writeQuery({ + query: aQuery, + data: { + a: "Ayy", + }, + }); + }, - updateCache(cache) { - cache.writeQuery({ - query: aQuery, - data: { - a: "Ayy", - }, - }); - }, + onQueryUpdated(obs, diff) { + if (obs === aObs) { + expect(diff.result).toEqual({ a: "Ayy" }); + } else if (obs === bObs) { + expect(diff.result).toEqual({ b: "B" }); + } else if (obs === abObs) { + expect(diff.result).toEqual({ a: "Ayy", b: "B" }); + } else { + throw new Error( + `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` + ); + } + return Promise.resolve(diff.result); + }, + }); - onQueryUpdated(obs, diff) { - if (obs === aObs) { - expect(diff.result).toEqual({ a: "Ayy" }); - } else if (obs === bObs) { - expect(diff.result).toEqual({ b: "B" }); - } else if (obs === abObs) { - expect(diff.result).toEqual({ a: "Ayy", b: "B" }); - } else { - reject( - `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` - ); - } - return Promise.resolve(diff.result); - }, - }); + sortObjects(ayyResults); - sortObjects(ayyResults); + expect(ayyResults).toEqual([ + { a: "Ayy" }, + { a: "Ayy", b: "B" }, + { b: "B" }, + ]); - expect(ayyResults).toEqual([ - { a: "Ayy" }, - { a: "Ayy", b: "B" }, - { b: "B" }, - ]); + const beeResults = await client.refetchQueries({ + include: "all", - const beeResults = await client.refetchQueries({ - include: "all", + updateCache(cache) { + cache.writeQuery({ + query: bQuery, + data: { + b: "Bee", + }, + }); + }, - updateCache(cache) { - cache.writeQuery({ - query: bQuery, - data: { - b: "Bee", - }, - }); - }, + onQueryUpdated(obs, diff) { + if (obs === aObs) { + expect(diff.result).toEqual({ a: "Ayy" }); + } else if (obs === bObs) { + expect(diff.result).toEqual({ b: "Bee" }); + } else if (obs === abObs) { + expect(diff.result).toEqual({ a: "Ayy", b: "Bee" }); + } else { + throw new Error( + `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` + ); + } + return diff.result; + }, + }); - onQueryUpdated(obs, diff) { - if (obs === aObs) { - expect(diff.result).toEqual({ a: "Ayy" }); - } else if (obs === bObs) { - expect(diff.result).toEqual({ b: "Bee" }); - } else if (obs === abObs) { - expect(diff.result).toEqual({ a: "Ayy", b: "Bee" }); - } else { - reject( - `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` - ); - } - return diff.result; - }, - }); + sortObjects(beeResults); - sortObjects(beeResults); + expect(beeResults).toEqual([ + { a: "Ayy" }, + { a: "Ayy", b: "Bee" }, + { b: "Bee" }, + ]); - expect(beeResults).toEqual([ - { a: "Ayy" }, - { a: "Ayy", b: "Bee" }, - { b: "Bee" }, - ]); + unsubscribe(); + }); - unsubscribe(); - resolve(); - } - ); - - itAsync( - 'includes all active queries when options.include === "active"', - async (resolve, reject) => { - const client = makeClient(); - const [aObs, bObs, abObs] = await setup(client); - - const extraObs = client.watchQuery({ query: abQuery }); - expect(extraObs.hasObservers()).toBe(false); - - const activeResults = await client.refetchQueries({ - include: "active", - - onQueryUpdated(obs, diff) { - if (obs === aObs) { - expect(diff.result).toEqual({ a: "A" }); - } else if (obs === bObs) { - expect(diff.result).toEqual({ b: "B" }); - } else if (obs === abObs) { - expect(diff.result).toEqual({ a: "A", b: "B" }); - } else { - reject( - `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` - ); - } - return Promise.resolve(diff.result); - }, - }); + it('includes all active queries when options.include === "active"', async () => { + const client = makeClient(); + const [aObs, bObs, abObs] = await setup(client); + + const extraObs = client.watchQuery({ query: abQuery }); + expect(extraObs.hasObservers()).toBe(false); + + const activeResults = await client.refetchQueries({ + include: "active", + + onQueryUpdated(obs, diff) { + if (obs === aObs) { + expect(diff.result).toEqual({ a: "A" }); + } else if (obs === bObs) { + expect(diff.result).toEqual({ b: "B" }); + } else if (obs === abObs) { + expect(diff.result).toEqual({ a: "A", b: "B" }); + } else { + throw new Error( + `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` + ); + } + return Promise.resolve(diff.result); + }, + }); - sortObjects(activeResults); + sortObjects(activeResults); - expect(activeResults).toEqual([ - { a: "A" }, - { a: "A", b: "B" }, - { b: "B" }, - ]); + expect(activeResults).toEqual([{ a: "A" }, { a: "A", b: "B" }, { b: "B" }]); - subs.push( - extraObs.subscribe({ - next(result) { - expect(result).toEqual({ a: "A", b: "B" }); - }, - }) - ); - expect(extraObs.hasObservers()).toBe(true); - - const resultsAfterSubscribe = await client.refetchQueries({ - include: "active", - - onQueryUpdated(obs, diff) { - if (obs === aObs) { - expect(diff.result).toEqual({ a: "A" }); - } else if (obs === bObs) { - expect(diff.result).toEqual({ b: "B" }); - } else if (obs === abObs) { - expect(diff.result).toEqual({ a: "A", b: "B" }); - } else if (obs === extraObs) { - expect(diff.result).toEqual({ a: "A", b: "B" }); - } else { - reject( - `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` - ); - } - return Promise.resolve(diff.result); + subs.push( + extraObs.subscribe({ + next(result) { + expect(result).toEqual({ a: "A", b: "B" }); }, - }); - - sortObjects(resultsAfterSubscribe); + }) + ); + expect(extraObs.hasObservers()).toBe(true); + + const resultsAfterSubscribe = await client.refetchQueries({ + include: "active", + + onQueryUpdated(obs, diff) { + if (obs === aObs) { + expect(diff.result).toEqual({ a: "A" }); + } else if (obs === bObs) { + expect(diff.result).toEqual({ b: "B" }); + } else if (obs === abObs) { + expect(diff.result).toEqual({ a: "A", b: "B" }); + } else if (obs === extraObs) { + expect(diff.result).toEqual({ a: "A", b: "B" }); + } else { + throw new Error( + `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` + ); + } + return Promise.resolve(diff.result); + }, + }); - expect(resultsAfterSubscribe).toEqual([ - { a: "A" }, - { a: "A", b: "B" }, - // Included thanks to extraObs this time. - { a: "A", b: "B" }, - // Sorted last by sortObjects. - { b: "B" }, - ]); + sortObjects(resultsAfterSubscribe); - unsubscribe(); - resolve(); - } - ); - - itAsync( - "includes queries named in refetchQueries even if they have no observers", - async (resolve, reject) => { - const client = makeClient(); - - const aObs = client.watchQuery({ query: aQuery }); - const bObs = client.watchQuery({ query: bQuery }); - const abObs = client.watchQuery({ query: abQuery }); - - // These ObservableQuery objects have no observers yet, but should - // nevertheless be refetched if identified explicitly in an options.include - // array passed to client.refetchQueries. - expect(aObs.hasObservers()).toBe(false); - expect(bObs.hasObservers()).toBe(false); - expect(abObs.hasObservers()).toBe(false); - - const activeResults = await client.refetchQueries({ - include: ["A", abQuery], - - onQueryUpdated(obs, diff) { - if (obs === aObs) { - expect(diff.complete).toBe(false); - expect(diff.result).toEqual({}); - } else if (obs === abObs) { - expect(diff.complete).toBe(false); - expect(diff.result).toEqual({}); - } else { - reject( - `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` - ); - } - return Promise.resolve(diff.result); - }, - }); + expect(resultsAfterSubscribe).toEqual([ + { a: "A" }, + { a: "A", b: "B" }, + // Included thanks to extraObs this time. + { a: "A", b: "B" }, + // Sorted last by sortObjects. + { b: "B" }, + ]); - sortObjects(activeResults); - expect(activeResults).toEqual([{}, {}]); - - subs.push( - abObs.subscribe({ - next(result) { - expect(result.data).toEqual({ a: "A", b: "B" }); - - client - .refetchQueries({ - include: [aQuery, "B"], - - onQueryUpdated(obs, diff) { - if (obs === aObs) { - expect(diff.result).toEqual({ a: "A" }); - } else if (obs === bObs) { - expect(diff.result).toEqual({ b: "B" }); - } else if (obs === abObs) { - expect(diff.result).toEqual({ a: "A", b: "B" }); - } else { - reject( - `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` - ); - } - return Promise.resolve(diff.result); - }, - }) - .then((resultsAfterSubscribe) => { - sortObjects(resultsAfterSubscribe); - expect(resultsAfterSubscribe).toEqual([{ a: "A" }, { b: "B" }]); - - unsubscribe(); - }) - .then(resolve, reject); - }, - }) - ); + unsubscribe(); + }); - expect(abObs.hasObservers()).toBe(true); - } - ); - - itAsync( - "should not include unwatched single queries", - async (resolve, reject) => { - const client = makeClient(); - const [aObs, bObs, abObs] = await setup(client); - - const delayedQuery = gql` - query DELAYED { - d - e - l - a - y - e - d + it("includes queries named in refetchQueries even if they have no observers", async () => { + const client = makeClient(); + + const aObs = client.watchQuery({ query: aQuery }); + const bObs = client.watchQuery({ query: bQuery }); + const abObs = client.watchQuery({ query: abQuery }); + + // These ObservableQuery objects have no observers yet, but should + // nevertheless be refetched if identified explicitly in an options.include + // array passed to client.refetchQueries. + expect(aObs.hasObservers()).toBe(false); + expect(bObs.hasObservers()).toBe(false); + expect(abObs.hasObservers()).toBe(false); + + const activeResults = await client.refetchQueries({ + include: ["A", abQuery], + + onQueryUpdated(obs, diff) { + if (obs === aObs) { + expect(diff.complete).toBe(false); + expect(diff.result).toEqual({}); + } else if (obs === abObs) { + expect(diff.complete).toBe(false); + expect(diff.result).toEqual({}); + } else { + throw new Error( + `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` + ); } - `; - - client - .query({ - query: delayedQuery, - variables: { - // Delay this query by 10 seconds so it stays in-flight. - delay: 10000, - }, - }) - .catch(reject); - - const queries = client["queryManager"]["queries"]; - expect(queries.size).toBe(4); - - queries.forEach((queryInfo, queryId) => { - if (queryId === "1" || queryId === "2" || queryId === "3") { - expect(queryInfo.observableQuery).toBeInstanceOf(ObservableQuery); - } else if (queryId === "4") { - // One-off client.query-style queries never get an ObservableQuery, so - // they should not be included by include: "active". - expect(queryInfo.observableQuery).toBe(null); - expect(queryInfo.document).toBe(delayedQuery); + return Promise.resolve(diff.result); + }, + }); + + sortObjects(activeResults); + expect(activeResults).toEqual([{}, {}]); + + const stream = new ObservableStream(abObs); + subs.push(stream as unknown as Subscription); + expect(abObs.hasObservers()).toBe(true); + + await expect(stream).toEmitMatchedValue({ data: { a: "A", b: "B" } }); + + const resultsAfterSubscribe = await client.refetchQueries({ + include: [aQuery, "B"], + + onQueryUpdated(obs, diff) { + if (obs === aObs) { + expect(diff.result).toEqual({ a: "A" }); + } else if (obs === bObs) { + expect(diff.result).toEqual({ b: "B" }); + } else if (obs === abObs) { + expect(diff.result).toEqual({ a: "A", b: "B" }); + } else { + throw new Error( + `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` + ); } - }); + return Promise.resolve(diff.result); + }, + }); - const activeResults = await client.refetchQueries({ - include: "active", - - onQueryUpdated(obs, diff) { - if (obs === aObs) { - expect(diff.result).toEqual({ a: "A" }); - } else if (obs === bObs) { - expect(diff.result).toEqual({ b: "B" }); - } else if (obs === abObs) { - expect(diff.result).toEqual({ a: "A", b: "B" }); - } else { - reject( - `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` - ); - } - return Promise.resolve(diff.result); - }, - }); + sortObjects(resultsAfterSubscribe); + expect(resultsAfterSubscribe).toEqual([{ a: "A" }, { b: "B" }]); - sortObjects(activeResults); + unsubscribe(); + }); - expect(activeResults).toEqual([ - { a: "A" }, - { a: "A", b: "B" }, - { b: "B" }, - ]); + it("should not include unwatched single queries", async () => { + const client = makeClient(); + const [aObs, bObs, abObs] = await setup(client); + + const delayedQuery = gql` + query DELAYED { + d + e + l + a + y + e + d + } + `; + + void client.query({ + query: delayedQuery, + variables: { + // Delay this query by 10 seconds so it stays in-flight. + delay: 10000, + }, + }); - const allResults = await client.refetchQueries({ - include: "all", - - onQueryUpdated(obs, diff) { - if (obs === aObs) { - expect(diff.result).toEqual({ a: "A" }); - } else if (obs === bObs) { - expect(diff.result).toEqual({ b: "B" }); - } else if (obs === abObs) { - expect(diff.result).toEqual({ a: "A", b: "B" }); - } else { - reject( - `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` - ); - } - return Promise.resolve(diff.result); - }, - }); + const queries = client["queryManager"]["queries"]; + expect(queries.size).toBe(4); + + queries.forEach((queryInfo, queryId) => { + if (queryId === "1" || queryId === "2" || queryId === "3") { + expect(queryInfo.observableQuery).toBeInstanceOf(ObservableQuery); + } else if (queryId === "4") { + // One-off client.query-style queries never get an ObservableQuery, so + // they should not be included by include: "active". + expect(queryInfo.observableQuery).toBe(null); + expect(queryInfo.document).toBe(delayedQuery); + } + }); - sortObjects(allResults); + const activeResults = await client.refetchQueries({ + include: "active", + + onQueryUpdated(obs, diff) { + if (obs === aObs) { + expect(diff.result).toEqual({ a: "A" }); + } else if (obs === bObs) { + expect(diff.result).toEqual({ b: "B" }); + } else if (obs === abObs) { + expect(diff.result).toEqual({ a: "A", b: "B" }); + } else { + throw new Error( + `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` + ); + } + return Promise.resolve(diff.result); + }, + }); - expect(allResults).toEqual([{ a: "A" }, { a: "A", b: "B" }, { b: "B" }]); + sortObjects(activeResults); - unsubscribe(); - client.stop(); + expect(activeResults).toEqual([{ a: "A" }, { a: "A", b: "B" }, { b: "B" }]); - expect(queries.size).toBe(0); + const allResults = await client.refetchQueries({ + include: "all", - resolve(); - } - ); - - itAsync( - "refetches watched queries if onQueryUpdated not provided", - async (resolve, reject) => { - const client = makeClient(); - const [aObs, bObs, abObs] = await setup(client); - - const aSpy = jest.spyOn(aObs, "refetch"); - const bSpy = jest.spyOn(bObs, "refetch"); - const abSpy = jest.spyOn(abObs, "refetch"); - - const ayyResults = ( - await client.refetchQueries({ - include: ["B"], - updateCache(cache) { - cache.writeQuery({ - query: aQuery, - data: { - a: "Ayy", - }, - }); - }, - }) - ).map((result) => result.data as object); + onQueryUpdated(obs, diff) { + if (obs === aObs) { + expect(diff.result).toEqual({ a: "A" }); + } else if (obs === bObs) { + expect(diff.result).toEqual({ b: "B" }); + } else if (obs === abObs) { + expect(diff.result).toEqual({ a: "A", b: "B" }); + } else { + throw new Error( + `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` + ); + } + return Promise.resolve(diff.result); + }, + }); - sortObjects(ayyResults); + sortObjects(allResults); - // These results have reverted back to what the ApolloLink returns ("A" - // rather than "Ayy"), because we let them be refetched (by not providing - // an onQueryUpdated function). - expect(ayyResults).toEqual([{ a: "A" }, { a: "A", b: "B" }, { b: "B" }]); + expect(allResults).toEqual([{ a: "A" }, { a: "A", b: "B" }, { b: "B" }]); - expect(aSpy).toHaveBeenCalledTimes(1); - expect(bSpy).toHaveBeenCalledTimes(1); - expect(abSpy).toHaveBeenCalledTimes(1); + unsubscribe(); + client.stop(); - unsubscribe(); - resolve(); - } - ); - - itAsync( - "can run updateQuery function against optimistic cache layer", - async (resolve, reject) => { - const client = makeClient(); - const [aObs, bObs, abObs] = await setup(client); - - client.cache.watch({ - query: abQuery, - optimistic: false, - callback(diff) { - reject("should not have notified non-optimistic watcher"); - }, - }); + expect(queries.size).toBe(0); + }); - expect(client.cache.extract(true)).toEqual({ - ROOT_QUERY: { - __typename: "Query", - a: "A", - b: "B", - }, - }); + it("refetches watched queries if onQueryUpdated not provided", async () => { + const client = makeClient(); + const [aObs, bObs, abObs] = await setup(client); - const results = await client.refetchQueries({ - // This causes the update to run against a temporary optimistic layer. - optimistic: true, + const aSpy = jest.spyOn(aObs, "refetch"); + const bSpy = jest.spyOn(bObs, "refetch"); + const abSpy = jest.spyOn(abObs, "refetch"); + const ayyResults = ( + await client.refetchQueries({ + include: ["B"], updateCache(cache) { - const modified = cache.modify({ - fields: { - a(value, { DELETE }) { - expect(value).toEqual("A"); - return DELETE; - }, + cache.writeQuery({ + query: aQuery, + data: { + a: "Ayy", }, }); - expect(modified).toBe(true); }, + }) + ).map((result) => result.data as object); - onQueryUpdated(obs, diff) { - expect(diff.complete).toBe(true); - - // Even though we evicted the Query.a field in the updateCache function, - // that optimistic layer was discarded before broadcasting results, so - // we're back to the original (non-optimistic) data. - if (obs === aObs) { - expect(diff.result).toEqual({ a: "A" }); - } else if (obs === bObs) { - reject("bQuery should not have been updated"); - } else if (obs === abObs) { - expect(diff.result).toEqual({ a: "A", b: "B" }); - } else { - reject( - `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` - ); - } - - return diff.result; - }, - }); + sortObjects(ayyResults); - sortObjects(results); + // These results have reverted back to what the ApolloLink returns ("A" + // rather than "Ayy"), because we let them be refetched (by not providing + // an onQueryUpdated function). + expect(ayyResults).toEqual([{ a: "A" }, { a: "A", b: "B" }, { b: "B" }]); - expect(results).toEqual([{ a: "A" }, { a: "A", b: "B" }]); + expect(aSpy).toHaveBeenCalledTimes(1); + expect(bSpy).toHaveBeenCalledTimes(1); + expect(abSpy).toHaveBeenCalledTimes(1); - expect(client.cache.extract(true)).toEqual({ - ROOT_QUERY: { - __typename: "Query", - a: "A", - b: "B", - }, - }); + unsubscribe(); + }); - resolve(); - } - ); - - itAsync( - "can return true from onQueryUpdated to choose default refetching behavior", - async (resolve, reject) => { - const client = makeClient(); - const [aObs, bObs, abObs] = await setup(client); - - const refetchResult = client.refetchQueries({ - include: ["A", "B"], - onQueryUpdated(obs, diff) { - if (obs === aObs) { - expect(diff.result).toEqual({ a: "A" }); - } else if (obs === bObs) { - expect(diff.result).toEqual({ b: "B" }); - } else if (obs === abObs) { - reject("abQuery should not have been updated"); - } else { - reject( - `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` - ); - } - return true; - }, - }); + it("can run updateQuery function against optimistic cache layer", async () => { + const client = makeClient(); + const [aObs, bObs, abObs] = await setup(client); - expect(refetchResult.results.length).toBe(2); - refetchResult.results.forEach((result) => { - expect(result).toBeInstanceOf(Promise); - }); + client.cache.watch({ + query: abQuery, + optimistic: false, + callback(diff) { + throw new Error("should not have notified non-optimistic watcher"); + }, + }); - expect( - refetchResult.queries - .map((obs) => { - expect(obs).toBeInstanceOf(ObservableQuery); - return obs.queryName; - }) - .sort() - ).toEqual(["A", "B"]); - - const results = (await refetchResult).map((result) => { - // These results are ApolloQueryResult, as inferred by TypeScript. - expect(Object.keys(result).sort()).toEqual([ - "data", - "loading", - "networkStatus", - ]); - return result.data; - }); + expect(client.cache.extract(true)).toEqual({ + ROOT_QUERY: { + __typename: "Query", + a: "A", + b: "B", + }, + }); - sortObjects(results); + const results = await client.refetchQueries({ + // This causes the update to run against a temporary optimistic layer. + optimistic: true, - expect(results).toEqual([{ a: "A" }, { b: "B" }]); + updateCache(cache) { + const modified = cache.modify({ + fields: { + a(value, { DELETE }) { + expect(value).toEqual("A"); + return DELETE; + }, + }, + }); + expect(modified).toBe(true); + }, - resolve(); - } - ); + onQueryUpdated(obs, diff) { + expect(diff.complete).toBe(true); + + // Even though we evicted the Query.a field in the updateCache function, + // that optimistic layer was discarded before broadcasting results, so + // we're back to the original (non-optimistic) data. + if (obs === aObs) { + expect(diff.result).toEqual({ a: "A" }); + } else if (obs === bObs) { + throw new Error("bQuery should not have been updated"); + } else if (obs === abObs) { + expect(diff.result).toEqual({ a: "A", b: "B" }); + } else { + throw new Error( + `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` + ); + } - itAsync( - "can return true from onQueryUpdated when using options.updateCache", - async (resolve, reject) => { - const client = makeClient(); - const [aObs, bObs, abObs] = await setup(client); + return diff.result; + }, + }); - const refetchResult = client.refetchQueries({ - updateCache(cache) { - cache.writeQuery({ - query: bQuery, - data: { - b: "Beetlejuice", - }, - }); - }, + sortObjects(results); - onQueryUpdated(obs, diff) { - if (obs === aObs) { - reject("aQuery should not have been updated"); - } else if (obs === bObs) { - expect(diff.result).toEqual({ b: "Beetlejuice" }); - } else if (obs === abObs) { - expect(diff.result).toEqual({ a: "A", b: "Beetlejuice" }); - } else { - reject( - `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` - ); - } - - expect(client.cache.extract()).toEqual({ - ROOT_QUERY: { - __typename: "Query", - a: "A", - b: "Beetlejuice", - }, - }); + expect(results).toEqual([{ a: "A" }, { a: "A", b: "B" }]); - return true; - }, - }); + expect(client.cache.extract(true)).toEqual({ + ROOT_QUERY: { + __typename: "Query", + a: "A", + b: "B", + }, + }); + }); - expect(refetchResult.results.length).toBe(2); - refetchResult.results.forEach((result) => { - expect(result).toBeInstanceOf(Promise); - }); + it("can return true from onQueryUpdated to choose default refetching behavior", async () => { + const client = makeClient(); + const [aObs, bObs, abObs] = await setup(client); + + const refetchResult = client.refetchQueries({ + include: ["A", "B"], + onQueryUpdated(obs, diff) { + if (obs === aObs) { + expect(diff.result).toEqual({ a: "A" }); + } else if (obs === bObs) { + expect(diff.result).toEqual({ b: "B" }); + } else if (obs === abObs) { + throw new Error("abQuery should not have been updated"); + } else { + throw new Error( + `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` + ); + } + return true; + }, + }); - expect( - refetchResult.queries - .map((obs) => { - expect(obs).toBeInstanceOf(ObservableQuery); - return obs.queryName; - }) - .sort() - ).toEqual(["AB", "B"]); - - const results = (await refetchResult).map((result) => { - // These results are ApolloQueryResult, as inferred by TypeScript. - expect(Object.keys(result).sort()).toEqual([ - "data", - "loading", - "networkStatus", - ]); - return result.data; - }); + expect(refetchResult.results.length).toBe(2); + refetchResult.results.forEach((result) => { + expect(result).toBeInstanceOf(Promise); + }); + + expect( + refetchResult.queries + .map((obs) => { + expect(obs).toBeInstanceOf(ObservableQuery); + return obs.queryName; + }) + .sort() + ).toEqual(["A", "B"]); + + const results = (await refetchResult).map((result) => { + // These results are ApolloQueryResult, as inferred by TypeScript. + expect(Object.keys(result).sort()).toEqual([ + "data", + "loading", + "networkStatus", + ]); + return result.data; + }); - sortObjects(results); + sortObjects(results); - expect(results).toEqual([ - // Since we returned true from onQueryUpdated, the results were refetched, - // replacing "Beetlejuice" with "B" again. - { a: "A", b: "B" }, - { b: "B" }, + expect(results).toEqual([{ a: "A" }, { b: "B" }]); + }); + + it("can return true from onQueryUpdated when using options.updateCache", async () => { + const client = makeClient(); + const [aObs, bObs, abObs] = await setup(client); + + const refetchResult = client.refetchQueries({ + updateCache(cache) { + cache.writeQuery({ + query: bQuery, + data: { + b: "Beetlejuice", + }, + }); + }, + + onQueryUpdated(obs, diff) { + if (obs === aObs) { + throw new Error("aQuery should not have been updated"); + } else if (obs === bObs) { + expect(diff.result).toEqual({ b: "Beetlejuice" }); + } else if (obs === abObs) { + expect(diff.result).toEqual({ a: "A", b: "Beetlejuice" }); + } else { + throw new Error( + `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` + ); + } + + expect(client.cache.extract()).toEqual({ + ROOT_QUERY: { + __typename: "Query", + a: "A", + b: "Beetlejuice", + }, + }); + + return true; + }, + }); + + expect(refetchResult.results.length).toBe(2); + refetchResult.results.forEach((result) => { + expect(result).toBeInstanceOf(Promise); + }); + + expect( + refetchResult.queries + .map((obs) => { + expect(obs).toBeInstanceOf(ObservableQuery); + return obs.queryName; + }) + .sort() + ).toEqual(["AB", "B"]); + + const results = (await refetchResult).map((result) => { + // These results are ApolloQueryResult, as inferred by TypeScript. + expect(Object.keys(result).sort()).toEqual([ + "data", + "loading", + "networkStatus", ]); + return result.data; + }); - expect(client.cache.extract()).toEqual({ - ROOT_QUERY: { - __typename: "Query", - a: "A", - b: "B", - }, - }); + sortObjects(results); - resolve(); - } - ); - - itAsync( - "can return false from onQueryUpdated to skip/ignore a query", - async (resolve, reject) => { - const client = makeClient(); - const [aObs, bObs, abObs] = await setup(client); - - const refetchResult = client.refetchQueries({ - include: ["A", "B"], - onQueryUpdated(obs, diff) { - if (obs === aObs) { - expect(diff.result).toEqual({ a: "A" }); - } else if (obs === bObs) { - expect(diff.result).toEqual({ b: "B" }); - } else if (obs === abObs) { - reject("abQuery should not have been updated"); - } else { - reject( - `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` - ); - } - // Skip refetching all but the B query. - return obs.queryName === "B"; - }, - }); + expect(results).toEqual([ + // Since we returned true from onQueryUpdated, the results were refetched, + // replacing "Beetlejuice" with "B" again. + { a: "A", b: "B" }, + { b: "B" }, + ]); - expect(refetchResult.results.length).toBe(1); - refetchResult.results.forEach((result) => { - expect(result).toBeInstanceOf(Promise); - }); + expect(client.cache.extract()).toEqual({ + ROOT_QUERY: { + __typename: "Query", + a: "A", + b: "B", + }, + }); + }); - expect( - refetchResult.queries - .map((obs) => { - expect(obs).toBeInstanceOf(ObservableQuery); - return obs.queryName; - }) - .sort() - ).toEqual(["B"]); - - const results = (await refetchResult).map((result) => { - // These results are ApolloQueryResult, as inferred by TypeScript. - expect(Object.keys(result).sort()).toEqual([ - "data", - "loading", - "networkStatus", - ]); - return result.data; - }); + it("can return false from onQueryUpdated to skip/ignore a query", async () => { + const client = makeClient(); + const [aObs, bObs, abObs] = await setup(client); + + const refetchResult = client.refetchQueries({ + include: ["A", "B"], + onQueryUpdated(obs, diff) { + if (obs === aObs) { + expect(diff.result).toEqual({ a: "A" }); + } else if (obs === bObs) { + expect(diff.result).toEqual({ b: "B" }); + } else if (obs === abObs) { + throw new Error("abQuery should not have been updated"); + } else { + throw new Error( + `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` + ); + } + // Skip refetching all but the B query. + return obs.queryName === "B"; + }, + }); + + expect(refetchResult.results.length).toBe(1); + refetchResult.results.forEach((result) => { + expect(result).toBeInstanceOf(Promise); + }); - sortObjects(results); + expect( + refetchResult.queries + .map((obs) => { + expect(obs).toBeInstanceOf(ObservableQuery); + return obs.queryName; + }) + .sort() + ).toEqual(["B"]); + + const results = (await refetchResult).map((result) => { + // These results are ApolloQueryResult, as inferred by TypeScript. + expect(Object.keys(result).sort()).toEqual([ + "data", + "loading", + "networkStatus", + ]); + return result.data; + }); - expect(results).toEqual([{ b: "B" }]); + sortObjects(results); - resolve(); - } - ); + expect(results).toEqual([{ b: "B" }]); + }); it("can refetch no-cache queries", () => { // TODO The options.updateCache function won't work for these queries, but diff --git a/src/__tests__/subscribeToMore.ts b/src/__tests__/subscribeToMore.ts index 419b0b696d0..6216eaa85c9 100644 --- a/src/__tests__/subscribeToMore.ts +++ b/src/__tests__/subscribeToMore.ts @@ -4,7 +4,8 @@ import { DocumentNode, OperationDefinitionNode } from "graphql"; import { ApolloClient } from "../core"; import { InMemoryCache } from "../cache"; import { ApolloLink, Operation } from "../link/core"; -import { itAsync, mockSingleLink, mockObservableLink } from "../testing"; +import { mockSingleLink, mockObservableLink, wait } from "../testing"; +import { ObservableStream, spyOnConsole } from "../testing/internal"; const isSub = (operation: Operation) => (operation.query as DocumentNode).definitions @@ -57,13 +58,11 @@ describe("subscribeToMore", () => { name: string; } - itAsync("triggers new result from subscription data", (resolve, reject) => { - let latestResult: any = null; + it("triggers new result from subscription data", async () => { const wSLink = mockObservableLink(); - const httpLink = mockSingleLink(req1).setOnError(reject); + const httpLink = mockSingleLink(req1); const link = ApolloLink.split(isSub, wSLink, httpLink); - let counter = 0; const client = new ApolloClient({ cache: new InMemoryCache({ addTypename: false }), @@ -71,21 +70,7 @@ describe("subscribeToMore", () => { }); const obsHandle = client.watchQuery({ query }); - - const sub = obsHandle.subscribe({ - next(queryResult: any) { - latestResult = queryResult; - if (++counter === 3) { - sub.unsubscribe(); - expect(latestResult).toEqual({ - data: { entry: { value: "Amanda Liu" } }, - loading: false, - networkStatus: 7, - }); - resolve(); - } - }, - }); + const stream = new ObservableStream(obsHandle); obsHandle.subscribeToMore({ document: gql` @@ -98,26 +83,35 @@ describe("subscribeToMore", () => { }, }); - let i = 0; - function simulate() { - const result = results[i++]; - if (result) { - wSLink.simulateResult(result); - setTimeout(simulate, 10); - } - } - simulate(); + await expect(stream).toEmitValue({ + data: { entry: { value: "1" } }, + loading: false, + networkStatus: 7, + }); + + wSLink.simulateResult(results[0]); + + await expect(stream).toEmitValue({ + data: { entry: { value: "Dahivat Pandya" } }, + loading: false, + networkStatus: 7, + }); + + await wait(10); + wSLink.simulateResult(results[1]); + + await expect(stream).toEmitValue({ + data: { entry: { value: "Amanda Liu" } }, + loading: false, + networkStatus: 7, + }); }); - itAsync("calls error callback on error", (resolve, reject) => { - let latestResult: any = null; + it("calls error callback on error", async () => { const wSLink = mockObservableLink(); - const httpLink = mockSingleLink(req1).setOnError(reject); - + const httpLink = mockSingleLink(req1); const link = ApolloLink.split(isSub, wSLink, httpLink); - let counter = 0; - const client = new ApolloClient({ link, cache: new InMemoryCache({ addTypename: false }), @@ -126,14 +120,9 @@ describe("subscribeToMore", () => { const obsHandle = client.watchQuery<(typeof req1)["result"]["data"]>({ query, }); - const sub = obsHandle.subscribe({ - next(queryResult: any) { - latestResult = queryResult; - counter++; - }, - }); + const stream = new ObservableStream(obsHandle); - let errorCount = 0; + const onError = jest.fn(); obsHandle.subscribeToMore({ document: gql` @@ -144,98 +133,84 @@ describe("subscribeToMore", () => { updateQuery: (_, { subscriptionData }) => { return { entry: { value: subscriptionData.data.name } }; }, - onError: () => { - errorCount += 1; - }, + onError, }); - setTimeout(() => { - sub.unsubscribe(); - expect(latestResult).toEqual({ - data: { entry: { value: "Amanda Liu" } }, - loading: false, - networkStatus: 7, - }); - expect(counter).toBe(2); - expect(errorCount).toBe(1); - resolve(); - }, 15); - - for (let i = 0; i < 2; i++) { - wSLink.simulateResult(results2[i]); + for (const result of results2) { + wSLink.simulateResult(result); } - }); - itAsync( - "prints unhandled subscription errors to the console", - (resolve, reject) => { - let latestResult: any = null; + await expect(stream).toEmitValue({ + data: { entry: { value: "1" } }, + loading: false, + networkStatus: 7, + }); - const wSLink = mockObservableLink(); - const httpLink = mockSingleLink(req1).setOnError(reject); + await expect(stream).toEmitValue({ + data: { entry: { value: "Amanda Liu" } }, + loading: false, + networkStatus: 7, + }); - const link = ApolloLink.split(isSub, wSLink, httpLink); + await wait(15); - let counter = 0; + expect(onError).toHaveBeenCalledTimes(1); + expect(onError).toHaveBeenCalledWith(new Error("You cant touch this")); + }); - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ addTypename: false }), - }); + it("prints unhandled subscription errors to the console", async () => { + using _ = spyOnConsole("error"); - const obsHandle = client.watchQuery({ - query, - }); - const sub = obsHandle.subscribe({ - next(queryResult: any) { - latestResult = queryResult; - counter++; - }, - }); + const wSLink = mockObservableLink(); + const httpLink = mockSingleLink(req1); + const link = ApolloLink.split(isSub, wSLink, httpLink); - let errorCount = 0; - const consoleErr = console.error; - console.error = (_: Error) => { - errorCount += 1; - }; + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ addTypename: false }), + }); - obsHandle.subscribeToMore({ - document: gql` - subscription newValues { - name - } - `, - updateQuery: () => { - throw new Error("should not be called because of initial error"); - }, - }); - - setTimeout(() => { - sub.unsubscribe(); - expect(latestResult).toEqual({ - data: { entry: { value: "1" } }, - loading: false, - networkStatus: 7, - }); - expect(counter).toBe(1); - expect(errorCount).toBe(1); - console.error = consoleErr; - resolve(); - }, 15); - - for (let i = 0; i < 2; i++) { - wSLink.simulateResult(results3[i]); - } + const obsHandle = client.watchQuery({ + query, + }); + const stream = new ObservableStream(obsHandle); + + obsHandle.subscribeToMore({ + document: gql` + subscription newValues { + name + } + `, + updateQuery: () => { + throw new Error("should not be called because of initial error"); + }, + }); + + for (const result of results3) { + wSLink.simulateResult(result); } - ); - itAsync("should not corrupt the cache (#3062)", async (resolve, reject) => { - let latestResult: any = null; - const wSLink = mockObservableLink(); - const httpLink = mockSingleLink(req4).setOnError(reject); + await expect(stream).toEmitValue({ + data: { entry: { value: "1" } }, + loading: false, + networkStatus: 7, + }); + + await wait(15); + + expect(console.error).toHaveBeenCalledTimes(1); + expect(console.error).toHaveBeenCalledWith( + "Unhandled GraphQL subscription error", + new Error("You cant touch this") + ); + + await expect(stream).not.toEmitAnything(); + }); + it("should not corrupt the cache (#3062)", async () => { + const wSLink = mockObservableLink(); + const httpLink = mockSingleLink(req4); const link = ApolloLink.split(isSub, wSLink, httpLink); - let counter = 0; const client = new ApolloClient({ cache: new InMemoryCache({ addTypename: false }).restore({ @@ -256,13 +231,7 @@ describe("subscribeToMore", () => { const obsHandle = client.watchQuery<(typeof req4)["result"]["data"]>({ query, }); - - const sub = obsHandle.subscribe({ - next(queryResult: any) { - latestResult = queryResult; - counter++; - }, - }); + const stream = new ObservableStream(obsHandle); let nextMutation: { value: string }; obsHandle.subscribeToMore({ @@ -279,9 +248,6 @@ describe("subscribeToMore", () => { }, }); - const wait = (dur: any) => - new Promise((resolve) => setTimeout(resolve, dur)); - for (let i = 0; i < 2; i++) { // init optimistic mutation let data = client.cache.readQuery<(typeof req4)["result"]["data"]>( @@ -302,105 +268,104 @@ describe("subscribeToMore", () => { client.cache.removeOptimistic(i.toString()); // note: we don't complete mutation with performTransaction because a real example would detect duplicates } - sub.unsubscribe(); - expect(counter).toBe(3); - expect(latestResult).toEqual({ + + await expect(stream).toEmitValue({ + data: { entry: [{ value: 1 }, { value: 2 }] }, + loading: false, + networkStatus: 7, + }); + + await expect(stream).toEmitValue({ + data: { + entry: [{ value: 1 }, { value: 2 }, { value: "Dahivat Pandya" }], + }, + loading: false, + networkStatus: 7, + }); + + await expect(stream).toEmitValue({ data: { entry: [ - { - value: 1, - }, - { - value: 2, - }, - { - value: "Dahivat Pandya", - }, - { - value: "Amanda Liu", - }, + { value: 1 }, + { value: 2 }, + { value: "Dahivat Pandya" }, + { value: "Amanda Liu" }, ], }, loading: false, networkStatus: 7, }); - resolve(); + + await expect(stream).not.toEmitAnything(); }); // TODO add a test that checks that subscriptions are cancelled when obs is unsubscribed from. - itAsync( - "allows specification of custom types for variables and payload (#4246)", - (resolve, reject) => { - interface TypedOperation extends Operation { - variables: { - someNumber: number; - }; - } - const typedReq = { - request: { query, variables: { someNumber: 1 } } as TypedOperation, - result, + it("allows specification of custom types for variables and payload (#4246)", async () => { + interface TypedOperation extends Operation { + variables: { + someNumber: number; }; - interface TypedSubscriptionVariables { - someString: string; - } + } + const typedReq = { + request: { query, variables: { someNumber: 1 } } as TypedOperation, + result, + }; + interface TypedSubscriptionVariables { + someString: string; + } - let latestResult: any = null; - const wSLink = mockObservableLink(); - const httpLink = mockSingleLink(typedReq).setOnError(reject); - - const link = ApolloLink.split(isSub, wSLink, httpLink); - let counter = 0; - - const client = new ApolloClient({ - cache: new InMemoryCache({ addTypename: false }), - link, - }); - - type TData = (typeof typedReq)["result"]["data"]; - type TVars = (typeof typedReq)["request"]["variables"]; - const obsHandle = client.watchQuery({ - query, - variables: { someNumber: 1 }, - }); - - const sub = obsHandle.subscribe({ - next(queryResult: any) { - latestResult = queryResult; - if (++counter === 3) { - sub.unsubscribe(); - expect(latestResult).toEqual({ - data: { entry: { value: "Amanda Liu" } }, - loading: false, - networkStatus: 7, - }); - resolve(); - } - }, - }); - - obsHandle.subscribeToMore({ - document: gql` - subscription newValues { - name - } - `, - variables: { - someString: "foo", - }, - updateQuery: (_, { subscriptionData }) => { - return { entry: { value: subscriptionData.data.name } }; - }, - }); - - let i = 0; - function simulate() { - const result = results[i++]; - if (result) { - wSLink.simulateResult(result); - setTimeout(simulate, 10); + const wSLink = mockObservableLink(); + const httpLink = mockSingleLink(typedReq); + const link = ApolloLink.split(isSub, wSLink, httpLink); + + const client = new ApolloClient({ + cache: new InMemoryCache({ addTypename: false }), + link, + }); + + type TData = (typeof typedReq)["result"]["data"]; + type TVars = (typeof typedReq)["request"]["variables"]; + const obsHandle = client.watchQuery({ + query, + variables: { someNumber: 1 }, + }); + const stream = new ObservableStream(obsHandle); + + obsHandle.subscribeToMore({ + document: gql` + subscription newValues { + name } - } - simulate(); - } - ); + `, + variables: { + someString: "foo", + }, + updateQuery: (_, { subscriptionData }) => { + return { entry: { value: subscriptionData.data.name } }; + }, + }); + + await expect(stream).toEmitValue({ + data: { entry: { value: "1" } }, + loading: false, + networkStatus: 7, + }); + + wSLink.simulateResult(results[0]); + + await expect(stream).toEmitValue({ + data: { entry: { value: "Dahivat Pandya" } }, + loading: false, + networkStatus: 7, + }); + + await wait(10); + wSLink.simulateResult(results[1]); + + await expect(stream).toEmitValue({ + data: { entry: { value: "Amanda Liu" } }, + loading: false, + networkStatus: 7, + }); + }); }); diff --git a/src/cache/inmemory/__tests__/fragmentMatcher.ts b/src/cache/inmemory/__tests__/fragmentMatcher.ts index 29bce194c2d..6823819226b 100644 --- a/src/cache/inmemory/__tests__/fragmentMatcher.ts +++ b/src/cache/inmemory/__tests__/fragmentMatcher.ts @@ -1,6 +1,5 @@ import gql from "graphql-tag"; -import { itAsync } from "../../../testing"; import { InMemoryCache } from "../inMemoryCache"; import { visit, FragmentDefinitionNode } from "graphql"; import { hasOwn } from "../helpers"; @@ -242,7 +241,7 @@ describe("policies.fragmentMatches", () => { console.warn = warn; }); - itAsync("can infer fuzzy subtypes heuristically", (resolve, reject) => { + it("can infer fuzzy subtypes heuristically", async () => { const cache = new InMemoryCache({ possibleTypes: { A: ["B", "C"], @@ -279,7 +278,7 @@ describe("policies.fragmentMatches", () => { FragmentDefinition(frag) { function check(typename: string, result: boolean) { if (result !== cache.policies.fragmentMatches(frag, typename)) { - reject( + throw new Error( `fragment ${frag.name.value} should${ result ? "" : " not" } have matched typename ${typename}` @@ -577,7 +576,5 @@ describe("policies.fragmentMatches", () => { }, }).size ).toBe("ABCDEF".length); - - resolve(); }); }); diff --git a/src/cache/inmemory/__tests__/writeToStore.ts b/src/cache/inmemory/__tests__/writeToStore.ts index 8c8bf1c5b48..f147f5d49d4 100644 --- a/src/cache/inmemory/__tests__/writeToStore.ts +++ b/src/cache/inmemory/__tests__/writeToStore.ts @@ -19,7 +19,6 @@ import { cloneDeep, getMainDefinition, } from "../../../utilities"; -import { itAsync } from "../../../testing/core"; import { StoreWriter } from "../writeToStore"; import { defaultNormalizedCacheFactory, writeQueryToStore } from "./helpers"; import { InMemoryCache } from "../inMemoryCache"; @@ -1860,137 +1859,132 @@ describe("writing to the store", () => { expect(cache.extract()).toMatchSnapshot(); }); - itAsync( - "should allow a union of objects of a different type, when overwriting a generated id with a real id", - (resolve, reject) => { - const dataWithPlaceholder = { - author: { - hello: "Foo", - __typename: "Placeholder", - }, - }; - const dataWithAuthor = { - author: { - firstName: "John", - lastName: "Smith", - id: "129", - __typename: "Author", - }, - }; - const query = gql` - query { - author { - ... on Author { - firstName - lastName - id - __typename - } - ... on Placeholder { - hello - __typename - } + it("should allow a union of objects of a different type, when overwriting a generated id with a real id", async () => { + const dataWithPlaceholder = { + author: { + hello: "Foo", + __typename: "Placeholder", + }, + }; + const dataWithAuthor = { + author: { + firstName: "John", + lastName: "Smith", + id: "129", + __typename: "Author", + }, + }; + const query = gql` + query { + author { + ... on Author { + firstName + lastName + id + __typename + } + ... on Placeholder { + hello + __typename } } - `; + } + `; - let mergeCount = 0; - const cache = new InMemoryCache({ - typePolicies: { - Query: { - fields: { - author: { - merge(existing, incoming, { isReference, readField }) { - switch (++mergeCount) { - case 1: - expect(existing).toBeUndefined(); - expect(isReference(incoming)).toBe(false); - expect(incoming).toEqual(dataWithPlaceholder.author); - break; - case 2: - expect(existing).toEqual(dataWithPlaceholder.author); - expect(isReference(incoming)).toBe(true); - expect(readField("__typename", incoming)).toBe("Author"); - break; - case 3: - expect(isReference(existing)).toBe(true); - expect(readField("__typename", existing)).toBe("Author"); - expect(incoming).toEqual(dataWithPlaceholder.author); - break; - default: - reject("unreached"); - } - return incoming; - }, + let mergeCount = 0; + const cache = new InMemoryCache({ + typePolicies: { + Query: { + fields: { + author: { + merge(existing, incoming, { isReference, readField }) { + switch (++mergeCount) { + case 1: + expect(existing).toBeUndefined(); + expect(isReference(incoming)).toBe(false); + expect(incoming).toEqual(dataWithPlaceholder.author); + break; + case 2: + expect(existing).toEqual(dataWithPlaceholder.author); + expect(isReference(incoming)).toBe(true); + expect(readField("__typename", incoming)).toBe("Author"); + break; + case 3: + expect(isReference(existing)).toBe(true); + expect(readField("__typename", existing)).toBe("Author"); + expect(incoming).toEqual(dataWithPlaceholder.author); + break; + default: + throw new Error("unreached"); + } + return incoming; }, }, }, }, - }); + }, + }); - // write the first object, without an ID, placeholder - cache.writeQuery({ - query, - data: dataWithPlaceholder, - }); + // write the first object, without an ID, placeholder + cache.writeQuery({ + query, + data: dataWithPlaceholder, + }); - expect(cache.extract()).toEqual({ - ROOT_QUERY: { - __typename: "Query", - author: { - hello: "Foo", - __typename: "Placeholder", - }, + expect(cache.extract()).toEqual({ + ROOT_QUERY: { + __typename: "Query", + author: { + hello: "Foo", + __typename: "Placeholder", }, - }); + }, + }); - // replace with another one of different type with ID - cache.writeQuery({ - query, - data: dataWithAuthor, - }); + // replace with another one of different type with ID + cache.writeQuery({ + query, + data: dataWithAuthor, + }); - expect(cache.extract()).toEqual({ - "Author:129": { - firstName: "John", - lastName: "Smith", - id: "129", - __typename: "Author", - }, - ROOT_QUERY: { - __typename: "Query", - author: makeReference("Author:129"), - }, - }); + expect(cache.extract()).toEqual({ + "Author:129": { + firstName: "John", + lastName: "Smith", + id: "129", + __typename: "Author", + }, + ROOT_QUERY: { + __typename: "Query", + author: makeReference("Author:129"), + }, + }); - // and go back to the original: - cache.writeQuery({ - query, - data: dataWithPlaceholder, - }); + // and go back to the original: + cache.writeQuery({ + query, + data: dataWithPlaceholder, + }); - // Author__129 will remain in the store, - // but will not be referenced by any of the fields, - // hence we combine, and in that very order - expect(cache.extract()).toEqual({ - "Author:129": { - firstName: "John", - lastName: "Smith", - id: "129", - __typename: "Author", - }, - ROOT_QUERY: { - __typename: "Query", - author: { - hello: "Foo", - __typename: "Placeholder", - }, + // Author__129 will remain in the store, + // but will not be referenced by any of the fields, + // hence we combine, and in that very order + expect(cache.extract()).toEqual({ + "Author:129": { + firstName: "John", + lastName: "Smith", + id: "129", + __typename: "Author", + }, + ROOT_QUERY: { + __typename: "Query", + author: { + hello: "Foo", + __typename: "Placeholder", }, - }); - - resolve(); - } - ); + }, + }); + }); it("does not swallow errors other than field errors", () => { const query = gql` @@ -2888,29 +2882,28 @@ describe("writing to the store", () => { expect(mergeCounts).toEqual({ first: 1, second: 1, third: 1, fourth: 1 }); }); - itAsync( - "should allow silencing broadcast of cache updates", - function (resolve, reject) { - const cache = new InMemoryCache({ - typePolicies: { - Counter: { - // Counter is a singleton, but we want to be able to test - // writing to it with writeFragment, so it needs to have an ID. - keyFields: [], - }, + it("should allow silencing broadcast of cache updates", async () => { + const cache = new InMemoryCache({ + typePolicies: { + Counter: { + // Counter is a singleton, but we want to be able to test + // writing to it with writeFragment, so it needs to have an ID. + keyFields: [], }, - }); + }, + }); - const query = gql` - query { - counter { - count - } + const query = gql` + query { + counter { + count } - `; + } + `; - const results: number[] = []; + const results: number[] = []; + const promise = new Promise((resolve) => { cache.watch({ query, optimistic: true, @@ -2925,101 +2918,103 @@ describe("writing to the store", () => { resolve(); }, }); + }); - let count = 0; - - cache.writeQuery({ - query, - data: { - counter: { - __typename: "Counter", - count: ++count, - }, - }, - broadcast: false, - }); + let count = 0; - expect(cache.extract()).toEqual({ - ROOT_QUERY: { - __typename: "Query", - counter: { __ref: "Counter:{}" }, - }, - "Counter:{}": { + cache.writeQuery({ + query, + data: { + counter: { __typename: "Counter", - count: 1, + count: ++count, }, - }); + }, + broadcast: false, + }); + + expect(cache.extract()).toEqual({ + ROOT_QUERY: { + __typename: "Query", + counter: { __ref: "Counter:{}" }, + }, + "Counter:{}": { + __typename: "Counter", + count: 1, + }, + }); + + expect(results).toEqual([]); + + const counterId = cache.identify({ + __typename: "Counter", + })!; + + cache.writeFragment({ + id: counterId, + fragment: gql` + fragment Count on Counter { + count + } + `, + data: { + count: ++count, + }, + broadcast: false, + }); - expect(results).toEqual([]); + const counterMeta = { + extraRootIds: ["Counter:{}"], + }; - const counterId = cache.identify({ + expect(cache.extract()).toEqual({ + __META: counterMeta, + ROOT_QUERY: { + __typename: "Query", + counter: { __ref: "Counter:{}" }, + }, + "Counter:{}": { __typename: "Counter", - })!; + count: 2, + }, + }); - cache.writeFragment({ + expect(results).toEqual([]); + + expect( + cache.evict({ id: counterId, - fragment: gql` - fragment Count on Counter { - count - } - `, - data: { - count: ++count, - }, + fieldName: "count", broadcast: false, - }); - - const counterMeta = { - extraRootIds: ["Counter:{}"], - }; - - expect(cache.extract()).toEqual({ - __META: counterMeta, - ROOT_QUERY: { - __typename: "Query", - counter: { __ref: "Counter:{}" }, - }, - "Counter:{}": { - __typename: "Counter", - count: 2, - }, - }); + }) + ).toBe(true); - expect(results).toEqual([]); + expect(cache.extract()).toEqual({ + __META: counterMeta, + ROOT_QUERY: { + __typename: "Query", + counter: { __ref: "Counter:{}" }, + }, + "Counter:{}": { + __typename: "Counter", + }, + }); - expect( - cache.evict({ - id: counterId, - fieldName: "count", - broadcast: false, - }) - ).toBe(true); + expect(results).toEqual([]); - expect(cache.extract()).toEqual({ - __META: counterMeta, - ROOT_QUERY: { - __typename: "Query", - counter: { __ref: "Counter:{}" }, - }, - "Counter:{}": { + // Only this write should trigger a broadcast. + cache.writeQuery({ + query, + data: { + counter: { __typename: "Counter", + count: 3, }, - }); - - expect(results).toEqual([]); + }, + }); - // Only this write should trigger a broadcast. - cache.writeQuery({ - query, - data: { - counter: { - __typename: "Counter", - count: 3, - }, - }, - }); - } - ); + await promise; + }); it("writeFragment should be able to infer ROOT_QUERY", () => { const cache = new InMemoryCache(); diff --git a/src/core/__tests__/QueryManager/index.ts b/src/core/__tests__/QueryManager/index.ts index 5d6d9592bcc..160352f709d 100644 --- a/src/core/__tests__/QueryManager/index.ts +++ b/src/core/__tests__/QueryManager/index.ts @@ -4,7 +4,7 @@ import { map } from "rxjs/operators"; import { assign } from "lodash"; import gql from "graphql-tag"; import { DocumentNode, GraphQLError } from "graphql"; -import { setVerbosity } from "ts-invariant"; +import { InvariantError, setVerbosity } from "ts-invariant"; import { Observable, @@ -28,32 +28,22 @@ import { } from "../../../testing/core/mocking/mockLink"; // core -import { ApolloQueryResult } from "../../types"; import { NetworkStatus } from "../../networkStatus"; import { ObservableQuery } from "../../ObservableQuery"; -import { - MutationBaseOptions, - MutationOptions, - WatchQueryOptions, -} from "../../watchQueryOptions"; +import { WatchQueryOptions } from "../../watchQueryOptions"; import { QueryManager } from "../../QueryManager"; import { ApolloError } from "../../../errors"; // testing utils import { waitFor } from "@testing-library/react"; -import wrap from "../../../testing/core/wrap"; -import observableToPromise, { - observableToPromiseAndSubscription, -} from "../../../testing/core/observableToPromise"; -import { itAsync, wait } from "../../../testing/core"; +import { wait } from "../../../testing/core"; import { ApolloClient } from "../../../core"; import { mockFetchQuery } from "../ObservableQuery"; import { Concast, print } from "../../../utilities"; -import { ObservableStream } from "../../../testing/internal"; +import { ObservableStream, spyOnConsole } from "../../../testing/internal"; interface MockedMutation { - reject: (reason: any) => any; mutation: DocumentNode; data?: Object; errors?: GraphQLError[]; @@ -107,23 +97,19 @@ describe("QueryManager", () => { // Helper method that sets up a mockQueryManager and then passes on the // results to an observer. const assertWithObserver = ({ - reject, query, variables = {}, queryOptions = {}, result, error, delay, - observer, }: { - reject: (reason: any) => any; query: DocumentNode; variables?: Object; queryOptions?: Object; error?: Error; result?: FetchResult; delay?: number; - observer: Observer>; }) => { const queryManager = mockQueryManager({ request: { query, variables }, @@ -131,18 +117,13 @@ describe("QueryManager", () => { error, delay, }); - const finalOptions = assign( - { query, variables }, - queryOptions - ) as WatchQueryOptions; - return queryManager.watchQuery(finalOptions).subscribe({ - next: wrap(reject, observer.next!), - error: observer.error, - }); + + return new ObservableStream( + queryManager.watchQuery(assign({ query, variables }, queryOptions)) + ); }; const mockMutation = ({ - reject, mutation, data, errors, @@ -152,7 +133,7 @@ describe("QueryManager", () => { const link = mockSingleLink({ request: { query: mutation, variables }, result: { data, errors }, - }).setOnError(reject); + }); const queryManager = createQueryManager({ link, @@ -174,18 +155,6 @@ describe("QueryManager", () => { }); }; - const assertMutationRoundtrip = ( - resolve: (result: any) => any, - opts: MockedMutation - ) => { - const { reject } = opts; - return mockMutation(opts) - .then(({ result }) => { - expect(result.data).toEqual(opts.data); - }) - .then(resolve, reject); - }; - // Helper method that takes a query with a first response and a second response. // Used to assert stuff about refetches. const mockRefetch = ({ @@ -230,9 +199,8 @@ describe("QueryManager", () => { }; } - itAsync("handles GraphQL errors", (resolve, reject) => { - assertWithObserver({ - reject, + it("handles GraphQL errors", async () => { + const stream = assertWithObserver({ query: gql` query people { allPeople(first: 1) { @@ -246,24 +214,17 @@ describe("QueryManager", () => { result: { errors: [new GraphQLError("This is an error message.")], }, - observer: { - next() { - reject( - new Error("Returned a result when it was supposed to error out") - ); - }, - - error(apolloError) { - expect(apolloError).toBeDefined(); - resolve(); - }, - }, }); + + await expect(stream).toEmitError( + new ApolloError({ + graphQLErrors: [{ message: "This is an error message." }], + }) + ); }); - itAsync("handles GraphQL errors as data", (resolve, reject) => { - assertWithObserver({ - reject, + it("handles GraphQL errors as data", async () => { + const stream = assertWithObserver({ query: gql` query people { allPeople(first: 1) { @@ -280,26 +241,18 @@ describe("QueryManager", () => { result: { errors: [new GraphQLError("This is an error message.")], }, - observer: { - next({ errors }) { - expect(errors).toBeDefined(); - expect(errors![0].message).toBe("This is an error message."); - resolve(); - }, - error(apolloError) { - reject( - new Error( - "Called observer.error instead of passing errors to observer.next" - ) - ); - }, - }, + }); + + await expect(stream).toEmitValue({ + data: undefined, + loading: false, + networkStatus: 8, + errors: [{ message: "This is an error message." }], }); }); - itAsync("handles GraphQL errors with data returned", (resolve, reject) => { - assertWithObserver({ - reject, + it("handles GraphQL errors with data returned", async () => { + const stream = assertWithObserver({ query: gql` query people { allPeople(first: 1) { @@ -319,90 +272,56 @@ describe("QueryManager", () => { }, errors: [new GraphQLError("This is an error message.")], }, - observer: { - next() { - reject(new Error("Returned data when it was supposed to error out.")); - }, - - error(apolloError) { - expect(apolloError).toBeDefined(); - resolve(); - }, - }, }); + + await expect(stream).toEmitError( + new ApolloError({ + graphQLErrors: [{ message: "This is an error message." }], + }) + ); }); - itAsync( - "empty error array (handle non-spec-compliant server) #156", - (resolve, reject) => { - assertWithObserver({ - reject, - query: gql` - query people { - allPeople(first: 1) { - people { - name - } + it("empty error array (handle non-spec-compliant server) #156", async () => { + const stream = assertWithObserver({ + query: gql` + query people { + allPeople(first: 1) { + people { + name } } - `, - result: { - data: { - allPeople: { - people: { - name: "Ada Lovelace", - }, + } + `, + result: { + data: { + allPeople: { + people: { + name: "Ada Lovelace", }, }, - errors: [], }, - observer: { - next(result) { - expect(result.data["allPeople"].people.name).toBe("Ada Lovelace"); - expect(result["errors"]).toBeUndefined(); - resolve(); + errors: [], + }, + }); + + await expect(stream).toEmitValue({ + errors: undefined, + data: { + allPeople: { + people: { + name: "Ada Lovelace", }, }, - }); - } - ); + }, + networkStatus: 7, + loading: false, + }); + }); // Easy to get into this state if you write an incorrect `formatError` // function with graphql-server or express-graphql - itAsync( - "error array with nulls (handle non-spec-compliant server) #1185", - (resolve, reject) => { - assertWithObserver({ - reject, - query: gql` - query people { - allPeople(first: 1) { - people { - name - } - } - } - `, - result: { - errors: [null as any], - }, - observer: { - next() { - reject(new Error("Should not fire next for an error")); - }, - error(error) { - expect((error as any).graphQLErrors).toEqual([null]); - expect(error.message).toBe("Error message not found."); - resolve(); - }, - }, - }); - } - ); - - itAsync("handles network errors", (resolve, reject) => { - assertWithObserver({ - reject, + it("error array with nulls (handle non-spec-compliant server) #1185", async () => { + const stream = assertWithObserver({ query: gql` query people { allPeople(first: 1) { @@ -412,30 +331,20 @@ describe("QueryManager", () => { } } `, - error: new Error("Network error"), - observer: { - next: () => { - reject(new Error("Should not deliver result")); - }, - error: (error) => { - const apolloError = error as ApolloError; - expect(apolloError.networkError).toBeDefined(); - expect(apolloError.networkError!.message).toMatch("Network error"); - resolve(); - }, + result: { + errors: [null as any], }, }); - }); - itAsync("uses console.error to log unhandled errors", (resolve, reject) => { - const oldError = console.error; - let printed: any; - console.error = (...args: any[]) => { - printed = args; - }; + await expect(stream).toEmitError( + new ApolloError({ + graphQLErrors: [null as any], + }) + ); + }); - assertWithObserver({ - reject, + it("handles network errors", async () => { + const stream = assertWithObserver({ query: gql` query people { allPeople(first: 1) { @@ -446,53 +355,71 @@ describe("QueryManager", () => { } `, error: new Error("Network error"), - observer: { - next: () => { - reject(new Error("Should not deliver result")); - }, + }); + + await expect(stream).toEmitError( + new ApolloError({ + networkError: new Error("Network error"), + }) + ); + }); + + it("uses console.error to log unhandled errors", async () => { + using _ = spyOnConsole("error"); + const query = gql` + query people { + allPeople(first: 1) { + people { + name + } + } + } + `; + const error = new Error("Network error"); + + const queryManager = mockQueryManager({ + request: { query }, + error, + }); + + const observable = queryManager.watchQuery({ query }); + observable.subscribe({ + next: () => { + throw new Error("Should not have been called"); }, }); - setTimeout(() => { - expect(printed[0]).toMatch(/error/); - console.error = oldError; - resolve(); - }, 10); + await wait(10); + + expect(console.error).toHaveBeenCalledTimes(1); + expect(console.error).toHaveBeenCalledWith( + "Unhandled error", + "Network error", + expect.anything() + ); }); // XXX this looks like a bug in zen-observable but we should figure // out a solution for it - itAsync.skip( - "handles an unsubscribe action that happens before data returns", - (resolve, reject) => { - const subscription = assertWithObserver({ - reject, - query: gql` - query people { - allPeople(first: 1) { - people { - name - } + it.skip("handles an unsubscribe action that happens before data returns", async () => { + const stream = assertWithObserver({ + query: gql` + query people { + allPeople(first: 1) { + people { + name } } - `, - delay: 1000, - observer: { - next: () => { - reject(new Error("Should not deliver result")); - }, - error: () => { - reject(new Error("Should not deliver result")); - }, - }, - }); + } + `, + delay: 1000, + }); - expect(subscription.unsubscribe).not.toThrow(); - } - ); + expect(stream.unsubscribe).not.toThrow(); + }); // Query should be aborted on last .unsubscribe() - itAsync("causes link unsubscription if unsubscribed", (resolve, reject) => { + it("causes link unsubscription if unsubscribed", async () => { const expResult = { data: { allPeople: { @@ -557,25 +484,18 @@ describe("QueryManager", () => { notifyOnNetworkStatusChange: false, }); - const observerCallback = wrap(reject, () => { - reject(new Error("Link subscription should have been cancelled")); - }); + const stream = new ObservableStream(observableQuery); - const subscription = observableQuery.subscribe({ - next: observerCallback, - error: observerCallback, - complete: observerCallback, - }); + stream.unsubscribe(); - subscription.unsubscribe(); + // Wait longer than the setTimeout in the observable callback + await expect(stream).not.toEmitAnything({ timeout: 150 }); - return waitFor(() => { - expect(onRequestUnsubscribe).toHaveBeenCalledTimes(1); - expect(onRequestSubscribe).toHaveBeenCalledTimes(1); - }).then(resolve, reject); + expect(onRequestUnsubscribe).toHaveBeenCalledTimes(1); + expect(onRequestSubscribe).toHaveBeenCalledTimes(1); }); - itAsync("causes link unsubscription after reobserve", (resolve, reject) => { + it("causes link unsubscription after reobserve", async () => { const expResult = { data: { allPeople: { @@ -654,86 +574,72 @@ describe("QueryManager", () => { variables: request.variables, }); - const observerCallback = wrap(reject, () => { - reject(new Error("Link subscription should have been cancelled")); - }); - - const subscription = observableQuery.subscribe({ - next: observerCallback, - error: observerCallback, - complete: observerCallback, - }); + const stream = new ObservableStream(observableQuery); expect(onRequestSubscribe).toHaveBeenCalledTimes(1); // This is the most important part of this test // Check that reobserve cancels the previous connection while watchQuery remains active - observableQuery.reobserve({ variables: { offset: 20 } }); + void observableQuery.reobserve({ variables: { offset: 20 } }); - return waitFor(() => { + await waitFor(() => { // Verify that previous connection was aborted by reobserve expect(onRequestUnsubscribe).toHaveBeenCalledTimes(1); - }) - .then(async () => { - subscription.unsubscribe(); - await waitFor(() => { - expect(onRequestSubscribe).toHaveBeenCalledTimes(2); - }); - await waitFor(() => { - expect(onRequestUnsubscribe).toHaveBeenCalledTimes(2); - }); - }) - .then(resolve, reject); + }); + + stream.unsubscribe(); + + await expect(stream).not.toEmitAnything(); + + expect(onRequestSubscribe).toHaveBeenCalledTimes(2); + expect(onRequestUnsubscribe).toHaveBeenCalledTimes(2); }); - itAsync( - "supports interoperability with other Observable implementations like RxJS", - (resolve, reject) => { - const expResult = { - data: { - allPeople: { - people: [ - { - name: "Luke Skywalker", - }, - ], - }, + it("supports interoperability with other Observable implementations like RxJS", async () => { + const expResult = { + data: { + allPeople: { + people: [ + { + name: "Luke Skywalker", + }, + ], }, - }; + }, + }; - const handle = mockWatchQuery({ - request: { - query: gql` - query people { - allPeople(first: 1) { - people { - name - } + const handle = mockWatchQuery({ + request: { + query: gql` + query people { + allPeople(first: 1) { + people { + name } } - `, - }, - result: expResult, - }); + } + `, + }, + result: expResult, + }); - const observable = from(handle as any); + const observable = from(handle as any); - observable - .pipe(map((result) => assign({ fromRx: true }, result))) - .subscribe({ - next: wrap(reject, (newResult) => { - const expectedResult = assign( - { fromRx: true, loading: false, networkStatus: 7 }, - expResult - ); - expect(newResult).toEqual(expectedResult); - resolve(); - }), - }); - } - ); + const stream = new ObservableStream( + observable.pipe( + map((result) => assign({ fromRx: true }, result)) + ) as unknown as Observable + ); + + await expect(stream).toEmitValue({ + fromRx: true, + loading: false, + networkStatus: 7, + ...expResult, + }); + }); - itAsync("allows you to subscribe twice to one query", (resolve, reject) => { + it("allows you to subscribe twice to one query", async () => { const request = { query: gql` query fetchLuke($id: String) { @@ -782,59 +688,30 @@ describe("QueryManager", () => { } ); - let subOneCount = 0; - // pre populate data to avoid contention - queryManager.query(request).then(() => { - const handle = queryManager.watchQuery(request); + await queryManager.query(request); - const subOne = handle.subscribe({ - next(result) { - subOneCount++; + const handle = queryManager.watchQuery(request); - if (subOneCount === 1) { - expect(result.data).toEqual(data1); - } else if (subOneCount === 2) { - expect(result.data).toEqual(data2); - } - }, - }); + const stream1 = new ObservableStream(handle); + const stream2 = new ObservableStream(handle); - let subTwoCount = 0; - handle.subscribe({ - next(result) { - subTwoCount++; - if (subTwoCount === 1) { - expect(result.data).toEqual(data1); - handle.refetch(); - } else if (subTwoCount === 2) { - expect(result.data).toEqual(data2); - setTimeout(() => { - try { - expect(subOneCount).toBe(2); - - subOne.unsubscribe(); - handle.refetch(); - } catch (e) { - reject(e); - } - }, 0); - } else if (subTwoCount === 3) { - setTimeout(() => { - try { - expect(subOneCount).toBe(2); - resolve(); - } catch (e) { - reject(e); - } - }, 0); - } - }, - }); - }); + await expect(stream1).toEmitMatchedValue({ data: data1 }); + await expect(stream2).toEmitMatchedValue({ data: data1 }); + + void handle.refetch(); + + await expect(stream1).toEmitMatchedValue({ data: data2 }); + await expect(stream2).toEmitMatchedValue({ data: data2 }); + + stream1.unsubscribe(); + void handle.refetch(); + + await expect(stream1).not.toEmitAnything(); + await expect(stream2).toEmitMatchedValue({ data: data3 }); }); - itAsync("resolves all queries when one finishes after another", (resolve) => { + it("resolves all queries when one finishes after another", async () => { const request = { query: gql` query fetchLuke($id: String) { @@ -914,23 +791,16 @@ describe("QueryManager", () => { const ob2 = queryManager.watchQuery(request2); const ob3 = queryManager.watchQuery(request3); - let finishCount = 0; - ob1.subscribe((result) => { - expect(result.data).toEqual(data1); - finishCount++; - }); - ob2.subscribe((result) => { - expect(result.data).toEqual(data2); - expect(finishCount).toBe(2); - resolve(); - }); - ob3.subscribe((result) => { - expect(result.data).toEqual(data3); - finishCount++; - }); + const stream1 = new ObservableStream(ob1); + const stream2 = new ObservableStream(ob2); + const stream3 = new ObservableStream(ob3); + + await expect(stream1).toEmitMatchedValue({ data: data1 }); + await expect(stream2).toEmitMatchedValue({ data: data2 }); + await expect(stream3).toEmitMatchedValue({ data: data3 }); }); - itAsync("allows you to refetch queries", (resolve, reject) => { + it("allows you to refetch queries", async () => { const request = { query: gql` query fetchLuke($id: String) { @@ -963,336 +833,220 @@ describe("QueryManager", () => { }); const observable = queryManager.watchQuery(request); - return observableToPromise( - { observable }, - (result) => { - expect(result.data).toEqual(data1); - observable.refetch(); - }, - (result) => expect(result.data).toEqual(data2) - ).then(resolve, reject); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitMatchedValue({ data: data1 }); + void observable.refetch(); + await expect(stream).toEmitMatchedValue({ data: data2 }); }); - itAsync( - "will return referentially equivalent data if nothing changed in a refetch", - (resolve, reject) => { - const request: WatchQueryOptions = { - query: gql` - { - a - b { - c - } - d { - e - f { - g - } + it("will return referentially equivalent data if nothing changed in a refetch", async () => { + const request: WatchQueryOptions = { + query: gql` + { + a + b { + c + } + d { + e + f { + g } } - `, - notifyOnNetworkStatusChange: false, - canonizeResults: true, - }; + } + `, + notifyOnNetworkStatusChange: false, + canonizeResults: true, + }; - const data1 = { - a: 1, - b: { c: 2 }, - d: { e: 3, f: { g: 4 } }, - }; + const data1 = { + a: 1, + b: { c: 2 }, + d: { e: 3, f: { g: 4 } }, + }; - const data2 = { - a: 1, - b: { c: 2 }, - d: { e: 30, f: { g: 4 } }, - }; + const data2 = { + a: 1, + b: { c: 2 }, + d: { e: 30, f: { g: 4 } }, + }; - const data3 = { - a: 1, - b: { c: 2 }, - d: { e: 3, f: { g: 4 } }, - }; + const data3 = { + a: 1, + b: { c: 2 }, + d: { e: 3, f: { g: 4 } }, + }; - const queryManager = mockRefetch({ - request, - firstResult: { data: data1 }, - secondResult: { data: data2 }, - thirdResult: { data: data3 }, - }); + const queryManager = mockRefetch({ + request, + firstResult: { data: data1 }, + secondResult: { data: data2 }, + thirdResult: { data: data3 }, + }); - const observable = queryManager.watchQuery(request); + const observable = queryManager.watchQuery(request); + const stream = new ObservableStream(observable); - let count = 0; - let firstResultData: any; + const { data: firstResultData } = await stream.takeNext(); + expect(firstResultData).toEqual(data1); - observable.subscribe({ - next: (result) => { - try { - switch (count++) { - case 0: - expect(result.data).toEqual(data1); - firstResultData = result.data; - observable.refetch(); - break; - case 1: - expect(result.data).toEqual(data2); - expect(result.data).not.toEqual(firstResultData); - expect(result.data.b).toEqual(firstResultData.b); - expect(result.data.d).not.toEqual(firstResultData.d); - expect(result.data.d.f).toEqual(firstResultData.d.f); - observable.refetch(); - break; - case 2: - expect(result.data).toEqual(data3); - expect(result.data).toBe(firstResultData); - resolve(); - break; - default: - throw new Error("Next run too many times."); - } - } catch (error) { - reject(error); - } - }, - error: reject, - }); - } - ); + void observable.refetch(); - itAsync( - "will return referentially equivalent data in getCurrentResult if nothing changed", - (resolve, reject) => { - const request = { - query: gql` - { - a - b { - c - } - d { - e - f { - g - } - } - } - `, - notifyOnNetworkStatusChange: false, - }; + { + const result = await stream.takeNext(); - const data1 = { - a: 1, - b: { c: 2 }, - d: { e: 3, f: { g: 4 } }, - }; + expect(result.data).toEqual(data2); + expect(result.data).not.toEqual(firstResultData); + expect(result.data.b).toEqual(firstResultData.b); + expect(result.data.d).not.toEqual(firstResultData.d); + expect(result.data.d.f).toEqual(firstResultData.d.f); + } - const queryManager = mockQueryManager({ - request, - result: { data: data1 }, - }); + void observable.refetch(); - const observable = queryManager.watchQuery(request); + { + const result = await stream.takeNext(); - observable.subscribe({ - next: (result) => { - try { - expect(result.data).toEqual(data1); - expect(result.data).toEqual(observable.getCurrentResult().data); - resolve(); - } catch (error) { - reject(error); - } - }, - error: reject, - }); + expect(result.data).toEqual(data3); + expect(result.data).toBe(firstResultData); } - ); + }); - itAsync( - "sets networkStatus to `refetch` when refetching", - (resolve, reject) => { - const request: WatchQueryOptions = { - query: gql` - query fetchLuke($id: String) { - people_one(id: $id) { - name + it("will return referentially equivalent data in getCurrentResult if nothing changed", async () => { + const request = { + query: gql` + { + a + b { + c + } + d { + e + f { + g } } - `, - variables: { - id: "1", - }, - notifyOnNetworkStatusChange: true, - // This causes a loading:true result to be delivered from the cache - // before the final data2 result is delivered. - fetchPolicy: "cache-and-network", - }; - const data1 = { - people_one: { - name: "Luke Skywalker", - }, - }; - - const data2 = { - people_one: { - name: "Luke Skywalker has a new name", - }, - }; - - const queryManager = mockRefetch({ - request, - firstResult: { data: data1 }, - secondResult: { data: data2 }, - }); - - const observable = queryManager.watchQuery(request); - return observableToPromise( - { observable }, - (result) => { - expect(result.data).toEqual(data1); - observable.refetch(); - }, - (result) => expect(result.networkStatus).toBe(NetworkStatus.refetch), - (result) => { - expect(result.networkStatus).toBe(NetworkStatus.ready); - expect(result.data).toEqual(data2); } - ).then(resolve, reject); - } - ); + `, + notifyOnNetworkStatusChange: false, + }; - itAsync( - "allows you to refetch queries with promises", - async (resolve, reject) => { - const request = { - query: gql` - { - people_one(id: 1) { - name - } - } - `, - }; - const data1 = { - people_one: { - name: "Luke Skywalker", - }, - }; + const data1 = { + a: 1, + b: { c: 2 }, + d: { e: 3, f: { g: 4 } }, + }; - const data2 = { - people_one: { - name: "Luke Skywalker has a new name", - }, - }; + const queryManager = mockQueryManager({ + request, + result: { data: data1 }, + }); - const queryManager = mockRefetch({ - request, - firstResult: { data: data1 }, - secondResult: { data: data2 }, - }); + const observable = queryManager.watchQuery(request); + const stream = new ObservableStream(observable); - const handle = queryManager.watchQuery(request); - handle.subscribe({}); + const { data } = await stream.takeNext(); - return handle - .refetch() - .then((result) => expect(result.data).toEqual(data2)) - .then(resolve, reject); - } - ); + expect(data).toEqual(data1); + expect(data).toBe(observable.getCurrentResult().data); + }); - itAsync( - "allows you to refetch queries with new variables", - (resolve, reject) => { - const query = gql` - { - people_one(id: 1) { + it("sets networkStatus to `refetch` when refetching", async () => { + const request: WatchQueryOptions = { + query: gql` + query fetchLuke($id: String) { + people_one(id: $id) { name } } - `; + `, + variables: { + id: "1", + }, + notifyOnNetworkStatusChange: true, + // This causes a loading:true result to be delivered from the cache + // before the final data2 result is delivered. + fetchPolicy: "cache-and-network", + }; + const data1 = { + people_one: { + name: "Luke Skywalker", + }, + }; - const data1 = { - people_one: { - name: "Luke Skywalker", - }, - }; + const data2 = { + people_one: { + name: "Luke Skywalker has a new name", + }, + }; - const data2 = { - people_one: { - name: "Luke Skywalker has a new name", - }, - }; + const queryManager = mockRefetch({ + request, + firstResult: { data: data1 }, + secondResult: { data: data2 }, + }); - const data3 = { - people_one: { - name: "Luke Skywalker has a new name and age", - }, - }; + const observable = queryManager.watchQuery(request); + const stream = new ObservableStream(observable); - const data4 = { - people_one: { - name: "Luke Skywalker has a whole new bag", - }, - }; + await expect(stream).toEmitValue({ + data: data1, + loading: false, + networkStatus: NetworkStatus.ready, + }); - const variables1 = { - test: "I am your father", - }; + void observable.refetch(); - const variables2 = { - test: "No. No! That's not true! That's impossible!", - }; + await expect(stream).toEmitValue({ + data: data1, + loading: true, + networkStatus: NetworkStatus.refetch, + }); + await expect(stream).toEmitValue({ + data: data2, + loading: false, + networkStatus: NetworkStatus.ready, + }); + }); - const queryManager = mockQueryManager( - { - request: { query: query }, - result: { data: data1 }, - }, - { - request: { query: query }, - result: { data: data2 }, - }, - { - request: { query: query, variables: variables1 }, - result: { data: data3 }, - }, + it("allows you to refetch queries with promises", async () => { + const request = { + query: gql` { - request: { query: query, variables: variables2 }, - result: { data: data4 }, + people_one(id: 1) { + name + } } - ); + `, + }; + const data1 = { + people_one: { + name: "Luke Skywalker", + }, + }; - const observable = queryManager.watchQuery({ - query, - notifyOnNetworkStatusChange: false, - }); - return observableToPromise( - { observable }, - (result) => { - expect(result.loading).toBe(false); - expect(result.data).toEqual(data1); - return observable.refetch(); - }, - (result) => { - expect(result.loading).toBe(false); - expect(result.data).toEqual(data2); - return observable.refetch(variables1); - }, - (result) => { - expect(result.loading).toBe(false); - expect(result.data).toEqual(data3); - return observable.refetch(variables2); - }, - (result) => { - expect(result.loading).toBe(false); - expect(result.data).toEqual(data4); - } - ).then(resolve, reject); - } - ); + const data2 = { + people_one: { + name: "Luke Skywalker has a new name", + }, + }; + + const queryManager = mockRefetch({ + request, + firstResult: { data: data1 }, + secondResult: { data: data2 }, + }); + + const handle = queryManager.watchQuery(request); + handle.subscribe({}); + + const result = await handle.refetch(); + + expect(result.data).toEqual(data2); + }); - itAsync("only modifies varaibles when refetching", (resolve, reject) => { + it("allows you to refetch queries with new variables", async () => { const query = gql` { people_one(id: 1) { @@ -1313,6 +1067,26 @@ describe("QueryManager", () => { }, }; + const data3 = { + people_one: { + name: "Luke Skywalker has a new name and age", + }, + }; + + const data4 = { + people_one: { + name: "Luke Skywalker has a whole new bag", + }, + }; + + const variables1 = { + test: "I am your father", + }; + + const variables2 = { + test: "No. No! That's not true! That's impossible!", + }; + const queryManager = mockQueryManager( { request: { query: query }, @@ -1321,6 +1095,14 @@ describe("QueryManager", () => { { request: { query: query }, result: { data: data2 }, + }, + { + request: { query: query, variables: variables1 }, + result: { data: data3 }, + }, + { + request: { query: query, variables: variables2 }, + result: { data: data4 }, } ); @@ -1328,24 +1110,40 @@ describe("QueryManager", () => { query, notifyOnNetworkStatusChange: false, }); - const originalOptions = assign({}, observable.options); - return observableToPromise( - { observable }, - (result) => { - expect(result.data).toEqual(data1); - observable.refetch(); - }, - (result) => { - expect(result.data).toEqual(data2); - const updatedOptions = assign({}, observable.options); - delete originalOptions.variables; - delete updatedOptions.variables; - expect(updatedOptions).toEqual(originalOptions); - } - ).then(resolve, reject); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue({ + data: data1, + loading: false, + networkStatus: NetworkStatus.ready, + }); + + void observable.refetch(); + + await expect(stream).toEmitValue({ + data: data2, + loading: false, + networkStatus: NetworkStatus.ready, + }); + + void observable.refetch(variables1); + + await expect(stream).toEmitValue({ + data: data3, + loading: false, + networkStatus: NetworkStatus.ready, + }); + + void observable.refetch(variables2); + + await expect(stream).toEmitValue({ + data: data4, + loading: false, + networkStatus: NetworkStatus.ready, + }); }); - itAsync("continues to poll after refetch", (resolve, reject) => { + it("only modifies varaibles when refetching", async () => { const query = gql` { people_one(id: 1) { @@ -1366,115 +1164,184 @@ describe("QueryManager", () => { }, }; - const data3 = { - people_one: { - name: "Patsy", - }, - }; - const queryManager = mockQueryManager( { - request: { query }, + request: { query: query }, result: { data: data1 }, }, { - request: { query }, + request: { query: query }, result: { data: data2 }, - }, - { - request: { query }, - result: { data: data3 }, } ); const observable = queryManager.watchQuery({ query, - pollInterval: 200, notifyOnNetworkStatusChange: false, }); + const stream = new ObservableStream(observable); + const originalOptions = assign({}, observable.options); - return observableToPromise( - { observable }, - (result) => { - expect(result.data).toEqual(data1); - observable.refetch(); - }, - (result) => expect(result.data).toEqual(data2), - (result) => { - expect(result.data).toEqual(data3); - observable.stopPolling(); - } - ).then(resolve, reject); - }); + await expect(stream).toEmitValue({ + data: data1, + loading: false, + networkStatus: NetworkStatus.ready, + }); - itAsync( - "sets networkStatus to `poll` if a polling query is in flight", - (resolve) => { - const query = gql` - { - people_one(id: 1) { - name - } + void observable.refetch(); + + await expect(stream).toEmitValue({ + data: data2, + loading: false, + networkStatus: NetworkStatus.ready, + }); + + const updatedOptions = assign({}, observable.options); + delete originalOptions.variables; + delete updatedOptions.variables; + expect(updatedOptions).toEqual(originalOptions); + }); + + it("continues to poll after refetch", async () => { + const query = gql` + { + people_one(id: 1) { + name } - `; + } + `; - const data1 = { - people_one: { - name: "Luke Skywalker", - }, - }; + const data1 = { + people_one: { + name: "Luke Skywalker", + }, + }; - const data2 = { - people_one: { - name: "Luke Skywalker has a new name", - }, - }; + const data2 = { + people_one: { + name: "Luke Skywalker has a new name", + }, + }; - const data3 = { - people_one: { - name: "Patsy", - }, - }; + const data3 = { + people_one: { + name: "Patsy", + }, + }; - const queryManager = mockQueryManager( - { - request: { query }, - result: { data: data1 }, - }, - { - request: { query }, - result: { data: data2 }, - }, - { - request: { query }, - result: { data: data3 }, + const queryManager = mockQueryManager( + { + request: { query }, + result: { data: data1 }, + }, + { + request: { query }, + result: { data: data2 }, + }, + { + request: { query }, + result: { data: data3 }, + } + ); + + const observable = queryManager.watchQuery({ + query, + pollInterval: 200, + notifyOnNetworkStatusChange: false, + }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue({ + data: data1, + loading: false, + networkStatus: NetworkStatus.ready, + }); + + void observable.refetch(); + + await expect(stream).toEmitValue({ + data: data2, + loading: false, + networkStatus: NetworkStatus.ready, + }); + + await expect(stream).toEmitValue( + { + data: data3, + loading: false, + networkStatus: NetworkStatus.ready, + }, + { timeout: 250 } + ); + + observable.stopPolling(); + }); + + it("sets networkStatus to `poll` if a polling query is in flight", async () => { + const query = gql` + { + people_one(id: 1) { + name } - ); + } + `; - const observable = queryManager.watchQuery({ - query, - pollInterval: 30, - notifyOnNetworkStatusChange: true, - }); + const data1 = { + people_one: { + name: "Luke Skywalker", + }, + }; - let counter = 0; - const handle = observable.subscribe({ - next(result) { - counter += 1; - - if (counter === 1) { - expect(result.networkStatus).toBe(NetworkStatus.ready); - } else if (counter === 2) { - expect(result.networkStatus).toBe(NetworkStatus.poll); - handle.unsubscribe(); - resolve(); - } - }, - }); - } - ); + const data2 = { + people_one: { + name: "Luke Skywalker has a new name", + }, + }; + + const data3 = { + people_one: { + name: "Patsy", + }, + }; + + const queryManager = mockQueryManager( + { + request: { query }, + result: { data: data1 }, + }, + { + request: { query }, + result: { data: data2 }, + }, + { + request: { query }, + result: { data: data3 }, + } + ); + + const observable = queryManager.watchQuery({ + query, + pollInterval: 30, + notifyOnNetworkStatusChange: true, + }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue({ + data: data1, + loading: false, + networkStatus: NetworkStatus.ready, + }); + + await expect(stream).toEmitValue({ + data: data1, + loading: true, + networkStatus: NetworkStatus.poll, + }); + + stream.unsubscribe(); + }); - itAsync("can handle null values in arrays (#1551)", (resolve) => { + it("can handle null values in arrays (#1551)", async () => { const query = gql` { list { @@ -1488,72 +1355,61 @@ describe("QueryManager", () => { result: { data }, }); const observable = queryManager.watchQuery({ query }); + const stream = new ObservableStream(observable); - observable.subscribe({ - next: (result) => { - expect(result.data).toEqual(data); - expect(observable.getCurrentResult().data).toEqual(data); - resolve(); - }, - }); + await expect(stream).toEmitMatchedValue({ data }); + expect(observable.getCurrentResult().data).toEqual(data); }); - itAsync( - "supports cache-only fetchPolicy fetching only cached data", - (resolve, reject) => { - const primeQuery = gql` - query primeQuery { - luke: people_one(id: 1) { - name - } + it("supports cache-only fetchPolicy fetching only cached data", async () => { + const primeQuery = gql` + query primeQuery { + luke: people_one(id: 1) { + name } - `; + } + `; - const complexQuery = gql` - query complexQuery { - luke: people_one(id: 1) { - name - } - vader: people_one(id: 4) { - name - } + const complexQuery = gql` + query complexQuery { + luke: people_one(id: 1) { + name } - `; + vader: people_one(id: 4) { + name + } + } + `; - const data1 = { - luke: { - name: "Luke Skywalker", - }, - }; + const data1 = { + luke: { + name: "Luke Skywalker", + }, + }; - const queryManager = mockQueryManager({ - request: { query: primeQuery }, - result: { data: data1 }, - }); + const queryManager = mockQueryManager({ + request: { query: primeQuery }, + result: { data: data1 }, + }); - // First, prime the cache - return queryManager - .query({ - query: primeQuery, - }) - .then(() => { - const handle = queryManager.watchQuery({ - query: complexQuery, - fetchPolicy: "cache-only", - }); + // First, prime the cache + await queryManager.query({ + query: primeQuery, + }); - return handle.result().then((result) => { - expect(result.data["luke"].name).toBe("Luke Skywalker"); - expect(result.data).not.toHaveProperty("vader"); - }); - }) - .then(resolve, reject); - } - ); + const handle = queryManager.watchQuery({ + query: complexQuery, + fetchPolicy: "cache-only", + }); + + const result = await handle.result(); - itAsync("runs a mutation", (resolve, reject) => { - return assertMutationRoundtrip(resolve, { - reject, + expect(result.data["luke"].name).toBe("Luke Skywalker"); + expect(result.data).not.toHaveProperty("vader"); + }); + + it("runs a mutation", async () => { + const { result } = await mockMutation({ mutation: gql` mutation makeListPrivate { makeListPrivate(id: "5") @@ -1561,31 +1417,29 @@ describe("QueryManager", () => { `, data: { makeListPrivate: true }, }); + + expect(result.data).toEqual({ makeListPrivate: true }); }); - itAsync( - "runs a mutation even when errors is empty array #2912", - (resolve, reject) => { - return assertMutationRoundtrip(resolve, { - reject, - mutation: gql` - mutation makeListPrivate { - makeListPrivate(id: "5") - } - `, - errors: [], - data: { makeListPrivate: true }, - }); - } - ); + it("runs a mutation even when errors is empty array #2912", async () => { + const { result } = await mockMutation({ + mutation: gql` + mutation makeListPrivate { + makeListPrivate(id: "5") + } + `, + errors: [], + data: { makeListPrivate: true }, + }); + + expect(result.data).toEqual({ makeListPrivate: true }); + }); - itAsync( - 'runs a mutation with default errorPolicy equal to "none"', - (resolve, reject) => { - const errors = [new GraphQLError("foo")]; + it('runs a mutation with default errorPolicy equal to "none"', async () => { + const errors = [new GraphQLError("foo")]; - return mockMutation({ - reject, + await expect( + mockMutation({ mutation: gql` mutation makeListPrivate { makeListPrivate(id: "5") @@ -1593,23 +1447,15 @@ describe("QueryManager", () => { `, errors, }) - .then( - (result) => { - throw new Error( - "Mutation should not be successful with default errorPolicy" - ); - }, - (error) => { - expect(error.graphQLErrors).toEqual(errors); - } - ) - .then(resolve, reject); - } - ); + ).rejects.toThrow( + expect.objectContaining({ + graphQLErrors: errors, + }) + ); + }); - itAsync("runs a mutation with variables", (resolve, reject) => { - return assertMutationRoundtrip(resolve, { - reject, + it("runs a mutation with variables", async () => { + const { result } = await mockMutation({ mutation: gql` mutation makeListPrivate($listId: ID!) { makeListPrivate(id: $listId) @@ -1618,126 +1464,108 @@ describe("QueryManager", () => { variables: { listId: "1" }, data: { makeListPrivate: true }, }); + + expect(result.data).toEqual({ makeListPrivate: true }); }); const getIdField = (obj: any) => obj.id; - itAsync( - "runs a mutation with object parameters and puts the result in the store", - (resolve, reject) => { - const data = { - makeListPrivate: { - id: "5", - isPrivate: true, - }, - }; - return mockMutation({ - reject, - mutation: gql` - mutation makeListPrivate { - makeListPrivate(input: { id: "5" }) { - id - isPrivate - } + it("runs a mutation with object parameters and puts the result in the store", async () => { + const data = { + makeListPrivate: { + id: "5", + isPrivate: true, + }, + }; + const { result, queryManager } = await mockMutation({ + mutation: gql` + mutation makeListPrivate { + makeListPrivate(input: { id: "5" }) { + id + isPrivate } - `, - data, - config: { dataIdFromObject: getIdField }, - }) - .then(({ result, queryManager }) => { - expect(result.data).toEqual(data); + } + `, + data, + config: { dataIdFromObject: getIdField }, + }); - // Make sure we updated the store with the new data - expect(queryManager.cache.extract()["5"]).toEqual({ - id: "5", - isPrivate: true, - }); - }) - .then(resolve, reject); - } - ); + expect(result.data).toEqual(data); - itAsync( - "runs a mutation and puts the result in the store", - (resolve, reject) => { - const data = { - makeListPrivate: { - id: "5", - isPrivate: true, - }, - }; + // Make sure we updated the store with the new data + expect(queryManager.cache.extract()["5"]).toEqual({ + id: "5", + isPrivate: true, + }); + }); - return mockMutation({ - reject, - mutation: gql` - mutation makeListPrivate { - makeListPrivate(id: "5") { - id - isPrivate - } - } - `, - data, - config: { dataIdFromObject: getIdField }, - }) - .then(({ result, queryManager }) => { - expect(result.data).toEqual(data); - - // Make sure we updated the store with the new data - expect(queryManager.cache.extract()["5"]).toEqual({ - id: "5", - isPrivate: true, - }); - }) - .then(resolve, reject); - } - ); + it("runs a mutation and puts the result in the store", async () => { + const data = { + makeListPrivate: { + id: "5", + isPrivate: true, + }, + }; - itAsync( - "runs a mutation and puts the result in the store with root key", - (resolve, reject) => { - const mutation = gql` + const { result, queryManager } = await mockMutation({ + mutation: gql` mutation makeListPrivate { makeListPrivate(id: "5") { id isPrivate } } - `; + `, + data, + config: { dataIdFromObject: getIdField }, + }); - const data = { - makeListPrivate: { - id: "5", - isPrivate: true, - }, - }; + expect(result.data).toEqual(data); - const queryManager = createQueryManager({ - link: mockSingleLink({ - request: { query: mutation }, - result: { data }, - }).setOnError(reject), - config: { dataIdFromObject: getIdField }, - }); + // Make sure we updated the store with the new data + expect(queryManager.cache.extract()["5"]).toEqual({ + id: "5", + isPrivate: true, + }); + }); - return queryManager - .mutate({ - mutation, - }) - .then((result) => { - expect(result.data).toEqual(data); + it("runs a mutation and puts the result in the store with root key", async () => { + const mutation = gql` + mutation makeListPrivate { + makeListPrivate(id: "5") { + id + isPrivate + } + } + `; - // Make sure we updated the store with the new data - expect(queryManager.cache.extract()["5"]).toEqual({ - id: "5", - isPrivate: true, - }); - }) - .then(resolve, reject); - } - ); + const data = { + makeListPrivate: { + id: "5", + isPrivate: true, + }, + }; + + const queryManager = createQueryManager({ + link: mockSingleLink({ + request: { query: mutation }, + result: { data }, + }), + config: { dataIdFromObject: getIdField }, + }); + + const result = await queryManager.mutate({ mutation }); + + expect(result.data).toEqual(data); - itAsync(`doesn't return data while query is loading`, (resolve, reject) => { + // Make sure we updated the store with the new data + expect(queryManager.cache.extract()["5"]).toEqual({ + id: "5", + isPrivate: true, + }); + }); + + it(`doesn't return data while query is loading`, async () => { const query1 = gql` { people_one(id: 1) { @@ -1781,14 +1609,11 @@ describe("QueryManager", () => { const observable1 = queryManager.watchQuery({ query: query1 }); const observable2 = queryManager.watchQuery({ query: query2 }); - return Promise.all([ - observableToPromise({ observable: observable1 }, (result) => - expect(result.data).toEqual(data1) - ), - observableToPromise({ observable: observable2 }, (result) => - expect(result.data).toEqual(data2) - ), - ]).then(resolve, reject); + const stream1 = new ObservableStream(observable1); + const stream2 = new ObservableStream(observable2); + + await expect(stream1).toEmitMatchedValue({ data: data1 }); + await expect(stream2).toEmitMatchedValue({ data: data2 }); }); it("updates result of previous query if the result of a new query overlaps", async () => { @@ -1867,7 +1692,7 @@ describe("QueryManager", () => { await expect(stream).not.toEmitAnything(); }); - itAsync("warns if you forget the template literal tag", async (resolve) => { + it("warns if you forget the template literal tag", async () => { const queryManager = mockQueryManager(); expect(() => { void queryManager.query({ @@ -1889,57 +1714,49 @@ describe("QueryManager", () => { query: "string" as any as DocumentNode, }); }).toThrowError(/wrap the query string in a "gql" tag/); - - resolve(); }); - itAsync( - "should transform queries correctly when given a QueryTransformer", - (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } + it("should transform queries correctly when given a QueryTransformer", async () => { + const query = gql` + query { + author { + firstName + lastName } - `; - const transformedQuery = gql` - query { - author { - firstName - lastName - __typename - } + } + `; + const transformedQuery = gql` + query { + author { + firstName + lastName + __typename } - `; + } + `; - const transformedQueryResult = { - author: { - firstName: "John", - lastName: "Smith", - __typename: "Author", - }, - }; + const transformedQueryResult = { + author: { + firstName: "John", + lastName: "Smith", + __typename: "Author", + }, + }; - //make sure that the query is transformed within the query - //manager - createQueryManager({ - link: mockSingleLink({ - request: { query: transformedQuery }, - result: { data: transformedQueryResult }, - }).setOnError(reject), - config: { addTypename: true }, - }) - .query({ query: query }) - .then((result) => { - expect(result.data).toEqual(transformedQueryResult); - }) - .then(resolve, reject); - } - ); + //make sure that the query is transformed within the query + //manager + const result = await createQueryManager({ + link: mockSingleLink({ + request: { query: transformedQuery }, + result: { data: transformedQueryResult }, + }), + config: { addTypename: true }, + }).query({ query: query }); + + expect(result.data).toEqual(transformedQueryResult); + }); - itAsync("should transform mutations correctly", (resolve, reject) => { + it("should transform mutations correctly", async () => { const mutation = gql` mutation { createAuthor(firstName: "John", lastName: "Smith") { @@ -1966,1667 +1783,1933 @@ describe("QueryManager", () => { }, }; - createQueryManager({ + const result = await createQueryManager({ link: mockSingleLink({ request: { query: transformedMutation }, result: { data: transformedMutationResult }, - }).setOnError(reject), + }), config: { addTypename: true }, - }) - .mutate({ mutation: mutation }) - .then((result) => { - expect(result.data).toEqual(transformedMutationResult); - resolve(); - }); + }).mutate({ mutation: mutation }); + + expect(result.data).toEqual(transformedMutationResult); }); - itAsync( - "should reject a query promise given a network error", - (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } + it("should reject a query promise given a network error", async () => { + const query = gql` + query { + author { + firstName + lastName } - `; - const networkError = new Error("Network error"); + } + `; + const networkError = new Error("Network error"); + + await expect( mockQueryManager({ request: { query }, error: networkError, - }) - .query({ query }) - .then(() => { - reject(new Error("Returned result on an errored fetchQuery")); - }) - .catch((error) => { - const apolloError = error as ApolloError; - - expect(apolloError.message).toBeDefined(); - expect(apolloError.networkError).toBe(networkError); - expect(apolloError.graphQLErrors).toEqual([]); - resolve(); - }) - .then(resolve, reject); - } - ); + }).query({ query }) + ).rejects.toEqual(new ApolloError({ networkError })); + }); - itAsync( - "should reject a query promise given a GraphQL error", - (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } + it("should reject a query promise given a GraphQL error", async () => { + const query = gql` + query { + author { + firstName + lastName } - `; - const graphQLErrors = [new GraphQLError("GraphQL error")]; - return mockQueryManager({ + } + `; + const graphQLErrors = [new GraphQLError("GraphQL error")]; + await expect( + mockQueryManager({ request: { query }, result: { errors: graphQLErrors }, - }) - .query({ query }) - .then( - () => { - throw new Error("Returned result on an errored fetchQuery"); - }, - // don't use .catch() for this or it will catch the above error - (error) => { - const apolloError = error as ApolloError; - expect(apolloError.graphQLErrors).toEqual(graphQLErrors); - expect(!apolloError.networkError).toBeTruthy(); - } - ) - .then(resolve, reject); - } - ); - - itAsync( - "should not empty the store when a non-polling query fails due to a network error", - (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } - } - `; - const data = { - author: { - firstName: "Dhaivat", - lastName: "Pandya", - }, - }; - const queryManager = mockQueryManager( - { - request: { query }, - result: { data }, - }, - { - request: { query }, - error: new Error("Network error ocurred"), - } - ); - queryManager - .query({ query }) - .then((result) => { - expect(result.data).toEqual(data); - - queryManager - .query({ query, fetchPolicy: "network-only" }) - .then(() => { - reject( - new Error("Returned a result when it was not supposed to.") - ); - }) - .catch(() => { - // make that the error thrown doesn't empty the state - expect(queryManager.cache.extract().ROOT_QUERY!.author).toEqual( - data.author - ); - resolve(); - }); - }) - .catch(() => { - reject(new Error("Threw an error on the first query.")); - }); - } - ); + }).query({ query }) + ).rejects.toEqual(new ApolloError({ graphQLErrors })); + }); - itAsync( - "should be able to unsubscribe from a polling query subscription", - (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } + it("should not empty the store when a non-polling query fails due to a network error", async () => { + const query = gql` + query { + author { + firstName + lastName } - `; - const data = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; - - const observable = mockQueryManager({ + } + `; + const data = { + author: { + firstName: "Dhaivat", + lastName: "Pandya", + }, + }; + const queryManager = mockQueryManager( + { request: { query }, result: { data }, - }).watchQuery({ query, pollInterval: 20 }); + }, + { + request: { query }, + error: new Error("Network error ocurred"), + } + ); + const result = await queryManager.query({ query }); - const { promise, subscription } = observableToPromiseAndSubscription( - { - observable, - wait: 60, - }, - (result: any) => { - expect(result.data).toEqual(data); - subscription.unsubscribe(); - } - ); + expect(result.data).toEqual(data); - return promise.then(resolve, reject); - } - ); + await expect( + queryManager.query({ query, fetchPolicy: "network-only" }) + ).rejects.toThrow(); - itAsync( - "should not empty the store when a polling query fails due to a network error", - (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } - } - `; - const data = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; - const queryManager = mockQueryManager( - { - request: { query }, - result: { data }, - }, - { - request: { query }, - error: new Error("Network error occurred."), - } - ); - const observable = queryManager.watchQuery({ - query, - pollInterval: 20, - notifyOnNetworkStatusChange: false, - }); + expect(queryManager.cache.extract().ROOT_QUERY!.author).toEqual( + data.author + ); + }); - return observableToPromise( - { - observable, - errorCallbacks: [ - () => { - expect(queryManager.cache.extract().ROOT_QUERY!.author).toEqual( - data.author - ); - }, - ], - }, - (result) => { - expect(result.data).toEqual(data); - expect(queryManager.cache.extract().ROOT_QUERY!.author).toEqual( - data.author - ); + it("should be able to unsubscribe from a polling query subscription", async () => { + const query = gql` + query { + author { + firstName + lastName } - ).then(resolve, reject); - } - ); - - itAsync( - "should not fire next on an observer if there is no change in the result", - (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } + } + `; + const data = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; + + const observable = mockQueryManager({ + request: { query }, + result: { data }, + }).watchQuery({ query, pollInterval: 20 }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitMatchedValue({ data }); + + stream.unsubscribe(); + + await expect(stream).not.toEmitAnything(); + }); + + it("should not empty the store when a polling query fails due to a network error", async () => { + const query = gql` + query { + author { + firstName + lastName + } + } + `; + const data = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; + const queryManager = mockQueryManager( + { + request: { query }, + result: { data }, + }, + { + request: { query }, + error: new Error("Network error occurred."), + } + ); + const observable = queryManager.watchQuery({ + query, + pollInterval: 20, + notifyOnNetworkStatusChange: false, + }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitMatchedValue({ data }); + expect(queryManager.cache.extract().ROOT_QUERY!.author).toEqual( + data.author + ); + + await expect(stream).toEmitError( + new ApolloError({ networkError: new Error("Network error occurred.") }) + ); + expect(queryManager.cache.extract().ROOT_QUERY!.author).toEqual( + data.author + ); + }); + + it("should not fire next on an observer if there is no change in the result", async () => { + const query = gql` + query { + author { + firstName + lastName + } + } + `; + + const data = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; + const queryManager = mockQueryManager( + { + request: { query }, + result: { data }, + }, + { + request: { query }, + result: { data }, + } + ); + + const observable = queryManager.watchQuery({ query }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitMatchedValue({ data }); + + const result = await queryManager.query({ query }); + expect(result.data).toEqual(data); + + await expect(stream).not.toEmitAnything(); + }); + + it("should not return stale data when we orphan a real-id node in the store with a real-id node", async () => { + const query1 = gql` + query { + author { + name { + firstName + lastName + } + age + id + __typename + } + } + `; + const query2 = gql` + query { + author { + name { + firstName + } + id + __typename + } + } + `; + const data1 = { + author: { + name: { + firstName: "John", + lastName: "Smith", + }, + age: 18, + id: "187", + __typename: "Author", + }, + }; + const data2 = { + author: { + name: { + firstName: "John", + }, + id: "197", + __typename: "Author", + }, + }; + const reducerConfig = { dataIdFromObject }; + const queryManager = createQueryManager({ + link: mockSingleLink( + { + request: { query: query1 }, + result: { data: data1 }, + }, + { + request: { query: query2 }, + result: { data: data2 }, + }, + { + request: { query: query1 }, + result: { data: data1 }, + } + ), + config: reducerConfig, + }); + + const observable1 = queryManager.watchQuery({ query: query1 }); + const observable2 = queryManager.watchQuery({ query: query2 }); + + const stream1 = new ObservableStream(observable1); + const stream2 = new ObservableStream(observable2); + + await expect(stream1).toEmitValue({ + data: data1, + loading: false, + networkStatus: NetworkStatus.ready, + }); + await expect(stream2).toEmitValue({ + data: data2, + loading: false, + networkStatus: NetworkStatus.ready, + }); + }); + + it("should return partial data when configured when we orphan a real-id node in the store with a real-id node", async () => { + const query1 = gql` + query { + author { + name { + firstName + lastName + } + age + id + __typename + } + } + `; + const query2 = gql` + query { + author { + name { + firstName + } + id + __typename + } + } + `; + const data1 = { + author: { + name: { + firstName: "John", + lastName: "Smith", + }, + age: 18, + id: "187", + __typename: "Author", + }, + }; + const data2 = { + author: { + name: { + firstName: "John", + }, + id: "197", + __typename: "Author", + }, + }; + + const queryManager = createQueryManager({ + link: mockSingleLink( + { + request: { query: query1 }, + result: { data: data1 }, + }, + { + request: { query: query2 }, + result: { data: data2 }, + } + ), + }); + + const observable1 = queryManager.watchQuery({ + query: query1, + returnPartialData: true, + }); + const observable2 = queryManager.watchQuery({ query: query2 }); + + const stream1 = new ObservableStream(observable1); + const stream2 = new ObservableStream(observable2); + + await expect(stream1).toEmitValue({ + data: {}, + loading: true, + networkStatus: NetworkStatus.loading, + partial: true, + }); + await expect(stream2).toEmitValue({ + data: data2, + loading: false, + networkStatus: NetworkStatus.ready, + }); + await expect(stream1).toEmitValue({ + data: data1, + loading: false, + networkStatus: NetworkStatus.ready, + }); + }); + + it("should not write unchanged network results to cache", async () => { + const cache = new InMemoryCache({ + typePolicies: { + Query: { + fields: { + info: { + merge: false, + }, + }, + }, + }, + }); + + const client = new ApolloClient({ + cache, + link: new ApolloLink( + (operation) => + new Observable((observer: Observer) => { + switch (operation.operationName) { + case "A": + observer.next!({ data: { info: { a: "ay" } } }); + break; + case "B": + observer.next!({ data: { info: { b: "bee" } } }); + break; + } + observer.complete!(); + }) + ), + }); + + const queryA = gql` + query A { + info { + a + } + } + `; + const queryB = gql` + query B { + info { + b + } + } + `; + + const obsA = client.watchQuery({ + query: queryA, + returnPartialData: true, + }); + + const obsB = client.watchQuery({ + query: queryB, + returnPartialData: true, + }); + + const aStream = new ObservableStream(obsA); + const bStream = new ObservableStream(obsB); + + await expect(aStream).toEmitValue({ + loading: true, + networkStatus: NetworkStatus.loading, + data: {}, + partial: true, + }); + + await expect(bStream).toEmitValue({ + loading: true, + networkStatus: NetworkStatus.loading, + data: {}, + partial: true, + }); + + await expect(aStream).toEmitValue({ + loading: false, + networkStatus: NetworkStatus.ready, + data: { + info: { + a: "ay", + }, + }, + }); + + await expect(bStream).toEmitValue({ + loading: false, + networkStatus: NetworkStatus.ready, + data: { + info: { + b: "bee", + }, + }, + }); + + await expect(aStream).toEmitValue({ + loading: true, + networkStatus: NetworkStatus.loading, + data: { + info: {}, + }, + partial: true, + }); + + await expect(aStream).toEmitValue({ + loading: false, + networkStatus: NetworkStatus.ready, + data: { + info: { + a: "ay", + }, + }, + }); + + await expect(aStream).not.toEmitAnything(); + await expect(bStream).not.toEmitAnything(); + }); + + it("should disable feud-stopping logic after evict or modify", async () => { + const cache = new InMemoryCache({ + typePolicies: { + Query: { + fields: { + info: { + merge: false, + }, + }, + }, + }, + }); + + const client = new ApolloClient({ + cache, + link: new ApolloLink( + () => + new Observable((observer: Observer) => { + observer.next!({ data: { info: { c: "see" } } }); + observer.complete!(); + }) + ), + }); + + const query = gql` + query { + info { + c + } + } + `; + + const obs = client.watchQuery({ + query, + returnPartialData: true, + }); + + const stream = new ObservableStream(obs); + + await expect(stream).toEmitValue({ + loading: true, + networkStatus: NetworkStatus.loading, + data: {}, + partial: true, + }); + + await expect(stream).toEmitValue({ + loading: false, + networkStatus: NetworkStatus.ready, + data: { + info: { + c: "see", + }, + }, + }); + + cache.evict({ fieldName: "info" }); + + await expect(stream).toEmitValue({ + loading: true, + networkStatus: NetworkStatus.loading, + data: {}, + partial: true, + }); + + await expect(stream).toEmitValue({ + loading: false, + networkStatus: NetworkStatus.ready, + data: { + info: { + c: "see", + }, + }, + }); + + cache.modify({ + fields: { + info(_, { DELETE }) { + return DELETE; + }, + }, + }); + + await expect(stream).toEmitValue({ + loading: true, + networkStatus: NetworkStatus.loading, + data: {}, + partial: true, + }); + + await expect(stream).toEmitValue({ + loading: false, + networkStatus: NetworkStatus.ready, + data: { + info: { + c: "see", + }, + }, + }); + + await expect(stream).not.toEmitAnything(); + }); + + it("should not error when replacing unidentified data with a normalized ID", async () => { + const queryWithoutId = gql` + query { + author { + name { + firstName + lastName + } + age + __typename + } + } + `; + + const queryWithId = gql` + query { + author { + name { + firstName + } + id + __typename + } + } + `; + + const dataWithoutId = { + author: { + name: { + firstName: "John", + lastName: "Smith", + }, + age: "124", + __typename: "Author", + }, + }; + + const dataWithId = { + author: { + name: { + firstName: "Jane", + }, + id: "129", + __typename: "Author", + }, + }; + + let mergeCount = 0; + const queryManager = createQueryManager({ + link: mockSingleLink( + { + request: { query: queryWithoutId }, + result: { data: dataWithoutId }, + }, + { + request: { query: queryWithId }, + result: { data: dataWithId }, + } + ), + config: { + typePolicies: { + Query: { + fields: { + author: { + merge(existing, incoming, { isReference, readField }) { + switch (++mergeCount) { + case 1: + expect(existing).toBeUndefined(); + expect(isReference(incoming)).toBe(false); + expect(incoming).toEqual(dataWithoutId.author); + break; + case 2: + expect(existing).toEqual(dataWithoutId.author); + expect(isReference(incoming)).toBe(true); + expect(readField("id", incoming)).toBe("129"); + expect(readField("name", incoming)).toEqual( + dataWithId.author.name + ); + break; + default: + fail("unreached"); + } + return incoming; + }, + }, + }, + }, + }, + }, + }); + + const observableWithId = queryManager.watchQuery({ + query: queryWithId, + }); + + const observableWithoutId = queryManager.watchQuery({ + query: queryWithoutId, + }); + + const stream1 = new ObservableStream(observableWithoutId); + const stream2 = new ObservableStream(observableWithId); + + await expect(stream1).toEmitMatchedValue({ data: dataWithoutId }); + await expect(stream2).toEmitMatchedValue({ data: dataWithId }); + }); + + it("exposes errors on a refetch as a rejection", async () => { + const request = { + query: gql` + { + people_one(id: 1) { + name + } + } + `, + }; + const firstResult = { + data: { + people_one: { + name: "Luke Skywalker", + }, + }, + }; + const secondResult = { + errors: [new GraphQLError("This is not the person you are looking for.")], + }; + + const queryManager = mockRefetch({ + request, + firstResult, + secondResult, + }); + + const handle = queryManager.watchQuery(request); + const stream = new ObservableStream(handle); + + await expect(stream).toEmitValue({ + data: firstResult.data, + loading: false, + networkStatus: NetworkStatus.ready, + }); + + const expectedError = new ApolloError({ + graphQLErrors: secondResult.errors, + }); + + await expect(handle.refetch()).rejects.toThrow(expectedError); + await expect(stream).toEmitError(expectedError); + }); + + it("does not return incomplete data when two queries for the same item are executed", async () => { + const queryA = gql` + query queryA { + person(id: "abc") { + __typename + id + firstName + lastName + } + } + `; + const queryB = gql` + query queryB { + person(id: "abc") { + __typename + id + lastName + age + } + } + `; + const dataA = { + person: { + __typename: "Person", + id: "abc", + firstName: "Luke", + lastName: "Skywalker", + }, + }; + const dataB = { + person: { + __typename: "Person", + id: "abc", + lastName: "Skywalker", + age: "32", + }, + }; + const queryManager = new QueryManager( + getDefaultOptionsForQueryManagerTests({ + link: mockSingleLink( + { request: { query: queryA }, result: { data: dataA } }, + { request: { query: queryB }, result: { data: dataB }, delay: 20 } + ), + cache: new InMemoryCache({}), + ssrMode: true, + }) + ); + + const observableA = queryManager.watchQuery({ + query: queryA, + }); + const observableB = queryManager.watchQuery({ + query: queryB, + }); + const streamA = new ObservableStream(observableA); + const streamB = new ObservableStream(observableB); + + await expect(streamA).toEmitNext(); + expect(getCurrentQueryResult(observableA)).toEqual({ + data: dataA, + partial: false, + }); + expect(getCurrentQueryResult(observableB)).toEqual({ + data: undefined, + partial: true, + }); + + await expect(streamB).toEmitNext(); + expect(getCurrentQueryResult(observableA)).toEqual({ + data: dataA, + partial: false, + }); + expect(getCurrentQueryResult(observableB)).toEqual({ + data: dataB, + partial: false, + }); + }); + + it('only increments "queryInfo.lastRequestId" when fetching data from network', async () => { + const query = gql` + query query($id: ID!) { + people_one(id: $id) { + name + } + } + `; + const variables = { id: 1 }; + const dataOne = { + people_one: { + name: "Luke Skywalker", + }, + }; + const mockedResponses = [ + { + request: { query, variables }, + result: { data: dataOne }, + }, + ]; + + const queryManager = mockQueryManager(...mockedResponses); + const queryOptions: WatchQueryOptions = { + query, + variables, + fetchPolicy: "cache-and-network", + }; + const observable = queryManager.watchQuery(queryOptions); + + const mocks = mockFetchQuery(queryManager); + const queryId = "1"; + const getQuery: QueryManager["getQuery"] = ( + queryManager as any + ).getQuery.bind(queryManager); + + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + + { + const query = getQuery(queryId); + const fqbpCalls = mocks.fetchQueryByPolicy.mock.calls; + + expect(query.lastRequestId).toEqual(1); + expect(fqbpCalls.length).toBe(1); + + // Simulate updating the options of the query, which will trigger + // fetchQueryByPolicy, but it should just read from cache and not + // update "queryInfo.lastRequestId". For more information, see + // https://github.com/apollographql/apollo-client/pull/7956#issue-610298427 + await observable.setOptions({ + ...queryOptions, + fetchPolicy: "cache-first", + }); + + expect(query.lastRequestId).toEqual(1); + expect(fqbpCalls.length).toBe(2); + } + }); + + describe("polling queries", () => { + it("allows you to poll queries", async () => { + const query = gql` + query fetchLuke($id: String) { + people_one(id: $id) { + name + } } `; - const data = { - author: { - firstName: "John", - lastName: "Smith", + const variables = { + id: "1", + }; + + const data1 = { + people_one: { + name: "Luke Skywalker", + }, + }; + + const data2 = { + people_one: { + name: "Luke Skywalker has a new name", }, }; + const queryManager = mockQueryManager( { - request: { query }, - result: { data }, + request: { query, variables }, + result: { data: data1 }, }, { - request: { query }, - result: { data }, + request: { query, variables }, + result: { data: data2 }, } ); + const observable = queryManager.watchQuery({ + query, + variables, + pollInterval: 50, + notifyOnNetworkStatusChange: false, + }); + const stream = new ObservableStream(observable); - const observable = queryManager.watchQuery({ query }); - return Promise.all([ - // we wait for a little bit to ensure the result of the second query - // don't trigger another subscription event - observableToPromise({ observable, wait: 100 }, (result) => { - expect(result.data).toEqual(data); - }), - queryManager.query({ query }).then((result) => { - expect(result.data).toEqual(data); - }), - ]).then(resolve, reject); - } - ); + await expect(stream).toEmitMatchedValue({ data: data1 }); + await expect(stream).toEmitMatchedValue({ data: data2 }); + }); - itAsync( - "should not return stale data when we orphan a real-id node in the store with a real-id node", - (resolve, reject) => { - const query1 = gql` - query { - author { - name { - firstName - lastName - } - age - id - __typename - } - } - `; - const query2 = gql` - query { - author { - name { - firstName - } - id - __typename + it("does not poll during SSR", async () => { + const query = gql` + query fetchLuke($id: String) { + people_one(id: $id) { + name } } `; + + const variables = { + id: "1", + }; + const data1 = { - author: { - name: { - firstName: "John", - lastName: "Smith", - }, - age: 18, - id: "187", - __typename: "Author", + people_one: { + name: "Luke Skywalker", }, }; + const data2 = { - author: { - name: { - firstName: "John", - }, - id: "197", - __typename: "Author", + people_one: { + name: "Luke Skywalker has a new name", }, }; - const reducerConfig = { dataIdFromObject }; - const queryManager = createQueryManager({ - link: mockSingleLink( - { - request: { query: query1 }, - result: { data: data1 }, - }, - { - request: { query: query2 }, - result: { data: data2 }, - }, - { - request: { query: query1 }, - result: { data: data1 }, - } - ).setOnError(reject), - config: reducerConfig, - }); - const observable1 = queryManager.watchQuery({ query: query1 }); - const observable2 = queryManager.watchQuery({ query: query2 }); + const queryManager = new QueryManager( + getDefaultOptionsForQueryManagerTests({ + link: mockSingleLink( + { + request: { query, variables }, + result: { data: data1 }, + }, + { + request: { query, variables }, + result: { data: data2 }, + }, + { + request: { query, variables }, + result: { data: data2 }, + } + ), + cache: new InMemoryCache({ addTypename: false }), + ssrMode: true, + }) + ); - // I'm not sure the waiting 60 here really is required, but the test used to do it - return Promise.all([ - observableToPromise( - { - observable: observable1, - wait: 60, - }, - (result) => { - expect(result).toEqual({ - data: data1, - loading: false, - networkStatus: NetworkStatus.ready, - }); - } - ), - observableToPromise( - { - observable: observable2, - wait: 60, - }, - (result) => { - expect(result).toEqual({ - data: data2, - loading: false, - networkStatus: NetworkStatus.ready, - }); - } - ), - ]).then(resolve, reject); - } - ); + const observable = queryManager.watchQuery({ + query, + variables, + pollInterval: 10, + notifyOnNetworkStatusChange: false, + }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitMatchedValue({ data: data1 }); + await expect(stream).not.toEmitAnything(); + }); - itAsync( - "should return partial data when configured when we orphan a real-id node in the store with a real-id node", - (resolve, reject) => { + it("should let you handle multiple polled queries and unsubscribe from one of them", async () => { const query1 = gql` query { author { - name { - firstName - lastName - } - age - id - __typename + firstName + lastName } } `; const query2 = gql` query { - author { - name { - firstName - } - id - __typename + person { + name } } `; - const data1 = { + const data11 = { author: { - name: { - firstName: "John", - lastName: "Smith", - }, - age: 18, - id: "187", - __typename: "Author", + firstName: "John", + lastName: "Smith", }, }; - const data2 = { + const data12 = { author: { - name: { - firstName: "John", - }, - id: "197", - __typename: "Author", + firstName: "Jack", + lastName: "Smith", }, }; - - const queryManager = createQueryManager({ - link: mockSingleLink( - { - request: { query: query1 }, - result: { data: data1 }, - }, - { - request: { query: query2 }, - result: { data: data2 }, - } - ).setOnError(reject), - }); - - const observable1 = queryManager.watchQuery({ - query: query1, - returnPartialData: true, - }); - const observable2 = queryManager.watchQuery({ query: query2 }); - - return Promise.all([ - observableToPromise( - { - observable: observable1, - }, - (result) => { - expect(result).toEqual({ - data: {}, - loading: true, - networkStatus: NetworkStatus.loading, - partial: true, - }); - }, - (result) => { - expect(result).toEqual({ - data: data1, - loading: false, - networkStatus: NetworkStatus.ready, - }); - } - ), - observableToPromise( - { - observable: observable2, - }, - (result) => { - expect(result).toEqual({ - data: data2, - loading: false, - networkStatus: NetworkStatus.ready, - }); - } - ), - ]).then(resolve, reject); - } - ); - - it("should not write unchanged network results to cache", async () => { - const cache = new InMemoryCache({ - typePolicies: { - Query: { - fields: { - info: { - merge: false, - }, - }, + const data13 = { + author: { + firstName: "Jolly", + lastName: "Smith", + }, + }; + const data14 = { + author: { + firstName: "Jared", + lastName: "Smith", + }, + }; + const data21 = { + person: { + name: "Jane Smith", + }, + }; + const data22 = { + person: { + name: "Josey Smith", + }, + }; + const queryManager = mockQueryManager( + { + request: { query: query1 }, + result: { data: data11 }, + }, + { + request: { query: query1 }, + result: { data: data12 }, + }, + { + request: { query: query1 }, + result: { data: data13 }, + }, + { + request: { query: query1 }, + result: { data: data14 }, + }, + { + request: { query: query2 }, + result: { data: data21 }, }, - }, - }); + { + request: { query: query2 }, + result: { data: data22 }, + } + ); + let handle1Count = 0; + let handleCount = 0; + let setMilestone = false; - const client = new ApolloClient({ - cache, - link: new ApolloLink( - (operation) => - new Observable((observer: Observer) => { - switch (operation.operationName) { - case "A": - observer.next!({ data: { info: { a: "ay" } } }); - break; - case "B": - observer.next!({ data: { info: { b: "bee" } } }); - break; + const subscription1 = queryManager + .watchQuery({ + query: query1, + pollInterval: 150, + }) + .subscribe({ + next() { + handle1Count++; + handleCount++; + if (handle1Count > 1 && !setMilestone) { + subscription1.unsubscribe(); + setMilestone = true; } - observer.complete!(); - }) - ), - }); + }, + }); - const queryA = gql` - query A { - info { - a - } - } - `; - const queryB = gql` - query B { - info { - b - } - } - `; + const subscription2 = queryManager + .watchQuery({ + query: query2, + pollInterval: 2000, + }) + .subscribe({ + next() { + handleCount++; + }, + }); - const obsA = client.watchQuery({ - query: queryA, - returnPartialData: true, - }); + await wait(400); - const obsB = client.watchQuery({ - query: queryB, - returnPartialData: true, + expect(handleCount).toBe(3); + subscription1.unsubscribe(); + subscription2.unsubscribe(); }); - const aStream = new ObservableStream(obsA); - const bStream = new ObservableStream(obsB); + it("allows you to unsubscribe from polled queries", async () => { + const query = gql` + query fetchLuke($id: String) { + people_one(id: $id) { + name + } + } + `; - await expect(aStream).toEmitValue({ - loading: true, - networkStatus: NetworkStatus.loading, - data: {}, - partial: true, - }); + const variables = { + id: "1", + }; - await expect(bStream).toEmitValue({ - loading: true, - networkStatus: NetworkStatus.loading, - data: {}, - partial: true, - }); + const data1 = { + people_one: { + name: "Luke Skywalker", + }, + }; - await expect(aStream).toEmitValue({ - loading: false, - networkStatus: NetworkStatus.ready, - data: { - info: { - a: "ay", + const data2 = { + people_one: { + name: "Luke Skywalker has a new name", }, - }, - }); + }; - await expect(bStream).toEmitValue({ - loading: false, - networkStatus: NetworkStatus.ready, - data: { - info: { - b: "bee", + const queryManager = mockQueryManager( + { + request: { query, variables }, + result: { data: data1 }, }, - }, - }); + { + request: { query, variables }, + result: { data: data2 }, + } + ); + const observable = queryManager.watchQuery({ + query, + variables, + pollInterval: 50, + notifyOnNetworkStatusChange: false, + }); + const stream = new ObservableStream(observable); - await expect(aStream).toEmitValue({ - loading: true, - networkStatus: NetworkStatus.loading, - data: { - info: {}, - }, - partial: true, - }); + await expect(stream).toEmitMatchedValue({ data: data1 }); + await expect(stream).toEmitMatchedValue({ data: data2 }); - await expect(aStream).toEmitValue({ - loading: false, - networkStatus: NetworkStatus.ready, - data: { - info: { - a: "ay", - }, - }, + stream.unsubscribe(); + + await expect(stream).not.toEmitAnything(); }); - await expect(aStream).not.toEmitAnything(); - await expect(bStream).not.toEmitAnything(); - }); + it("allows you to unsubscribe from polled query errors", async () => { + const query = gql` + query fetchLuke($id: String) { + people_one(id: $id) { + name + } + } + `; - it("should disable feud-stopping logic after evict or modify", async () => { - const cache = new InMemoryCache({ - typePolicies: { - Query: { - fields: { - info: { - merge: false, - }, - }, + const variables = { + id: "1", + }; + + const data1 = { + people_one: { + name: "Luke Skywalker", }, - }, - }); + }; - const client = new ApolloClient({ - cache, - link: new ApolloLink( - () => - new Observable((observer: Observer) => { - observer.next!({ data: { info: { c: "see" } } }); - observer.complete!(); - }) - ), - }); + const data2 = { + people_one: { + name: "Luke Skywalker has a new name", + }, + }; - const query = gql` - query { - info { - c + const queryManager = mockQueryManager( + { + request: { query, variables }, + result: { data: data1 }, + }, + { + request: { query, variables }, + error: new Error("Network error"), + }, + { + request: { query, variables }, + result: { data: data2 }, } - } - `; + ); - const obs = client.watchQuery({ - query, - returnPartialData: true, - }); + const observable = queryManager.watchQuery({ + query, + variables, + pollInterval: 50, + notifyOnNetworkStatusChange: false, + }); + const stream = new ObservableStream(observable); - const stream = new ObservableStream(obs); + await expect(stream).toEmitMatchedValue({ data: data1 }); + await expect(stream).toEmitError( + new ApolloError({ networkError: new Error("Network error") }) + ); - await expect(stream).toEmitValue({ - loading: true, - networkStatus: NetworkStatus.loading, - data: {}, - partial: true, - }); + stream.unsubscribe(); - await expect(stream).toEmitValue({ - loading: false, - networkStatus: NetworkStatus.ready, - data: { - info: { - c: "see", - }, - }, + await expect(stream).not.toEmitAnything(); }); - cache.evict({ fieldName: "info" }); + it("exposes a way to start a polling query", async () => { + const query = gql` + query fetchLuke($id: String) { + people_one(id: $id) { + name + } + } + `; - await expect(stream).toEmitValue({ - loading: true, - networkStatus: NetworkStatus.loading, - data: {}, - partial: true, - }); + const variables = { + id: "1", + }; - await expect(stream).toEmitValue({ - loading: false, - networkStatus: NetworkStatus.ready, - data: { - info: { - c: "see", + const data1 = { + people_one: { + name: "Luke Skywalker", + }, + }; + + const data2 = { + people_one: { + name: "Luke Skywalker has a new name", }, - }, - }); + }; - cache.modify({ - fields: { - info(_, { DELETE }) { - return DELETE; + const queryManager = mockQueryManager( + { + request: { query, variables }, + result: { data: data1 }, }, - }, - }); + { + request: { query, variables }, + result: { data: data2 }, + } + ); - await expect(stream).toEmitValue({ - loading: true, - networkStatus: NetworkStatus.loading, - data: {}, - partial: true, - }); + const observable = queryManager.watchQuery({ + query, + variables, + notifyOnNetworkStatusChange: false, + }); + observable.startPolling(50); + const stream = new ObservableStream(observable); - await expect(stream).toEmitValue({ - loading: false, - networkStatus: NetworkStatus.ready, - data: { - info: { - c: "see", - }, - }, + await expect(stream).toEmitMatchedValue({ data: data1 }); + await expect(stream).toEmitMatchedValue({ data: data2 }); }); - await expect(stream).not.toEmitAnything(); - }); - - itAsync( - "should not error when replacing unidentified data with a normalized ID", - (resolve, reject) => { - const queryWithoutId = gql` - query { - author { - name { - firstName - lastName - } - age - __typename + it("exposes a way to stop a polling query", async () => { + const query = gql` + query fetchLeia($id: String) { + people_one(id: $id) { + name } } `; - const queryWithId = gql` - query { - author { - name { - firstName - } - id - __typename - } - } - `; + const variables = { + id: "2", + }; - const dataWithoutId = { - author: { - name: { - firstName: "John", - lastName: "Smith", - }, - age: "124", - __typename: "Author", + const data1 = { + people_one: { + name: "Leia Skywalker", }, }; - const dataWithId = { - author: { - name: { - firstName: "Jane", - }, - id: "129", - __typename: "Author", + const data2 = { + people_one: { + name: "Leia Skywalker has a new name", }, }; - let mergeCount = 0; - const queryManager = createQueryManager({ - link: mockSingleLink( - { - request: { query: queryWithoutId }, - result: { data: dataWithoutId }, - }, - { - request: { query: queryWithId }, - result: { data: dataWithId }, - } - ).setOnError(reject), - config: { - typePolicies: { - Query: { - fields: { - author: { - merge(existing, incoming, { isReference, readField }) { - switch (++mergeCount) { - case 1: - expect(existing).toBeUndefined(); - expect(isReference(incoming)).toBe(false); - expect(incoming).toEqual(dataWithoutId.author); - break; - case 2: - expect(existing).toEqual(dataWithoutId.author); - expect(isReference(incoming)).toBe(true); - expect(readField("id", incoming)).toBe("129"); - expect(readField("name", incoming)).toEqual( - dataWithId.author.name - ); - break; - default: - fail("unreached"); - } - return incoming; - }, - }, - }, - }, - }, + const queryManager = mockQueryManager( + { + request: { query, variables }, + result: { data: data1 }, }, + { + request: { query, variables }, + result: { data: data2 }, + } + ); + const observable = queryManager.watchQuery({ + query, + variables, + pollInterval: 50, }); + const stream = new ObservableStream(observable); - const observableWithId = queryManager.watchQuery({ - query: queryWithId, - }); + await expect(stream).toEmitMatchedValue({ data: data1 }); - const observableWithoutId = queryManager.watchQuery({ - query: queryWithoutId, - }); + observable.stopPolling(); - return Promise.all([ - observableToPromise({ observable: observableWithoutId }, (result) => - expect(result.data).toEqual(dataWithoutId) - ), - observableToPromise({ observable: observableWithId }, (result) => - expect(result.data).toEqual(dataWithId) - ), - ]).then(resolve, reject); - } - ); + await expect(stream).not.toEmitAnything(); + }); - itAsync( - "exposes errors on a refetch as a rejection", - async (resolve, reject) => { - const request = { - query: gql` - { - people_one(id: 1) { - name - } + it("stopped polling queries still get updates", async () => { + const query = gql` + query fetchLeia($id: String) { + people_one(id: $id) { + name } - `, + } + `; + + const variables = { + id: "2", }; - const firstResult = { - data: { - people_one: { - name: "Luke Skywalker", - }, + + const data1 = { + people_one: { + name: "Leia Skywalker", }, }; - const secondResult = { - errors: [ - new GraphQLError("This is not the person you are looking for."), - ], + + const data2 = { + people_one: { + name: "Leia Skywalker has a new name", + }, }; - const queryManager = mockRefetch({ - request, - firstResult, - secondResult, - }); + const queryManager = mockQueryManager( + { + request: { query, variables }, + result: { data: data1 }, + }, + { + request: { query, variables }, + result: { data: data2 }, + } + ); - const handle = queryManager.watchQuery(request); + const observable = queryManager.watchQuery({ + query, + variables, + pollInterval: 50, + }); + const stream = new ObservableStream(observable); - const checkError = (error: ApolloError) => { - expect(error.graphQLErrors[0].message).toEqual( - "This is not the person you are looking for." - ); - }; + await expect(stream).toEmitMatchedValue({ data: data1 }); - handle.subscribe({ - error: checkError, + const result = await queryManager.query({ + query, + variables, + fetchPolicy: "network-only", }); - handle - .refetch() - .then(() => { - reject(new Error("Error on refetch should reject promise")); - }) - .catch((error) => { - checkError(error); - }) - .then(resolve, reject); - } - ); - - itAsync( - "does not return incomplete data when two queries for the same item are executed", - (resolve, reject) => { - const queryA = gql` - query queryA { - person(id: "abc") { - __typename - id + expect(result.data).toEqual(data2); + await expect(stream).toEmitMatchedValue({ data: data2 }); + }); + }); + + describe("store resets", () => { + it("returns a promise resolving when all queries have been refetched", async () => { + const query = gql` + query { + author { firstName lastName } } `; - const queryB = gql` - query queryB { - person(id: "abc") { - __typename - id + + const data = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; + + const dataChanged = { + author: { + firstName: "John changed", + lastName: "Smith", + }, + }; + + const query2 = gql` + query { + author2 { + firstName lastName - age } } `; - const dataA = { - person: { - __typename: "Person", - id: "abc", - firstName: "Luke", - lastName: "Skywalker", + + const data2 = { + author2: { + firstName: "John", + lastName: "Smith", }, }; - const dataB = { - person: { - __typename: "Person", - id: "abc", - lastName: "Skywalker", - age: "32", + + const data2Changed = { + author2: { + firstName: "John changed", + lastName: "Smith", }, }; - const queryManager = new QueryManager( - getDefaultOptionsForQueryManagerTests({ - link: mockSingleLink( - { request: { query: queryA }, result: { data: dataA } }, - { request: { query: queryB }, result: { data: dataB }, delay: 20 } - ).setOnError(reject), - cache: new InMemoryCache({}), - ssrMode: true, - }) - ); - const observableA = queryManager.watchQuery({ - query: queryA, - }); - const observableB = queryManager.watchQuery({ - query: queryB, + const queryManager = createQueryManager({ + link: mockSingleLink( + { + request: { query }, + result: { data }, + }, + { + request: { query: query2 }, + result: { data: data2 }, + }, + { + request: { query }, + result: { data: dataChanged }, + }, + { + request: { query: query2 }, + result: { data: data2Changed }, + } + ), }); - return Promise.all([ - observableToPromise({ observable: observableA }, () => { - expect(getCurrentQueryResult(observableA)).toEqual({ - data: dataA, - partial: false, - }); - expect(getCurrentQueryResult(observableB)).toEqual({ - data: undefined, - partial: true, - }); - }), - observableToPromise({ observable: observableB }, () => { - expect(getCurrentQueryResult(observableA)).toEqual({ - data: dataA, - partial: false, - }); - expect(getCurrentQueryResult(observableB)).toEqual({ - data: dataB, - partial: false, - }); - }), - ]).then(resolve, reject); - } - ); - - it('only increments "queryInfo.lastRequestId" when fetching data from network', async () => { - const query = gql` - query query($id: ID!) { - people_one(id: $id) { - name - } - } - `; - const variables = { id: 1 }; - const dataOne = { - people_one: { - name: "Luke Skywalker", - }, - }; - const mockedResponses = [ - { - request: { query, variables }, - result: { data: dataOne }, - }, - ]; - - const queryManager = mockQueryManager(...mockedResponses); - const queryOptions: WatchQueryOptions = { - query, - variables, - fetchPolicy: "cache-and-network", - }; - const observable = queryManager.watchQuery(queryOptions); + const observable = queryManager.watchQuery({ query }); + const observable2 = queryManager.watchQuery({ query: query2 }); - const mocks = mockFetchQuery(queryManager); - const queryId = "1"; - const getQuery: QueryManager["getQuery"] = ( - queryManager as any - ).getQuery.bind(queryManager); + const stream = new ObservableStream(observable); + const stream2 = new ObservableStream(observable2); - const stream = new ObservableStream(observable); + await expect(stream).toEmitMatchedValue({ data }); + await expect(stream2).toEmitMatchedValue({ data: data2 }); - await expect(stream).toEmitNext(); + await resetStore(queryManager); - { - const query = getQuery(queryId); - const fqbpCalls = mocks.fetchQueryByPolicy.mock.calls; + const result = getCurrentQueryResult(observable); + expect(result.partial).toBe(false); + expect(result.data).toEqual(dataChanged); - expect(query.lastRequestId).toEqual(1); - expect(fqbpCalls.length).toBe(1); + const result2 = getCurrentQueryResult(observable2); + expect(result2.partial).toBe(false); + expect(result2.data).toEqual(data2Changed); + }); - // Simulate updating the options of the query, which will trigger - // fetchQueryByPolicy, but it should just read from cache and not - // update "queryInfo.lastRequestId". For more information, see - // https://github.com/apollographql/apollo-client/pull/7956#issue-610298427 - await observable.setOptions({ - ...queryOptions, - fetchPolicy: "cache-first", + it("should change the store state to an empty state", () => { + const queryManager = createQueryManager({ + link: mockSingleLink(), }); - expect(query.lastRequestId).toEqual(1); - expect(fqbpCalls.length).toBe(2); - } - }); + void resetStore(queryManager); - describe("polling queries", () => { - itAsync("allows you to poll queries", (resolve, reject) => { + expect(queryManager.cache.extract()).toEqual({}); + expect(queryManager.getQueryStore()).toEqual({}); + expect(queryManager.mutationStore).toEqual({}); + }); + + xit("should only refetch once when we store reset", async () => { + let queryManager: QueryManager; const query = gql` - query fetchLuke($id: String) { - people_one(id: $id) { - name + query { + author { + firstName + lastName } } `; - - const variables = { - id: "1", - }; - - const data1 = { - people_one: { - name: "Luke Skywalker", + const data = { + author: { + firstName: "John", + lastName: "Smith", }, }; const data2 = { - people_one: { - name: "Luke Skywalker has a new name", + author: { + firstName: "Johnny", + lastName: "Smith", }, }; - const queryManager = mockQueryManager( - { - request: { query, variables }, - result: { data: data1 }, - }, - { - request: { query, variables }, - result: { data: data2 }, - } + let timesFired = 0; + const link: ApolloLink = new ApolloLink( + (op) => + new Observable((observer) => { + timesFired += 1; + if (timesFired > 1) { + observer.next({ data: data2 }); + } else { + observer.next({ data }); + } + observer.complete(); + return; + }) ); - const observable = queryManager.watchQuery({ - query, - variables, - pollInterval: 50, - notifyOnNetworkStatusChange: false, - }); + queryManager = createQueryManager({ link }); + const observable = queryManager.watchQuery({ query }); + const stream = new ObservableStream(observable); - return observableToPromise( - { observable }, - (result) => expect(result.data).toEqual(data1), - (result) => expect(result.data).toEqual(data2) - ).then(resolve, reject); + await expect(stream).toEmitMatchedValue({ data }); + expect(timesFired).toBe(1); + + // reset the store after data has returned + void resetStore(queryManager); + + // only refetch once and make sure data has changed + await expect(stream).toEmitMatchedValue({ data: data2 }); + expect(timesFired).toBe(2); + + await expect(stream).not.toEmitAnything(); }); - itAsync("does not poll during SSR", (resolve, reject) => { + it("should not refetch torn-down queries", async () => { + let queryManager: QueryManager; + let observable: ObservableQuery; const query = gql` - query fetchLuke($id: String) { - people_one(id: $id) { - name + query { + author { + firstName + lastName } } `; - - const variables = { - id: "1", - }; - - const data1 = { - people_one: { - name: "Luke Skywalker", + const data = { + author: { + firstName: "John", + lastName: "Smith", }, }; - const data2 = { - people_one: { - name: "Luke Skywalker has a new name", + let timesFired = 0; + const link: ApolloLink = ApolloLink.from([ + () => + new Observable((observer) => { + timesFired += 1; + observer.next({ data }); + }), + ]); + + queryManager = createQueryManager({ link }); + observable = queryManager.watchQuery({ query }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitMatchedValue({ data }); + + stream.unsubscribe(); + + expect(timesFired).toBe(1); + + void resetStore(queryManager); + await wait(50); + + expect(timesFired).toBe(1); + }); + + it("should not error when resetStore called", async () => { + const query = gql` + query { + author { + firstName + lastName + } + } + `; + const data = { + author: { + firstName: "John", + lastName: "Smith", }, }; - const queryManager = new QueryManager( - getDefaultOptionsForQueryManagerTests({ - link: mockSingleLink( - { - request: { query, variables }, - result: { data: data1 }, - }, - { - request: { query, variables }, - result: { data: data2 }, - }, - { - request: { query, variables }, - result: { data: data2 }, - } - ).setOnError(reject), - cache: new InMemoryCache({ addTypename: false }), - ssrMode: true, - }) - ); + let timesFired = 0; + const link = ApolloLink.from([ + new ApolloLink( + () => + new Observable((observer) => { + timesFired += 1; + observer.next({ data }); + observer.complete(); + return; + }) + ), + ]); + + const queryManager = createQueryManager({ link }); const observable = queryManager.watchQuery({ query, - variables, - pollInterval: 10, notifyOnNetworkStatusChange: false, }); + const stream = new ObservableStream(observable); - let count = 1; - const subHandle = observable.subscribe({ - next: (result: any) => { - switch (count) { - case 1: - expect(result.data).toEqual(data1); - setTimeout(() => { - subHandle.unsubscribe(); - resolve(); - }, 15); - count++; - break; - case 2: - default: - reject(new Error("Only expected one result, not multiple")); - } - }, - }); + await expect(stream).toEmitMatchedValue({ data }); + expect(timesFired).toBe(1); + + void resetStore(queryManager); + + await expect(stream).toEmitMatchedValue({ data }); + expect(timesFired).toBe(2); }); - itAsync( - "should let you handle multiple polled queries and unsubscribe from one of them", - (resolve) => { - const query1 = gql` - query { - author { - firstName - lastName - } - } - `; - const query2 = gql` - query { - person { - name - } - } - `; - const data11 = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; - const data12 = { - author: { - firstName: "Jack", - lastName: "Smith", - }, - }; - const data13 = { - author: { - firstName: "Jolly", - lastName: "Smith", - }, - }; - const data14 = { - author: { - firstName: "Jared", - lastName: "Smith", - }, - }; - const data21 = { - person: { - name: "Jane Smith", - }, - }; - const data22 = { - person: { - name: "Josey Smith", - }, - }; - const queryManager = mockQueryManager( - { - request: { query: query1 }, - result: { data: data11 }, - }, - { - request: { query: query1 }, - result: { data: data12 }, - }, - { - request: { query: query1 }, - result: { data: data13 }, - }, - { - request: { query: query1 }, - result: { data: data14 }, - }, - { - request: { query: query2 }, - result: { data: data21 }, - }, - { - request: { query: query2 }, - result: { data: data22 }, + it("should not error on a stopped query()", async () => { + let queryManager: QueryManager; + const query = gql` + query { + author { + firstName + lastName } - ); - let handle1Count = 0; - let handleCount = 0; - let setMilestone = false; - - const subscription1 = queryManager - .watchQuery({ - query: query1, - pollInterval: 150, - }) - .subscribe({ - next() { - handle1Count++; - handleCount++; - if (handle1Count > 1 && !setMilestone) { - subscription1.unsubscribe(); - setMilestone = true; - } - }, - }); + } + `; + + const data = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; - const subscription2 = queryManager - .watchQuery({ - query: query2, - pollInterval: 2000, + const link = new ApolloLink( + () => + new Observable((observer) => { + observer.next({ data }); }) - .subscribe({ - next() { - handleCount++; - }, - }); + ); + + queryManager = createQueryManager({ link }); + + const queryId = "1"; + const promise = queryManager.fetchQuery(queryId, { query }); - setTimeout(() => { - expect(handleCount).toBe(3); - subscription1.unsubscribe(); - subscription2.unsubscribe(); + queryManager.removeQuery(queryId); - resolve(); - }, 400); - } - ); + await resetStore(queryManager); + // Ensure the promise doesn't reject + await Promise.race([wait(50), promise]); + }); - itAsync( - "allows you to unsubscribe from polled queries", - (resolve, reject) => { - const query = gql` - query fetchLuke($id: String) { - people_one(id: $id) { - name - } + it("should throw an error on an inflight fetch query if the store is reset", async () => { + const query = gql` + query { + author { + firstName + lastName } - `; - - const variables = { - id: "1", - }; + } + `; + const data = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; + const queryManager = mockQueryManager({ + request: { query }, + result: { data }, + delay: 10000, //i.e. forever + }); + const promise = queryManager.fetchQuery("made up id", { query }); - const data1 = { - people_one: { - name: "Luke Skywalker", - }, - }; + // Need to delay the reset at least until the fetchRequest method + // has had a chance to enter this request into fetchQueryRejectFns. + await wait(100); + void resetStore(queryManager); - const data2 = { - people_one: { - name: "Luke Skywalker has a new name", - }, - }; + await expect(promise).rejects.toThrow( + new InvariantError( + "Store reset while query was in flight (not completed in link chain)" + ) + ); + }); - const queryManager = mockQueryManager( - { - request: { query, variables }, - result: { data: data1 }, - }, - { - request: { query, variables }, - result: { data: data2 }, + it("should call refetch on a mocked Observable if the store is reset", async () => { + const query = gql` + query { + author { + firstName + lastName } - ); - const observable = queryManager.watchQuery({ - query, - variables, - pollInterval: 50, - notifyOnNetworkStatusChange: false, - }); + } + `; + const data = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; + const queryManager = mockQueryManager({ + request: { query }, + result: { data }, + }); + const obs = queryManager.watchQuery({ query }); + obs.subscribe({}); + obs.refetch = jest.fn(); - const { promise, subscription } = observableToPromiseAndSubscription( - { - observable, - wait: 60, - }, - (result) => expect(result.data).toEqual(data1), - (result) => { - expect(result.data).toEqual(data2); + void resetStore(queryManager); - // we unsubscribe here manually, rather than waiting for the timeout. - subscription.unsubscribe(); - } - ); + await wait(0); - return promise.then(resolve, reject); - } - ); + expect(obs.refetch).toHaveBeenCalledTimes(1); + }); - itAsync( - "allows you to unsubscribe from polled query errors", - (resolve, reject) => { - const query = gql` - query fetchLuke($id: String) { - people_one(id: $id) { - name - } + it("should not call refetch on a cache-only Observable if the store is reset", async () => { + const query = gql` + query { + author { + firstName + lastName } - `; + } + `; - const variables = { - id: "1", - }; + const queryManager = createQueryManager({ + link: mockSingleLink(), + }); - const data1 = { - people_one: { - name: "Luke Skywalker", - }, - }; + const options = { + query, + fetchPolicy: "cache-only", + } as WatchQueryOptions; - const data2 = { - people_one: { - name: "Luke Skywalker has a new name", - }, - }; + let refetchCount = 0; - const queryManager = mockQueryManager( - { - request: { query, variables }, - result: { data: data1 }, - }, - { - request: { query, variables }, - error: new Error("Network error"), - }, - { - request: { query, variables }, - result: { data: data2 }, - } - ); + const obs = queryManager.watchQuery(options); + obs.subscribe({}); + obs.refetch = () => { + ++refetchCount; + return null as never; + }; - const observable = queryManager.watchQuery({ - query, - variables, - pollInterval: 50, - notifyOnNetworkStatusChange: false, - }); + void resetStore(queryManager); - let isFinished = false; - process.once("unhandledRejection", () => { - if (!isFinished) reject("unhandledRejection from network"); - }); + await wait(50); - const { promise, subscription } = observableToPromiseAndSubscription( - { - observable, - wait: 60, - errorCallbacks: [ - (error) => { - expect(error.message).toMatch("Network error"); - subscription.unsubscribe(); - }, - ], - }, - (result) => expect(result.data).toEqual(data1) - ); - - promise.then(() => { - setTimeout(() => { - isFinished = true; - resolve(); - }, 4); - }); - } - ); + expect(refetchCount).toEqual(0); + }); - itAsync("exposes a way to start a polling query", (resolve, reject) => { + it("should not call refetch on a standby Observable if the store is reset", async () => { const query = gql` - query fetchLuke($id: String) { - people_one(id: $id) { - name + query { + author { + firstName + lastName } } `; - const variables = { - id: "1", - }; + const queryManager = createQueryManager({ + link: mockSingleLink(), + }); - const data1 = { - people_one: { - name: "Luke Skywalker", - }, - }; + const options = { + query, + fetchPolicy: "standby", + } as WatchQueryOptions; - const data2 = { - people_one: { - name: "Luke Skywalker has a new name", - }, + let refetchCount = 0; + + const obs = queryManager.watchQuery(options); + obs.subscribe({}); + obs.refetch = () => { + ++refetchCount; + return null as never; }; - const queryManager = mockQueryManager( - { - request: { query, variables }, - result: { data: data1 }, - }, - { - request: { query, variables }, - result: { data: data2 }, - } - ); + void resetStore(queryManager); - const observable = queryManager.watchQuery({ - query, - variables, - notifyOnNetworkStatusChange: false, - }); - observable.startPolling(50); + await wait(50); - return observableToPromise( - { observable }, - (result) => expect(result.data).toEqual(data1), - (result) => expect(result.data).toEqual(data2) - ).then(resolve, reject); + expect(refetchCount).toEqual(0); }); - itAsync("exposes a way to stop a polling query", (resolve, reject) => { + it("should not call refetch on a non-subscribed Observable if the store is reset", async () => { const query = gql` - query fetchLeia($id: String) { - people_one(id: $id) { - name + query { + author { + firstName + lastName } } `; - const variables = { - id: "2", - }; + const queryManager = createQueryManager({ + link: mockSingleLink(), + }); - const data1 = { - people_one: { - name: "Leia Skywalker", - }, - }; + const options = { + query, + } as WatchQueryOptions; - const data2 = { - people_one: { - name: "Leia Skywalker has a new name", - }, + let refetchCount = 0; + + const obs = queryManager.watchQuery(options); + obs.refetch = () => { + ++refetchCount; + return null as never; }; - const queryManager = mockQueryManager( - { - request: { query, variables }, - result: { data: data1 }, - }, - { - request: { query, variables }, - result: { data: data2 }, - } - ); - const observable = queryManager.watchQuery({ - query, - variables, - pollInterval: 50, - }); + void resetStore(queryManager); + + await wait(50); - return observableToPromise({ observable, wait: 60 }, (result) => { - expect(result.data).toEqual(data1); - observable.stopPolling(); - }).then(resolve, reject); + expect(refetchCount).toEqual(0); }); - itAsync("stopped polling queries still get updates", (resolve, reject) => { + it("should throw an error on an inflight query() if the store is reset", async () => { + let queryManager: QueryManager; const query = gql` - query fetchLeia($id: String) { - people_one(id: $id) { - name + query { + author { + firstName + lastName } } `; - const variables = { - id: "2", - }; - - const data1 = { - people_one: { - name: "Leia Skywalker", + const data = { + author: { + firstName: "John", + lastName: "Smith", }, }; + const link = new ApolloLink( + () => + new Observable((observer) => { + // reset the store as soon as we hear about the query + void resetStore(queryManager); + observer.next({ data }); + return; + }) + ); - const data2 = { - people_one: { - name: "Leia Skywalker has a new name", + queryManager = createQueryManager({ link }); + + await expect(queryManager.query({ query })).rejects.toBeTruthy(); + }); + }); + + describe("refetching observed queries", () => { + it("returns a promise resolving when all queries have been refetched", async () => { + const query = gql` + query { + author { + firstName + lastName + } + } + `; + + const data = { + author: { + firstName: "John", + lastName: "Smith", }, }; - const queryManager = mockQueryManager( - { - request: { query, variables }, - result: { data: data1 }, + const dataChanged = { + author: { + firstName: "John changed", + lastName: "Smith", }, - { - request: { query, variables }, - result: { data: data2 }, - } - ); - - const observable = queryManager.watchQuery({ - query, - variables, - pollInterval: 50, - }); + }; - return Promise.all([ - observableToPromise( - { observable }, - (result) => { - expect(result.data).toEqual(data1); - queryManager - .query({ - query, - variables, - fetchPolicy: "network-only", - }) - .then((result) => { - expect(result.data).toEqual(data2); - }) - .catch(reject); - }, - (result) => { - expect(result.data).toEqual(data2); - } - ), - ]).then(resolve, reject); - }); - }); - describe("store resets", () => { - itAsync( - "returns a promise resolving when all queries have been refetched", - (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } + const query2 = gql` + query { + author2 { + firstName + lastName } - `; - - const data = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; + } + `; - const dataChanged = { - author: { - firstName: "John changed", - lastName: "Smith", - }, - }; + const data2 = { + author2: { + firstName: "John", + lastName: "Smith", + }, + }; - const query2 = gql` - query { - author2 { - firstName - lastName - } - } - `; + const data2Changed = { + author2: { + firstName: "John changed", + lastName: "Smith", + }, + }; - const data2 = { - author2: { - firstName: "John", - lastName: "Smith", + const queryManager = createQueryManager({ + link: mockSingleLink( + { + request: { query }, + result: { data }, }, - }; - - const data2Changed = { - author2: { - firstName: "John changed", - lastName: "Smith", + { + request: { query: query2 }, + result: { data: data2 }, }, - }; - - const queryManager = createQueryManager({ - link: mockSingleLink( - { - request: { query }, - result: { data }, - }, - { - request: { query: query2 }, - result: { data: data2 }, - }, - { - request: { query }, - result: { data: dataChanged }, - }, - { - request: { query: query2 }, - result: { data: data2Changed }, - } - ).setOnError(reject), - }); + { + request: { query }, + result: { data: dataChanged }, + }, + { + request: { query: query2 }, + result: { data: data2Changed }, + } + ), + }); - const observable = queryManager.watchQuery({ query }); - const observable2 = queryManager.watchQuery({ query: query2 }); + const observable = queryManager.watchQuery({ query }); + const observable2 = queryManager.watchQuery({ query: query2 }); - return Promise.all([ - observableToPromise({ observable }, (result) => - expect(result.data).toEqual(data) - ), - observableToPromise({ observable: observable2 }, (result) => - expect(result.data).toEqual(data2) - ), - ]) - .then(() => { - observable.subscribe({ next: () => null }); - observable2.subscribe({ next: () => null }); - - return resetStore(queryManager).then(() => { - const result = getCurrentQueryResult(observable); - expect(result.partial).toBe(false); - expect(result.data).toEqual(dataChanged); - - const result2 = getCurrentQueryResult(observable2); - expect(result2.partial).toBe(false); - expect(result2.data).toEqual(data2Changed); - }); - }) - .then(resolve, reject); - } - ); + const stream = new ObservableStream(observable); + const stream2 = new ObservableStream(observable2); - itAsync( - "should change the store state to an empty state", - (resolve, reject) => { - const queryManager = createQueryManager({ - link: mockSingleLink().setOnError(reject), - }); + await expect(stream).toEmitMatchedValue({ data }); + await expect(stream2).toEmitMatchedValue({ data: data2 }); - resetStore(queryManager); + await queryManager.reFetchObservableQueries(); - expect(queryManager.cache.extract()).toEqual({}); - expect(queryManager.getQueryStore()).toEqual({}); - expect(queryManager.mutationStore).toEqual({}); + const result = getCurrentQueryResult(observable); + expect(result.partial).toBe(false); + expect(result.data).toEqual(dataChanged); - resolve(); - } - ); + const result2 = getCurrentQueryResult(observable2); + expect(result2.partial).toBe(false); + expect(result2.data).toEqual(data2Changed); + }); - xit("should only refetch once when we store reset", () => { - let queryManager: QueryManager; + it("should only refetch once when we refetch observable queries", async () => { const query = gql` query { author { @@ -3663,29 +3746,21 @@ describe("QueryManager", () => { return; }) ); - queryManager = createQueryManager({ link }); + const queryManager = createQueryManager({ link }); const observable = queryManager.watchQuery({ query }); + const stream = new ObservableStream(observable); - // wait just to make sure the observable doesn't fire again - return observableToPromise( - { observable, wait: 0 }, - (result) => { - expect(result.data).toEqual(data); - expect(timesFired).toBe(1); - // reset the store after data has returned - resetStore(queryManager); - }, - (result) => { - // only refetch once and make sure data has changed - expect(result.data).toEqual(data2); - expect(timesFired).toBe(2); - } - ); + await expect(stream).toEmitMatchedValue({ data }); + expect(timesFired).toBe(1); + + // refetch the observed queries after data has returned + void queryManager.reFetchObservableQueries(); + + await expect(stream).toEmitMatchedValue({ data: data2 }); + expect(timesFired).toBe(2); }); - itAsync("should not refetch torn-down queries", (resolve) => { - let queryManager: QueryManager; - let observable: ObservableQuery; + it("should not refetch torn-down queries", async () => { const query = gql` query { author { @@ -3711,27 +3786,264 @@ describe("QueryManager", () => { }), ]); - queryManager = createQueryManager({ link }); - observable = queryManager.watchQuery({ query }); + const queryManager = createQueryManager({ link }); + const observable = queryManager.watchQuery({ query }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitMatchedValue({ data }); + expect(timesFired).toBe(1); + + stream.unsubscribe(); + void queryManager.reFetchObservableQueries(); + + await wait(50); + + expect(timesFired).toBe(1); + }); + + it("should not error after reFetchObservableQueries", async () => { + const query = gql` + query { + author { + firstName + lastName + } + } + `; + const data = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; + + let timesFired = 0; + const link = ApolloLink.from([ + () => + new Observable((observer) => { + timesFired += 1; + observer.next({ data }); + observer.complete(); + }), + ]); + + const queryManager = createQueryManager({ link }); + + const observable = queryManager.watchQuery({ + query, + notifyOnNetworkStatusChange: false, + }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitMatchedValue({ data }); + expect(timesFired).toBe(1); + + void queryManager.reFetchObservableQueries(); + + await expect(stream).toEmitMatchedValue({ data }); + expect(timesFired).toBe(2); + + await expect(stream).not.toEmitAnything(); + }); + + it("should NOT throw an error on an inflight fetch query if the observable queries are refetched", async () => { + const query = gql` + query { + author { + firstName + lastName + } + } + `; + const data = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; + const queryManager = mockQueryManager({ + request: { query }, + result: { data }, + delay: 100, + }); + const promise = queryManager.fetchQuery("made up id", { query }); + void queryManager.reFetchObservableQueries(); + + await expect(promise).resolves.toBeTruthy(); + }); + + it("should call refetch on a mocked Observable if the observed queries are refetched", async () => { + const query = gql` + query { + author { + firstName + lastName + } + } + `; + const data = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; + const queryManager = mockQueryManager({ + request: { query }, + result: { data }, + }); + + const obs = queryManager.watchQuery({ query }); + obs.subscribe({}); + obs.refetch = jest.fn(); + + void queryManager.reFetchObservableQueries(); + + await wait(0); + + expect(obs.refetch).toHaveBeenCalledTimes(1); + }); + + it("should not call refetch on a cache-only Observable if the observed queries are refetched", async () => { + const query = gql` + query { + author { + firstName + lastName + } + } + `; + + const queryManager = createQueryManager({ + link: mockSingleLink(), + }); + + const options = { + query, + fetchPolicy: "cache-only", + } as WatchQueryOptions; + + let refetchCount = 0; - observableToPromise({ observable, wait: 0 }, (result) => - expect(result.data).toEqual(data) - ).then(() => { - expect(timesFired).toBe(1); + const obs = queryManager.watchQuery(options); + obs.subscribe({}); + obs.refetch = () => { + ++refetchCount; + return null as never; + }; + + void queryManager.reFetchObservableQueries(); + + await wait(50); + + expect(refetchCount).toEqual(0); + }); + + it("should not call refetch on a standby Observable if the observed queries are refetched", async () => { + const query = gql` + query { + author { + firstName + lastName + } + } + `; + + const queryManager = createQueryManager({ + link: mockSingleLink(), + }); + + const options = { + query, + fetchPolicy: "standby", + } as WatchQueryOptions; + + let refetchCount = 0; + + const obs = queryManager.watchQuery(options); + obs.subscribe({}); + obs.refetch = () => { + ++refetchCount; + return null as never; + }; + + void queryManager.reFetchObservableQueries(); + + await wait(50); + + expect(refetchCount).toEqual(0); + }); + + it("should refetch on a standby Observable if the observed queries are refetched and the includeStandby parameter is set to true", async () => { + const query = gql` + query { + author { + firstName + lastName + } + } + `; + + const queryManager = createQueryManager({ + link: mockSingleLink(), + }); + + const options = { + query, + fetchPolicy: "standby", + } as WatchQueryOptions; + + let refetchCount = 0; + + const obs = queryManager.watchQuery(options); + obs.subscribe({}); + obs.refetch = () => { + ++refetchCount; + return null as never; + }; + + const includeStandBy = true; + void queryManager.reFetchObservableQueries(includeStandBy); + + await wait(50); + + expect(refetchCount).toEqual(1); + }); + + it("should not call refetch on a non-subscribed Observable", async () => { + const query = gql` + query { + author { + firstName + lastName + } + } + `; + + const queryManager = createQueryManager({ + link: mockSingleLink(), + }); + + const options = { + query, + } as WatchQueryOptions; + + let refetchCount = 0; + + const obs = queryManager.watchQuery(options); + obs.refetch = () => { + ++refetchCount; + return null as never; + }; - // at this point the observable query has been torn down - // because observableToPromise unsubscribe before resolving - resetStore(queryManager); + void queryManager.reFetchObservableQueries(); - setTimeout(() => { - expect(timesFired).toBe(1); + await wait(50); - resolve(); - }, 50); - }); + expect(refetchCount).toEqual(0); }); - itAsync("should not error when resetStore called", (resolve, reject) => { + it("should NOT throw an error on an inflight query() if the observed queries are refetched", async () => { + let queryManager: QueryManager; const query = gql` query { author { @@ -3740,52 +4052,33 @@ describe("QueryManager", () => { } } `; + const data = { author: { firstName: "John", lastName: "Smith", }, }; + const link = new ApolloLink( + () => + new Observable((observer) => { + // refetch observed queries as soon as we hear about the query + void queryManager.reFetchObservableQueries(); + observer.next({ data }); + observer.complete(); + }) + ); - let timesFired = 0; - const link = ApolloLink.from([ - new ApolloLink( - () => - new Observable((observer) => { - timesFired += 1; - observer.next({ data }); - observer.complete(); - return; - }) - ), - ]); - - const queryManager = createQueryManager({ link }); - - const observable = queryManager.watchQuery({ - query, - notifyOnNetworkStatusChange: false, - }); + queryManager = createQueryManager({ link }); - // wait to make sure store reset happened - return observableToPromise( - { observable, wait: 20 }, - (result) => { - expect(result.data).toEqual(data); - expect(timesFired).toBe(1); - resetStore(queryManager).catch(reject); - }, - (result) => { - expect(result.data).toEqual(data); - expect(timesFired).toBe(2); - } - ).then(resolve, reject); + await expect(queryManager.query({ query })).resolves.toBeTruthy(); }); + }); - itAsync("should not error on a stopped query()", (resolve, reject) => { - let queryManager: QueryManager; + describe("refetching specified queries", () => { + it("returns a promise resolving when all queries have been refetched", async () => { const query = gql` - query { + query GetAuthor { author { firstName lastName @@ -3800,414 +4093,187 @@ describe("QueryManager", () => { }, }; - const link = new ApolloLink( - () => - new Observable((observer) => { - observer.next({ data }); - }) - ); + const dataChanged = { + author: { + firstName: "John changed", + lastName: "Smith", + }, + }; - queryManager = createQueryManager({ link }); + const query2 = gql` + query GetAuthor2 { + author2 { + firstName + lastName + } + } + `; - const queryId = "1"; - queryManager - .fetchQuery(queryId, { query }) - .catch((e) => reject("Exception thrown for stopped query")); + const data2 = { + author2: { + firstName: "John", + lastName: "Smith", + }, + }; - queryManager.removeQuery(queryId); - resetStore(queryManager).then(resolve, reject); - }); + const data2Changed = { + author2: { + firstName: "John changed", + lastName: "Smith", + }, + }; - itAsync( - "should throw an error on an inflight fetch query if the store is reset", - (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } - } - `; - const data = { - author: { - firstName: "John", - lastName: "Smith", + const queryManager = createQueryManager({ + link: mockSingleLink( + { + request: { query }, + result: { data }, }, - }; - const queryManager = mockQueryManager({ - request: { query }, - result: { data }, - delay: 10000, //i.e. forever - }); - queryManager - .fetchQuery("made up id", { query }) - .then(() => { - reject(new Error("Returned a result.")); - }) - .catch((error) => { - expect(error.message).toMatch("Store reset"); - resolve(); - }); - // Need to delay the reset at least until the fetchRequest method - // has had a chance to enter this request into fetchQueryRejectFns. - setTimeout(() => resetStore(queryManager), 100); - } - ); - - itAsync( - "should call refetch on a mocked Observable if the store is reset", - (resolve) => { - const query = gql` - query { - author { - firstName - lastName - } - } - `; - const data = { - author: { - firstName: "John", - lastName: "Smith", + { + request: { query: query2 }, + result: { data: data2 }, }, - }; - const queryManager = mockQueryManager({ - request: { query }, - result: { data }, - }); - const obs = queryManager.watchQuery({ query }); - obs.subscribe({}); - obs.refetch = resolve as any; - - resetStore(queryManager); - } - ); - - itAsync( - "should not call refetch on a cache-only Observable if the store is reset", - (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } + { + request: { query }, + result: { data: dataChanged }, + }, + { + request: { query: query2 }, + result: { data: data2Changed }, } - `; - - const queryManager = createQueryManager({ - link: mockSingleLink().setOnError(reject), - }); - - const options = { - query, - fetchPolicy: "cache-only", - } as WatchQueryOptions; - - let refetchCount = 0; + ), + }); - const obs = queryManager.watchQuery(options); - obs.subscribe({}); - obs.refetch = () => { - ++refetchCount; - return null as never; - }; + const observable = queryManager.watchQuery({ query }); + const observable2 = queryManager.watchQuery({ query: query2 }); - resetStore(queryManager); + const stream = new ObservableStream(observable); + const stream2 = new ObservableStream(observable2); - setTimeout(() => { - expect(refetchCount).toEqual(0); - resolve(); - }, 50); - } - ); + await expect(stream).toEmitMatchedValue({ data }); + await expect(stream2).toEmitMatchedValue({ data: data2 }); - itAsync( - "should not call refetch on a standby Observable if the store is reset", - (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } - } - `; + const results: any[] = []; + queryManager + .refetchQueries({ + include: ["GetAuthor", "GetAuthor2"], + }) + .forEach((result) => results.push(result)); - const queryManager = createQueryManager({ - link: mockSingleLink().setOnError(reject), - }); + await Promise.all(results); - const options = { - query, - fetchPolicy: "standby", - } as WatchQueryOptions; + const result = getCurrentQueryResult(observable); + expect(result.partial).toBe(false); + expect(result.data).toEqual(dataChanged); - let refetchCount = 0; + const result2 = getCurrentQueryResult(observable2); + expect(result2.partial).toBe(false); + expect(result2.data).toEqual(data2Changed); + }); + }); - const obs = queryManager.watchQuery(options); - obs.subscribe({}); - obs.refetch = () => { - ++refetchCount; - return null as never; - }; + describe("loading state", () => { + it("should be passed as false if we are not watching a query", async () => { + const query = gql` + query { + fortuneCookie + } + `; + const data = { + fortuneCookie: "Buy it", + }; + const result = await mockQueryManager({ + request: { query }, + result: { data }, + }).query({ query }); - resetStore(queryManager); + expect(result.loading).toBe(false); + expect(result.data).toEqual(data); + }); - setTimeout(() => { - expect(refetchCount).toEqual(0); - resolve(); - }, 50); - } - ); + it("should be passed to the observer as true if we are returning partial data", async () => { + const fortuneCookie = + "You must stick to your goal but rethink your approach"; + const primeQuery = gql` + query { + fortuneCookie + } + `; + const primeData = { fortuneCookie }; - itAsync( - "should not call refetch on a non-subscribed Observable if the store is reset", - (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } + const author = { name: "John" }; + const query = gql` + query { + fortuneCookie + author { + name } - `; - - const queryManager = createQueryManager({ - link: mockSingleLink().setOnError(reject), - }); - - const options = { - query, - } as WatchQueryOptions; - - let refetchCount = 0; - - const obs = queryManager.watchQuery(options); - obs.refetch = () => { - ++refetchCount; - return null as never; - }; + } + `; + const fullData = { fortuneCookie, author }; - resetStore(queryManager); + const queryManager = mockQueryManager( + { + request: { query }, + result: { data: fullData }, + delay: 5, + }, + { + request: { query: primeQuery }, + result: { data: primeData }, + } + ); - setTimeout(() => { - expect(refetchCount).toEqual(0); - resolve(); - }, 50); - } - ); + await queryManager.query({ query: primeQuery }); - itAsync( - "should throw an error on an inflight query() if the store is reset", - (resolve, reject) => { - let queryManager: QueryManager; - const query = gql` - query { - author { - firstName - lastName - } - } - `; + const observable = queryManager.watchQuery({ + query, + returnPartialData: true, + }); + const stream = new ObservableStream(observable); - const data = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; - const link = new ApolloLink( - () => - new Observable((observer) => { - // reset the store as soon as we hear about the query - resetStore(queryManager); - observer.next({ data }); - return; - }) - ); + await expect(stream).toEmitValue({ + data: primeData, + loading: true, + networkStatus: NetworkStatus.loading, + partial: true, + }); + await expect(stream).toEmitValue({ + data: fullData, + loading: false, + networkStatus: NetworkStatus.ready, + }); + }); - queryManager = createQueryManager({ link }); - queryManager - .query({ query }) - .then(() => { - reject(new Error("query() gave results on a store reset")); - }) - .catch(() => { - resolve(); - }); - } - ); - }); - describe("refetching observed queries", () => { - itAsync( - "returns a promise resolving when all queries have been refetched", - (resolve, reject) => { - const query = gql` + it("should be passed to the observer as false if we are returning all the data", async () => { + const stream = assertWithObserver({ + query: gql` query { author { firstName lastName } } - `; - - const data = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; - - const dataChanged = { - author: { - firstName: "John changed", - lastName: "Smith", - }, - }; - - const query2 = gql` - query { - author2 { - firstName - lastName - } - } - `; - - const data2 = { - author2: { - firstName: "John", - lastName: "Smith", - }, - }; - - const data2Changed = { - author2: { - firstName: "John changed", - lastName: "Smith", - }, - }; - - const queryManager = createQueryManager({ - link: mockSingleLink( - { - request: { query }, - result: { data }, - }, - { - request: { query: query2 }, - result: { data: data2 }, - }, - { - request: { query }, - result: { data: dataChanged }, + `, + result: { + data: { + author: { + firstName: "John", + lastName: "Smith", }, - { - request: { query: query2 }, - result: { data: data2Changed }, - } - ).setOnError(reject), - }); - - const observable = queryManager.watchQuery({ query }); - const observable2 = queryManager.watchQuery({ query: query2 }); - - return Promise.all([ - observableToPromise({ observable }, (result) => - expect(result.data).toEqual(data) - ), - observableToPromise({ observable: observable2 }, (result) => - expect(result.data).toEqual(data2) - ), - ]) - .then(() => { - observable.subscribe({ next: () => null }); - observable2.subscribe({ next: () => null }); - - return queryManager.reFetchObservableQueries().then(() => { - const result = getCurrentQueryResult(observable); - expect(result.partial).toBe(false); - expect(result.data).toEqual(dataChanged); - - const result2 = getCurrentQueryResult(observable2); - expect(result2.partial).toBe(false); - expect(result2.data).toEqual(data2Changed); - }); - }) - .then(resolve, reject); - } - ); - - itAsync( - "should only refetch once when we refetch observable queries", - (resolve, reject) => { - let queryManager: QueryManager; - const query = gql` - query { - author { - firstName - lastName - } - } - `; - const data = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; - - const data2 = { - author: { - firstName: "Johnny", - lastName: "Smith", }, - }; + }, + }); - let timesFired = 0; - const link: ApolloLink = new ApolloLink( - (op) => - new Observable((observer) => { - timesFired += 1; - if (timesFired > 1) { - observer.next({ data: data2 }); - } else { - observer.next({ data }); - } - observer.complete(); - return; - }) - ); - queryManager = createQueryManager({ link }); - const observable = queryManager.watchQuery({ query }); - - // wait just to make sure the observable doesn't fire again - return observableToPromise( - { observable, wait: 0 }, - (result) => { - expect(result.data).toEqual(data); - expect(timesFired).toBe(1); - // refetch the observed queries after data has returned - queryManager.reFetchObservableQueries(); - }, - (result) => { - // only refetch once and make sure data has changed - expect(result.data).toEqual(data2); - expect(timesFired).toBe(2); - resolve(); - } - ).catch((e) => { - reject(e); - }); - } - ); + await expect(stream).toEmitValue({ + data: { author: { firstName: "John", lastName: "Smith" } }, + loading: false, + networkStatus: NetworkStatus.ready, + }); + }); - itAsync("should not refetch torn-down queries", (resolve) => { - let queryManager: QueryManager; - let observable: ObservableQuery; - const query = gql` + it("will update on `resetStore`", async () => { + const testQuery = gql` query { author { firstName @@ -4215,951 +4281,626 @@ describe("QueryManager", () => { } } `; - const data = { + const data1 = { author: { firstName: "John", lastName: "Smith", }, }; + const data2 = { + author: { + firstName: "John", + lastName: "Smith 2", + }, + }; + const queryManager = mockQueryManager( + { + request: { query: testQuery }, + result: { data: data1 }, + }, + { + request: { query: testQuery }, + result: { data: data2 }, + } + ); - let timesFired = 0; - const link: ApolloLink = ApolloLink.from([ - () => - new Observable((observer) => { - timesFired += 1; - observer.next({ data }); - return; - }), - ]); - - queryManager = createQueryManager({ link }); - observable = queryManager.watchQuery({ query }); - - observableToPromise({ observable, wait: 0 }, (result) => - expect(result.data).toEqual(data) - ).then(() => { - expect(timesFired).toBe(1); - - // at this point the observable query has been torn down - // because observableToPromise unsubscribe before resolving - queryManager.reFetchObservableQueries(); - - setTimeout(() => { - expect(timesFired).toBe(1); + const stream = new ObservableStream( + queryManager.watchQuery({ + query: testQuery, + notifyOnNetworkStatusChange: false, + }) + ); - resolve(); - }, 50); + await expect(stream).toEmitValue({ + data: data1, + loading: false, + networkStatus: NetworkStatus.ready, }); - }); - - itAsync( - "should not error after reFetchObservableQueries", - (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } - } - `; - const data = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; - let timesFired = 0; - const link = ApolloLink.from([ - () => - new Observable((observer) => { - timesFired += 1; - observer.next({ data }); - observer.complete(); - }), - ]); + await wait(0); + void resetStore(queryManager); - const queryManager = createQueryManager({ link }); + await expect(stream).toEmitValue({ + data: data2, + loading: false, + networkStatus: NetworkStatus.ready, + }); - const observable = queryManager.watchQuery({ - query, - notifyOnNetworkStatusChange: false, - }); + await expect(stream).not.toEmitAnything(); + }); - // wait to make sure store reset happened - return observableToPromise( - { observable, wait: 20 }, - (result) => { - expect(result.data).toEqual(data); - expect(timesFired).toBe(1); - queryManager.reFetchObservableQueries(); - }, - (result) => { - expect(result.data).toEqual(data); - expect(timesFired).toBe(2); + it("will be true when partial data may be returned", async () => { + const query1 = gql` + { + a { + x1 + y1 + z1 } - ).then(resolve, reject); - } - ); - - itAsync( - "should NOT throw an error on an inflight fetch query if the observable queries are refetched", - (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } + } + `; + const query2 = gql` + { + a { + x1 + y1 + z1 } - `; - const data = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; - const queryManager = mockQueryManager({ - request: { query }, - result: { data }, - delay: 100, - }); - queryManager - .fetchQuery("made up id", { query }) - .then(resolve) - .catch((error) => { - reject(new Error("Should not return an error")); - }); - queryManager.reFetchObservableQueries(); - } - ); - - itAsync( - "should call refetch on a mocked Observable if the observed queries are refetched", - (resolve) => { - const query = gql` - query { - author { - firstName - lastName - } + b { + x2 + y2 + z2 } - `; - const data = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; - const queryManager = mockQueryManager({ - request: { query }, - result: { data }, - }); + } + `; + const data1 = { + a: { x1: 1, y1: 2, z1: 3 }, + }; + const data2 = { + a: { x1: 1, y1: 2, z1: 3 }, + b: { x2: 3, y2: 2, z2: 1 }, + }; + const queryManager = mockQueryManager( + { + request: { query: query1 }, + result: { data: data1 }, + }, + { + request: { query: query2 }, + result: { data: data2 }, + delay: 5, + } + ); - const obs = queryManager.watchQuery({ query }); - obs.subscribe({}); - obs.refetch = resolve as any; + const result1 = await queryManager.query({ query: query1 }); + expect(result1.loading).toBe(false); + expect(result1.data).toEqual(data1); - queryManager.reFetchObservableQueries(); - } - ); + const observable = queryManager.watchQuery({ + query: query2, + returnPartialData: true, + }); + const stream = new ObservableStream(observable); - itAsync( - "should not call refetch on a cache-only Observable if the observed queries are refetched", - (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } - } - `; + await expect(stream).toEmitValue({ + data: data1, + loading: true, + networkStatus: NetworkStatus.loading, + partial: true, + }); + await expect(stream).toEmitValue({ + data: data2, + loading: false, + networkStatus: NetworkStatus.ready, + }); - const queryManager = createQueryManager({ - link: mockSingleLink().setOnError(reject), - }); + await expect(stream).not.toEmitAnything(); + }); + }); - const options = { - query, - fetchPolicy: "cache-only", - } as WatchQueryOptions; + describe("refetchQueries", () => { + let consoleWarnSpy: jest.SpyInstance; + beforeEach(() => { + consoleWarnSpy = jest.spyOn(console, "warn").mockImplementation(); + }); + afterEach(() => { + consoleWarnSpy.mockRestore(); + }); - let refetchCount = 0; + it("should refetch the right query when a result is successfully returned", async () => { + const mutation = gql` + mutation changeAuthorName { + changeAuthorName(newName: "Jack Smith") { + firstName + lastName + } + } + `; + const mutationData = { + changeAuthorName: { + firstName: "Jack", + lastName: "Smith", + }, + }; + const query = gql` + query getAuthors($id: ID!) { + author(id: $id) { + firstName + lastName + } + } + `; + const data = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; + const secondReqData = { + author: { + firstName: "Jane", + lastName: "Johnson", + }, + }; + const variables = { id: "1234" }; + const queryManager = mockQueryManager( + { + request: { query, variables }, + result: { data }, + }, + { + request: { query, variables }, + result: { data: secondReqData }, + }, + { + request: { query: mutation }, + result: { data: mutationData }, + } + ); + const observable = queryManager.watchQuery({ + query, + variables, + notifyOnNetworkStatusChange: false, + }); + const stream = new ObservableStream(observable); - const obs = queryManager.watchQuery(options); - obs.subscribe({}); - obs.refetch = () => { - ++refetchCount; - return null as never; - }; + await expect(stream).toEmitMatchedValue({ data }); - queryManager.reFetchObservableQueries(); + void queryManager.mutate({ mutation, refetchQueries: ["getAuthors"] }); - setTimeout(() => { - expect(refetchCount).toEqual(0); - resolve(); - }, 50); - } - ); + await expect(stream).toEmitMatchedValue({ data: secondReqData }); + expect(observable.getCurrentResult().data).toEqual(secondReqData); + }); - itAsync( - "should not call refetch on a standby Observable if the observed queries are refetched", - (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } + it("should not warn and continue when an unknown query name is asked to refetch", async () => { + const mutation = gql` + mutation changeAuthorName { + changeAuthorName(newName: "Jack Smith") { + firstName + lastName } - `; - - const queryManager = createQueryManager({ - link: mockSingleLink().setOnError(reject), - }); - - const options = { - query, - fetchPolicy: "standby", - } as WatchQueryOptions; - - let refetchCount = 0; + } + `; + const mutationData = { + changeAuthorName: { + firstName: "Jack", + lastName: "Smith", + }, + }; + const query = gql` + query getAuthors { + author { + firstName + lastName + } + } + `; + const data = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; + const secondReqData = { + author: { + firstName: "Jane", + lastName: "Johnson", + }, + }; + const queryManager = mockQueryManager( + { + request: { query }, + result: { data }, + }, + { + request: { query }, + result: { data: secondReqData }, + }, + { + request: { query: mutation }, + result: { data: mutationData }, + } + ); + const observable = queryManager.watchQuery({ + query, + notifyOnNetworkStatusChange: false, + }); + const stream = new ObservableStream(observable); - const obs = queryManager.watchQuery(options); - obs.subscribe({}); - obs.refetch = () => { - ++refetchCount; - return null as never; - }; + await expect(stream).toEmitMatchedValue({ data }); - queryManager.reFetchObservableQueries(); + void queryManager.mutate({ + mutation, + refetchQueries: ["fakeQuery", "getAuthors"], + }); - setTimeout(() => { - expect(refetchCount).toEqual(0); - resolve(); - }, 50); - } - ); + await expect(stream).toEmitMatchedValue({ data: secondReqData }); + expect(consoleWarnSpy).toHaveBeenLastCalledWith( + 'Unknown query named "%s" requested in refetchQueries options.include array', + "fakeQuery" + ); + }); - itAsync( - "should refetch on a standby Observable if the observed queries are refetched and the includeStandby parameter is set to true", - (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } + it("should ignore (with warning) a query named in refetchQueries that has no active subscriptions", async () => { + const mutation = gql` + mutation changeAuthorName { + changeAuthorName(newName: "Jack Smith") { + firstName + lastName } - `; - - const queryManager = createQueryManager({ - link: mockSingleLink().setOnError(reject), - }); - - const options = { - query, - fetchPolicy: "standby", - } as WatchQueryOptions; - - let refetchCount = 0; - - const obs = queryManager.watchQuery(options); - obs.subscribe({}); - obs.refetch = () => { - ++refetchCount; - return null as never; - }; - - const includeStandBy = true; - queryManager.reFetchObservableQueries(includeStandBy); - - setTimeout(() => { - expect(refetchCount).toEqual(1); - resolve(); - }, 50); - } - ); - - itAsync( - "should not call refetch on a non-subscribed Observable", - (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } + } + `; + const mutationData = { + changeAuthorName: { + firstName: "Jack", + lastName: "Smith", + }, + }; + const query = gql` + query getAuthors { + author { + firstName + lastName } - `; - - const queryManager = createQueryManager({ - link: mockSingleLink().setOnError(reject), - }); - - const options = { - query, - } as WatchQueryOptions; - - let refetchCount = 0; - - const obs = queryManager.watchQuery(options); - obs.refetch = () => { - ++refetchCount; - return null as never; - }; - - queryManager.reFetchObservableQueries(); + } + `; + const data = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; + const secondReqData = { + author: { + firstName: "Jane", + lastName: "Johnson", + }, + }; + const queryManager = mockQueryManager( + { + request: { query }, + result: { data }, + }, + { + request: { query }, + result: { data: secondReqData }, + }, + { + request: { query: mutation }, + result: { data: mutationData }, + } + ); - setTimeout(() => { - expect(refetchCount).toEqual(0); - resolve(); - }, 50); - } - ); + const observable = queryManager.watchQuery({ query }); + const stream = new ObservableStream(observable); - itAsync( - "should NOT throw an error on an inflight query() if the observed queries are refetched", - (resolve, reject) => { - let queryManager: QueryManager; - const query = gql` - query { - author { - firstName - lastName - } - } - `; + await expect(stream).toEmitMatchedValue({ data }); - const data = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; - const link = new ApolloLink( - () => - new Observable((observer) => { - // refetch observed queries as soon as we hear about the query - queryManager.reFetchObservableQueries(); - observer.next({ data }); - observer.complete(); - }) - ); + stream.unsubscribe(); + await queryManager.mutate({ + mutation, + refetchQueries: ["getAuthors"], + }); - queryManager = createQueryManager({ link }); - queryManager - .query({ query }) - .then(() => { - resolve(); - }) - .catch((e) => { - reject( - new Error( - "query() should not throw error when refetching observed queriest" - ) - ); - }); - } - ); - }); + expect(consoleWarnSpy).toHaveBeenLastCalledWith( + 'Unknown query named "%s" requested in refetchQueries options.include array', + "getAuthors" + ); + }); - describe("refetching specified queries", () => { - itAsync( - "returns a promise resolving when all queries have been refetched", - (resolve, reject) => { - const query = gql` - query GetAuthor { - author { - firstName - lastName - } + it("also works with a query document and variables", async () => { + const mutation = gql` + mutation changeAuthorName($id: ID!) { + changeAuthorName(newName: "Jack Smith", id: $id) { + firstName + lastName } - `; - - const data = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; - - const dataChanged = { - author: { - firstName: "John changed", - lastName: "Smith", - }, - }; - - const query2 = gql` - query GetAuthor2 { - author2 { - firstName - lastName - } + } + `; + const mutationData = { + changeAuthorName: { + firstName: "Jack", + lastName: "Smith", + }, + }; + const query = gql` + query getAuthors($id: ID!) { + author(id: $id) { + firstName + lastName } - `; + } + `; + const data = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; + const secondReqData = { + author: { + firstName: "Jane", + lastName: "Johnson", + }, + }; - const data2 = { - author2: { - firstName: "John", - lastName: "Smith", - }, - }; + const variables = { id: "1234" }; + const mutationVariables = { id: "2345" }; + const queryManager = mockQueryManager( + { + request: { query, variables }, + result: { data }, + delay: 10, + }, + { + request: { query, variables }, + result: { data: secondReqData }, + delay: 100, + }, + { + request: { query: mutation, variables: mutationVariables }, + result: { data: mutationData }, + delay: 10, + } + ); + const observable = queryManager.watchQuery({ query, variables }); + const stream = new ObservableStream(observable); - const data2Changed = { - author2: { - firstName: "John changed", - lastName: "Smith", - }, - }; + await expect(stream).toEmitMatchedValue({ data }); - const queryManager = createQueryManager({ - link: mockSingleLink( - { - request: { query }, - result: { data }, - }, - { - request: { query: query2 }, - result: { data: data2 }, - }, - { - request: { query }, - result: { data: dataChanged }, - }, - { - request: { query: query2 }, - result: { data: data2Changed }, - } - ).setOnError(reject), - }); + await queryManager.mutate({ + mutation, + variables: mutationVariables, + refetchQueries: [{ query, variables }], + }); - const observable = queryManager.watchQuery({ query }); - const observable2 = queryManager.watchQuery({ query: query2 }); + await expect(stream).toEmitMatchedValue( + { data: secondReqData }, + { timeout: 150 } + ); + expect(observable.getCurrentResult().data).toEqual(secondReqData); - return Promise.all([ - observableToPromise({ observable }, (result) => - expect(result.data).toEqual(data) - ), - observableToPromise({ observable: observable2 }, (result) => - expect(result.data).toEqual(data2) - ), - ]) - .then(() => { - observable.subscribe({ next: () => null }); - observable2.subscribe({ next: () => null }); - - const results: any[] = []; - queryManager - .refetchQueries({ - include: ["GetAuthor", "GetAuthor2"], - }) - .forEach((result) => results.push(result)); + await wait(10); - return Promise.all(results).then(() => { - const result = getCurrentQueryResult(observable); - expect(result.partial).toBe(false); - expect(result.data).toEqual(dataChanged); + queryManager["queries"].forEach((_, queryId) => { + expect(queryId).not.toContain("legacyOneTimeQuery"); + }); - const result2 = getCurrentQueryResult(observable2); - expect(result2.partial).toBe(false); - expect(result2.data).toEqual(data2Changed); - }); - }) - .then(resolve, reject); - } - ); - }); + await expect(stream).not.toEmitAnything(); + }); - describe("loading state", () => { - itAsync( - "should be passed as false if we are not watching a query", - (resolve, reject) => { - const query = gql` - query { - fortuneCookie + it("also works with a conditional function that returns false", async () => { + const mutation = gql` + mutation changeAuthorName { + changeAuthorName(newName: "Jack Smith") { + firstName + lastName } - `; - const data = { - fortuneCookie: "Buy it", - }; - return mockQueryManager({ + } + `; + const mutationData = { + changeAuthorName: { + firstName: "Jack", + lastName: "Smith", + }, + }; + const query = gql` + query getAuthors { + author { + firstName + lastName + } + } + `; + const data = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; + const secondReqData = { + author: { + firstName: "Jane", + lastName: "Johnson", + }, + }; + const queryManager = mockQueryManager( + { request: { query }, result: { data }, - }) - .query({ query }) - .then((result) => { - expect(!result.loading).toBeTruthy(); - expect(result.data).toEqual(data); - }) - .then(resolve, reject); - } - ); - - itAsync( - "should be passed to the observer as true if we are returning partial data", - (resolve, reject) => { - const fortuneCookie = - "You must stick to your goal but rethink your approach"; - const primeQuery = gql` - query { - fortuneCookie - } - `; - const primeData = { fortuneCookie }; + }, + { + request: { query }, + result: { data: secondReqData }, + }, + { + request: { query: mutation }, + result: { data: mutationData }, + } + ); + const observable = queryManager.watchQuery({ query }); + const stream = new ObservableStream(observable); - const author = { name: "John" }; - const query = gql` - query { - fortuneCookie - author { - name - } - } - `; - const fullData = { fortuneCookie, author }; + await expect(stream).toEmitMatchedValue({ data }); - const queryManager = mockQueryManager( - { - request: { query }, - result: { data: fullData }, - delay: 5, - }, - { - request: { query: primeQuery }, - result: { data: primeData }, - } - ); - - return queryManager - .query({ query: primeQuery }) - .then((primeResult) => { - const observable = queryManager.watchQuery({ - query, - returnPartialData: true, - }); - - return observableToPromise( - { observable }, - (result) => { - expect(result.loading).toBe(true); - expect(result.data).toEqual(primeData); - }, - (result) => { - expect(result.loading).toBe(false); - expect(result.data).toEqual(fullData); - } - ); - }) - .then(resolve, reject); - } - ); + const conditional = jest.fn(() => []); + await queryManager.mutate({ mutation, refetchQueries: conditional }); - itAsync( - "should be passed to the observer as false if we are returning all the data", - (resolve, reject) => { - assertWithObserver({ - reject, - query: gql` - query { - author { - firstName - lastName - } - } - `, - result: { - data: { - author: { - firstName: "John", - lastName: "Smith", - }, - }, - }, - observer: { - next(result) { - expect(!result.loading).toBeTruthy(); - resolve(); - }, - }, - }); - } - ); + expect(conditional).toHaveBeenCalledTimes(1); + expect(conditional).toHaveBeenCalledWith( + expect.objectContaining({ data: mutationData }) + ); + }); - itAsync("will update on `resetStore`", (resolve, reject) => { - const testQuery = gql` - query { + it("also works with a conditional function that returns an array of refetches", async () => { + const mutation = gql` + mutation changeAuthorName { + changeAuthorName(newName: "Jack Smith") { + firstName + lastName + } + } + `; + const mutationData = { + changeAuthorName: { + firstName: "Jack", + lastName: "Smith", + }, + }; + const query = gql` + query getAuthors { author { firstName lastName } } `; - const data1 = { + const data = { author: { firstName: "John", lastName: "Smith", }, }; - const data2 = { + const secondReqData = { author: { - firstName: "John", - lastName: "Smith 2", + firstName: "Jane", + lastName: "Johnson", }, }; const queryManager = mockQueryManager( { - request: { query: testQuery }, - result: { data: data1 }, + request: { query }, + result: { data }, }, { - request: { query: testQuery }, - result: { data: data2 }, + request: { query }, + result: { data: secondReqData }, + }, + { + request: { query: mutation }, + result: { data: mutationData }, } ); - let count = 0; + const observable = queryManager.watchQuery({ query }); + const stream = new ObservableStream(observable); - queryManager - .watchQuery({ - query: testQuery, - notifyOnNetworkStatusChange: false, - }) - .subscribe({ - next: (result) => { - switch (count++) { - case 0: - expect(result.loading).toBe(false); - expect(result.data).toEqual(data1); - setTimeout(() => { - resetStore(queryManager); - }, 0); - break; - case 1: - expect(result.loading).toBe(false); - expect(result.data).toEqual(data2); - resolve(); - break; - default: - reject(new Error("`next` was called to many times.")); - } - }, - error: (error) => reject(error), - }); - }); + await expect(stream).toEmitMatchedValue({ data }); - itAsync( - "will be true when partial data may be returned", - (resolve, reject) => { - const query1 = gql` - { - a { - x1 - y1 - z1 - } - } - `; - const query2 = gql` - { - a { - x1 - y1 - z1 - } - b { - x2 - y2 - z2 - } - } - `; - const data1 = { - a: { x1: 1, y1: 2, z1: 3 }, - }; - const data2 = { - a: { x1: 1, y1: 2, z1: 3 }, - b: { x2: 3, y2: 2, z2: 1 }, - }; - const queryManager = mockQueryManager( - { - request: { query: query1 }, - result: { data: data1 }, - }, - { - request: { query: query2 }, - result: { data: data2 }, - delay: 5, - } - ); - - queryManager - .query({ query: query1 }) - .then((result1) => { - expect(result1.loading).toBe(false); - expect(result1.data).toEqual(data1); - - let count = 0; - queryManager - .watchQuery({ query: query2, returnPartialData: true }) - .subscribe({ - next: (result2) => { - switch (count++) { - case 0: - expect(result2.loading).toBe(true); - expect(result2.data).toEqual(data1); - break; - case 1: - expect(result2.loading).toBe(false); - expect(result2.data).toEqual(data2); - resolve(); - break; - default: - reject(new Error("`next` was called to many times.")); - } - }, - error: reject, - }); - }) - .then(resolve, reject); - } - ); - }); + const conditional = jest.fn(() => [{ query }]); + await queryManager.mutate({ mutation, refetchQueries: conditional }); - describe("refetchQueries", () => { - let consoleWarnSpy: jest.SpyInstance; - beforeEach(() => { - consoleWarnSpy = jest.spyOn(console, "warn").mockImplementation(); - }); - afterEach(() => { - consoleWarnSpy.mockRestore(); + expect(conditional).toHaveBeenCalledTimes(1); + expect(conditional).toHaveBeenCalledWith( + expect.objectContaining({ data: mutationData }) + ); + + await expect(stream).toEmitMatchedValue({ data: secondReqData }); }); - itAsync( - "should refetch the right query when a result is successfully returned", - (resolve, reject) => { - const mutation = gql` - mutation changeAuthorName { - changeAuthorName(newName: "Jack Smith") { - firstName - lastName - } - } - `; - const mutationData = { - changeAuthorName: { - firstName: "Jack", - lastName: "Smith", - }, - }; - const query = gql` - query getAuthors($id: ID!) { - author(id: $id) { - firstName - lastName - } - } - `; - const data = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; - const secondReqData = { - author: { - firstName: "Jane", - lastName: "Johnson", - }, - }; - const variables = { id: "1234" }; - const queryManager = mockQueryManager( - { - request: { query, variables }, - result: { data }, - }, - { - request: { query, variables }, - result: { data: secondReqData }, - }, - { - request: { query: mutation }, - result: { data: mutationData }, + it("should refetch using the original query context (if any)", async () => { + const mutation = gql` + mutation changeAuthorName { + changeAuthorName(newName: "Jack Smith") { + firstName + lastName } - ); - const observable = queryManager.watchQuery({ - query, - variables, - notifyOnNetworkStatusChange: false, - }); - return observableToPromise( - { observable }, - (result) => { - expect(result.data).toEqual(data); - queryManager.mutate({ mutation, refetchQueries: ["getAuthors"] }); - }, - (result) => { - expect(observable.getCurrentResult().data).toEqual(secondReqData); - expect(result.data).toEqual(secondReqData); + } + `; + const mutationData = { + changeAuthorName: { + firstName: "Jack", + lastName: "Smith", + }, + }; + const query = gql` + query getAuthors($id: ID!) { + author(id: $id) { + firstName + lastName } - ).then(resolve, reject); - } - ); + } + `; + const data = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; + const secondReqData = { + author: { + firstName: "Jane", + lastName: "Johnson", + }, + }; + const variables = { id: "1234" }; + const queryManager = mockQueryManager( + { + request: { query, variables }, + result: { data }, + }, + { + request: { query, variables }, + result: { data: secondReqData }, + }, + { + request: { query: mutation }, + result: { data: mutationData }, + } + ); - itAsync( - "should not warn and continue when an unknown query name is asked to refetch", - (resolve, reject) => { - const mutation = gql` - mutation changeAuthorName { - changeAuthorName(newName: "Jack Smith") { - firstName - lastName - } - } - `; - const mutationData = { - changeAuthorName: { - firstName: "Jack", - lastName: "Smith", - }, - }; - const query = gql` - query getAuthors { - author { - firstName - lastName - } - } - `; - const data = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; - const secondReqData = { - author: { - firstName: "Jane", - lastName: "Johnson", - }, - }; - const queryManager = mockQueryManager( - { - request: { query }, - result: { data }, - }, - { - request: { query }, - result: { data: secondReqData }, - }, - { - request: { query: mutation }, - result: { data: mutationData }, - } - ); - const observable = queryManager.watchQuery({ - query, - notifyOnNetworkStatusChange: false, - }); - return observableToPromise( - { observable }, - (result) => { - expect(result.data).toEqual(data); - queryManager.mutate({ - mutation, - refetchQueries: ["fakeQuery", "getAuthors"], - }); - }, - (result) => { - expect(result.data).toEqual(secondReqData); - expect(consoleWarnSpy).toHaveBeenLastCalledWith( - 'Unknown query named "%s" requested in refetchQueries options.include array', - "fakeQuery" - ); - } - ).then(resolve, reject); - } - ); + const headers = { + someHeader: "some value", + }; + const observable = queryManager.watchQuery({ + query, + variables, + context: { + headers, + }, + notifyOnNetworkStatusChange: false, + }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + + void queryManager.mutate({ + mutation, + refetchQueries: ["getAuthors"], + }); - itAsync( - "should ignore (with warning) a query named in refetchQueries that has no active subscriptions", - (resolve, reject) => { - const mutation = gql` - mutation changeAuthorName { - changeAuthorName(newName: "Jack Smith") { - firstName - lastName - } - } - `; - const mutationData = { - changeAuthorName: { - firstName: "Jack", - lastName: "Smith", - }, - }; - const query = gql` - query getAuthors { - author { - firstName - lastName - } - } - `; - const data = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; - const secondReqData = { - author: { - firstName: "Jane", - lastName: "Johnson", - }, - }; - const queryManager = mockQueryManager( - { - request: { query }, - result: { data }, - }, - { - request: { query }, - result: { data: secondReqData }, - }, - { - request: { query: mutation }, - result: { data: mutationData }, - } - ); + await expect(stream).toEmitNext(); - const observable = queryManager.watchQuery({ query }); - return observableToPromise({ observable }, (result) => { - expect(result.data).toEqual(data); - }) - .then(() => { - // The subscription has been stopped already - return queryManager.mutate({ - mutation, - refetchQueries: ["getAuthors"], - }); - }) - .then(() => { - expect(consoleWarnSpy).toHaveBeenLastCalledWith( - 'Unknown query named "%s" requested in refetchQueries options.include array', - "getAuthors" - ); - }) - .then(resolve, reject); - } - ); + const context = ( + queryManager.link as MockApolloLink + ).operation!.getContext(); + expect(context.headers).not.toBeUndefined(); + expect(context.headers.someHeader).toEqual(headers.someHeader); + }); - it("also works with a query document and variables", async () => { + it("should refetch using the specified context, if provided", async () => { const mutation = gql` - mutation changeAuthorName($id: ID!) { - changeAuthorName(newName: "Jack Smith", id: $id) { + mutation changeAuthorName { + changeAuthorName(newName: "Jack Smith") { firstName lastName } @@ -5191,886 +4932,714 @@ describe("QueryManager", () => { lastName: "Johnson", }, }; - const variables = { id: "1234" }; - const mutationVariables = { id: "2345" }; const queryManager = mockQueryManager( { request: { query, variables }, result: { data }, - delay: 10, }, { request: { query, variables }, result: { data: secondReqData }, - delay: 100, }, { - request: { query: mutation, variables: mutationVariables }, + request: { query: mutation }, result: { data: mutationData }, - delay: 10, } ); - const observable = queryManager.watchQuery({ query, variables }); + + const observable = queryManager.watchQuery({ + query, + variables, + notifyOnNetworkStatusChange: false, + }); + const stream = new ObservableStream(observable); + + const headers = { + someHeader: "some value", + }; + + await expect(stream).toEmitNext(); + + void queryManager.mutate({ + mutation, + refetchQueries: [ + { + query, + variables, + context: { + headers, + }, + }, + ], + }); + + await expect(stream).toEmitNext(); + + const context = ( + queryManager.link as MockApolloLink + ).operation!.getContext(); + expect(context.headers).not.toBeUndefined(); + expect(context.headers.someHeader).toEqual(headers.someHeader); + }); + }); + + describe("onQueryUpdated", () => { + const mutation = gql` + mutation changeAuthorName { + changeAuthorName(newName: "Jack Smith") { + firstName + lastName + } + } + `; + + const mutationData = { + changeAuthorName: { + firstName: "Jack", + lastName: "Smith", + }, + }; + + const query = gql` + query getAuthors($id: ID!) { + author(id: $id) { + firstName + lastName + } + } + `; + + const data = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; + + const secondReqData = { + author: { + firstName: "Jane", + lastName: "Johnson", + }, + }; + + const variables = { id: "1234" }; + + function makeQueryManager() { + return mockQueryManager( + { + request: { query, variables }, + result: { data }, + }, + { + request: { query, variables }, + result: { data: secondReqData }, + }, + { + request: { query: mutation }, + result: { data: mutationData }, + } + ); + } + + it("should refetch the right query when a result is successfully returned", async () => { + const queryManager = makeQueryManager(); + + const observable = queryManager.watchQuery({ + query, + variables, + notifyOnNetworkStatusChange: false, + }); const stream = new ObservableStream(observable); + let finishedRefetch = false; + await expect(stream).toEmitMatchedValue({ data }); await queryManager.mutate({ mutation, - variables: mutationVariables, - refetchQueries: [{ query, variables }], + + update(cache) { + cache.modify({ + fields: { + author(_, { INVALIDATE }) { + return INVALIDATE; + }, + }, + }); + }, + + async onQueryUpdated(obsQuery) { + expect(obsQuery.options.query).toBe(query); + const result = await obsQuery.refetch(); + + // Wait a bit to make sure the mutation really awaited the + // refetching of the query. + await wait(100); + finishedRefetch = true; + return result; + }, }); - await expect(stream).toEmitMatchedValue( - { data: secondReqData }, - { timeout: 150 } - ); + expect(finishedRefetch).toBe(true); + await expect(stream).toEmitMatchedValue({ data: secondReqData }); expect(observable.getCurrentResult().data).toEqual(secondReqData); + }); - await wait(10); + it("should refetch using the original query context (if any)", async () => { + const queryManager = makeQueryManager(); - queryManager["queries"].forEach((_, queryId) => { - expect(queryId).not.toContain("legacyOneTimeQuery"); + const headers = { + someHeader: "some value", + }; + + const observable = queryManager.watchQuery({ + query, + variables, + context: { + headers, + }, + notifyOnNetworkStatusChange: false, }); + const stream = new ObservableStream(observable); - await expect(stream).not.toEmitAnything(); - }); + await expect(stream).toEmitMatchedValue({ data }); - itAsync( - "also works with a conditional function that returns false", - (resolve, reject) => { - const mutation = gql` - mutation changeAuthorName { - changeAuthorName(newName: "Jack Smith") { - firstName - lastName - } - } - `; - const mutationData = { - changeAuthorName: { - firstName: "Jack", - lastName: "Smith", - }, - }; - const query = gql` - query getAuthors { - author { - firstName - lastName - } - } - `; - const data = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; - const secondReqData = { - author: { - firstName: "Jane", - lastName: "Johnson", - }, - }; - const queryManager = mockQueryManager( - { - request: { query }, - result: { data }, - }, - { - request: { query }, - result: { data: secondReqData }, - }, - { - request: { query: mutation }, - result: { data: mutationData }, - } - ); - const observable = queryManager.watchQuery({ query }); - const conditional = (result: FetchResult) => { - expect(result.data).toEqual(mutationData); - return []; - }; + void queryManager.mutate({ + mutation, - return observableToPromise({ observable }, (result) => { - expect(result.data).toEqual(data); - queryManager.mutate({ mutation, refetchQueries: conditional }); - }).then(resolve, reject); - } - ); + update(cache) { + cache.modify({ + fields: { + author(_, { INVALIDATE }) { + return INVALIDATE; + }, + }, + }); + }, - itAsync( - "also works with a conditional function that returns an array of refetches", - (resolve, reject) => { - const mutation = gql` - mutation changeAuthorName { - changeAuthorName(newName: "Jack Smith") { - firstName - lastName - } - } - `; - const mutationData = { - changeAuthorName: { - firstName: "Jack", - lastName: "Smith", - }, - }; - const query = gql` - query getAuthors { - author { - firstName - lastName - } - } - `; - const data = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; - const secondReqData = { - author: { - firstName: "Jane", - lastName: "Johnson", - }, - }; - const queryManager = mockQueryManager( - { - request: { query }, - result: { data }, - }, - { - request: { query }, - result: { data: secondReqData }, - }, - { - request: { query: mutation }, - result: { data: mutationData }, - } - ); - const observable = queryManager.watchQuery({ query }); - const conditional = (result: FetchResult) => { - expect(result.data).toEqual(mutationData); - return [{ query }]; - }; + onQueryUpdated(obsQuery) { + expect(obsQuery.options.query).toBe(query); + return obsQuery.refetch(); + }, + }); - return observableToPromise( - { observable }, - (result) => { - expect(result.data).toEqual(data); - queryManager.mutate({ mutation, refetchQueries: conditional }); - }, - (result) => expect(result.data).toEqual(secondReqData) - ).then(resolve, reject); - } - ); + await expect(stream).toEmitMatchedValue({ data: secondReqData }); - itAsync( - "should refetch using the original query context (if any)", - (resolve, reject) => { - const mutation = gql` - mutation changeAuthorName { - changeAuthorName(newName: "Jack Smith") { - firstName - lastName - } - } - `; - const mutationData = { - changeAuthorName: { - firstName: "Jack", - lastName: "Smith", - }, - }; - const query = gql` - query getAuthors($id: ID!) { - author(id: $id) { - firstName - lastName - } - } - `; - const data = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; - const secondReqData = { - author: { - firstName: "Jane", - lastName: "Johnson", - }, - }; - const variables = { id: "1234" }; - const queryManager = mockQueryManager( - { - request: { query, variables }, - result: { data }, - }, - { - request: { query, variables }, - result: { data: secondReqData }, - }, - { - request: { query: mutation }, - result: { data: mutationData }, - } - ); + const context = ( + queryManager.link as MockApolloLink + ).operation!.getContext(); + expect(context.headers).not.toBeUndefined(); + expect(context.headers.someHeader).toEqual(headers.someHeader); + }); + + it("should refetch using the specified context, if provided", async () => { + const queryManager = makeQueryManager(); + + const observable = queryManager.watchQuery({ + query, + variables, + notifyOnNetworkStatusChange: false, + }); + const stream = new ObservableStream(observable); + + const headers = { + someHeader: "some value", + }; - const headers = { - someHeader: "some value", - }; - const observable = queryManager.watchQuery({ - query, - variables, - context: { - headers, - }, - notifyOnNetworkStatusChange: false, - }); + await expect(stream).toEmitMatchedValue({ data }); - return observableToPromise( - { observable }, - (result) => { - queryManager.mutate({ - mutation, - refetchQueries: ["getAuthors"], - }); - }, - (result) => { - const context = ( - queryManager.link as MockApolloLink - ).operation!.getContext(); - expect(context.headers).not.toBeUndefined(); - expect(context.headers.someHeader).toEqual(headers.someHeader); - } - ).then(resolve, reject); - } - ); + void queryManager.mutate({ + mutation, - itAsync( - "should refetch using the specified context, if provided", - (resolve, reject) => { - const mutation = gql` - mutation changeAuthorName { - changeAuthorName(newName: "Jack Smith") { - firstName - lastName - } - } - `; - const mutationData = { - changeAuthorName: { - firstName: "Jack", - lastName: "Smith", - }, - }; - const query = gql` - query getAuthors($id: ID!) { - author(id: $id) { - firstName - lastName - } - } - `; - const data = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; - const secondReqData = { - author: { - firstName: "Jane", - lastName: "Johnson", - }, - }; - const variables = { id: "1234" }; - const queryManager = mockQueryManager( - { - request: { query, variables }, - result: { data }, - }, - { - request: { query, variables }, - result: { data: secondReqData }, - }, - { - request: { query: mutation }, - result: { data: mutationData }, - } - ); + update(cache) { + cache.evict({ fieldName: "author" }); + }, - const observable = queryManager.watchQuery({ - query, - variables, - notifyOnNetworkStatusChange: false, - }); + onQueryUpdated(obsQuery) { + expect(obsQuery.options.query).toBe(query); + return obsQuery.reobserve({ + fetchPolicy: "network-only", + context: { + ...obsQuery.options.context, + headers, + }, + }); + }, + }); - const headers = { - someHeader: "some value", - }; + await expect(stream).toEmitMatchedValue({ data: secondReqData }); - return observableToPromise( - { observable }, - (result) => { - queryManager.mutate({ - mutation, - refetchQueries: [ - { - query, - variables, - context: { - headers, - }, - }, - ], - }); - }, - (result) => { - const context = ( - queryManager.link as MockApolloLink - ).operation!.getContext(); - expect(context.headers).not.toBeUndefined(); - expect(context.headers.someHeader).toEqual(headers.someHeader); - } - ).then(resolve, reject); - } - ); + const context = ( + queryManager.link as MockApolloLink + ).operation!.getContext(); + expect(context.headers).not.toBeUndefined(); + expect(context.headers.someHeader).toEqual(headers.someHeader); + }); }); - describe("onQueryUpdated", () => { - const mutation = gql` - mutation changeAuthorName { - changeAuthorName(newName: "Jack Smith") { - firstName - lastName + describe("awaitRefetchQueries", () => { + it("should not wait for `refetchQueries` to complete before resolving the mutation, when `awaitRefetchQueries` is undefined", async () => { + const query = gql` + query getAuthors($id: ID!) { + author(id: $id) { + firstName + lastName + } } - } - `; + `; - const mutationData = { - changeAuthorName: { - firstName: "Jack", - lastName: "Smith", - }, - }; + const queryData = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; - const query = gql` - query getAuthors($id: ID!) { - author(id: $id) { - firstName - lastName + const mutation = gql` + mutation changeAuthorName { + changeAuthorName(newName: "Jack Smith") { + firstName + lastName + } } - } - `; + `; - const data = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; + const mutationData = { + changeAuthorName: { + firstName: "Jack", + lastName: "Smith", + }, + }; - const secondReqData = { - author: { - firstName: "Jane", - lastName: "Johnson", - }, - }; + const secondReqData = { + author: { + firstName: "Jane", + lastName: "Johnson", + }, + }; - const variables = { id: "1234" }; + const variables = { id: "1234" }; - function makeQueryManager() { - return mockQueryManager( + const queryManager = mockQueryManager( { request: { query, variables }, - result: { data }, + result: { data: queryData }, + }, + { + request: { query: mutation }, + result: { data: mutationData }, }, { request: { query, variables }, result: { data: secondReqData }, + } + ); + + const observable = queryManager.watchQuery({ + query, + variables, + notifyOnNetworkStatusChange: false, + }); + const stream = new ObservableStream(observable); + let mutationComplete = false; + + await expect(stream).toEmitMatchedValue({ data: queryData }); + + void queryManager + .mutate({ + mutation, + refetchQueries: ["getAuthors"], + awaitRefetchQueries: false, + }) + .then(() => { + mutationComplete = true; + }); + + await expect(stream).toEmitMatchedValue({ data: secondReqData }); + expect(observable.getCurrentResult().data).toEqual(secondReqData); + expect(mutationComplete).toBe(true); + }); + + it("should not wait for `refetchQueries` to complete before resolving the mutation, when `awaitRefetchQueries` is false", async () => { + const query = gql` + query getAuthors($id: ID!) { + author(id: $id) { + firstName + lastName + } + } + `; + + const queryData = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; + + const mutation = gql` + mutation changeAuthorName { + changeAuthorName(newName: "Jack Smith") { + firstName + lastName + } + } + `; + + const mutationData = { + changeAuthorName: { + firstName: "Jack", + lastName: "Smith", + }, + }; + + const secondReqData = { + author: { + firstName: "Jane", + lastName: "Johnson", + }, + }; + + const variables = { id: "1234" }; + + const queryManager = mockQueryManager( + { + request: { query, variables }, + result: { data: queryData }, }, { request: { query: mutation }, result: { data: mutationData }, + }, + { + request: { query, variables }, + result: { data: secondReqData }, } ); - } - itAsync( - "should refetch the right query when a result is successfully returned", - (resolve, reject) => { - const queryManager = makeQueryManager(); + const observable = queryManager.watchQuery({ + query, + variables, + notifyOnNetworkStatusChange: false, + }); + const stream = new ObservableStream(observable); + let mutationComplete = false; + + await expect(stream).toEmitMatchedValue({ data: queryData }); - const observable = queryManager.watchQuery({ - query, - variables, - notifyOnNetworkStatusChange: false, + void queryManager + .mutate({ mutation, refetchQueries: ["getAuthors"] }) + .then(() => { + mutationComplete = true; }); - let finishedRefetch = false; - - return observableToPromise( - { observable }, - (result) => { - expect(result.data).toEqual(data); - - return queryManager - .mutate({ - mutation, - - update(cache) { - cache.modify({ - fields: { - author(_, { INVALIDATE }) { - return INVALIDATE; - }, - }, - }); - }, - - onQueryUpdated(obsQuery) { - expect(obsQuery.options.query).toBe(query); - return obsQuery.refetch().then(async (result) => { - // Wait a bit to make sure the mutation really awaited the - // refetching of the query. - await new Promise((resolve) => setTimeout(resolve, 100)); - finishedRefetch = true; - return result; - }); - }, - }) - .then(() => { - expect(finishedRefetch).toBe(true); - }); - }, + await expect(stream).toEmitMatchedValue({ data: secondReqData }); + expect(observable.getCurrentResult().data).toEqual(secondReqData); + expect(mutationComplete).toBe(true); + }); - (result) => { - expect(observable.getCurrentResult().data).toEqual(secondReqData); - expect(result.data).toEqual(secondReqData); - expect(finishedRefetch).toBe(true); + it("should wait for `refetchQueries` to complete before resolving the mutation, when `awaitRefetchQueries` is `true`", async () => { + const query = gql` + query getAuthors($id: ID!) { + author(id: $id) { + firstName + lastName } - ).then(resolve, reject); - } - ); + } + `; - itAsync( - "should refetch using the original query context (if any)", - (resolve, reject) => { - const queryManager = makeQueryManager(); + const queryData = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; - const headers = { - someHeader: "some value", - }; + const mutation = gql` + mutation changeAuthorName { + changeAuthorName(newName: "Jack Smith") { + firstName + lastName + } + } + `; - const observable = queryManager.watchQuery({ - query, - variables, - context: { - headers, - }, - notifyOnNetworkStatusChange: false, - }); + const mutationData = { + changeAuthorName: { + firstName: "Jack", + lastName: "Smith", + }, + }; - return observableToPromise( - { observable }, - (result) => { - expect(result.data).toEqual(data); - - queryManager.mutate({ - mutation, - - update(cache) { - cache.modify({ - fields: { - author(_, { INVALIDATE }) { - return INVALIDATE; - }, - }, - }); - }, + const secondReqData = { + author: { + firstName: "Jane", + lastName: "Johnson", + }, + }; - onQueryUpdated(obsQuery) { - expect(obsQuery.options.query).toBe(query); - return obsQuery.refetch(); - }, - }); - }, + const variables = { id: "1234" }; + + const queryManager = mockQueryManager( + { + request: { query, variables }, + result: { data: queryData }, + }, + { + request: { query: mutation }, + result: { data: mutationData }, + }, + { + request: { query, variables }, + result: { data: secondReqData }, + } + ); - (result) => { - expect(result.data).toEqual(secondReqData); - const context = ( - queryManager.link as MockApolloLink - ).operation!.getContext(); - expect(context.headers).not.toBeUndefined(); - expect(context.headers.someHeader).toEqual(headers.someHeader); - } - ).then(resolve, reject); - } - ); + const observable = queryManager.watchQuery({ + query, + variables, + notifyOnNetworkStatusChange: false, + }); + const stream = new ObservableStream(observable); + let mutationComplete = false; - itAsync( - "should refetch using the specified context, if provided", - (resolve, reject) => { - const queryManager = makeQueryManager(); + await expect(stream).toEmitMatchedValue({ data: queryData }); - const observable = queryManager.watchQuery({ - query, - variables, - notifyOnNetworkStatusChange: false, + void queryManager + .mutate({ + mutation, + refetchQueries: ["getAuthors"], + awaitRefetchQueries: true, + }) + .then(() => { + mutationComplete = true; }); - const headers = { - someHeader: "some value", - }; - - return observableToPromise( - { observable }, - (result) => { - expect(result.data).toEqual(data); - - queryManager.mutate({ - mutation, - - update(cache) { - cache.evict({ fieldName: "author" }); - }, - - onQueryUpdated(obsQuery) { - expect(obsQuery.options.query).toBe(query); - return obsQuery.reobserve({ - fetchPolicy: "network-only", - context: { - ...obsQuery.options.context, - headers, - }, - }); - }, - }); - }, - - (result) => { - expect(result.data).toEqual(secondReqData); - const context = ( - queryManager.link as MockApolloLink - ).operation!.getContext(); - expect(context.headers).not.toBeUndefined(); - expect(context.headers.someHeader).toEqual(headers.someHeader); - } - ).then(resolve, reject); - } - ); - }); + await expect(stream).toEmitMatchedValue({ data: secondReqData }); + expect(observable.getCurrentResult().data).toEqual(secondReqData); + expect(mutationComplete).toBe(false); + }); - describe("awaitRefetchQueries", () => { - const awaitRefetchTest = ({ - awaitRefetchQueries, - testQueryError = false, - }: MutationBaseOptions & { testQueryError?: boolean }) => - new Promise((resolve, reject) => { - const query = gql` - query getAuthors($id: ID!) { - author(id: $id) { - firstName - lastName - } + it("should allow catching errors from `refetchQueries` when `awaitRefetchQueries` is `true`", async () => { + const query = gql` + query getAuthors($id: ID!) { + author(id: $id) { + firstName + lastName } - `; + } + `; - const queryData = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; + const queryData = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; - const mutation = gql` - mutation changeAuthorName { - changeAuthorName(newName: "Jack Smith") { - firstName - lastName - } + const mutation = gql` + mutation changeAuthorName { + changeAuthorName(newName: "Jack Smith") { + firstName + lastName } - `; + } + `; - const mutationData = { - changeAuthorName: { - firstName: "Jack", - lastName: "Smith", - }, - }; + const mutationData = { + changeAuthorName: { + firstName: "Jack", + lastName: "Smith", + }, + }; - const secondReqData = { - author: { - firstName: "Jane", - lastName: "Johnson", - }, - }; + const secondReqData = { + author: { + firstName: "Jane", + lastName: "Johnson", + }, + }; - const variables = { id: "1234" }; + const variables = { id: "1234" }; + const refetchError = new Error("Refetch failed"); - const refetchError = - testQueryError ? new Error("Refetch failed") : undefined; + const queryManager = mockQueryManager( + { + request: { query, variables }, + result: { data: queryData }, + }, + { + request: { query: mutation }, + result: { data: mutationData }, + }, + { + request: { query, variables }, + result: { data: secondReqData }, + error: refetchError, + } + ); - const queryManager = mockQueryManager( - { - request: { query, variables }, - result: { data: queryData }, - }, - { - request: { query: mutation }, - result: { data: mutationData }, - }, - { - request: { query, variables }, - result: { data: secondReqData }, - error: refetchError, - } - ); + const observable = queryManager.watchQuery({ + query, + variables, + notifyOnNetworkStatusChange: false, + }); + const stream = new ObservableStream(observable); + let isRefetchErrorCaught = false; - const observable = queryManager.watchQuery({ - query, - variables, - notifyOnNetworkStatusChange: false, + await expect(stream).toEmitMatchedValue({ data: queryData }); + + void queryManager + .mutate({ + mutation, + refetchQueries: ["getAuthors"], + awaitRefetchQueries: true, + }) + .catch((error) => { + expect(error).toBeDefined(); + isRefetchErrorCaught = true; }); - let isRefetchErrorCaught = false; - let mutationComplete = false; - return observableToPromise( - { observable }, - (result) => { - expect(result.data).toEqual(queryData); - const mutateOptions: MutationOptions = { - mutation, - refetchQueries: ["getAuthors"], - }; - if (awaitRefetchQueries) { - mutateOptions.awaitRefetchQueries = awaitRefetchQueries; - } - queryManager - .mutate(mutateOptions) - .then(() => { - mutationComplete = true; - }) - .catch((error) => { - expect(error).toBeDefined(); - isRefetchErrorCaught = true; - }); - }, - (result) => { - if (awaitRefetchQueries) { - expect(mutationComplete).not.toBeTruthy(); - } else { - expect(mutationComplete).toBeTruthy(); - } - expect(observable.getCurrentResult().data).toEqual(secondReqData); - expect(result.data).toEqual(secondReqData); - } - ) - .then(() => resolve()) - .catch((error) => { - const isRefetchError = - awaitRefetchQueries && - testQueryError && - error.message.includes(refetchError?.message); - - if (isRefetchError) { - return setTimeout(() => { - expect(isRefetchErrorCaught).toBe(true); - resolve(); - }, 10); - } + await expect(stream).toEmitError( + new ApolloError({ networkError: refetchError }) + ); + expect(isRefetchErrorCaught).toBe(true); + }); + }); - reject(error); - }); - }); + describe("store watchers", () => { + it("does not fill up the store on resolved queries", async () => { + const query1 = gql` + query One { + one + } + `; + const query2 = gql` + query Two { + two + } + `; + const query3 = gql` + query Three { + three + } + `; + const query4 = gql` + query Four { + four + } + `; - it( - "should not wait for `refetchQueries` to complete before resolving " + - "the mutation, when `awaitRefetchQueries` is undefined", - () => awaitRefetchTest({ awaitRefetchQueries: void 0 }) - ); + const link = mockSingleLink( + { request: { query: query1 }, result: { data: { one: 1 } } }, + { request: { query: query2 }, result: { data: { two: 2 } } }, + { request: { query: query3 }, result: { data: { three: 3 } } }, + { request: { query: query4 }, result: { data: { four: 4 } } } + ); + const cache = new InMemoryCache(); - it( - "should not wait for `refetchQueries` to complete before resolving " + - "the mutation, when `awaitRefetchQueries` is false", - () => awaitRefetchTest({ awaitRefetchQueries: false }) - ); + const queryManager = new QueryManager( + getDefaultOptionsForQueryManagerTests({ + link, + cache, + }) + ); - it( - "should wait for `refetchQueries` to complete before resolving " + - "the mutation, when `awaitRefetchQueries` is `true`", - () => awaitRefetchTest({ awaitRefetchQueries: true }) - ); + await queryManager.query({ query: query1 }); + await queryManager.query({ query: query2 }); + await queryManager.query({ query: query3 }); + await queryManager.query({ query: query4 }); + await wait(10); - it( - "should allow catching errors from `refetchQueries` when " + - "`awaitRefetchQueries` is `true`", - () => - awaitRefetchTest({ awaitRefetchQueries: true, testQueryError: true }) - ); + expect(cache["watches"].size).toBe(0); + }); }); - describe("store watchers", () => { - itAsync( - "does not fill up the store on resolved queries", - (resolve, reject) => { - const query1 = gql` - query One { - one - } - `; - const query2 = gql` - query Two { - two - } - `; - const query3 = gql` - query Three { - three - } - `; - const query4 = gql` - query Four { - four + describe("`no-cache` handling", () => { + it("should return a query result (if one exists) when a `no-cache` fetch policy is used", async () => { + const query = gql` + query { + author { + firstName + lastName } - `; - - const link = mockSingleLink( - { request: { query: query1 }, result: { data: { one: 1 } } }, - { request: { query: query2 }, result: { data: { two: 2 } } }, - { request: { query: query3 }, result: { data: { three: 3 } } }, - { request: { query: query4 }, result: { data: { four: 4 } } } - ).setOnError(reject); - const cache = new InMemoryCache(); - - const queryManager = new QueryManager( - getDefaultOptionsForQueryManagerTests({ - link, - cache, - }) - ); + } + `; - return queryManager - .query({ query: query1 }) - .then((one) => { - return queryManager.query({ query: query2 }); - }) - .then(() => { - return queryManager.query({ query: query3 }); - }) - .then(() => { - return queryManager.query({ query: query4 }); - }) - .then(() => { - return new Promise((r) => { - setTimeout(r, 10); - }); - }) - .then(() => { - // @ts-ignore - expect(cache.watches.size).toBe(0); - }) - .then(resolve, reject); - } - ); - }); + const data = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; - describe("`no-cache` handling", () => { - itAsync( - "should return a query result (if one exists) when a `no-cache` fetch policy is used", - (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } - } - `; + const queryManager = createQueryManager({ + link: mockSingleLink({ + request: { query }, + result: { data }, + }), + }); - const data = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; + const observable = queryManager.watchQuery({ + query, + fetchPolicy: "no-cache", + }); + const stream = new ObservableStream(observable); - const queryManager = createQueryManager({ - link: mockSingleLink({ - request: { query }, - result: { data }, - }).setOnError(reject), - }); + await expect(stream).toEmitMatchedValue({ data }); - const observable = queryManager.watchQuery({ - query, - fetchPolicy: "no-cache", - }); - observableToPromise({ observable }, (result) => { - expect(result.data).toEqual(data); - const currentResult = getCurrentQueryResult(observable); - expect(currentResult.data).toEqual(data); - resolve(); - }); - } - ); + const currentResult = getCurrentQueryResult(observable); + expect(currentResult.data).toEqual(data); + }); }); describe("client awareness", () => { - itAsync( - "should pass client awareness settings into the link chain via context", - (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } + it("should pass client awareness settings into the link chain via context", async () => { + const query = gql` + query { + author { + firstName + lastName } - `; + } + `; - const data = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; + const data = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; - const link = mockSingleLink({ - request: { query }, - result: { data }, - }).setOnError(reject); + const link = mockSingleLink({ + request: { query }, + result: { data }, + }); - const clientAwareness = { - name: "Test", - version: "1.0.0", - }; + const clientAwareness = { + name: "Test", + version: "1.0.0", + }; - const queryManager = createQueryManager({ - link, - clientAwareness, - }); + const queryManager = createQueryManager({ + link, + clientAwareness, + }); - const observable = queryManager.watchQuery({ - query, - fetchPolicy: "no-cache", - }); + const observable = queryManager.watchQuery({ + query, + fetchPolicy: "no-cache", + }); + const stream = new ObservableStream(observable); - observableToPromise({ observable }, (result) => { - const context = link.operation!.getContext(); - expect(context.clientAwareness).toBeDefined(); - expect(context.clientAwareness).toEqual(clientAwareness); - resolve(); - }); - } - ); + await expect(stream).toEmitNext(); + + const context = link.operation!.getContext(); + expect(context.clientAwareness).toBeDefined(); + expect(context.clientAwareness).toEqual(clientAwareness); + }); }); describe("queryDeduplication", () => { @@ -6093,7 +5662,7 @@ describe("QueryManager", () => { }), }); - queryManager.query({ query, context: { queryDeduplication: true } }); + void queryManager.query({ query, context: { queryDeduplication: true } }); expect( queryManager["inFlightLinkObservables"].peek(print(query), "{}") @@ -6144,11 +5713,9 @@ describe("QueryManager", () => { spy.mockRestore(); }); - function validateWarnings( - resolve: (result?: any) => void, - reject: (reason?: any) => void, - returnPartialData = false, - expectedWarnCount = 1 + async function validateWarnings( + returnPartialData: boolean, + expectedWarnCount: number ) { const query1 = gql` query { @@ -6194,38 +5761,34 @@ describe("QueryManager", () => { returnPartialData, }); - return observableToPromise({ observable: observable1 }, (result) => { - expect(result).toEqual({ - loading: false, - data: data1, - networkStatus: NetworkStatus.ready, - }); - }).then(() => { - observableToPromise({ observable: observable2 }, (result) => { - expect(result).toEqual({ - data: data1, - loading: false, - networkStatus: NetworkStatus.ready, - partial: true, - }); - expect(spy).toHaveBeenCalledTimes(expectedWarnCount); - }).then(resolve, reject); + const stream1 = new ObservableStream(observable1); + + await expect(stream1).toEmitValue({ + data: data1, + loading: false, + networkStatus: NetworkStatus.ready, + }); + + stream1.unsubscribe(); + + const stream2 = new ObservableStream(observable2); + + await expect(stream2).toEmitMatchedValue({ + data: data1, + loading: false, + networkStatus: NetworkStatus.ready, + partial: true, }); + expect(spy).toHaveBeenCalledTimes(expectedWarnCount); } - itAsync( - "should show missing cache result fields warning when returnPartialData is false", - (resolve, reject) => { - validateWarnings(resolve, reject, false, 1); - } - ); + it("should show missing cache result fields warning when returnPartialData is false", async () => { + await validateWarnings(false, 1); + }); - itAsync( - "should not show missing cache result fields warning when returnPartialData is true", - (resolve, reject) => { - validateWarnings(resolve, reject, true, 0); - } - ); + it("should not show missing cache result fields warning when returnPartialData is true", async () => { + await validateWarnings(true, 0); + }); }); describe("defaultContext", () => { diff --git a/src/core/__tests__/QueryManager/links.ts b/src/core/__tests__/QueryManager/links.ts index 53d3b22f6bb..de43efdf8b9 100644 --- a/src/core/__tests__/QueryManager/links.ts +++ b/src/core/__tests__/QueryManager/links.ts @@ -10,7 +10,7 @@ import { ApolloLink } from "../../../link/core"; import { InMemoryCache } from "../../../cache/inmemory/inMemoryCache"; // mocks -import { itAsync, MockSubscriptionLink } from "../../../testing/core"; +import { MockSubscriptionLink } from "../../../testing/core"; // core import { QueryManager } from "../../QueryManager"; @@ -18,308 +18,277 @@ import { NextLink, Operation, Reference } from "../../../core"; import { getDefaultOptionsForQueryManagerTests } from "../../../testing/core/mocking/mockQueryManager"; describe("Link interactions", () => { - itAsync( - "includes the cache on the context for eviction links", - (resolve, reject) => { - const query = gql` - query CachedLuke { - people_one(id: 1) { + it("includes the cache on the context for eviction links", (done) => { + const query = gql` + query CachedLuke { + people_one(id: 1) { + name + friends { name - friends { - name - } } } - `; + } + `; - const initialData = { - people_one: { - name: "Luke Skywalker", - friends: [{ name: "Leia Skywalker" }], - }, - }; - - const evictionLink = (operation: Operation, forward: NextLink) => { - const { cache } = operation.getContext(); - expect(cache).toBeDefined(); - return forward(operation).map((result) => { - setTimeout(() => { - const cacheResult = cache.read({ query }); - expect(cacheResult).toEqual(initialData); - expect(cacheResult).toEqual(result.data); - if (count === 1) { - resolve(); - } - }, 10); - return result; - }); - }; - - const mockLink = new MockSubscriptionLink(); - const link = ApolloLink.from([evictionLink, mockLink]); - const queryManager = new QueryManager( - getDefaultOptionsForQueryManagerTests({ - cache: new InMemoryCache({ addTypename: false }), - link, - }) - ); - - const observable = queryManager.watchQuery({ - query, - variables: {}, - }); + const initialData = { + people_one: { + name: "Luke Skywalker", + friends: [{ name: "Leia Skywalker" }], + }, + }; - let count = 0; - observable.subscribe({ - next: (result) => { - count++; - }, - error: (e) => { - console.error(e); - }, + const evictionLink = (operation: Operation, forward: NextLink) => { + const { cache } = operation.getContext(); + expect(cache).toBeDefined(); + return forward(operation).map((result) => { + setTimeout(() => { + const cacheResult = cache.read({ query }); + expect(cacheResult).toEqual(initialData); + expect(cacheResult).toEqual(result.data); + if (count === 1) { + done(); + } + }, 10); + return result; }); + }; + + const mockLink = new MockSubscriptionLink(); + const link = ApolloLink.from([evictionLink, mockLink]); + const queryManager = new QueryManager( + getDefaultOptionsForQueryManagerTests({ + cache: new InMemoryCache({ addTypename: false }), + link, + }) + ); + + const observable = queryManager.watchQuery({ + query, + variables: {}, + }); - // fire off first result - mockLink.simulateResult({ result: { data: initialData } }); - } - ); - itAsync( - "cleans up all links on the final unsubscribe from watchQuery", - (resolve, reject) => { - const query = gql` - query WatchedLuke { - people_one(id: 1) { + let count = 0; + observable.subscribe({ + next: (result) => { + count++; + }, + error: (e) => { + console.error(e); + }, + }); + + // fire off first result + mockLink.simulateResult({ result: { data: initialData } }); + }); + + it("cleans up all links on the final unsubscribe from watchQuery", (done) => { + const query = gql` + query WatchedLuke { + people_one(id: 1) { + name + friends { name - friends { - name - } } } - `; + } + `; - const initialData = { - people_one: { - name: "Luke Skywalker", - friends: [{ name: "Leia Skywalker" }], - }, - }; - - const link = new MockSubscriptionLink(); - const queryManager = new QueryManager( - getDefaultOptionsForQueryManagerTests({ - cache: new InMemoryCache({ addTypename: false }), - link, - }) - ); - - const observable = queryManager.watchQuery({ - query, - variables: {}, - }); + const initialData = { + people_one: { + name: "Luke Skywalker", + friends: [{ name: "Leia Skywalker" }], + }, + }; - let count = 0; - let four: ObservableSubscription; - // first watch - const one = observable.subscribe((result) => count++); - // second watch - const two = observable.subscribe((result) => count++); - // third watch (to be unsubscribed) - const three = observable.subscribe((result) => { - count++; - three.unsubscribe(); - // fourth watch - four = observable.subscribe((x) => count++); - }); + const link = new MockSubscriptionLink(); + const queryManager = new QueryManager( + getDefaultOptionsForQueryManagerTests({ + cache: new InMemoryCache({ addTypename: false }), + link, + }) + ); - // fire off first result - link.simulateResult({ result: { data: initialData } }); - setTimeout(() => { - one.unsubscribe(); - - link.simulateResult({ - result: { - data: { - people_one: { - name: "Luke Skywalker", - friends: [{ name: "R2D2" }], - }, + const observable = queryManager.watchQuery({ + query, + variables: {}, + }); + + let count = 0; + let four: ObservableSubscription; + // first watch + const one = observable.subscribe((result) => count++); + // second watch + const two = observable.subscribe((result) => count++); + // third watch (to be unsubscribed) + const three = observable.subscribe((result) => { + count++; + three.unsubscribe(); + // fourth watch + four = observable.subscribe((x) => count++); + }); + + // fire off first result + link.simulateResult({ result: { data: initialData } }); + setTimeout(() => { + one.unsubscribe(); + + link.simulateResult({ + result: { + data: { + people_one: { + name: "Luke Skywalker", + friends: [{ name: "R2D2" }], }, }, - }); - setTimeout(() => { - four.unsubscribe(); - // final unsubscribe should be called now - two.unsubscribe(); - }, 10); + }, + }); + setTimeout(() => { + four.unsubscribe(); + // final unsubscribe should be called now + two.unsubscribe(); }, 10); + }, 10); - link.onUnsubscribe(() => { - expect(count).toEqual(6); - resolve(); - }); - } - ); - itAsync( - "cleans up all links on the final unsubscribe from watchQuery [error]", - (resolve, reject) => { - const query = gql` - query WatchedLuke { - people_one(id: 1) { + link.onUnsubscribe(() => { + expect(count).toEqual(6); + done(); + }); + }); + + it("cleans up all links on the final unsubscribe from watchQuery [error]", (done) => { + const query = gql` + query WatchedLuke { + people_one(id: 1) { + name + friends { name - friends { - name - } } } - `; + } + `; - const initialData = { - people_one: { - name: "Luke Skywalker", - friends: [{ name: "Leia Skywalker" }], - }, - }; - - const link = new MockSubscriptionLink(); - const queryManager = new QueryManager( - getDefaultOptionsForQueryManagerTests({ - cache: new InMemoryCache({ addTypename: false }), - link, - }) - ); - - const observable = queryManager.watchQuery({ - query, - variables: {}, - }); + const initialData = { + people_one: { + name: "Luke Skywalker", + friends: [{ name: "Leia Skywalker" }], + }, + }; - let count = 0; - let four: ObservableSubscription; - // first watch - const one = observable.subscribe((result) => count++); - // second watch - observable.subscribe({ - next: () => count++, - error: () => { - count = 0; - }, - }); - // third watch (to be unsubscribed) - const three = observable.subscribe((result) => { - count++; - three.unsubscribe(); - // fourth watch - four = observable.subscribe((x) => count++); - }); + const link = new MockSubscriptionLink(); + const queryManager = new QueryManager( + getDefaultOptionsForQueryManagerTests({ + cache: new InMemoryCache({ addTypename: false }), + link, + }) + ); - // fire off first result - link.simulateResult({ result: { data: initialData } }); - setTimeout(() => { - one.unsubscribe(); - four.unsubscribe(); + const observable = queryManager.watchQuery({ + query, + variables: {}, + }); - // final unsubscribe should be called now - // since errors clean up subscriptions - link.simulateResult({ error: new Error("dang") }); + let count = 0; + let four: ObservableSubscription; + // first watch + const one = observable.subscribe((result) => count++); + // second watch + observable.subscribe({ + next: () => count++, + error: () => { + count = 0; + }, + }); + // third watch (to be unsubscribed) + const three = observable.subscribe((result) => { + count++; + three.unsubscribe(); + // fourth watch + four = observable.subscribe((x) => count++); + }); - setTimeout(() => { - expect(count).toEqual(0); - resolve(); - }, 10); + // fire off first result + link.simulateResult({ result: { data: initialData } }); + setTimeout(() => { + one.unsubscribe(); + four.unsubscribe(); + + // final unsubscribe should be called now + // since errors clean up subscriptions + link.simulateResult({ error: new Error("dang") }); + + setTimeout(() => { + expect(count).toEqual(0); + done(); }, 10); + }, 10); - link.onUnsubscribe(() => { - expect(count).toEqual(4); - }); - } - ); - itAsync( - "includes the cache on the context for mutations", - (resolve, reject) => { - const mutation = gql` - mutation UpdateLuke { - people_one(id: 1) { + link.onUnsubscribe(() => { + expect(count).toEqual(4); + }); + }); + + it("includes the cache on the context for mutations", (done) => { + const mutation = gql` + mutation UpdateLuke { + people_one(id: 1) { + name + friends { name - friends { - name - } } } - `; + } + `; - const initialData = { - people_one: { - name: "Luke Skywalker", - friends: [{ name: "Leia Skywalker" }], - }, - }; - - const evictionLink = (operation: Operation, forward: NextLink) => { - const { cache } = operation.getContext(); - expect(cache).toBeDefined(); - resolve(); - return forward(operation); - }; - - const mockLink = new MockSubscriptionLink(); - const link = ApolloLink.from([evictionLink, mockLink]); - const queryManager = new QueryManager( - getDefaultOptionsForQueryManagerTests({ - cache: new InMemoryCache({ addTypename: false }), - link, - }) - ); - - queryManager.mutate({ mutation }); - - // fire off first result - mockLink.simulateResult({ result: { data: initialData } }); - } - ); - - itAsync( - "includes passed context in the context for mutations", - (resolve, reject) => { - const mutation = gql` - mutation UpdateLuke { - people_one(id: 1) { + const evictionLink = (operation: Operation, forward: NextLink) => { + const { cache } = operation.getContext(); + expect(cache).toBeDefined(); + done(); + return forward(operation); + }; + + const mockLink = new MockSubscriptionLink(); + const link = ApolloLink.from([evictionLink, mockLink]); + const queryManager = new QueryManager( + getDefaultOptionsForQueryManagerTests({ + cache: new InMemoryCache({ addTypename: false }), + link, + }) + ); + + void queryManager.mutate({ mutation }); + }); + + it("includes passed context in the context for mutations", (done) => { + const mutation = gql` + mutation UpdateLuke { + people_one(id: 1) { + name + friends { name - friends { - name - } } } - `; + } + `; + + const evictionLink = (operation: Operation, forward: NextLink) => { + const { planet } = operation.getContext(); + expect(planet).toBe("Tatooine"); + done(); + return forward(operation); + }; + + const mockLink = new MockSubscriptionLink(); + const link = ApolloLink.from([evictionLink, mockLink]); + const queryManager = new QueryManager( + getDefaultOptionsForQueryManagerTests({ + cache: new InMemoryCache({ addTypename: false }), + link, + }) + ); + + void queryManager.mutate({ mutation, context: { planet: "Tatooine" } }); + }); - const initialData = { - people_one: { - name: "Luke Skywalker", - friends: [{ name: "Leia Skywalker" }], - }, - }; - - const evictionLink = (operation: Operation, forward: NextLink) => { - const { planet } = operation.getContext(); - expect(planet).toBe("Tatooine"); - resolve(); - return forward(operation); - }; - - const mockLink = new MockSubscriptionLink(); - const link = ApolloLink.from([evictionLink, mockLink]); - const queryManager = new QueryManager( - getDefaultOptionsForQueryManagerTests({ - cache: new InMemoryCache({ addTypename: false }), - link, - }) - ); - - queryManager.mutate({ mutation, context: { planet: "Tatooine" } }); - - // fire off first result - mockLink.simulateResult({ result: { data: initialData } }); - } - ); it("includes getCacheKey function on the context for cache resolvers", async () => { const query = gql` { diff --git a/src/core/__tests__/QueryManager/multiple-results.ts b/src/core/__tests__/QueryManager/multiple-results.ts index 1d49bbb770b..a8458d0ff13 100644 --- a/src/core/__tests__/QueryManager/multiple-results.ts +++ b/src/core/__tests__/QueryManager/multiple-results.ts @@ -3,15 +3,17 @@ import gql from "graphql-tag"; import { InMemoryCache } from "../../../cache/inmemory/inMemoryCache"; // mocks -import { itAsync, MockSubscriptionLink } from "../../../testing/core"; +import { MockSubscriptionLink, wait } from "../../../testing/core"; // core import { QueryManager } from "../../QueryManager"; import { GraphQLError } from "graphql"; import { getDefaultOptionsForQueryManagerTests } from "../../../testing/core/mocking/mockQueryManager"; +import { ObservableStream } from "../../../testing/internal"; +import { ApolloError } from "../../../errors"; describe("mutiple results", () => { - itAsync("allows multiple query results from link", (resolve, reject) => { + it("allows multiple query results from link", async () => { const query = gql` query LazyLoadLuke { people_one(id: 1) { @@ -49,102 +51,162 @@ describe("mutiple results", () => { query, variables: {}, }); + const stream = new ObservableStream(observable); - let count = 0; - observable.subscribe({ - next: (result) => { - count++; - if (count === 1) { - link.simulateResult({ result: { data: laterData } }); - } - if (count === 2) { - resolve(); + // fire off first result + link.simulateResult({ result: { data: initialData } }); + + await expect(stream).toEmitValue({ + data: initialData, + loading: false, + networkStatus: 7, + }); + + link.simulateResult({ result: { data: laterData } }); + + await expect(stream).toEmitValue({ + data: laterData, + loading: false, + networkStatus: 7, + }); + }); + + it("allows multiple query results from link with ignored errors", async () => { + const query = gql` + query LazyLoadLuke { + people_one(id: 1) { + name + friends @defer { + name + } } + } + `; + + const initialData = { + people_one: { + name: "Luke Skywalker", + friends: null, }, - error: (e) => { - console.error(e); + }; + + const laterData = { + people_one: { + // XXX true defer's wouldn't send this + name: "Luke Skywalker", + friends: [{ name: "Leia Skywalker" }], }, + }; + const link = new MockSubscriptionLink(); + const queryManager = new QueryManager( + getDefaultOptionsForQueryManagerTests({ + cache: new InMemoryCache({ addTypename: false }), + link, + }) + ); + + const observable = queryManager.watchQuery({ + query, + variables: {}, + errorPolicy: "ignore", }); + const stream = new ObservableStream(observable); // fire off first result link.simulateResult({ result: { data: initialData } }); + + await expect(stream).toEmitValue({ + data: initialData, + loading: false, + networkStatus: 7, + }); + + link.simulateResult({ + result: { errors: [new GraphQLError("defer failed")] }, + }); + + await expect(stream).toEmitValueStrict({ + data: undefined, + loading: false, + networkStatus: 7, + }); + + await wait(20); + link.simulateResult({ result: { data: laterData } }); + + await expect(stream).toEmitValue({ + data: laterData, + loading: false, + networkStatus: 7, + }); }); - itAsync( - "allows multiple query results from link with ignored errors", - (resolve, reject) => { - const query = gql` - query LazyLoadLuke { - people_one(id: 1) { + it("strips errors from a result if ignored", async () => { + const query = gql` + query LazyLoadLuke { + people_one(id: 1) { + name + friends @defer { name - friends @defer { - name - } } } - `; - - const initialData = { - people_one: { - name: "Luke Skywalker", - friends: null, - }, - }; - - const laterData = { - people_one: { - // XXX true defer's wouldn't send this - name: "Luke Skywalker", - friends: [{ name: "Leia Skywalker" }], - }, - }; - const link = new MockSubscriptionLink(); - const queryManager = new QueryManager( - getDefaultOptionsForQueryManagerTests({ - cache: new InMemoryCache({ addTypename: false }), - link, - }) - ); - - const observable = queryManager.watchQuery({ - query, - variables: {}, - errorPolicy: "ignore", - }); - - let count = 0; - observable.subscribe({ - next: (result) => { - // errors should never be passed since they are ignored - expect(result.errors).toBeUndefined(); - count++; - if (count === 1) { - // this shouldn't fire the next event again - link.simulateResult({ - result: { errors: [new GraphQLError("defer failed")] }, - }); - setTimeout(() => { - link.simulateResult({ result: { data: laterData } }); - }, 20); - } - if (count === 2) { - // make sure the count doesn't go up by accident - setTimeout(() => { - if (count === 3) throw new Error("error was not ignored"); - resolve(); - }); - } - }, - error: (e) => { - console.error(e); - }, - }); - - // fire off first result - link.simulateResult({ result: { data: initialData } }); - } - ); - itAsync("strips errors from a result if ignored", (resolve, reject) => { + } + `; + + const initialData = { + people_one: { + name: "Luke Skywalker", + friends: null, + }, + }; + + const laterData = { + people_one: { + // XXX true defer's wouldn't send this + name: "Luke Skywalker", + friends: [{ name: "Leia Skywalker" }], + }, + }; + const link = new MockSubscriptionLink(); + const queryManager = new QueryManager( + getDefaultOptionsForQueryManagerTests({ + cache: new InMemoryCache({ addTypename: false }), + link, + }) + ); + + const observable = queryManager.watchQuery({ + query, + variables: {}, + errorPolicy: "ignore", + }); + const stream = new ObservableStream(observable); + + // fire off first result + link.simulateResult({ result: { data: initialData } }); + + await expect(stream).toEmitValue({ + data: initialData, + loading: false, + networkStatus: 7, + }); + + // this should fire the `next` event without this error + link.simulateResult({ + result: { + errors: [new GraphQLError("defer failed")], + data: laterData, + }, + }); + + await expect(stream).toEmitValueStrict({ + data: laterData, + loading: false, + networkStatus: 7, + }); + }); + + it.skip("allows multiple query results from link with all errors", async () => { const query = gql` query LazyLoadLuke { people_one(id: 1) { @@ -181,185 +243,105 @@ describe("mutiple results", () => { const observable = queryManager.watchQuery({ query, variables: {}, - errorPolicy: "ignore", + errorPolicy: "all", + }); + const stream = new ObservableStream(observable); + + // fire off first result + link.simulateResult({ result: { data: initialData } }); + + await expect(stream).toEmitValue({ + data: initialData, + loading: false, + networkStatus: 7, + }); + + // this should fire the next event again + link.simulateResult({ + error: new Error("defer failed"), + }); + + await expect(stream).toEmitValue({ + data: initialData, + loading: false, + networkStatus: 7, + errors: [new Error("defer failed")], + }); + + link.simulateResult({ result: { data: laterData } }); + + await expect(stream).toEmitValueStrict({ + data: laterData, + loading: false, + networkStatus: 7, + }); + }); + + it("closes the observable if an error is set with the none policy", async () => { + const query = gql` + query LazyLoadLuke { + people_one(id: 1) { + name + friends @defer { + name + } + } + } + `; + + const initialData = { + people_one: { + name: "Luke Skywalker", + friends: null, + }, + }; + + const link = new MockSubscriptionLink(); + const queryManager = new QueryManager( + getDefaultOptionsForQueryManagerTests({ + cache: new InMemoryCache({ addTypename: false }), + link, + }) + ); + + const observable = queryManager.watchQuery({ + query, + variables: {}, + // errorPolicy: 'none', // this is the default }); + const stream = new ObservableStream(observable); let count = 0; observable.subscribe({ next: (result) => { // errors should never be passed since they are ignored - expect(result.errors).toBeUndefined(); count++; - if (count === 1) { - expect(result.data).toEqual(initialData); - // this should fire the `next` event without this error - link.simulateResult({ - result: { - errors: [new GraphQLError("defer failed")], - data: laterData, - }, - }); + expect(result.errors).toBeUndefined(); } if (count === 2) { - expect(result.data).toEqual(laterData); - expect(result.errors).toBeUndefined(); - // make sure the count doesn't go up by accident - setTimeout(() => { - if (count === 3) reject(new Error("error was not ignored")); - resolve(); - }, 10); + console.log(new Error("result came after an error")); } }, error: (e) => { - console.error(e); + expect(e).toBeDefined(); + expect(e.graphQLErrors).toBeDefined(); }, }); // fire off first result link.simulateResult({ result: { data: initialData } }); - }); - itAsync.skip( - "allows multiple query results from link with all errors", - (resolve, reject) => { - const query = gql` - query LazyLoadLuke { - people_one(id: 1) { - name - friends @defer { - name - } - } - } - `; - - const initialData = { - people_one: { - name: "Luke Skywalker", - friends: null, - }, - }; - - const laterData = { - people_one: { - // XXX true defer's wouldn't send this - name: "Luke Skywalker", - friends: [{ name: "Leia Skywalker" }], - }, - }; - const link = new MockSubscriptionLink(); - const queryManager = new QueryManager( - getDefaultOptionsForQueryManagerTests({ - cache: new InMemoryCache({ addTypename: false }), - link, - }) - ); - - const observable = queryManager.watchQuery({ - query, - variables: {}, - errorPolicy: "all", - }); - - let count = 0; - observable.subscribe({ - next: (result) => { - try { - // errors should never be passed since they are ignored - count++; - if (count === 1) { - expect(result.errors).toBeUndefined(); - // this should fire the next event again - link.simulateResult({ - error: new Error("defer failed"), - }); - } - if (count === 2) { - expect(result.errors).toBeDefined(); - link.simulateResult({ result: { data: laterData } }); - } - if (count === 3) { - expect(result.errors).toBeUndefined(); - // make sure the count doesn't go up by accident - setTimeout(() => { - if (count === 4) reject(new Error("error was not ignored")); - resolve(); - }); - } - } catch (e) { - reject(e); - } - }, - error: (e) => { - reject(e); - }, - }); - - // fire off first result - link.simulateResult({ result: { data: initialData } }); - } - ); - itAsync( - "closes the observable if an error is set with the none policy", - (resolve, reject) => { - const query = gql` - query LazyLoadLuke { - people_one(id: 1) { - name - friends @defer { - name - } - } - } - `; - - const initialData = { - people_one: { - name: "Luke Skywalker", - friends: null, - }, - }; - - const link = new MockSubscriptionLink(); - const queryManager = new QueryManager( - getDefaultOptionsForQueryManagerTests({ - cache: new InMemoryCache({ addTypename: false }), - link, - }) - ); - - const observable = queryManager.watchQuery({ - query, - variables: {}, - // errorPolicy: 'none', // this is the default - }); - - let count = 0; - observable.subscribe({ - next: (result) => { - // errors should never be passed since they are ignored - count++; - if (count === 1) { - expect(result.errors).toBeUndefined(); - // this should fire the next event again - link.simulateResult({ - error: new Error("defer failed"), - }); - } - if (count === 2) { - console.log(new Error("result came after an error")); - } - }, - error: (e) => { - expect(e).toBeDefined(); - expect(e.graphQLErrors).toBeDefined(); - resolve(); - }, - }); - - // fire off first result - link.simulateResult({ result: { data: initialData } }); - } - ); + await expect(stream).toEmitValue({ + data: initialData, + loading: false, + networkStatus: 7, + }); + + link.simulateResult({ error: new Error("defer failed") }); + + await expect(stream).toEmitError( + new ApolloError({ networkError: new Error("defer failed") }) + ); + }); }); diff --git a/src/core/__tests__/QueryManager/recycler.ts b/src/core/__tests__/QueryManager/recycler.ts index fccddc901de..65d00725dab 100644 --- a/src/core/__tests__/QueryManager/recycler.ts +++ b/src/core/__tests__/QueryManager/recycler.ts @@ -11,101 +11,101 @@ import gql from "graphql-tag"; import { QueryManager } from "../../QueryManager"; import { ObservableQuery } from "../../ObservableQuery"; import { ObservableSubscription } from "../../../utilities"; -import { itAsync } from "../../../testing"; import { InMemoryCache } from "../../../cache"; // mocks -import { MockSubscriptionLink } from "../../../testing/core"; +import { MockSubscriptionLink, wait } from "../../../testing/core"; import { getDefaultOptionsForQueryManagerTests } from "../../../testing/core/mocking/mockQueryManager"; +import { ObservableStream } from "../../../testing/internal"; +import { NetworkStatus } from "../../networkStatus"; describe("Subscription lifecycles", () => { - itAsync( - "cleans up and reuses data like QueryRecycler wants", - (resolve, reject) => { - const query = gql` - query Luke { - people_one(id: 1) { + it("cleans up and reuses data like QueryRecycler wants", async () => { + const query = gql` + query Luke { + people_one(id: 1) { + name + friends { name - friends { - name - } } } - `; - - const initialData = { - people_one: { - name: "Luke Skywalker", - friends: [{ name: "Leia Skywalker" }], - }, - }; - - const link = new MockSubscriptionLink(); - const queryManager = new QueryManager( - getDefaultOptionsForQueryManagerTests({ - cache: new InMemoryCache({ addTypename: false }), - link, - }) - ); - - // step 1, get some data - const observable = queryManager.watchQuery({ + } + `; + + const initialData = { + people_one: { + name: "Luke Skywalker", + friends: [{ name: "Leia Skywalker" }], + }, + }; + + const link = new MockSubscriptionLink(); + const queryManager = new QueryManager( + getDefaultOptionsForQueryManagerTests({ + cache: new InMemoryCache({ addTypename: false }), + link, + }) + ); + + // step 1, get some data + const observable = queryManager.watchQuery({ + query, + variables: {}, + fetchPolicy: "cache-and-network", + }); + + const observableQueries: Array<{ + observableQuery: ObservableQuery; + subscription: ObservableSubscription; + }> = []; + + const resubscribe = () => { + const { observableQuery, subscription } = observableQueries.pop()!; + subscription.unsubscribe(); + + void observableQuery.setOptions({ query, - variables: {}, fetchPolicy: "cache-and-network", }); - const observableQueries: Array<{ - observableQuery: ObservableQuery; - subscription: ObservableSubscription; - }> = []; - - const resubscribe = () => { - const { observableQuery, subscription } = observableQueries.pop()!; - subscription.unsubscribe(); - - observableQuery.setOptions({ - query, - fetchPolicy: "cache-and-network", - }); - - return observableQuery; - }; - - const sub = observable.subscribe({ - next(result: any) { - expect(result.loading).toBe(false); - expect(result.data).toEqual(initialData); - expect(observable.getCurrentResult().data).toEqual(initialData); - - // step 2, recycle it - observable.setOptions({ - fetchPolicy: "standby", - pollInterval: 0, - }); - - observableQueries.push({ - observableQuery: observable, - subscription: observable.subscribe({}), - }); - - // step 3, unsubscribe from observable - sub.unsubscribe(); - - setTimeout(() => { - // step 4, start new Subscription; - const recycled = resubscribe(); - const currentResult = recycled.getCurrentResult(); - expect(currentResult.data).toEqual(initialData); - resolve(); - }, 10); - }, - }); - - setInterval(() => { - // fire off first result - link.simulateResult({ result: { data: initialData } }); - }, 10); - } - ); + return observableQuery; + }; + + const stream = new ObservableStream(observable); + + await wait(10); + // fire off first result + link.simulateResult({ result: { data: initialData } }); + + await expect(stream).toEmitValue({ + loading: false, + data: initialData, + networkStatus: NetworkStatus.ready, + }); + expect(observable.getCurrentResult().data).toEqual(initialData); + + // step 2, recycle it + void observable.setOptions({ + fetchPolicy: "standby", + pollInterval: 0, + }); + + observableQueries.push({ + observableQuery: observable, + subscription: observable.subscribe({}), + }); + + // step 3, unsubscribe from observable + stream.unsubscribe(); + + await wait(10); + // fire off first result + link.simulateResult({ result: { data: initialData } }); + await wait(10); + + // step 4, start new Subscription; + const recycled = resubscribe(); + const currentResult = recycled.getCurrentResult(); + expect(currentResult.data).toEqual(initialData); + }); }); diff --git a/src/core/__tests__/fetchPolicies.ts b/src/core/__tests__/fetchPolicies.ts index 0208b6982c5..8042f2712b4 100644 --- a/src/core/__tests__/fetchPolicies.ts +++ b/src/core/__tests__/fetchPolicies.ts @@ -4,7 +4,7 @@ import { ApolloClient, NetworkStatus } from "../../core"; import { ApolloLink } from "../../link/core"; import { InMemoryCache } from "../../cache"; import { Observable } from "../../utilities"; -import { itAsync, mockSingleLink } from "../../testing"; +import { mockSingleLink } from "../../testing"; import { TypedDocumentNode } from "@graphql-typed-document-node/core"; import { WatchQueryFetchPolicy, WatchQueryOptions } from "../watchQueryOptions"; import { ApolloQueryResult } from "../types"; @@ -56,7 +56,7 @@ const mutationResult = { const merged = { author: { ...result.author, firstName: "James" } }; -const createLink = (reject: (reason: any) => any) => +const createLink = () => mockSingleLink( { request: { query }, @@ -66,7 +66,7 @@ const createLink = (reject: (reason: any) => any) => request: { query }, result: { data: result }, } - ).setOnError(reject); + ); const createFailureLink = () => mockSingleLink( @@ -80,7 +80,7 @@ const createFailureLink = () => } ); -const createMutationLink = (reject: (reason: any) => any) => +const createMutationLink = () => // fetch the data mockSingleLink( { @@ -95,41 +95,35 @@ const createMutationLink = (reject: (reason: any) => any) => request: { query }, result: { data: merged }, } - ).setOnError(reject); + ); describe("network-only", () => { - itAsync( - "requests from the network even if already in cache", - (resolve, reject) => { - let called = 0; - const inspector = new ApolloLink((operation, forward) => { + it("requests from the network even if already in cache", async () => { + let called = 0; + const inspector = new ApolloLink((operation, forward) => { + called++; + return forward(operation).map((result) => { called++; - return forward(operation).map((result) => { - called++; - return result; - }); + return result; }); + }); - const client = new ApolloClient({ - link: inspector.concat(createLink(reject)), - cache: new InMemoryCache({ addTypename: false }), - }); + const client = new ApolloClient({ + link: inspector.concat(createLink()), + cache: new InMemoryCache({ addTypename: false }), + }); - return client - .query({ query }) - .then(() => - client - .query({ fetchPolicy: "network-only", query }) - .then((actualResult) => { - expect(actualResult.data).toEqual(result); - expect(called).toBe(4); - }) - ) - .then(resolve, reject); - } - ); + await client.query({ query }); + const actualResult = await client.query({ + fetchPolicy: "network-only", + query, + }); + + expect(actualResult.data).toEqual(result); + expect(called).toBe(4); + }); - itAsync("saves data to the cache on success", (resolve, reject) => { + it("saves data to the cache on success", async () => { let called = 0; const inspector = new ApolloLink((operation, forward) => { called++; @@ -140,22 +134,18 @@ describe("network-only", () => { }); const client = new ApolloClient({ - link: inspector.concat(createLink(reject)), + link: inspector.concat(createLink()), cache: new InMemoryCache({ addTypename: false }), }); - return client - .query({ query, fetchPolicy: "network-only" }) - .then(() => - client.query({ query }).then((actualResult) => { - expect(actualResult.data).toEqual(result); - expect(called).toBe(2); - }) - ) - .then(resolve, reject); + await client.query({ query, fetchPolicy: "network-only" }); + const actualResult = await client.query({ query }); + + expect(actualResult.data).toEqual(result); + expect(called).toBe(2); }); - itAsync("does not save data to the cache on failure", (resolve, reject) => { + it("does not save data to the cache on failure", async () => { let called = 0; const inspector = new ApolloLink((operation, forward) => { called++; @@ -171,24 +161,20 @@ describe("network-only", () => { }); let didFail = false; - return client - .query({ query, fetchPolicy: "network-only" }) - .catch((e) => { - expect(e.message).toMatch("query failed"); - didFail = true; - }) - .then(() => - client.query({ query }).then((actualResult) => { - expect(actualResult.data).toEqual(result); - // the first error doesn't call .map on the inspector - expect(called).toBe(3); - expect(didFail).toBe(true); - }) - ) - .then(resolve, reject); + await client.query({ query, fetchPolicy: "network-only" }).catch((e) => { + expect(e.message).toMatch("query failed"); + didFail = true; + }); + + const actualResult = await client.query({ query }); + + expect(actualResult.data).toEqual(result); + // the first error doesn't call .map on the inspector + expect(called).toBe(3); + expect(didFail).toBe(true); }); - itAsync("updates the cache on a mutation", (resolve, reject) => { + it("updates the cache on a mutation", async () => { const inspector = new ApolloLink((operation, forward) => { return forward(operation).map((result) => { return result; @@ -196,28 +182,23 @@ describe("network-only", () => { }); const client = new ApolloClient({ - link: inspector.concat(createMutationLink(reject)), + link: inspector.concat(createMutationLink()), cache: new InMemoryCache({ addTypename: false }), }); - return client - .query({ query }) - .then(() => - // XXX currently only no-cache is supported as a fetchPolicy - // this mainly serves to ensure the cache is updated correctly - client.mutate({ mutation, variables }) - ) - .then(() => { - return client.query({ query }).then((actualResult) => { - expect(actualResult.data).toEqual(merged); - }); - }) - .then(resolve, reject); + await client.query({ query }); + // XXX currently only no-cache is supported as a fetchPolicy + // this mainly serves to ensure the cache is updated correctly + await client.mutate({ mutation, variables }); + + const actualResult = await client.query({ query }); + + expect(actualResult.data).toEqual(merged); }); }); describe("no-cache", () => { - itAsync("requests from the network when not in cache", (resolve, reject) => { + it("requests from the network when not in cache", async () => { let called = 0; const inspector = new ApolloLink((operation, forward) => { called++; @@ -228,81 +209,62 @@ describe("no-cache", () => { }); const client = new ApolloClient({ - link: inspector.concat(createLink(reject)), + link: inspector.concat(createLink()), cache: new InMemoryCache({ addTypename: false }), }); - return client - .query({ fetchPolicy: "no-cache", query }) - .then((actualResult) => { - expect(actualResult.data).toEqual(result); - expect(called).toBe(2); - }) - .then(resolve, reject); + const actualResult = await client.query({ fetchPolicy: "no-cache", query }); + + expect(actualResult.data).toEqual(result); + expect(called).toBe(2); }); - itAsync( - "requests from the network even if already in cache", - (resolve, reject) => { - let called = 0; - const inspector = new ApolloLink((operation, forward) => { + it("requests from the network even if already in cache", async () => { + let called = 0; + const inspector = new ApolloLink((operation, forward) => { + called++; + return forward(operation).map((result) => { called++; - return forward(operation).map((result) => { - called++; - return result; - }); + return result; }); + }); - const client = new ApolloClient({ - link: inspector.concat(createLink(reject)), - cache: new InMemoryCache({ addTypename: false }), - }); + const client = new ApolloClient({ + link: inspector.concat(createLink()), + cache: new InMemoryCache({ addTypename: false }), + }); - return client - .query({ query }) - .then(() => - client - .query({ fetchPolicy: "no-cache", query }) - .then((actualResult) => { - expect(actualResult.data).toEqual(result); - expect(called).toBe(4); - }) - ) - .then(resolve, reject); - } - ); + await client.query({ query }); + const actualResult = await client.query({ fetchPolicy: "no-cache", query }); - itAsync( - "does not save the data to the cache on success", - (resolve, reject) => { - let called = 0; - const inspector = new ApolloLink((operation, forward) => { + expect(actualResult.data).toEqual(result); + expect(called).toBe(4); + }); + + it("does not save the data to the cache on success", async () => { + let called = 0; + const inspector = new ApolloLink((operation, forward) => { + called++; + return forward(operation).map((result) => { called++; - return forward(operation).map((result) => { - called++; - return result; - }); + return result; }); + }); - const client = new ApolloClient({ - link: inspector.concat(createLink(reject)), - cache: new InMemoryCache({ addTypename: false }), - }); + const client = new ApolloClient({ + link: inspector.concat(createLink()), + cache: new InMemoryCache({ addTypename: false }), + }); - return client - .query({ query, fetchPolicy: "no-cache" }) - .then(() => - client.query({ query }).then((actualResult) => { - expect(actualResult.data).toEqual(result); - // the second query couldn't read anything from the cache - expect(called).toBe(4); - }) - ) - .then(resolve, reject); - } - ); + await client.query({ query, fetchPolicy: "no-cache" }); + const actualResult = await client.query({ query }); - itAsync("does not save data to the cache on failure", (resolve, reject) => { + expect(actualResult.data).toEqual(result); + // the second query couldn't read anything from the cache + expect(called).toBe(4); + }); + + it("does not save data to the cache on failure", async () => { let called = 0; const inspector = new ApolloLink((operation, forward) => { called++; @@ -318,24 +280,20 @@ describe("no-cache", () => { }); let didFail = false; - return client - .query({ query, fetchPolicy: "no-cache" }) - .catch((e) => { - expect(e.message).toMatch("query failed"); - didFail = true; - }) - .then(() => - client.query({ query }).then((actualResult) => { - expect(actualResult.data).toEqual(result); - // the first error doesn't call .map on the inspector - expect(called).toBe(3); - expect(didFail).toBe(true); - }) - ) - .then(resolve, reject); + await client.query({ query, fetchPolicy: "no-cache" }).catch((e) => { + expect(e.message).toMatch("query failed"); + didFail = true; + }); + + const actualResult = await client.query({ query }); + + expect(actualResult.data).toEqual(result); + // the first error doesn't call .map on the inspector + expect(called).toBe(3); + expect(didFail).toBe(true); }); - itAsync("does not update the cache on a mutation", (resolve, reject) => { + it("does not update the cache on a mutation", async () => { const inspector = new ApolloLink((operation, forward) => { return forward(operation).map((result) => { return result; @@ -343,59 +301,46 @@ describe("no-cache", () => { }); const client = new ApolloClient({ - link: inspector.concat(createMutationLink(reject)), + link: inspector.concat(createMutationLink()), cache: new InMemoryCache({ addTypename: false }), }); - return client - .query({ query }) - .then(() => - client.mutate({ mutation, variables, fetchPolicy: "no-cache" }) - ) - .then(() => { - return client.query({ query }).then((actualResult) => { - expect(actualResult.data).toEqual(result); - }); - }) - .then(resolve, reject); + await client.query({ query }); + await client.mutate({ mutation, variables, fetchPolicy: "no-cache" }); + const actualResult = await client.query({ query }); + + expect(actualResult.data).toEqual(result); }); describe("when notifyOnNetworkStatusChange is set", () => { - itAsync( - "does not save the data to the cache on success", - (resolve, reject) => { - let called = 0; - const inspector = new ApolloLink((operation, forward) => { + it("does not save the data to the cache on success", async () => { + let called = 0; + const inspector = new ApolloLink((operation, forward) => { + called++; + return forward(operation).map((result) => { called++; - return forward(operation).map((result) => { - called++; - return result; - }); + return result; }); + }); - const client = new ApolloClient({ - link: inspector.concat(createLink(reject)), - cache: new InMemoryCache({ addTypename: false }), - }); + const client = new ApolloClient({ + link: inspector.concat(createLink()), + cache: new InMemoryCache({ addTypename: false }), + }); - return client - .query({ - query, - fetchPolicy: "no-cache", - notifyOnNetworkStatusChange: true, - }) - .then(() => - client.query({ query }).then((actualResult) => { - expect(actualResult.data).toEqual(result); - // the second query couldn't read anything from the cache - expect(called).toBe(4); - }) - ) - .then(resolve, reject); - } - ); + await client.query({ + query, + fetchPolicy: "no-cache", + notifyOnNetworkStatusChange: true, + }); + const actualResult = await client.query({ query }); + + expect(actualResult.data).toEqual(result); + // the second query couldn't read anything from the cache + expect(called).toBe(4); + }); - itAsync("does not save data to the cache on failure", (resolve, reject) => { + it("does not save data to the cache on failure", async () => { let called = 0; const inspector = new ApolloLink((operation, forward) => { called++; @@ -411,7 +356,7 @@ describe("no-cache", () => { }); let didFail = false; - return client + await client .query({ query, fetchPolicy: "no-cache", @@ -420,16 +365,14 @@ describe("no-cache", () => { .catch((e) => { expect(e.message).toMatch("query failed"); didFail = true; - }) - .then(() => - client.query({ query }).then((actualResult) => { - expect(actualResult.data).toEqual(result); - // the first error doesn't call .map on the inspector - expect(called).toBe(3); - expect(didFail).toBe(true); - }) - ) - .then(resolve, reject); + }); + + const actualResult = await client.query({ query }); + + expect(actualResult.data).toEqual(result); + // the first error doesn't call .map on the inspector + expect(called).toBe(3); + expect(didFail).toBe(true); }); it("gives appropriate networkStatus for watched queries", async () => { @@ -543,11 +486,7 @@ describe("cache-first", () => { results.push(result); return result; }); - }).concat( - createMutationLink((error) => { - throw error; - }) - ), + }).concat(createMutationLink()), cache: new InMemoryCache(), }); diff --git a/src/link/batch-http/__tests__/batchHttpLink.ts b/src/link/batch-http/__tests__/batchHttpLink.ts index 1209b0414c1..033e97a62ac 100644 --- a/src/link/batch-http/__tests__/batchHttpLink.ts +++ b/src/link/batch-http/__tests__/batchHttpLink.ts @@ -10,8 +10,8 @@ import { Observer, } from "../../../utilities/observables/Observable"; import { BatchHttpLink } from "../batchHttpLink"; -import { itAsync } from "../../../testing"; import { FetchResult } from "../../core"; +import { ObservableStream } from "../../../testing/internal"; const sampleQuery = gql` query SampleQuery { @@ -29,22 +29,6 @@ const sampleMutation = gql` } `; -function makeCallback( - resolve: () => void, - reject: (error: Error) => void, - callback: (...args: TArgs) => any -) { - return function () { - try { - // @ts-expect-error - callback.apply(this, arguments); - resolve(); - } catch (error) { - reject(error as Error); - } - } as typeof callback; -} - describe("BatchHttpLink", () => { beforeAll(() => { jest.resetModules(); @@ -76,7 +60,7 @@ describe("BatchHttpLink", () => { expect(() => new BatchHttpLink()).not.toThrow(); }); - itAsync("handles batched requests", (resolve, reject) => { + it("handles batched requests", (done) => { const clientAwareness = { name: "Some Client Name", version: "1.0.1", @@ -91,45 +75,37 @@ describe("BatchHttpLink", () => { let nextCalls = 0; let completions = 0; const next = (expectedData: any) => (data: any) => { - try { - expect(data).toEqual(expectedData); - nextCalls++; - } catch (error) { - reject(error); - } + expect(data).toEqual(expectedData); + nextCalls++; }; const complete = () => { - try { - const calls = fetchMock.calls("begin:/batch"); - expect(calls.length).toBe(1); - expect(nextCalls).toBe(2); + const calls = fetchMock.calls("begin:/batch"); + expect(calls.length).toBe(1); + expect(nextCalls).toBe(2); - const options: any = fetchMock.lastOptions("begin:/batch"); - expect(options.credentials).toEqual("two"); + const options: any = fetchMock.lastOptions("begin:/batch"); + expect(options.credentials).toEqual("two"); - const { headers } = options; - expect(headers["apollographql-client-name"]).toBeDefined(); - expect(headers["apollographql-client-name"]).toEqual( - clientAwareness.name - ); - expect(headers["apollographql-client-version"]).toBeDefined(); - expect(headers["apollographql-client-version"]).toEqual( - clientAwareness.version - ); + const { headers } = options; + expect(headers["apollographql-client-name"]).toBeDefined(); + expect(headers["apollographql-client-name"]).toEqual( + clientAwareness.name + ); + expect(headers["apollographql-client-version"]).toBeDefined(); + expect(headers["apollographql-client-version"]).toEqual( + clientAwareness.version + ); - completions++; + completions++; - if (completions === 2) { - resolve(); - } - } catch (error) { - reject(error); + if (completions === 2) { + done(); } }; const error = (error: any) => { - reject(error); + throw error; }; execute(link, { @@ -146,37 +122,34 @@ describe("BatchHttpLink", () => { }).subscribe(next(data2), error, complete); }); - itAsync( - "errors on an incorrect number of results for a batch", - (resolve, reject) => { - const link = new BatchHttpLink({ - uri: "/batch", - batchInterval: 0, - batchMax: 3, - }); + it("errors on an incorrect number of results for a batch", (done) => { + const link = new BatchHttpLink({ + uri: "/batch", + batchInterval: 0, + batchMax: 3, + }); - let errors = 0; - const next = (data: any) => { - reject("next should not have been called"); - }; + let errors = 0; + const next = (data: any) => { + throw new Error("next should not have been called"); + }; - const complete = () => { - reject("complete should not have been called"); - }; + const complete = () => { + throw new Error("complete should not have been called"); + }; - const error = (error: any) => { - errors++; + const error = (error: any) => { + errors++; - if (errors === 3) { - resolve(); - } - }; + if (errors === 3) { + done(); + } + }; - execute(link, { query: sampleQuery }).subscribe(next, error, complete); - execute(link, { query: sampleQuery }).subscribe(next, error, complete); - execute(link, { query: sampleQuery }).subscribe(next, error, complete); - } - ); + execute(link, { query: sampleQuery }).subscribe(next, error, complete); + execute(link, { query: sampleQuery }).subscribe(next, error, complete); + execute(link, { query: sampleQuery }).subscribe(next, error, complete); + }); describe("batchKey", () => { const query = gql` @@ -188,71 +161,64 @@ describe("BatchHttpLink", () => { } `; - itAsync( - "should batch queries with different options separately", - (resolve, reject) => { - let key = true; - const batchKey = () => { - key = !key; - return "" + !key; - }; + it("should batch queries with different options separately", (done) => { + let key = true; + const batchKey = () => { + key = !key; + return "" + !key; + }; - const link = ApolloLink.from([ - new BatchHttpLink({ - uri: (operation) => { - return operation.variables.endpoint; - }, - batchInterval: 1, - //if batchKey does not work, then the batch size would be 3 - batchMax: 2, - batchKey, - }), - ]); - - let count = 0; - const next = (expected: any) => (received: any) => { - try { - expect(received).toEqual(expected); - } catch (e) { - reject(e); - } - }; - const complete = () => { - count++; - if (count === 4) { - try { - const lawlCalls = fetchMock.calls("begin:/lawl"); - expect(lawlCalls.length).toBe(1); - const roflCalls = fetchMock.calls("begin:/rofl"); - expect(roflCalls.length).toBe(1); - resolve(); - } catch (e) { - reject(e); - } - } - }; + const link = ApolloLink.from([ + new BatchHttpLink({ + uri: (operation) => { + return operation.variables.endpoint; + }, + batchInterval: 1, + //if batchKey does not work, then the batch size would be 3 + batchMax: 2, + batchKey, + }), + ]); - [1, 2].forEach((x) => { - execute(link, { - query, - variables: { endpoint: "/rofl" }, - }).subscribe({ - next: next(roflData), - error: reject, - complete, - }); + let count = 0; + const next = (expected: any) => (received: any) => { + expect(received).toEqual(expected); + }; + const complete = () => { + count++; + if (count === 4) { + const lawlCalls = fetchMock.calls("begin:/lawl"); + expect(lawlCalls.length).toBe(1); + const roflCalls = fetchMock.calls("begin:/rofl"); + expect(roflCalls.length).toBe(1); + done(); + } + }; - execute(link, { - query, - variables: { endpoint: "/lawl" }, - }).subscribe({ - next: next(lawlData), - error: reject, - complete, - }); + [1, 2].forEach((x) => { + execute(link, { + query, + variables: { endpoint: "/rofl" }, + }).subscribe({ + next: next(roflData), + error: (error) => { + throw error; + }, + complete, }); - } - ); + + execute(link, { + query, + variables: { endpoint: "/lawl" }, + }).subscribe({ + next: next(lawlData), + error: (error) => { + throw error; + }, + complete, + }); + }); + }); }); }); @@ -333,127 +299,101 @@ describe("SharedHttpTest", () => { expect(() => createHttpLink()).not.toThrow(); }); - itAsync("calls next and then complete", (resolve, reject) => { - const next = jest.fn(); + it("calls next and then complete", async () => { const link = createHttpLink({ uri: "/data" }); const observable = execute(link, { query: sampleQuery, }); - observable.subscribe({ - next, - error: (error) => reject(error), - complete: makeCallback(resolve, reject, () => { - expect(next).toHaveBeenCalledTimes(1); - }), - }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + await expect(stream).toComplete(); }); - itAsync("calls error when fetch fails", (resolve, reject) => { + it("calls error when fetch fails", async () => { const link = createHttpLink({ uri: "/error" }); const observable = execute(link, { query: sampleQuery, }); - observable.subscribe( - (result) => reject("next should not have been called"), - makeCallback(resolve, reject, (error: any) => { - expect(error).toEqual(mockError.throws); - }), - () => reject("complete should not have been called") - ); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitError(mockError.throws); }); - itAsync("calls error when fetch fails", (resolve, reject) => { + it("calls error when fetch fails", async () => { const link = createHttpLink({ uri: "/error" }); const observable = execute(link, { query: sampleMutation, }); - observable.subscribe( - (result) => reject("next should not have been called"), - makeCallback(resolve, reject, (error: any) => { - expect(error).toEqual(mockError.throws); - }), - () => reject("complete should not have been called") - ); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitError(mockError.throws); }); - itAsync( - "strips unused variables, respecting nested fragments", - (resolve, reject) => { - const link = createHttpLink({ uri: "/data" }); - - const query = gql` - query PEOPLE($declaredAndUsed: String, $declaredButUnused: Int) { - people(surprise: $undeclared, noSurprise: $declaredAndUsed) { - ... on Doctor { - specialty(var: $usedByInlineFragment) - } - ...LawyerFragment + it("strips unused variables, respecting nested fragments", async () => { + const link = createHttpLink({ uri: "/data" }); + + const query = gql` + query PEOPLE($declaredAndUsed: String, $declaredButUnused: Int) { + people(surprise: $undeclared, noSurprise: $declaredAndUsed) { + ... on Doctor { + specialty(var: $usedByInlineFragment) } + ...LawyerFragment } - fragment LawyerFragment on Lawyer { - caseCount(var: $usedByNamedFragment) - } - `; - - const variables = { - unused: "strip", - declaredButUnused: "strip", - declaredAndUsed: "keep", - undeclared: "keep", - usedByInlineFragment: "keep", - usedByNamedFragment: "keep", - }; + } + fragment LawyerFragment on Lawyer { + caseCount(var: $usedByNamedFragment) + } + `; - execute(link, { - query, - variables, - }).subscribe({ - next: makeCallback(resolve, reject, () => { - const [uri, options] = fetchMock.lastCall()!; - const { method, body } = options!; - expect(JSON.parse(body as string)).toEqual([ - { - operationName: "PEOPLE", - query: print(query), - variables: { - declaredAndUsed: "keep", - undeclared: "keep", - usedByInlineFragment: "keep", - usedByNamedFragment: "keep", - }, - }, - ]); - expect(method).toBe("POST"); - expect(uri).toBe("/data"); - }), - error: (error) => reject(error), - }); - } - ); + const variables = { + unused: "strip", + declaredButUnused: "strip", + declaredAndUsed: "keep", + undeclared: "keep", + usedByInlineFragment: "keep", + usedByNamedFragment: "keep", + }; + + const stream = new ObservableStream(execute(link, { query, variables })); + + await expect(stream).toEmitNext(); + + const [uri, options] = fetchMock.lastCall()!; + const { method, body } = options!; + expect(JSON.parse(body as string)).toEqual([ + { + operationName: "PEOPLE", + query: print(query), + variables: { + declaredAndUsed: "keep", + undeclared: "keep", + usedByInlineFragment: "keep", + usedByNamedFragment: "keep", + }, + }, + ]); + expect(method).toBe("POST"); + expect(uri).toBe("/data"); + }); - itAsync("unsubscribes without calling subscriber", (resolve, reject) => { + it("unsubscribes without calling subscriber", async () => { const link = createHttpLink({ uri: "/data" }); const observable = execute(link, { query: sampleQuery, }); - const subscription = observable.subscribe( - (result) => reject("next should not have been called"), - (error) => reject(error), - () => reject("complete should not have been called") - ); - subscription.unsubscribe(); - expect(subscription.closed).toBe(true); - setTimeout(resolve, 50); + const stream = new ObservableStream(observable); + stream.unsubscribe(); + + await expect(stream).not.toEmitAnything(); }); - const verifyRequest = ( + const verifyRequest = async ( link: ApolloLink, - after: () => void, includeExtensions: boolean, - includeUnusedVariables: boolean, - reject: (e: Error) => void + includeUnusedVariables: boolean ) => { - const next = jest.fn(); const context = { info: "stub" }; const variables = { params: "stub" }; @@ -462,61 +402,37 @@ describe("SharedHttpTest", () => { context, variables, }); - observable.subscribe({ - next, - error: (error) => reject(error), - complete: () => { - try { - let body = convertBatchedBody(fetchMock.lastCall()![1]!.body); - expect(body.query).toBe(print(sampleMutation)); - expect(body.variables).toEqual( - includeUnusedVariables ? variables : {} - ); - expect(body.context).not.toBeDefined(); - if (includeExtensions) { - expect(body.extensions).toBeDefined(); - } else { - expect(body.extensions).not.toBeDefined(); - } - expect(next).toHaveBeenCalledTimes(1); - - after(); - } catch (e) { - reject(e as Error); - } - }, - }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + await expect(stream).toComplete(); + + let body = convertBatchedBody(fetchMock.lastCall()![1]!.body); + expect(body.query).toBe(print(sampleMutation)); + expect(body.variables).toEqual(includeUnusedVariables ? variables : {}); + expect(body.context).not.toBeDefined(); + if (includeExtensions) { + expect(body.extensions).toBeDefined(); + } else { + expect(body.extensions).not.toBeDefined(); + } }; - itAsync( - "passes all arguments to multiple fetch body including extensions", - (resolve, reject) => { - const link = createHttpLink({ uri: "/data", includeExtensions: true }); - verifyRequest( - link, - () => verifyRequest(link, resolve, true, false, reject), - true, - false, - reject - ); - } - ); + it("passes all arguments to multiple fetch body including extensions", async () => { + const link = createHttpLink({ uri: "/data", includeExtensions: true }); - itAsync( - "passes all arguments to multiple fetch body excluding extensions", - (resolve, reject) => { - const link = createHttpLink({ uri: "/data" }); - verifyRequest( - link, - () => verifyRequest(link, resolve, false, false, reject), - false, - false, - reject - ); - } - ); + await verifyRequest(link, true, false); + await verifyRequest(link, true, false); + }); + + it("passes all arguments to multiple fetch body excluding extensions", async () => { + const link = createHttpLink({ uri: "/data" }); + + await verifyRequest(link, false, false); + await verifyRequest(link, false, false); + }); - itAsync("calls multiple subscribers", (resolve, reject) => { + it("calls multiple subscribers", (done) => { const link = createHttpLink({ uri: "/data" }); const context = { info: "stub" }; const variables = { params: "stub" }; @@ -536,57 +452,52 @@ describe("SharedHttpTest", () => { // only one call because batchHttpLink can handle more than one subscriber // without starting a new request expect(fetchMock.calls().length).toBe(1); - resolve(); + done(); }, 50); }); - itAsync( - "calls remaining subscribers after unsubscribe", - (resolve, reject) => { - const link = createHttpLink({ uri: "/data" }); - const context = { info: "stub" }; - const variables = { params: "stub" }; + it("calls remaining subscribers after unsubscribe", (done) => { + const link = createHttpLink({ uri: "/data" }); + const context = { info: "stub" }; + const variables = { params: "stub" }; - const observable = execute(link, { - query: sampleMutation, - context, - variables, - }); + const observable = execute(link, { + query: sampleMutation, + context, + variables, + }); - observable.subscribe(subscriber); + observable.subscribe(subscriber); - setTimeout(() => { - const subscription = observable.subscribe(subscriber); - subscription.unsubscribe(); - }, 10); + setTimeout(() => { + const subscription = observable.subscribe(subscriber); + subscription.unsubscribe(); + }, 10); - setTimeout( - makeCallback(resolve, reject, () => { - expect(subscriber.next).toHaveBeenCalledTimes(1); - expect(subscriber.complete).toHaveBeenCalledTimes(1); - expect(subscriber.error).not.toHaveBeenCalled(); - resolve(); - }), - 50 - ); - } - ); + setTimeout(() => { + expect(subscriber.next).toHaveBeenCalledTimes(1); + expect(subscriber.complete).toHaveBeenCalledTimes(1); + expect(subscriber.error).not.toHaveBeenCalled(); + done(); + }, 50); + }); - itAsync("allows for dynamic endpoint setting", (resolve, reject) => { + it("allows for dynamic endpoint setting", async () => { const variables = { params: "stub" }; const link = createHttpLink({ uri: "/data" }); - execute(link, { - query: sampleQuery, - variables, - context: { uri: "/data2" }, - }).subscribe((result) => { - expect(result).toEqual(data2); - resolve(); - }); + const stream = new ObservableStream( + execute(link, { + query: sampleQuery, + variables, + context: { uri: "/data2" }, + }) + ); + + await expect(stream).toEmitValue(data2); }); - itAsync("adds headers to the request from the context", (resolve, reject) => { + it("adds headers to the request from the context", async () => { const variables = { params: "stub" }; const middleware = new ApolloLink((operation, forward) => { operation.setContext({ @@ -594,43 +505,42 @@ describe("SharedHttpTest", () => { }); return forward(operation).map((result) => { const { headers } = operation.getContext(); - try { - expect(headers).toBeDefined(); - } catch (e) { - reject(e); - } + expect(headers).toBeDefined(); return result; }); }); const link = middleware.concat(createHttpLink({ uri: "/data" })); - - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, (result: any) => { - const headers: Record = fetchMock.lastCall()![1]! - .headers as Record; - expect(headers.authorization).toBe("1234"); - expect(headers["content-type"]).toBe("application/json"); - expect(headers.accept).toBe("*/*"); - }) + const stream = new ObservableStream( + execute(link, { query: sampleQuery, variables }) ); + + await expect(stream).toEmitNext(); + + const headers: Record = fetchMock.lastCall()![1]! + .headers as Record; + expect(headers.authorization).toBe("1234"); + expect(headers["content-type"]).toBe("application/json"); + expect(headers.accept).toBe("*/*"); }); - itAsync("adds headers to the request from the setup", (resolve, reject) => { + it("adds headers to the request from the setup", async () => { const variables = { params: "stub" }; const link = createHttpLink({ uri: "/data", headers: { authorization: "1234" }, }); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, (result: any) => { - const headers: Record = fetchMock.lastCall()![1]! - .headers as Record; - expect(headers.authorization).toBe("1234"); - expect(headers["content-type"]).toBe("application/json"); - expect(headers.accept).toBe("*/*"); - }) + const stream = new ObservableStream( + execute(link, { query: sampleQuery, variables }) ); + + await expect(stream).toEmitNext(); + + const headers: Record = fetchMock.lastCall()![1]! + .headers as Record; + expect(headers.authorization).toBe("1234"); + expect(headers["content-type"]).toBe("application/json"); + expect(headers.accept).toBe("*/*"); }); it("uses the latest window.fetch function if options.fetch not configured", (done) => { @@ -688,138 +598,133 @@ describe("SharedHttpTest", () => { ); }); - itAsync( - "prioritizes context headers over setup headers", - (resolve, reject) => { - const variables = { params: "stub" }; - const middleware = new ApolloLink((operation, forward) => { - operation.setContext({ - headers: { authorization: "1234" }, - }); - return forward(operation); + it("prioritizes context headers over setup headers", async () => { + const variables = { params: "stub" }; + const middleware = new ApolloLink((operation, forward) => { + operation.setContext({ + headers: { authorization: "1234" }, }); - const link = middleware.concat( - createHttpLink({ uri: "/data", headers: { authorization: "no user" } }) - ); + return forward(operation); + }); + const link = middleware.concat( + createHttpLink({ uri: "/data", headers: { authorization: "no user" } }) + ); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, (result: any) => { - const headers: Record = fetchMock.lastCall()![1]! - .headers as Record; - expect(headers.authorization).toBe("1234"); - expect(headers["content-type"]).toBe("application/json"); - expect(headers.accept).toBe("*/*"); - }) - ); - } - ); + const stream = new ObservableStream( + execute(link, { query: sampleQuery, variables }) + ); - itAsync( - "adds headers to the request from the context on an operation", - (resolve, reject) => { - const variables = { params: "stub" }; - const link = createHttpLink({ uri: "/data" }); + await expect(stream).toEmitNext(); - const context = { - headers: { authorization: "1234" }, - }; + const headers: Record = fetchMock.lastCall()![1]! + .headers as Record; + expect(headers.authorization).toBe("1234"); + expect(headers["content-type"]).toBe("application/json"); + expect(headers.accept).toBe("*/*"); + }); + + it("adds headers to the request from the context on an operation", async () => { + const variables = { params: "stub" }; + const link = createHttpLink({ uri: "/data" }); + + const context = { + headers: { authorization: "1234" }, + }; + const stream = new ObservableStream( execute(link, { query: sampleQuery, variables, context, - }).subscribe( - makeCallback(resolve, reject, (result: any) => { - const headers: Record = fetchMock.lastCall()![1]! - .headers as Record; - expect(headers.authorization).toBe("1234"); - expect(headers["content-type"]).toBe("application/json"); - expect(headers.accept).toBe("*/*"); - }) - ); - } - ); + }) + ); - itAsync( - "adds headers w/ preserved case to the request from the setup", - (resolve, reject) => { - const variables = { params: "stub" }; - const link = createHttpLink({ - uri: "/data", - headers: { - authorization: "1234", - AUTHORIZATION: "1234", - "CONTENT-TYPE": "application/json", - }, - preserveHeaderCase: true, - }); + await expect(stream).toEmitNext(); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, (result: any) => { - const headers: any = fetchMock.lastCall()![1]!.headers; - expect(headers.AUTHORIZATION).toBe("1234"); - expect(headers["CONTENT-TYPE"]).toBe("application/json"); - expect(headers.accept).toBe("*/*"); - }) - ); - } - ); - - itAsync( - "prioritizes context headers w/ preserved case over setup headers", - (resolve, reject) => { - const variables = { params: "stub" }; - const middleware = new ApolloLink((operation, forward) => { - operation.setContext({ - headers: { AUTHORIZATION: "1234" }, - http: { preserveHeaderCase: true }, - }); - return forward(operation); - }); - const link = middleware.concat( - createHttpLink({ - uri: "/data", - headers: { authorization: "no user" }, - preserveHeaderCase: false, - }) - ); + const headers: Record = fetchMock.lastCall()![1]! + .headers as Record; + expect(headers.authorization).toBe("1234"); + expect(headers["content-type"]).toBe("application/json"); + expect(headers.accept).toBe("*/*"); + }); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, (result: any) => { - const headers: any = fetchMock.lastCall()![1]!.headers; - expect(headers.AUTHORIZATION).toBe("1234"); - expect(headers["content-type"]).toBe("application/json"); - expect(headers.accept).toBe("*/*"); - }) - ); - } - ); + it("adds headers w/ preserved case to the request from the setup", async () => { + const variables = { params: "stub" }; + const link = createHttpLink({ + uri: "/data", + headers: { + authorization: "1234", + AUTHORIZATION: "1234", + "CONTENT-TYPE": "application/json", + }, + preserveHeaderCase: true, + }); - itAsync( - "adds headers w/ preserved case to the request from the context on an operation", - (resolve, reject) => { - const variables = { params: "stub" }; - const link = createHttpLink({ uri: "/data" }); + const stream = new ObservableStream( + execute(link, { query: sampleQuery, variables }) + ); + + await expect(stream).toEmitNext(); - const context = { + const headers: any = fetchMock.lastCall()![1]!.headers; + expect(headers.AUTHORIZATION).toBe("1234"); + expect(headers["CONTENT-TYPE"]).toBe("application/json"); + expect(headers.accept).toBe("*/*"); + }); + + it("prioritizes context headers w/ preserved case over setup headers", async () => { + const variables = { params: "stub" }; + const middleware = new ApolloLink((operation, forward) => { + operation.setContext({ headers: { AUTHORIZATION: "1234" }, http: { preserveHeaderCase: true }, - }; + }); + return forward(operation); + }); + const link = middleware.concat( + createHttpLink({ + uri: "/data", + headers: { authorization: "no user" }, + preserveHeaderCase: false, + }) + ); + + const stream = new ObservableStream( + execute(link, { query: sampleQuery, variables }) + ); + + await expect(stream).toEmitNext(); + + const headers: any = fetchMock.lastCall()![1]!.headers; + expect(headers.AUTHORIZATION).toBe("1234"); + expect(headers["content-type"]).toBe("application/json"); + expect(headers.accept).toBe("*/*"); + }); + + it("adds headers w/ preserved case to the request from the context on an operation", async () => { + const variables = { params: "stub" }; + const link = createHttpLink({ uri: "/data" }); + + const context = { + headers: { AUTHORIZATION: "1234" }, + http: { preserveHeaderCase: true }, + }; + const stream = new ObservableStream( execute(link, { query: sampleQuery, variables, context, - }).subscribe( - makeCallback(resolve, reject, (result: any) => { - const headers: any = fetchMock.lastCall()![1]!.headers; - expect(headers.AUTHORIZATION).toBe("1234"); - expect(headers["content-type"]).toBe("application/json"); - expect(headers.accept).toBe("*/*"); - }) - ); - } - ); + }) + ); + + await expect(stream).toEmitNext(); - itAsync("adds creds to the request from the context", (resolve, reject) => { + const headers: any = fetchMock.lastCall()![1]!.headers; + expect(headers.AUTHORIZATION).toBe("1234"); + expect(headers["content-type"]).toBe("application/json"); + expect(headers.accept).toBe("*/*"); + }); + + it("adds creds to the request from the context", async () => { const variables = { params: "stub" }; const middleware = new ApolloLink((operation, forward) => { operation.setContext({ @@ -829,50 +734,53 @@ describe("SharedHttpTest", () => { }); const link = middleware.concat(createHttpLink({ uri: "/data" })); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, (result: any) => { - const creds = fetchMock.lastCall()![1]!.credentials; - expect(creds).toBe("same-team-yo"); - }) + const stream = new ObservableStream( + execute(link, { query: sampleQuery, variables }) ); + + await expect(stream).toEmitNext(); + + const creds = fetchMock.lastCall()![1]!.credentials; + expect(creds).toBe("same-team-yo"); }); - itAsync("adds creds to the request from the setup", (resolve, reject) => { + it("adds creds to the request from the setup", async () => { const variables = { params: "stub" }; const link = createHttpLink({ uri: "/data", credentials: "same-team-yo" }); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, (result: any) => { - const creds = fetchMock.lastCall()![1]!.credentials; - expect(creds).toBe("same-team-yo"); - }) + const stream = new ObservableStream( + execute(link, { query: sampleQuery, variables }) ); + + await expect(stream).toEmitNext(); + + const creds = fetchMock.lastCall()![1]!.credentials; + expect(creds).toBe("same-team-yo"); }); - itAsync( - "prioritizes creds from the context over the setup", - (resolve, reject) => { - const variables = { params: "stub" }; - const middleware = new ApolloLink((operation, forward) => { - operation.setContext({ - credentials: "same-team-yo", - }); - return forward(operation); + it("prioritizes creds from the context over the setup", async () => { + const variables = { params: "stub" }; + const middleware = new ApolloLink((operation, forward) => { + operation.setContext({ + credentials: "same-team-yo", }); - const link = middleware.concat( - createHttpLink({ uri: "/data", credentials: "error" }) - ); + return forward(operation); + }); + const link = middleware.concat( + createHttpLink({ uri: "/data", credentials: "error" }) + ); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, (result: any) => { - const creds = fetchMock.lastCall()![1]!.credentials; - expect(creds).toBe("same-team-yo"); - }) - ); - } - ); + const stream = new ObservableStream( + execute(link, { query: sampleQuery, variables }) + ); + + await expect(stream).toEmitNext(); + + const creds = fetchMock.lastCall()![1]!.credentials; + expect(creds).toBe("same-team-yo"); + }); - itAsync("adds uri to the request from the context", (resolve, reject) => { + it("adds uri to the request from the context", async () => { const variables = { params: "stub" }; const middleware = new ApolloLink((operation, forward) => { operation.setContext({ @@ -882,27 +790,31 @@ describe("SharedHttpTest", () => { }); const link = middleware.concat(createHttpLink()); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, (result: any) => { - const uri = fetchMock.lastUrl(); - expect(uri).toBe("/data"); - }) + const stream = new ObservableStream( + execute(link, { query: sampleQuery, variables }) ); + + await expect(stream).toEmitNext(); + + const uri = fetchMock.lastUrl(); + expect(uri).toBe("/data"); }); - itAsync("adds uri to the request from the setup", (resolve, reject) => { + it("adds uri to the request from the setup", async () => { const variables = { params: "stub" }; const link = createHttpLink({ uri: "/data" }); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, (result: any) => { - const uri = fetchMock.lastUrl(); - expect(uri).toBe("/data"); - }) + const stream = new ObservableStream( + execute(link, { query: sampleQuery, variables }) ); + + await expect(stream).toEmitNext(); + + const uri = fetchMock.lastUrl(); + expect(uri).toBe("/data"); }); - itAsync("prioritizes context uri over setup uri", (resolve, reject) => { + it("prioritizes context uri over setup uri", async () => { const variables = { params: "stub" }; const middleware = new ApolloLink((operation, forward) => { operation.setContext({ @@ -914,82 +826,77 @@ describe("SharedHttpTest", () => { createHttpLink({ uri: "/data", credentials: "error" }) ); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, (result: any) => { - const uri = fetchMock.lastUrl(); - - expect(uri).toBe("/apollo"); - }) + const stream = new ObservableStream( + execute(link, { query: sampleQuery, variables }) ); + + await expect(stream).toEmitNext(); + + const uri = fetchMock.lastUrl(); + expect(uri).toBe("/apollo"); }); - itAsync("allows uri to be a function", (resolve, reject) => { + it("allows uri to be a function", async () => { const variables = { params: "stub" }; const customFetch = (_uri: any, options: any) => { const { operationName } = convertBatchedBody(options.body); - try { - expect(operationName).toBe("SampleQuery"); - } catch (e) { - reject(e); - } + expect(operationName).toBe("SampleQuery"); return fetch("/dataFunc", options); }; const link = createHttpLink({ fetch: customFetch }); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, (result: any) => { - expect(fetchMock.lastUrl()).toBe("/dataFunc"); - }) + const stream = new ObservableStream( + execute(link, { query: sampleQuery, variables }) ); + + await expect(stream).toEmitNext(); + + expect(fetchMock.lastUrl()).toBe("/dataFunc"); }); - itAsync( - "adds fetchOptions to the request from the setup", - (resolve, reject) => { - const variables = { params: "stub" }; - const link = createHttpLink({ - uri: "/data", - fetchOptions: { someOption: "foo", mode: "no-cors" }, - }); + it("adds fetchOptions to the request from the setup", async () => { + const variables = { params: "stub" }; + const link = createHttpLink({ + uri: "/data", + fetchOptions: { someOption: "foo", mode: "no-cors" }, + }); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, (result: any) => { - const { someOption, mode, headers } = - fetchMock.lastCall()![1]! as any; - expect(someOption).toBe("foo"); - expect(mode).toBe("no-cors"); - expect(headers["content-type"]).toBe("application/json"); - }) - ); - } - ); - - itAsync( - "adds fetchOptions to the request from the context", - (resolve, reject) => { - const variables = { params: "stub" }; - const middleware = new ApolloLink((operation, forward) => { - operation.setContext({ - fetchOptions: { - someOption: "foo", - }, - }); - return forward(operation); + const stream = new ObservableStream( + execute(link, { query: sampleQuery, variables }) + ); + + await expect(stream).toEmitNext(); + + const { someOption, mode, headers } = fetchMock.lastCall()![1]! as any; + expect(someOption).toBe("foo"); + expect(mode).toBe("no-cors"); + expect(headers["content-type"]).toBe("application/json"); + }); + + it("adds fetchOptions to the request from the context", async () => { + const variables = { params: "stub" }; + const middleware = new ApolloLink((operation, forward) => { + operation.setContext({ + fetchOptions: { + someOption: "foo", + }, }); - const link = middleware.concat(createHttpLink({ uri: "/data" })); + return forward(operation); + }); + const link = middleware.concat(createHttpLink({ uri: "/data" })); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, (result: any) => { - const { someOption } = fetchMock.lastCall()![1]! as any; - expect(someOption).toBe("foo"); - resolve(); - }) - ); - } - ); + const stream = new ObservableStream( + execute(link, { query: sampleQuery, variables }) + ); - itAsync("uses the print option function when defined", (resolve, reject) => { + await expect(stream).toEmitNext(); + + const { someOption } = fetchMock.lastCall()![1]! as any; + expect(someOption).toBe("foo"); + }); + + it("uses the print option function when defined", async () => { const customPrinter = jest.fn( (ast: ASTNode, originalPrint: typeof print) => { return stripIgnoredCharacters(originalPrint(ast)); @@ -998,16 +905,16 @@ describe("SharedHttpTest", () => { const httpLink = createHttpLink({ uri: "data", print: customPrinter }); - execute(httpLink, { - query: sampleQuery, - }).subscribe( - makeCallback(resolve, reject, () => { - expect(customPrinter).toHaveBeenCalledTimes(1); - }) + const stream = new ObservableStream( + execute(httpLink, { query: sampleQuery }) ); + + await expect(stream).toEmitNext(); + + expect(customPrinter).toHaveBeenCalledTimes(1); }); - itAsync("prioritizes context over setup", (resolve, reject) => { + it("prioritizes context over setup", async () => { const variables = { params: "stub" }; const middleware = new ApolloLink((operation, forward) => { operation.setContext({ @@ -1021,53 +928,53 @@ describe("SharedHttpTest", () => { createHttpLink({ uri: "/data", fetchOptions: { someOption: "bar" } }) ); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, (result: any) => { - const { someOption } = fetchMock.lastCall()![1]! as any; - expect(someOption).toBe("foo"); - }) + const stream = new ObservableStream( + execute(link, { query: sampleQuery, variables }) ); + + await expect(stream).toEmitNext(); + + const { someOption } = fetchMock.lastCall()![1]! as any; + expect(someOption).toBe("foo"); }); - itAsync( - "allows for not sending the query with the request", - (resolve, reject) => { - const variables = { params: "stub" }; - const middleware = new ApolloLink((operation, forward) => { - operation.setContext({ - http: { - includeQuery: false, - includeExtensions: true, - }, - }); - operation.extensions.persistedQuery = { hash: "1234" }; - return forward(operation); + it("allows for not sending the query with the request", async () => { + const variables = { params: "stub" }; + const middleware = new ApolloLink((operation, forward) => { + operation.setContext({ + http: { + includeQuery: false, + includeExtensions: true, + }, }); - const link = middleware.concat(createHttpLink({ uri: "/data" })); + operation.extensions.persistedQuery = { hash: "1234" }; + return forward(operation); + }); + const link = middleware.concat(createHttpLink({ uri: "/data" })); + + const stream = new ObservableStream( + execute(link, { query: sampleQuery, variables }) + ); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, (result: any) => { - let body = convertBatchedBody(fetchMock.lastCall()![1]!.body); + await expect(stream).toEmitNext(); - expect(body.query).not.toBeDefined(); - expect(body.extensions).toEqual({ persistedQuery: { hash: "1234" } }); - resolve(); - }) - ); - } - ); + let body = convertBatchedBody(fetchMock.lastCall()![1]!.body); + + expect(body.query).not.toBeDefined(); + expect(body.extensions).toEqual({ persistedQuery: { hash: "1234" } }); + }); - itAsync("sets the raw response on context", (resolve, reject) => { + it("sets the raw response on context", async () => { const middleware = new ApolloLink((operation, forward) => { return new Observable((ob) => { const op = forward(operation); const sub = op.subscribe({ next: ob.next.bind(ob), error: ob.error.bind(ob), - complete: makeCallback(resolve, reject, () => { + complete: () => { expect(operation.getContext().response.headers.toBeDefined); ob.complete(); - }), + }, }); return () => { @@ -1078,12 +985,10 @@ describe("SharedHttpTest", () => { const link = middleware.concat(createHttpLink({ uri: "/data", fetch })); - execute(link, { query: sampleQuery }).subscribe( - (result) => { - resolve(); - }, - () => {} - ); + const stream = new ObservableStream(execute(link, { query: sampleQuery })); + + await expect(stream).toEmitNext(); + await expect(stream).toComplete(); }); it("removes @client fields from the query before sending it to the server", async () => { diff --git a/src/link/batch/__tests__/batchLink.ts b/src/link/batch/__tests__/batchLink.ts index e5930924c27..ff5aa72edd0 100644 --- a/src/link/batch/__tests__/batchLink.ts +++ b/src/link/batch/__tests__/batchLink.ts @@ -4,13 +4,14 @@ import { print } from "graphql"; import { ApolloLink, execute } from "../../core"; import { Operation, FetchResult, GraphQLRequest } from "../../core/types"; import { Observable } from "../../../utilities"; -import { itAsync } from "../../../testing"; +import { wait } from "../../../testing"; import { BatchLink, OperationBatcher, BatchHandler, BatchableRequest, } from "../batchLink"; +import { ObservableStream } from "../../../testing/internal"; interface MockedResponse { request: GraphQLRequest; @@ -57,22 +58,6 @@ function createOperation(starting: any, operation: GraphQLRequest): Operation { return operation as Operation; } -function terminatingCheck( - resolve: () => any, - reject: (e: any) => any, - callback: (...args: TArgs) => any -) { - return function () { - try { - // @ts-expect-error - callback.apply(this, arguments); - resolve(); - } catch (error) { - reject(error); - } - } as typeof callback; -} - function requestToKey(request: GraphQLRequest): string { const queryString = typeof request.query === "string" ? request.query : print(request.query); @@ -221,195 +206,129 @@ describe("OperationBatcher", () => { } ); - itAsync( - "should be able to consume from a queue containing a single query", - (resolve, reject) => { - const myBatcher = new OperationBatcher({ - batchInterval: 10, - batchHandler, - }); + it("should be able to consume from a queue containing a single query", async () => { + const myBatcher = new OperationBatcher({ + batchInterval: 10, + batchHandler, + }); + + const observable = myBatcher.enqueueRequest({ operation }); + const stream = new ObservableStream(observable); + + const observables: (Observable | undefined)[] = + myBatcher.consumeQueue()!; - myBatcher.enqueueRequest({ operation }).subscribe( - terminatingCheck(resolve, reject, (resultObj: any) => { - expect(myBatcher["batchesByKey"].get("")).toBeUndefined(); - expect(resultObj).toEqual({ data }); - }) - ); - const observables: (Observable | undefined)[] = - myBatcher.consumeQueue()!; - - try { - expect(observables.length).toBe(1); - } catch (e) { - reject(e); + expect(observables.length).toBe(1); + expect(myBatcher["batchesByKey"].get("")).toBeUndefined(); + + await expect(stream).toEmitValue({ data }); + }); + + it("should be able to consume from a queue containing multiple queries", async () => { + const request2: Operation = createOperation( + {}, + { + query, } - } - ); + ); - itAsync( - "should be able to consume from a queue containing multiple queries", - (resolve, reject) => { - const request2: Operation = createOperation( - {}, - { - query, - } - ); + const BH = createMockBatchHandler( + { + request: { query }, + result: { data }, + }, + { + request: { query }, + result: { data }, + } + ); - const BH = createMockBatchHandler( - { - request: { query }, - result: { data }, - }, - { - request: { query }, - result: { data }, - } - ); + const myBatcher = new OperationBatcher({ + batchInterval: 10, + batchMax: 10, + batchHandler: BH, + }); + const observable1 = myBatcher.enqueueRequest({ operation }); + const observable2 = myBatcher.enqueueRequest({ operation: request2 }); - const myBatcher = new OperationBatcher({ - batchInterval: 10, - batchMax: 10, - batchHandler: BH, - }); - const observable1 = myBatcher.enqueueRequest({ operation }); - const observable2 = myBatcher.enqueueRequest({ operation: request2 }); - let notify = false; - observable1.subscribe((resultObj1) => { - try { - expect(resultObj1).toEqual({ data }); - } catch (e) { - reject(e); - } + const stream1 = new ObservableStream(observable1); + const stream2 = new ObservableStream(observable2); - if (notify) { - resolve(); - } else { - notify = true; - } - }); + expect(myBatcher["batchesByKey"].get("")!.size).toBe(2); + const observables: (Observable | undefined)[] = + myBatcher.consumeQueue()!; + expect(myBatcher["batchesByKey"].get("")).toBeUndefined(); + expect(observables.length).toBe(2); - observable2.subscribe((resultObj2) => { - try { - expect(resultObj2).toEqual({ data }); - } catch (e) { - reject(e); - } + await expect(stream1).toEmitValue({ data }); + await expect(stream2).toEmitValue({ data }); + }); - if (notify) { - resolve(); - } else { - notify = true; - } - }); + it("should be able to consume from a queue containing multiple queries with different batch keys", async () => { + // NOTE: this test was added to ensure that queries don't "hang" when consumed by BatchLink. + // "Hanging" in this case results in this test never resolving. So + // if this test times out it's probably a real issue and not a flake + const request2: Operation = createOperation( + {}, + { + query, + } + ); - try { - expect(myBatcher["batchesByKey"].get("")!.size).toBe(2); - const observables: (Observable | undefined)[] = - myBatcher.consumeQueue()!; - expect(myBatcher["batchesByKey"].get("")).toBeUndefined(); - expect(observables.length).toBe(2); - } catch (e) { - reject(e); + const BH = createMockBatchHandler( + { + request: { query }, + result: { data }, + }, + { + request: { query }, + result: { data }, } - } - ); + ); - itAsync( - "should be able to consume from a queue containing multiple queries with different batch keys", - (resolve, reject) => { - // NOTE: this test was added to ensure that queries don't "hang" when consumed by BatchLink. - // "Hanging" in this case results in this test never resolving. So - // if this test times out it's probably a real issue and not a flake - const request2: Operation = createOperation( - {}, - { - query, - } - ); + let key = true; + const batchKey = () => { + key = !key; + return "" + !key; + }; - const BH = createMockBatchHandler( - { - request: { query }, - result: { data }, - }, - { - request: { query }, - result: { data }, - } - ); - - let key = true; - const batchKey = () => { - key = !key; - return "" + !key; - }; - - const myBatcher = new OperationBatcher({ - batchInterval: 10, - batchMax: 10, - batchHandler: BH, - batchKey, - }); + const myBatcher = new OperationBatcher({ + batchInterval: 10, + batchMax: 10, + batchHandler: BH, + batchKey, + }); - const observable1 = myBatcher.enqueueRequest({ operation }); - const observable2 = myBatcher.enqueueRequest({ operation: request2 }); + const observable1 = myBatcher.enqueueRequest({ operation }); + const observable2 = myBatcher.enqueueRequest({ operation: request2 }); - let notify = false; - observable1.subscribe((resultObj1) => { - try { - expect(resultObj1).toEqual({ data }); - } catch (e) { - reject(e); - } + const stream1 = new ObservableStream(observable1); + const stream2 = new ObservableStream(observable2); - if (notify) { - resolve(); - } else { - notify = true; - } - }); + jest.runAllTimers(); - observable2.subscribe((resultObj2) => { - try { - expect(resultObj2).toEqual({ data }); - } catch (e) { - reject(e); - } + await expect(stream1).toEmitValue({ data }); + await expect(stream2).toEmitValue({ data }); + }); - if (notify) { - resolve(); - } else { - notify = true; - } - }); + it("should return a promise when we enqueue a request and resolve it with a result", async () => { + const BH = createMockBatchHandler({ + request: { query }, + result: { data }, + }); + const myBatcher = new OperationBatcher({ + batchInterval: 10, + batchHandler: BH, + }); + const observable = myBatcher.enqueueRequest({ operation }); + const stream = new ObservableStream(observable); - jest.runAllTimers(); - } - ); + myBatcher.consumeQueue(); - itAsync( - "should return a promise when we enqueue a request and resolve it with a result", - (resolve, reject) => { - const BH = createMockBatchHandler({ - request: { query }, - result: { data }, - }); - const myBatcher = new OperationBatcher({ - batchInterval: 10, - batchHandler: BH, - }); - const observable = myBatcher.enqueueRequest({ operation }); - observable.subscribe( - terminatingCheck(resolve, reject, (result: any) => { - expect(result).toEqual({ data }); - }) - ); - myBatcher.consumeQueue(); - } - ); + await expect(stream).toEmitValue({ data }); + }); - itAsync("should be able to debounce requests", (resolve, reject) => { + it("should be able to debounce requests", () => { const batchInterval = 10; const myBatcher = new OperationBatcher({ batchDebounce: true, @@ -442,11 +361,10 @@ describe("OperationBatcher", () => { // and expect the queue to be empty. jest.advanceTimersByTime(batchInterval / 2); expect(myBatcher["batchesByKey"].size).toEqual(0); - resolve(); }); }); - itAsync("should work when single query", (resolve, reject) => { + it("should work when single query", async () => { const data = { lastName: "Ever", firstName: "Greatest", @@ -470,152 +388,138 @@ describe("OperationBatcher", () => { const operation: Operation = createOperation({}, { query }); batcher.enqueueRequest({ operation }).subscribe({}); - try { - expect(batcher["batchesByKey"].get("")!.size).toBe(1); - } catch (e) { - reject(e); - } - - setTimeout( - terminatingCheck(resolve, reject, () => { - expect(batcher["batchesByKey"].get("")).toBeUndefined(); - }), - 20 - ); + expect(batcher["batchesByKey"].get("")!.size).toBe(1); + const promise = wait(20); jest.runAllTimers(); + await promise; + + expect(batcher["batchesByKey"].get("")).toBeUndefined(); }); - itAsync( - "should cancel single query in queue when unsubscribing", - (resolve, reject) => { - const data = { - lastName: "Ever", - firstName: "Greatest", - }; + it("should cancel single query in queue when unsubscribing", async () => { + const data = { + lastName: "Ever", + firstName: "Greatest", + }; - const batcher = new OperationBatcher({ - batchInterval: 10, - batchHandler: () => - new Observable((observer) => { - observer.next([{ data }]); - setTimeout(observer.complete.bind(observer)); - }), - }); + const batcher = new OperationBatcher({ + batchInterval: 10, + batchHandler: () => + new Observable((observer) => { + observer.next([{ data }]); + setTimeout(observer.complete.bind(observer)); + }), + }); - const query = gql` - query { - author { - firstName - lastName - } + const query = gql` + query { + author { + firstName + lastName } - `; + } + `; - batcher - .enqueueRequest({ - operation: createOperation({}, { query }), - }) - .subscribe(() => reject("next should never be called")) - .unsubscribe(); + batcher + .enqueueRequest({ + operation: createOperation({}, { query }), + }) + .subscribe(() => { + throw new Error("next should never be called"); + }) + .unsubscribe(); - expect(batcher["batchesByKey"].get("")).toBeUndefined(); - resolve(); - } - ); - - itAsync( - "should cancel single query in queue with multiple subscriptions", - (resolve, reject) => { - const data = { - lastName: "Ever", - firstName: "Greatest", - }; - const batcher = new OperationBatcher({ - batchInterval: 10, - batchHandler: () => - new Observable((observer) => { - observer.next([{ data }]); - setTimeout(observer.complete.bind(observer)); - }), - }); - const query = gql` - query { - author { - firstName - lastName - } + expect(batcher["batchesByKey"].get("")).toBeUndefined(); + }); + + it("should cancel single query in queue with multiple subscriptions", () => { + const data = { + lastName: "Ever", + firstName: "Greatest", + }; + const batcher = new OperationBatcher({ + batchInterval: 10, + batchHandler: () => + new Observable((observer) => { + observer.next([{ data }]); + setTimeout(observer.complete.bind(observer)); + }), + }); + const query = gql` + query { + author { + firstName + lastName } - `; - const operation: Operation = createOperation({}, { query }); + } + `; + const operation: Operation = createOperation({}, { query }); - const observable = batcher.enqueueRequest({ operation }); + const observable = batcher.enqueueRequest({ operation }); - const checkQueuedRequests = (expectedSubscriberCount: number) => { - const batch = batcher["batchesByKey"].get(""); - expect(batch).not.toBeUndefined(); - expect(batch!.size).toBe(1); - batch!.forEach((request) => { - expect(request.subscribers.size).toBe(expectedSubscriberCount); - }); - }; + const checkQueuedRequests = (expectedSubscriberCount: number) => { + const batch = batcher["batchesByKey"].get(""); + expect(batch).not.toBeUndefined(); + expect(batch!.size).toBe(1); + batch!.forEach((request) => { + expect(request.subscribers.size).toBe(expectedSubscriberCount); + }); + }; - const sub1 = observable.subscribe(() => - reject("next should never be called") - ); - checkQueuedRequests(1); + const sub1 = observable.subscribe(() => { + throw new Error("next should never be called"); + }); + checkQueuedRequests(1); - const sub2 = observable.subscribe(() => - reject("next should never be called") - ); - checkQueuedRequests(2); + const sub2 = observable.subscribe(() => { + throw new Error("next should never be called"); + }); + checkQueuedRequests(2); - sub1.unsubscribe(); - checkQueuedRequests(1); + sub1.unsubscribe(); + checkQueuedRequests(1); - sub2.unsubscribe(); - expect(batcher["batchesByKey"].get("")).toBeUndefined(); - resolve(); - } - ); + sub2.unsubscribe(); + expect(batcher["batchesByKey"].get("")).toBeUndefined(); + }); - itAsync( - "should cancel single query in flight when unsubscribing", - (resolve, reject) => { - const batcher = new OperationBatcher({ - batchInterval: 10, - batchHandler: () => - new Observable(() => { - // Instead of typically starting an XHR, we trigger the unsubscription from outside - setTimeout(() => subscription?.unsubscribe(), 5); - - return () => { - expect(batcher["batchesByKey"].get("")).toBeUndefined(); - resolve(); - }; - }), - }); + it("should cancel single query in flight when unsubscribing", (done) => { + const batcher = new OperationBatcher({ + batchInterval: 10, + batchHandler: () => + new Observable(() => { + // Instead of typically starting an XHR, we trigger the unsubscription from outside + setTimeout(() => subscription?.unsubscribe(), 5); + + return () => { + expect(batcher["batchesByKey"].get("")).toBeUndefined(); + done(); + }; + }), + }); - const query = gql` - query { - author { - firstName - lastName - } + const query = gql` + query { + author { + firstName + lastName } - `; + } + `; - const subscription = batcher - .enqueueRequest({ - operation: createOperation({}, { query }), - }) - .subscribe(() => reject("next should never be called")); + const subscription = batcher + .enqueueRequest({ + operation: createOperation({}, { query }), + }) + .subscribe(() => { + throw new Error("next should never be called"); + }); - jest.runAllTimers(); - } - ); + jest.runAllTimers(); + }); - itAsync("should correctly batch multiple queries", (resolve, reject) => { + it("should correctly batch multiple queries", async () => { const data = { lastName: "Ever", firstName: "Greatest", @@ -646,126 +550,109 @@ describe("OperationBatcher", () => { batcher.enqueueRequest({ operation }).subscribe({}); batcher.enqueueRequest({ operation: operation2 }).subscribe({}); - try { - expect(batcher["batchesByKey"].get("")!.size).toBe(2); - } catch (e) { - reject(e); - } + expect(batcher["batchesByKey"].get("")!.size).toBe(2); setTimeout(() => { // The batch shouldn't be fired yet, so we can add one more request. batcher.enqueueRequest({ operation: operation3 }).subscribe({}); - try { - expect(batcher["batchesByKey"].get("")!.size).toBe(3); - } catch (e) { - reject(e); - } + expect(batcher["batchesByKey"].get("")!.size).toBe(3); }, 5); - setTimeout( - terminatingCheck(resolve, reject, () => { - // The batch should've been fired by now. - expect(batcher["batchesByKey"].get("")).toBeUndefined(); - }), - 20 - ); - + const promise = wait(20); jest.runAllTimers(); + await promise; + + // The batch should've been fired by now. + expect(batcher["batchesByKey"].get("")).toBeUndefined(); }); - itAsync( - "should cancel multiple queries in queue when unsubscribing and let pass still subscribed one", - (resolve, reject) => { - const data2 = { - lastName: "Hauser", - firstName: "Evans", - }; + it("should cancel multiple queries in queue when unsubscribing and let pass still subscribed one", (done) => { + const data2 = { + lastName: "Hauser", + firstName: "Evans", + }; - const batcher = new OperationBatcher({ - batchInterval: 10, - batchHandler: () => - new Observable((observer) => { - observer.next([{ data: data2 }]); - setTimeout(observer.complete.bind(observer)); - }), - }); + const batcher = new OperationBatcher({ + batchInterval: 10, + batchHandler: () => + new Observable((observer) => { + observer.next([{ data: data2 }]); + setTimeout(observer.complete.bind(observer)); + }), + }); - const query = gql` - query { - author { - firstName - lastName - } + const query = gql` + query { + author { + firstName + lastName } - `; + } + `; + + const operation: Operation = createOperation({}, { query }); + const operation2: Operation = createOperation({}, { query }); + const operation3: Operation = createOperation({}, { query }); - const operation: Operation = createOperation({}, { query }); - const operation2: Operation = createOperation({}, { query }); - const operation3: Operation = createOperation({}, { query }); + const sub1 = batcher.enqueueRequest({ operation }).subscribe(() => { + throw new Error("next should never be called"); + }); + batcher.enqueueRequest({ operation: operation2 }).subscribe((result) => { + expect(result.data).toBe(data2); - const sub1 = batcher - .enqueueRequest({ operation }) - .subscribe(() => reject("next should never be called")); - batcher.enqueueRequest({ operation: operation2 }).subscribe((result) => { - expect(result.data).toBe(data2); + // The batch should've been fired by now. + expect(batcher["batchesByKey"].get("")).toBeUndefined(); - // The batch should've been fired by now. - expect(batcher["batchesByKey"].get("")).toBeUndefined(); + done(); + }); - resolve(); - }); + expect(batcher["batchesByKey"].get("")!.size).toBe(2); + sub1.unsubscribe(); + expect(batcher["batchesByKey"].get("")!.size).toBe(1); + + setTimeout(() => { + // The batch shouldn't be fired yet, so we can add one more request. + const sub3 = batcher + .enqueueRequest({ operation: operation3 }) + .subscribe(() => { + throw new Error("next should never be called"); + }); expect(batcher["batchesByKey"].get("")!.size).toBe(2); - sub1.unsubscribe(); + sub3.unsubscribe(); expect(batcher["batchesByKey"].get("")!.size).toBe(1); + }, 5); - setTimeout(() => { - // The batch shouldn't be fired yet, so we can add one more request. - const sub3 = batcher - .enqueueRequest({ operation: operation3 }) - .subscribe(() => reject("next should never be called")); - expect(batcher["batchesByKey"].get("")!.size).toBe(2); - - sub3.unsubscribe(); - expect(batcher["batchesByKey"].get("")!.size).toBe(1); - }, 5); + jest.runAllTimers(); + }); - jest.runAllTimers(); - } - ); - - itAsync( - "should reject the promise if there is a network error", - (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } + it("should reject the promise if there is a network error", async () => { + const query = gql` + query { + author { + firstName + lastName } - `; - const operation: Operation = createOperation({}, { query }); - const error = new Error("Network error"); - const BH = createMockBatchHandler({ - request: { query }, - error, - }); - const batcher = new OperationBatcher({ - batchInterval: 10, - batchHandler: BH, - }); + } + `; + const operation: Operation = createOperation({}, { query }); + const error = new Error("Network error"); + const BH = createMockBatchHandler({ + request: { query }, + error, + }); + const batcher = new OperationBatcher({ + batchInterval: 10, + batchHandler: BH, + }); - const observable = batcher.enqueueRequest({ operation }); - observable.subscribe({ - error: terminatingCheck(resolve, reject, (resError: Error) => { - expect(resError.message).toBe("Network error"); - }), - }); - batcher.consumeQueue(); - } - ); + const observable = batcher.enqueueRequest({ operation }); + const stream = new ObservableStream(observable); + batcher.consumeQueue(); + + await expect(stream).toEmitError(error); + }); }); describe("BatchLink", () => { @@ -781,25 +668,21 @@ describe("BatchLink", () => { ).not.toThrow(); }); - itAsync("passes forward on", (resolve, reject) => { + it("passes forward on", async () => { + expect.assertions(3); const link = ApolloLink.from([ new BatchLink({ batchInterval: 0, batchMax: 1, batchHandler: (operation, forward) => { - try { - expect(forward!.length).toBe(1); - expect(operation.length).toBe(1); - } catch (e) { - reject(e); - } + expect(forward!.length).toBe(1); + expect(operation.length).toBe(1); + return forward![0]!(operation[0]).map((result) => [result]); }, }), new ApolloLink((operation) => { - terminatingCheck(resolve, reject, () => { - expect(operation.query).toEqual(query); - })(); + expect(operation.query).toEqual(query); return null; }), ]); @@ -812,7 +695,7 @@ describe("BatchLink", () => { query, } ) - ).subscribe((result) => reject()); + ).subscribe(() => {}); }); it("raises warning if terminating", () => { @@ -849,28 +732,17 @@ describe("BatchLink", () => { expect(calls).toBe(2); }); - itAsync("correctly uses batch size", (resolve, reject) => { + it("correctly uses batch size", async () => { const sizes = [1, 2, 3]; const terminating = new ApolloLink((operation) => { - try { - expect(operation.query).toEqual(query); - } catch (e) { - reject(e); - } + expect(operation.query).toEqual(query); return Observable.of(operation.variables.count); }); - let runBatchSize = () => { - const size = sizes.pop(); - if (!size) resolve(); - + let runBatchSize = async (size: number) => { const batchHandler = jest.fn((operation, forward) => { - try { - expect(operation.length).toBe(size); - expect(forward.length).toBe(size); - } catch (e) { - reject(e); - } + expect(operation.length).toBe(size); + expect(forward.length).toBe(size); const observables = forward.map((f: any, i: any) => f(operation[i])); return new Observable((observer) => { const data: any[] = []; @@ -895,45 +767,43 @@ describe("BatchLink", () => { terminating, ]); - Array.from(new Array(size)).forEach((_, i) => { - execute(link, { - query, - variables: { count: i }, - }).subscribe({ - next: (data) => { - expect(data).toBe(i); - }, - complete: () => { - try { - expect(batchHandler.mock.calls.length).toBe(1); - } catch (e) { - reject(e); - } - runBatchSize(); - }, - }); - }); + return Promise.all( + Array.from(new Array(size)).map((_, i) => { + return new Promise((resolve) => { + execute(link, { + query, + variables: { count: i }, + }).subscribe({ + next: (data) => { + expect(data).toBe(i); + }, + complete: () => { + expect(batchHandler.mock.calls.length).toBe(1); + resolve(); + }, + }); + }); + }) + ); }; - runBatchSize(); + for (const size of sizes) { + await runBatchSize(size); + } }); - itAsync("correctly follows batch interval", (resolve, reject) => { + it("correctly follows batch interval", (done) => { const intervals = [10, 20, 30]; const runBatchInterval = () => { const mock = jest.fn(); const batchInterval = intervals.pop(); - if (!batchInterval) return resolve(); + if (!batchInterval) return done(); const batchHandler = jest.fn((operation, forward) => { - try { - expect(operation.length).toBe(1); - expect(forward.length).toBe(1); - } catch (e) { - reject(e); - } + expect(operation.length).toBe(1); + expect(forward.length).toBe(1); return forward[0](operation[0]).map((d: any) => [d]); }); @@ -957,11 +827,7 @@ describe("BatchLink", () => { ) ).subscribe({ next: (data) => { - try { - expect(data).toBe(42); - } catch (e) { - reject(e); - } + expect(data).toBe(42); }, complete: () => { mock(batchHandler.mock.calls.length); @@ -972,19 +838,15 @@ describe("BatchLink", () => { await delay(batchInterval); const checkCalls = mock.mock.calls.slice(0, -1); - try { - expect(checkCalls.length).toBe(2); - checkCalls.forEach((args) => expect(args[0]).toBe(0)); - expect(mock).lastCalledWith(1); - expect(batchHandler.mock.calls.length).toBe(1); - } catch (e) { - reject(e); - } + expect(checkCalls.length).toBe(2); + checkCalls.forEach((args) => expect(args[0]).toBe(0)); + expect(mock).lastCalledWith(1); + expect(batchHandler.mock.calls.length).toBe(1); runBatchInterval(); }; - delayedBatchInterval(); + void delayedBatchInterval(); mock(batchHandler.mock.calls.length); mock(batchHandler.mock.calls.length); @@ -994,97 +856,82 @@ describe("BatchLink", () => { runBatchInterval(); }); - itAsync( - "throws an error when more requests than results", - (resolve, reject) => { - const result = [{ data: {} }]; - const batchHandler = jest.fn((op) => Observable.of(result)); + it("throws an error when more requests than results", () => { + expect.assertions(4); + const result = [{ data: {} }]; + const batchHandler = jest.fn((op) => Observable.of(result)); + + const link = ApolloLink.from([ + new BatchLink({ + batchInterval: 10, + batchMax: 2, + batchHandler, + }), + ]); + + [1, 2].forEach((x) => { + execute(link, { + query, + }).subscribe({ + next: (data) => { + throw new Error("next should not be called"); + }, + error: (error: any) => { + expect(error).toBeDefined(); + expect(error.result).toEqual(result); + }, + complete: () => { + throw new Error("complete should not be called"); + }, + }); + }); + }); + + describe("batchKey", () => { + it("should allow different batches to be created separately", (done) => { + const data = { data: {} }; + const result = [data, data]; + + const batchHandler = jest.fn((op) => { + expect(op.length).toBe(2); + return Observable.of(result); + }); + let key = true; + const batchKey = () => { + key = !key; + return "" + !key; + }; const link = ApolloLink.from([ new BatchLink({ - batchInterval: 10, + batchInterval: 1, + //if batchKey does not work, then the batch size would be 3 batchMax: 2, batchHandler, + batchKey, }), ]); - [1, 2].forEach((x) => { + let count = 0; + [1, 2, 3, 4].forEach(() => { execute(link, { query, }).subscribe({ - next: (data) => { - reject("next should not be called"); + next: (d) => { + expect(d).toEqual(data); + }, + error: (e) => { + throw e; }, - error: terminatingCheck(resolve, reject, (error: any) => { - expect(error).toBeDefined(); - expect(error.result).toEqual(result); - }), complete: () => { - reject("complete should not be called"); + count++; + if (count === 4) { + expect(batchHandler.mock.calls.length).toBe(2); + done(); + } }, }); }); - } - ); - - describe("batchKey", () => { - itAsync( - "should allow different batches to be created separately", - (resolve, reject) => { - const data = { data: {} }; - const result = [data, data]; - - const batchHandler = jest.fn((op) => { - try { - expect(op.length).toBe(2); - } catch (e) { - reject(e); - } - return Observable.of(result); - }); - let key = true; - const batchKey = () => { - key = !key; - return "" + !key; - }; - - const link = ApolloLink.from([ - new BatchLink({ - batchInterval: 1, - //if batchKey does not work, then the batch size would be 3 - batchMax: 2, - batchHandler, - batchKey, - }), - ]); - - let count = 0; - [1, 2, 3, 4].forEach(() => { - execute(link, { - query, - }).subscribe({ - next: (d) => { - try { - expect(d).toEqual(data); - } catch (e) { - reject(e); - } - }, - error: reject, - complete: () => { - count++; - if (count === 4) { - try { - expect(batchHandler.mock.calls.length).toBe(2); - resolve(); - } catch (e) { - reject(e); - } - } - }, - }); - }); - } - ); + }); }); }); diff --git a/src/link/context/__tests__/index.ts b/src/link/context/__tests__/index.ts index 8aa7a6be03e..c80be213a95 100644 --- a/src/link/context/__tests__/index.ts +++ b/src/link/context/__tests__/index.ts @@ -4,7 +4,8 @@ import { ApolloLink } from "../../core"; import { Observable } from "../../../utilities/observables/Observable"; import { execute } from "../../core/execute"; import { setContext } from "../index"; -import { itAsync } from "../../../testing"; +import { wait } from "../../../testing"; +import { ObservableStream } from "../../../testing/internal"; const sleep = (ms: number) => new Promise((s) => setTimeout(s, ms)); const query = gql` @@ -18,68 +19,53 @@ const data = { foo: { bar: true }, }; -itAsync( - "can be used to set the context with a simple function", - (resolve, reject) => { - const withContext = setContext(() => ({ dynamicallySet: true })); +it("can be used to set the context with a simple function", async () => { + const withContext = setContext(() => ({ dynamicallySet: true })); - const mockLink = new ApolloLink((operation) => { - expect(operation.getContext().dynamicallySet).toBe(true); - return Observable.of({ data }); - }); + const mockLink = new ApolloLink((operation) => { + expect(operation.getContext().dynamicallySet).toBe(true); + return Observable.of({ data }); + }); - const link = withContext.concat(mockLink); + const link = withContext.concat(mockLink); + const stream = new ObservableStream(execute(link, { query })); - execute(link, { query }).subscribe((result) => { - expect(result.data).toEqual(data); - resolve(); - }); - } -); - -itAsync( - "can be used to set the context with a function returning a promise", - (resolve, reject) => { - const withContext = setContext(() => - Promise.resolve({ dynamicallySet: true }) - ); - - const mockLink = new ApolloLink((operation) => { - expect(operation.getContext().dynamicallySet).toBe(true); - return Observable.of({ data }); - }); + await expect(stream).toEmitValue({ data }); +}); - const link = withContext.concat(mockLink); +it("can be used to set the context with a function returning a promise", async () => { + const withContext = setContext(() => + Promise.resolve({ dynamicallySet: true }) + ); - execute(link, { query }).subscribe((result) => { - expect(result.data).toEqual(data); - resolve(); - }); - } -); - -itAsync( - "can be used to set the context with a function returning a promise that is delayed", - (resolve, reject) => { - const withContext = setContext(() => - sleep(25).then(() => ({ dynamicallySet: true })) - ); - - const mockLink = new ApolloLink((operation) => { - expect(operation.getContext().dynamicallySet).toBe(true); - return Observable.of({ data }); - }); + const mockLink = new ApolloLink((operation) => { + expect(operation.getContext().dynamicallySet).toBe(true); + return Observable.of({ data }); + }); - const link = withContext.concat(mockLink); + const link = withContext.concat(mockLink); + const stream = new ObservableStream(execute(link, { query })); - execute(link, { query }).subscribe((result) => { - expect(result.data).toEqual(data); - resolve(); - }); - } -); + await expect(stream).toEmitValue({ data }); +}); + +it("can be used to set the context with a function returning a promise that is delayed", async () => { + const withContext = setContext(() => + sleep(25).then(() => ({ dynamicallySet: true })) + ); + + const mockLink = new ApolloLink((operation) => { + expect(operation.getContext().dynamicallySet).toBe(true); + return Observable.of({ data }); + }); -itAsync("handles errors in the lookup correclty", (resolve, reject) => { + const link = withContext.concat(mockLink); + const stream = new ObservableStream(execute(link, { query })); + + await expect(stream).toEmitValue({ data }); +}); + +it("handles errors in the lookup correclty", async () => { const withContext = setContext(() => sleep(5).then(() => { throw new Error("dang"); @@ -92,32 +78,27 @@ itAsync("handles errors in the lookup correclty", (resolve, reject) => { const link = withContext.concat(mockLink); - execute(link, { query }).subscribe(reject, (e) => { - expect(e.message).toBe("dang"); - resolve(); - }); + const stream = new ObservableStream(execute(link, { query })); + + await expect(stream).toEmitError("dang"); }); -itAsync( - "handles errors in the lookup correclty with a normal function", - (resolve, reject) => { - const withContext = setContext(() => { - throw new Error("dang"); - }); - const mockLink = new ApolloLink((operation) => { - return Observable.of({ data }); - }); +it("handles errors in the lookup correctly with a normal function", async () => { + const withContext = setContext(() => { + throw new Error("dang"); + }); - const link = withContext.concat(mockLink); + const mockLink = new ApolloLink((operation) => { + return Observable.of({ data }); + }); - execute(link, { query }).subscribe(reject, (e) => { - expect(e.message).toBe("dang"); - resolve(); - }); - } -); + const link = withContext.concat(mockLink); + const stream = new ObservableStream(execute(link, { query })); -itAsync("has access to the request information", (resolve, reject) => { + await expect(stream).toEmitError("dang"); +}); + +it("has access to the request information", async () => { const withContext = setContext(({ operationName, query, variables }) => sleep(1).then(() => Promise.resolve({ @@ -137,13 +118,14 @@ itAsync("has access to the request information", (resolve, reject) => { }); const link = withContext.concat(mockLink); + const stream = new ObservableStream( + execute(link, { query, variables: { id: 1 } }) + ); - execute(link, { query, variables: { id: 1 } }).subscribe((result) => { - expect(result.data).toEqual(data); - resolve(); - }); + await expect(stream).toEmitValue({ data }); }); -itAsync("has access to the context at execution time", (resolve, reject) => { + +it("has access to the context at execution time", async () => { const withContext = setContext((_, { count }) => sleep(1).then(() => ({ count: count + 1 })) ); @@ -155,14 +137,14 @@ itAsync("has access to the context at execution time", (resolve, reject) => { }); const link = withContext.concat(mockLink); + const stream = new ObservableStream( + execute(link, { query, context: { count: 1 } }) + ); - execute(link, { query, context: { count: 1 } }).subscribe((result) => { - expect(result.data).toEqual(data); - resolve(); - }); + await expect(stream).toEmitValue({ data }); }); -itAsync("unsubscribes correctly", (resolve, reject) => { +it("unsubscribes correctly", async () => { const withContext = setContext((_, { count }) => sleep(1).then(() => ({ count: count + 1 })) ); @@ -175,18 +157,19 @@ itAsync("unsubscribes correctly", (resolve, reject) => { const link = withContext.concat(mockLink); - let handle = execute(link, { - query, - context: { count: 1 }, - }).subscribe((result) => { - expect(result.data).toEqual(data); - handle.unsubscribe(); - resolve(); - }); + const stream = new ObservableStream( + execute(link, { + query, + context: { count: 1 }, + }) + ); + + await expect(stream).toEmitValue({ data }); + stream.unsubscribe(); }); -itAsync("unsubscribes without throwing before data", (resolve, reject) => { - let called: boolean; +it("unsubscribes without throwing before data", async () => { + let called!: boolean; const withContext = setContext((_, { count }) => { called = true; return sleep(1).then(() => ({ count: count + 1 })); @@ -209,51 +192,43 @@ itAsync("unsubscribes without throwing before data", (resolve, reject) => { query, context: { count: 1 }, }).subscribe((result) => { - reject("should have unsubscribed"); + throw new Error("should have unsubscribed"); }); - setTimeout(() => { - handle.unsubscribe(); - expect(called).toBe(true); - resolve(); - }, 10); + await wait(10); + + handle.unsubscribe(); + expect(called).toBe(true); }); -itAsync( - "does not start the next link subscription if the upstream subscription is already closed", - (resolve, reject) => { - let promiseResolved = false; - const withContext = setContext(() => - sleep(5).then(() => { - promiseResolved = true; - return { dynamicallySet: true }; - }) - ); - - let mockLinkCalled = false; - const mockLink = new ApolloLink(() => { - mockLinkCalled = true; - reject("link should not be called"); - return new Observable((observer) => { - observer.error("link should not have been observed"); - }); - }); +it("does not start the next link subscription if the upstream subscription is already closed", async () => { + let promiseResolved = false; + const withContext = setContext(() => + sleep(5).then(() => { + promiseResolved = true; + return { dynamicallySet: true }; + }) + ); - const link = withContext.concat(mockLink); + let mockLinkCalled = false; + const mockLink = new ApolloLink(() => { + mockLinkCalled = true; + throw new Error("link should not be called"); + }); - let subscriptionReturnedData = false; - let handle = execute(link, { query }).subscribe((result) => { - subscriptionReturnedData = true; - reject("subscription should not return data"); - }); + const link = withContext.concat(mockLink); - handle.unsubscribe(); + let subscriptionReturnedData = false; + let handle = execute(link, { query }).subscribe((result) => { + subscriptionReturnedData = true; + throw new Error("subscription should not return data"); + }); - setTimeout(() => { - expect(promiseResolved).toBe(true); - expect(mockLinkCalled).toBe(false); - expect(subscriptionReturnedData).toBe(false); - resolve(); - }, 10); - } -); + handle.unsubscribe(); + + await wait(10); + + expect(promiseResolved).toBe(true); + expect(mockLinkCalled).toBe(false); + expect(subscriptionReturnedData).toBe(false); +}); diff --git a/src/link/core/__tests__/ApolloLink.ts b/src/link/core/__tests__/ApolloLink.ts index 1a97d149c44..506968090dc 100644 --- a/src/link/core/__tests__/ApolloLink.ts +++ b/src/link/core/__tests__/ApolloLink.ts @@ -2,12 +2,12 @@ import gql from "graphql-tag"; import { print } from "graphql"; import { Observable } from "../../../utilities/observables/Observable"; -import { itAsync } from "../../../testing"; import { FetchResult, Operation, NextLink, GraphQLRequest } from "../types"; import { ApolloLink } from "../ApolloLink"; -import { DocumentNode } from "graphql"; +import { ObservableStream } from "../../../testing/internal"; +import { execute } from "../execute"; -export class SetContextLink extends ApolloLink { +class SetContextLink extends ApolloLink { constructor( private setContext: ( context: Record @@ -25,7 +25,7 @@ export class SetContextLink extends ApolloLink { } } -export const sampleQuery = gql` +const sampleQuery = gql` query SampleQuery { stub { id @@ -33,50 +33,11 @@ export const sampleQuery = gql` } `; -function checkCalls(calls: any[] = [], results: Array) { - expect(calls.length).toBe(results.length); - calls.map((call, i) => expect(call.data).toEqual(results[i])); -} - -interface TestResultType { - link: ApolloLink; - results?: any[]; - query?: DocumentNode; - done?: () => void; - context?: any; - variables?: any; -} - -export function testLinkResults(params: TestResultType) { - const { link, context, variables } = params; - const results = params.results || []; - const query = params.query || sampleQuery; - const done = params.done || (() => void 0); - - const spy = jest.fn(); - ApolloLink.execute(link, { query, context, variables }).subscribe({ - next: spy, - error: (error: any) => { - expect(error).toEqual(results.pop()); - checkCalls(spy.mock.calls[0], results); - if (done) { - done(); - } - }, - complete: () => { - checkCalls(spy.mock.calls[0], results); - if (done) { - done(); - } - }, - }); -} - -export const setContext = () => ({ add: 1 }); +const setContext = () => ({ add: 1 }); describe("ApolloClient", () => { describe("context", () => { - itAsync("should merge context when using a function", (resolve, reject) => { + it("should merge context when using a function", async () => { const returnOne = new SetContextLink(setContext); const mock = new ApolloLink((op, forward) => { op.setContext((context: { add: number }) => ({ add: context.add + 2 })); @@ -91,70 +52,68 @@ describe("ApolloClient", () => { }); return Observable.of({ data: op.getContext().add }); }); + const stream = new ObservableStream( + execute(link, { query: sampleQuery }) + ); - testLinkResults({ - link, - results: [3], - done: resolve, - }); + await expect(stream).toEmitValue({ data: 3 }); + await expect(stream).toComplete(); }); - itAsync( - "should merge context when not using a function", - (resolve, reject) => { - const returnOne = new SetContextLink(setContext); - const mock = new ApolloLink((op, forward) => { - op.setContext({ add: 3 }); - op.setContext({ substract: 1 }); + it("should merge context when not using a function", async () => { + const returnOne = new SetContextLink(setContext); + const mock = new ApolloLink((op, forward) => { + op.setContext({ add: 3 }); + op.setContext({ substract: 1 }); - return forward(op); - }); - const link = returnOne.concat(mock).concat((op) => { - expect(op.getContext()).toEqual({ - add: 3, - substract: 1, - }); - return Observable.of({ data: op.getContext().add }); + return forward(op); + }); + const link = returnOne.concat(mock).concat((op) => { + expect(op.getContext()).toEqual({ + add: 3, + substract: 1, }); + return Observable.of({ data: op.getContext().add }); + }); + const stream = new ObservableStream( + execute(link, { query: sampleQuery }) + ); - testLinkResults({ - link, - results: [3], - done: resolve, - }); - } - ); + await expect(stream).toEmitValue({ data: 3 }); + await expect(stream).toComplete(); + }); }); describe("concat", () => { - itAsync("should concat a function", (resolve, reject) => { + it("should concat a function", async () => { const returnOne = new SetContextLink(setContext); const link = returnOne.concat((operation, forward) => { return Observable.of({ data: { count: operation.getContext().add } }); }); + const stream = new ObservableStream( + execute(link, { query: sampleQuery }) + ); - testLinkResults({ - link, - results: [{ count: 1 }], - done: resolve, - }); + await expect(stream).toEmitValue({ data: { count: 1 } }); + await expect(stream).toComplete(); }); - itAsync("should concat a Link", (resolve, reject) => { + it("should concat a Link", async () => { const returnOne = new SetContextLink(setContext); const mock = new ApolloLink((op) => Observable.of({ data: op.getContext().add }) ); const link = returnOne.concat(mock); - testLinkResults({ - link, - results: [1], - done: resolve, - }); + const stream = new ObservableStream( + execute(link, { query: sampleQuery }) + ); + + await expect(stream).toEmitValue({ data: 1 }); + await expect(stream).toComplete(); }); - itAsync("should pass error to observable's error", (resolve, reject) => { + it("should pass error to observable's error", async () => { const error = new Error("thrown"); const returnOne = new SetContextLink(setContext); const mock = new ApolloLink( @@ -166,14 +125,15 @@ describe("ApolloClient", () => { ); const link = returnOne.concat(mock); - testLinkResults({ - link, - results: [1, error], - done: resolve, - }); + const stream = new ObservableStream( + execute(link, { query: sampleQuery }) + ); + + await expect(stream).toEmitValue({ data: 1 }); + await expect(stream).toEmitError(error); }); - itAsync("should concat a Link and function", (resolve, reject) => { + it("should concat a Link and function", async () => { const returnOne = new SetContextLink(setContext); const mock = new ApolloLink((op, forward) => { op.setContext((context: { add: number }) => ({ add: context.add + 2 })); @@ -183,14 +143,15 @@ describe("ApolloClient", () => { return Observable.of({ data: op.getContext().add }); }); - testLinkResults({ - link, - results: [3], - done: resolve, - }); + const stream = new ObservableStream( + execute(link, { query: sampleQuery }) + ); + + await expect(stream).toEmitValue({ data: 3 }); + await expect(stream).toComplete(); }); - itAsync("should concat a function and Link", (resolve, reject) => { + it("should concat a function and Link", async () => { const returnOne = new SetContextLink(setContext); const mock = new ApolloLink((op, forward) => Observable.of({ data: op.getContext().add }) @@ -204,14 +165,16 @@ describe("ApolloClient", () => { return forward(operation); }) .concat(mock); - testLinkResults({ - link, - results: [3], - done: resolve, - }); + + const stream = new ObservableStream( + execute(link, { query: sampleQuery }) + ); + + await expect(stream).toEmitValue({ data: 3 }); + await expect(stream).toComplete(); }); - itAsync("should concat two functions", (resolve, reject) => { + it("should concat two functions", async () => { const returnOne = new SetContextLink(setContext); const link = returnOne .concat((operation, forward) => { @@ -221,14 +184,16 @@ describe("ApolloClient", () => { return forward(operation); }) .concat((op, forward) => Observable.of({ data: op.getContext().add })); - testLinkResults({ - link, - results: [3], - done: resolve, - }); + + const stream = new ObservableStream( + execute(link, { query: sampleQuery }) + ); + + await expect(stream).toEmitValue({ data: 3 }); + await expect(stream).toComplete(); }); - itAsync("should concat two Links", (resolve, reject) => { + it("should concat two Links", async () => { const returnOne = new SetContextLink(setContext); const mock1 = new ApolloLink((operation, forward) => { operation.setContext({ @@ -241,88 +206,93 @@ describe("ApolloClient", () => { ); const link = returnOne.concat(mock1).concat(mock2); - testLinkResults({ - link, - results: [3], - done: resolve, - }); + + const stream = new ObservableStream( + execute(link, { query: sampleQuery }) + ); + + await expect(stream).toEmitValue({ data: 3 }); + await expect(stream).toComplete(); }); - itAsync( - "should return an link that can be concat'd multiple times", - (resolve, reject) => { - const returnOne = new SetContextLink(setContext); - const mock1 = new ApolloLink((operation, forward) => { - operation.setContext({ - add: operation.getContext().add + 2, - }); - return forward(operation); + it("should return an link that can be concat'd multiple times", async () => { + const returnOne = new SetContextLink(setContext); + const mock1 = new ApolloLink((operation, forward) => { + operation.setContext({ + add: operation.getContext().add + 2, }); - const mock2 = new ApolloLink((op, forward) => - Observable.of({ data: op.getContext().add + 2 }) + return forward(operation); + }); + const mock2 = new ApolloLink((op, forward) => + Observable.of({ data: op.getContext().add + 2 }) + ); + const mock3 = new ApolloLink((op, forward) => + Observable.of({ data: op.getContext().add + 3 }) + ); + const link = returnOne.concat(mock1); + + { + const stream = new ObservableStream( + execute(link.concat(mock2), { query: sampleQuery }) ); - const mock3 = new ApolloLink((op, forward) => - Observable.of({ data: op.getContext().add + 3 }) + + await expect(stream).toEmitValue({ data: 5 }); + await expect(stream).toComplete(); + } + + { + const stream = new ObservableStream( + execute(link.concat(mock3), { query: sampleQuery }) ); - const link = returnOne.concat(mock1); - testLinkResults({ - link: link.concat(mock2), - results: [5], - }); - testLinkResults({ - link: link.concat(mock3), - results: [6], - done: resolve, - }); + await expect(stream).toEmitValue({ data: 6 }); + await expect(stream).toComplete(); } - ); + }); }); describe("empty", () => { - itAsync( - "should returns an immediately completed Observable", - (resolve, reject) => { - testLinkResults({ - link: ApolloLink.empty(), - done: resolve, - }); - } - ); + it("should returns an immediately completed Observable", async () => { + const stream = new ObservableStream( + execute(ApolloLink.empty(), { query: sampleQuery }) + ); + + await expect(stream).toComplete(); + }); }); describe("execute", () => { - itAsync( - "transforms an opearation with context into something serlizable", - (resolve, reject) => { - const query = gql` - { - id - } - `; - const link = new ApolloLink((operation) => { - const str = JSON.stringify({ - ...operation, - query: print(operation.query), - }); - - expect(str).toBe( - JSON.stringify({ - variables: { id: 1 }, - extensions: { cache: true }, - query: print(operation.query), - }) - ); - return Observable.of(); + it("transforms an opearation with context into something serlizable", async () => { + const query = gql` + { + id + } + `; + const link = new ApolloLink((operation) => { + const str = JSON.stringify({ + ...operation, + query: print(operation.query), }); - const noop = () => {}; - ApolloLink.execute(link, { + + expect(str).toBe( + JSON.stringify({ + variables: { id: 1 }, + extensions: { cache: true }, + query: print(operation.query), + }) + ); + return Observable.of(); + }); + const stream = new ObservableStream( + execute(link, { query, variables: { id: 1 }, extensions: { cache: true }, - }).subscribe(noop, noop, resolve); - } - ); + }) + ); + + await expect(stream).toComplete(); + }); describe("execute", () => { let _warn: (message?: any, ...originalParams: any[]) => void; @@ -340,92 +310,87 @@ describe("ApolloClient", () => { console.warn = _warn; }); - itAsync( - "should return an empty observable when a link returns null", - (resolve, reject) => { - const link = new ApolloLink(); - link.request = () => null; - testLinkResults({ - link, - results: [], - done: resolve, - }); - } - ); + it("should return an empty observable when a link returns null", async () => { + const link = new ApolloLink(); + link.request = () => null; - itAsync( - "should return an empty observable when a link is empty", - (resolve, reject) => { - testLinkResults({ - link: ApolloLink.empty(), - results: [], - done: resolve, - }); - } - ); + const stream = new ObservableStream( + execute(link, { query: sampleQuery }) + ); - itAsync( - "should return an empty observable when a concat'd link returns null", - (resolve, reject) => { - const link = new ApolloLink((operation, forward) => { - return forward(operation); - }).concat(() => null); - testLinkResults({ - link, - results: [], - done: resolve, - }); - } - ); + await expect(stream).toComplete(); + }); + + it("should return an empty observable when a link is empty", async () => { + const stream = new ObservableStream( + execute(ApolloLink.empty(), { query: sampleQuery }) + ); + + await expect(stream).toComplete(); + }); + + it("should return an empty observable when a concat'd link returns null", async () => { + const link = new ApolloLink((operation, forward) => { + return forward(operation); + }).concat(() => null); + + const stream = new ObservableStream( + execute(link, { query: sampleQuery }) + ); + + await expect(stream).toComplete(); + }); - itAsync( - "should return an empty observable when a split link returns null", - (resolve, reject) => { - let context = { test: true }; - const link = new SetContextLink(() => context).split( - (op) => op.getContext().test, - () => Observable.of(), - () => null + it("should return an empty observable when a split link returns null", async () => { + let context = { test: true }; + const link = new SetContextLink(() => context).split( + (op) => op.getContext().test, + () => Observable.of(), + () => null + ); + + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery }) ); - testLinkResults({ - link, - results: [], - }); - context.test = false; - testLinkResults({ - link, - results: [], - done: resolve, - }); + + await expect(stream).toComplete(); } - ); - itAsync( - "should set a default context, variable, and query on a copy of operation", - (resolve, reject) => { - const operation = { - query: gql` - { - id - } - `, - }; - const link = new ApolloLink((op: Operation) => { - expect((operation as any)["operationName"]).toBeUndefined(); - expect((operation as any)["variables"]).toBeUndefined(); - expect((operation as any)["context"]).toBeUndefined(); - expect((operation as any)["extensions"]).toBeUndefined(); - expect(op["variables"]).toBeDefined(); - expect((op as any)["context"]).toBeUndefined(); - expect(op["extensions"]).toBeDefined(); - return Observable.of(); - }); + context.test = false; - ApolloLink.execute(link, operation).subscribe({ - complete: resolve, - }); + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery }) + ); + + await expect(stream).toComplete(); } - ); + }); + + it("should set a default context, variable, and query on a copy of operation", async () => { + const operation = { + query: gql` + { + id + } + `, + }; + const link = new ApolloLink((op: Operation) => { + expect((operation as any)["operationName"]).toBeUndefined(); + expect((operation as any)["variables"]).toBeUndefined(); + expect((operation as any)["context"]).toBeUndefined(); + expect((operation as any)["extensions"]).toBeUndefined(); + expect(op["variables"]).toBeDefined(); + expect((op as any)["context"]).toBeUndefined(); + expect(op["extensions"]).toBeDefined(); + return Observable.of(); + }); + + const stream = new ObservableStream(execute(link, operation)); + + await expect(stream).toComplete(); + }); }); }); @@ -437,19 +402,14 @@ describe("ApolloClient", () => { extensions: {}, }; - itAsync( - "should create an observable that completes when passed an empty array", - (resolve, reject) => { - const observable = ApolloLink.execute(ApolloLink.from([]), { - query: sampleQuery, - }); - observable.subscribe( - () => expect(false), - () => expect(false), - resolve - ); - } - ); + it("should create an observable that completes when passed an empty array", async () => { + const observable = ApolloLink.execute(ApolloLink.from([]), { + query: sampleQuery, + }); + const stream = new ObservableStream(observable); + + await expect(stream).toComplete(); + }); it("can create chain of one", () => { expect(() => ApolloLink.from([new ApolloLink()])).not.toThrow(); @@ -464,7 +424,7 @@ describe("ApolloClient", () => { ).not.toThrow(); }); - itAsync("should receive result of one link", (resolve, reject) => { + it("should receive result of one link", async () => { const data: FetchResult = { data: { hello: "world", @@ -475,15 +435,10 @@ describe("ApolloClient", () => { ]); // Smoke tests execute as a static method const observable = ApolloLink.execute(chain, uniqueOperation); - observable.subscribe({ - next: (actualData) => { - expect(data).toEqual(actualData); - }, - error: () => { - throw new Error(); - }, - complete: () => resolve(), - }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue(data); + await expect(stream).toComplete(); }); it("should accept AST query and pass AST to link", () => { @@ -497,7 +452,7 @@ describe("ApolloClient", () => { const chain = ApolloLink.from([new ApolloLink(stub)]); ApolloLink.execute(chain, astOperation); - expect(stub).toBeCalledWith({ + expect(stub).toHaveBeenCalledWith({ query: sampleQuery, operationName: "SampleQuery", variables: {}, @@ -505,157 +460,131 @@ describe("ApolloClient", () => { }); }); - itAsync( - "should pass operation from one link to next with modifications", - (resolve, reject) => { - const chain = ApolloLink.from([ - new ApolloLink((op, forward) => - forward({ - ...op, - query: sampleQuery, - }) - ), - new ApolloLink((op) => { - expect({ - extensions: {}, - operationName: "SampleQuery", - query: sampleQuery, - variables: {}, - }).toEqual(op); - - resolve(); - - return new Observable((observer) => { - observer.error("should not have invoked observable"); - }); - }), - ]); - ApolloLink.execute(chain, uniqueOperation); - } - ); + it("should pass operation from one link to next with modifications", async () => { + const chain = ApolloLink.from([ + new ApolloLink((op, forward) => + forward({ + ...op, + query: sampleQuery, + }) + ), + new ApolloLink((op) => { + expect({ + extensions: {}, + operationName: "SampleQuery", + query: sampleQuery, + variables: {}, + }).toEqual(op); + + return new Observable((observer) => { + observer.complete(); + }); + }), + ]); + const observable = ApolloLink.execute(chain, uniqueOperation); + const stream = new ObservableStream(observable); - itAsync( - "should pass result of one link to another with forward", - (resolve, reject) => { - const data: FetchResult = { - data: { - hello: "world", - }, - }; + await expect(stream).toComplete(); + }); + + it("should pass result of one link to another with forward", async () => { + const data: FetchResult = { + data: { + hello: "world", + }, + }; + + const chain = ApolloLink.from([ + new ApolloLink((op, forward) => { + return forward(op); + }), + new ApolloLink(() => Observable.of(data)), + ]); + const observable = ApolloLink.execute(chain, uniqueOperation); + const stream = new ObservableStream(observable); - const chain = ApolloLink.from([ - new ApolloLink((op, forward) => { - const observable = forward(op); + await expect(stream).toEmitValue(data); + await expect(stream).toComplete(); + }); + it("should receive final result of two link chain", async () => { + const data: FetchResult = { + data: { + hello: "world", + }, + }; + + const chain = ApolloLink.from([ + new ApolloLink((op, forward) => { + const observable = forward(op); + + return new Observable((observer) => { observable.subscribe({ next: (actualData) => { expect(data).toEqual(actualData); + observer.next({ + data: { + ...actualData.data, + modification: "unique", + }, + }); }, - error: () => { - throw new Error(); - }, - complete: resolve, + error: (error) => observer.error(error), + complete: () => observer.complete(), }); + }); + }), + new ApolloLink(() => Observable.of(data)), + ]); - return observable; - }), - new ApolloLink(() => Observable.of(data)), - ]); - ApolloLink.execute(chain, uniqueOperation); - } - ); + const result = ApolloLink.execute(chain, uniqueOperation); + const stream = new ObservableStream(result); - itAsync( - "should receive final result of two link chain", - (resolve, reject) => { - const data: FetchResult = { - data: { - hello: "world", - }, - }; + await expect(stream).toEmitValue({ + data: { + ...data.data, + modification: "unique", + }, + }); + await expect(stream).toComplete(); + }); - const chain = ApolloLink.from([ - new ApolloLink((op, forward) => { - const observable = forward(op); - - return new Observable((observer) => { - observable.subscribe({ - next: (actualData) => { - expect(data).toEqual(actualData); - observer.next({ - data: { - ...actualData.data, - modification: "unique", - }, - }); - }, - error: (error) => observer.error(error), - complete: () => observer.complete(), - }); - }); - }), - new ApolloLink(() => Observable.of(data)), - ]); + it("should chain together a function with links", async () => { + const add1 = new ApolloLink((operation: Operation, forward: NextLink) => { + operation.setContext((context: { num: number }) => ({ + num: context.num + 1, + })); + return forward(operation); + }); + const add1Link = new ApolloLink((operation, forward) => { + operation.setContext((context: { num: number }) => ({ + num: context.num + 1, + })); + return forward(operation); + }); - const result = ApolloLink.execute(chain, uniqueOperation); + const link = ApolloLink.from([ + add1, + add1, + add1Link, + add1, + add1Link, + new ApolloLink((operation) => + Observable.of({ data: operation.getContext() }) + ), + ]); - result.subscribe({ - next: (modifiedData) => { - expect({ - data: { - ...data.data, - modification: "unique", - }, - }).toEqual(modifiedData); - }, - error: () => { - throw new Error(); - }, - complete: resolve, - }); - } - ); - - itAsync( - "should chain together a function with links", - (resolve, reject) => { - const add1 = new ApolloLink( - (operation: Operation, forward: NextLink) => { - operation.setContext((context: { num: number }) => ({ - num: context.num + 1, - })); - return forward(operation); - } - ); - const add1Link = new ApolloLink((operation, forward) => { - operation.setContext((context: { num: number }) => ({ - num: context.num + 1, - })); - return forward(operation); - }); + const stream = new ObservableStream( + execute(link, { query: sampleQuery, context: { num: 0 } }) + ); - const link = ApolloLink.from([ - add1, - add1, - add1Link, - add1, - add1Link, - new ApolloLink((operation) => - Observable.of({ data: operation.getContext() }) - ), - ]); - testLinkResults({ - link, - results: [{ num: 5 }], - context: { num: 0 }, - done: resolve, - }); - } - ); + await expect(stream).toEmitValue({ data: { num: 5 } }); + await expect(stream).toComplete(); + }); }); describe("split", () => { - itAsync("should split two functions", (resolve, reject) => { + it("should split two functions", async () => { const context = { add: 1 }; const returnOne = new SetContextLink(() => context); const link1 = returnOne.concat((operation, forward) => @@ -670,21 +599,28 @@ describe("ApolloClient", () => { link2 ); - testLinkResults({ - link, - results: [2], - }); + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery }) + ); + + await expect(stream).toEmitValue({ data: 2 }); + await expect(stream).toComplete(); + } context.add = 2; - testLinkResults({ - link, - results: [4], - done: resolve, - }); + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery }) + ); + + await expect(stream).toEmitValue({ data: 4 }); + await expect(stream).toComplete(); + } }); - itAsync("should split two Links", (resolve, reject) => { + it("should split two Links", async () => { const context = { add: 1 }; const returnOne = new SetContextLink(() => context); const link1 = returnOne.concat( @@ -703,21 +639,28 @@ describe("ApolloClient", () => { link2 ); - testLinkResults({ - link, - results: [2], - }); + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery }) + ); + + await expect(stream).toEmitValue({ data: 2 }); + await expect(stream).toComplete(); + } context.add = 2; - testLinkResults({ - link, - results: [4], - done: resolve, - }); + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery }) + ); + + await expect(stream).toEmitValue({ data: 4 }); + await expect(stream).toComplete(); + } }); - itAsync("should split a link and a function", (resolve, reject) => { + it("should split a link and a function", async () => { const context = { add: 1 }; const returnOne = new SetContextLink(() => context); const link1 = returnOne.concat((operation, forward) => @@ -734,21 +677,28 @@ describe("ApolloClient", () => { link2 ); - testLinkResults({ - link, - results: [2], - }); + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery }) + ); + + await expect(stream).toEmitValue({ data: 2 }); + await expect(stream).toComplete(); + } context.add = 2; - testLinkResults({ - link, - results: [4], - done: resolve, - }); + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery }) + ); + + await expect(stream).toEmitValue({ data: 4 }); + await expect(stream).toComplete(); + } }); - itAsync("should allow concat after split to be join", (resolve, reject) => { + it("should allow concat after split to be join", async () => { const context = { test: true, add: 1 }; const start = new SetContextLink(() => ({ ...context })); const link = start @@ -771,92 +721,105 @@ describe("ApolloClient", () => { Observable.of({ data: operation.getContext().add }) ); - testLinkResults({ - link, - context, - results: [2], - }); + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery, context }) + ); + + await expect(stream).toEmitValue({ data: 2 }); + await expect(stream).toComplete(); + } context.test = false; - testLinkResults({ - link, - context, - results: [3], - done: resolve, - }); + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery, context }) + ); + + await expect(stream).toEmitValue({ data: 3 }); + await expect(stream).toComplete(); + } }); - itAsync( - "should allow default right to be empty or passthrough when forward available", - (resolve, reject) => { - let context = { test: true }; - const start = new SetContextLink(() => context); - const link = start.split( - (operation) => operation.getContext().test, - (operation) => - Observable.of({ - data: { - count: 1, - }, - }) - ); - const concat = link.concat((operation) => + it("should allow default right to be empty or passthrough when forward available", async () => { + let context = { test: true }; + const start = new SetContextLink(() => context); + const link = start.split( + (operation) => operation.getContext().test, + (operation) => Observable.of({ data: { - count: 2, + count: 1, }, }) + ); + const concat = link.concat((operation) => + Observable.of({ + data: { + count: 2, + }, + }) + ); + + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery }) ); - testLinkResults({ - link, - results: [{ count: 1 }], - }); + await expect(stream).toEmitValue({ data: { count: 1 } }); + await expect(stream).toComplete(); + } - context.test = false; + context.test = false; - testLinkResults({ - link, - results: [], - }); + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery }) + ); - testLinkResults({ - link: concat, - results: [{ count: 2 }], - done: resolve, - }); + await expect(stream).toComplete(); } - ); - itAsync( - "should create filter when single link passed in", - (resolve, reject) => { - const link = ApolloLink.split( - (operation) => operation.getContext().test, - (operation, forward) => Observable.of({ data: { count: 1 } }) + { + const stream = new ObservableStream( + execute(concat, { query: sampleQuery }) ); - let context = { test: true }; + await expect(stream).toEmitValue({ data: { count: 2 } }); + await expect(stream).toComplete(); + } + }); - testLinkResults({ - link, - results: [{ count: 1 }], - context, - }); + it("should create filter when single link passed in", async () => { + const link = ApolloLink.split( + (operation) => operation.getContext().test, + (operation, forward) => Observable.of({ data: { count: 1 } }) + ); - context.test = false; + let context = { test: true }; - testLinkResults({ - link, - results: [], - context, - done: resolve, - }); + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery, context }) + ); + + await expect(stream).toEmitValue({ data: { count: 1 } }); + await expect(stream).toComplete(); } - ); - itAsync("should split two functions", (resolve, reject) => { + context.test = false; + + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery, context }) + ); + + await expect(stream).toComplete(); + } + }); + + it("should split two functions", async () => { const link = ApolloLink.split( (operation) => operation.getContext().test, (operation, forward) => Observable.of({ data: { count: 1 } }), @@ -865,23 +828,28 @@ describe("ApolloClient", () => { let context = { test: true }; - testLinkResults({ - link, - results: [{ count: 1 }], - context, - }); + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery, context }) + ); + + await expect(stream).toEmitValue({ data: { count: 1 } }); + await expect(stream).toComplete(); + } context.test = false; - testLinkResults({ - link, - results: [{ count: 2 }], - context, - done: resolve, - }); + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery, context }) + ); + + await expect(stream).toEmitValue({ data: { count: 2 } }); + await expect(stream).toComplete(); + } }); - itAsync("should split two Links", (resolve, reject) => { + it("should split two Links", async () => { const link = ApolloLink.split( (operation) => operation.getContext().test, (operation, forward) => Observable.of({ data: { count: 1 } }), @@ -892,23 +860,28 @@ describe("ApolloClient", () => { let context = { test: true }; - testLinkResults({ - link, - results: [{ count: 1 }], - context, - }); + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery, context }) + ); + + await expect(stream).toEmitValue({ data: { count: 1 } }); + await expect(stream).toComplete(); + } context.test = false; - testLinkResults({ - link, - results: [{ count: 2 }], - context, - done: resolve, - }); + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery, context }) + ); + + await expect(stream).toEmitValue({ data: { count: 2 } }); + await expect(stream).toComplete(); + } }); - itAsync("should split a link and a function", (resolve, reject) => { + it("should split a link and a function", async () => { const link = ApolloLink.split( (operation) => operation.getContext().test, (operation, forward) => Observable.of({ data: { count: 1 } }), @@ -919,23 +892,28 @@ describe("ApolloClient", () => { let context = { test: true }; - testLinkResults({ - link, - results: [{ count: 1 }], - context, - }); + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery, context }) + ); + + await expect(stream).toEmitValue({ data: { count: 1 } }); + await expect(stream).toComplete(); + } context.test = false; - testLinkResults({ - link, - results: [{ count: 2 }], - context, - done: resolve, - }); + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery, context }) + ); + + await expect(stream).toEmitValue({ data: { count: 2 } }); + await expect(stream).toComplete(); + } }); - itAsync("should allow concat after split to be join", (resolve, reject) => { + it("should allow concat after split to be join", async () => { const context = { test: true }; const link = ApolloLink.split( (operation) => operation.getContext().test, @@ -945,47 +923,53 @@ describe("ApolloClient", () => { })) ).concat(() => Observable.of({ data: { count: 1 } })); - testLinkResults({ - link, - context, - results: [{ count: 2 }], - }); + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery, context }) + ); + + await expect(stream).toEmitValue({ data: { count: 2 } }); + await expect(stream).toComplete(); + } context.test = false; + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery, context }) + ); - testLinkResults({ - link, - context, - results: [{ count: 1 }], - done: resolve, - }); + await expect(stream).toEmitValue({ data: { count: 1 } }); + await expect(stream).toComplete(); + } }); - itAsync( - "should allow default right to be passthrough", - (resolve, reject) => { - const context = { test: true }; - const link = ApolloLink.split( - (operation) => operation.getContext().test, - (operation) => Observable.of({ data: { count: 2 } }) - ).concat((operation) => Observable.of({ data: { count: 1 } })); + it("should allow default right to be passthrough", async () => { + const context = { test: true }; + const link = ApolloLink.split( + (operation) => operation.getContext().test, + (operation) => Observable.of({ data: { count: 2 } }) + ).concat((operation) => Observable.of({ data: { count: 1 } })); - testLinkResults({ - link, - context, - results: [{ count: 2 }], - }); + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery, context }) + ); - context.test = false; + await expect(stream).toEmitValue({ data: { count: 2 } }); + await expect(stream).toComplete(); + } - testLinkResults({ - link, - context, - results: [{ count: 1 }], - done: resolve, - }); + context.test = false; + + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery, context }) + ); + + await expect(stream).toEmitValue({ data: { count: 1 } }); + await expect(stream).toComplete(); } - ); + }); }); describe("Terminating links", () => { diff --git a/src/link/error/__tests__/index.ts b/src/link/error/__tests__/index.ts index 5e886ff58d3..23b33998968 100644 --- a/src/link/error/__tests__/index.ts +++ b/src/link/error/__tests__/index.ts @@ -5,10 +5,10 @@ import { execute } from "../../core/execute"; import { ServerError, throwServerError } from "../../utils/throwServerError"; import { Observable } from "../../../utilities/observables/Observable"; import { onError, ErrorLink } from "../"; -import { itAsync } from "../../../testing"; +import { ObservableStream } from "../../../testing/internal"; describe("error handling", () => { - itAsync("has an easy way to handle GraphQL errors", (resolve, reject) => { + it("has an easy way to handle GraphQL errors", async () => { const query = gql` { foo { @@ -17,7 +17,7 @@ describe("error handling", () => { } `; - let called: boolean; + let called = false; const errorLink = onError(({ graphQLErrors, networkError }) => { expect(graphQLErrors![0].message).toBe("resolver blew up"); called = true; @@ -34,47 +34,44 @@ describe("error handling", () => { ); const link = errorLink.concat(mockLink); + const stream = new ObservableStream(execute(link, { query })); - execute(link, { query }).subscribe((result) => { - expect(result.errors![0].message).toBe("resolver blew up"); - expect(called).toBe(true); - resolve(); - }); + const result = await stream.takeNext(); + + expect(result.errors![0].message).toBe("resolver blew up"); + expect(called).toBe(true); }); - itAsync( - "has an easy way to log client side (network) errors", - (resolve, reject) => { - const query = gql` - query Foo { - foo { - bar - } + + it("has an easy way to log client side (network) errors", async () => { + const query = gql` + query Foo { + foo { + bar } - `; + } + `; - let called: boolean; - const errorLink = onError(({ operation, networkError }) => { - expect(networkError!.message).toBe("app is crashing"); - expect(operation.operationName).toBe("Foo"); - called = true; - }); + let called = false; + const errorLink = onError(({ operation, networkError }) => { + expect(networkError!.message).toBe("app is crashing"); + expect(operation.operationName).toBe("Foo"); + called = true; + }); - const mockLink = new ApolloLink((operation) => { - throw new Error("app is crashing"); - }); + const mockLink = new ApolloLink((operation) => { + throw new Error("app is crashing"); + }); - const link = errorLink.concat(mockLink); + const link = errorLink.concat(mockLink); + const stream = new ObservableStream(execute(link, { query })); - execute(link, { query }).subscribe({ - error: (e) => { - expect(e.message).toBe("app is crashing"); - expect(called).toBe(true); - resolve(); - }, - }); - } - ); - itAsync("captures errors within links", (resolve, reject) => { + const error = await stream.takeError(); + + expect(error.message).toBe("app is crashing"); + expect(called).toBe(true); + }); + + it("captures errors within links", async () => { const query = gql` query Foo { foo { @@ -83,7 +80,7 @@ describe("error handling", () => { } `; - let called: boolean; + let called!: boolean; const errorLink = onError(({ operation, networkError }) => { expect(networkError!.message).toBe("app is crashing"); expect(operation.operationName).toBe("Foo"); @@ -97,89 +94,79 @@ describe("error handling", () => { }); const link = errorLink.concat(mockLink); + const stream = new ObservableStream(execute(link, { query })); - execute(link, { query }).subscribe({ - error: (e) => { - expect(e.message).toBe("app is crashing"); - expect(called).toBe(true); - resolve(); - }, - }); + const error = await stream.takeError(); + + expect(error.message).toBe("app is crashing"); + expect(called).toBe(true); }); - itAsync( - "captures networkError.statusCode within links", - (resolve, reject) => { - const query = gql` - query Foo { - foo { - bar - } + + it("captures networkError.statusCode within links", async () => { + const query = gql` + query Foo { + foo { + bar } - `; - - let called: boolean; - const errorLink = onError(({ operation, networkError }) => { - expect(networkError!.message).toBe("app is crashing"); - expect(networkError!.name).toBe("ServerError"); - expect((networkError as ServerError).statusCode).toBe(500); - expect((networkError as ServerError).response.ok).toBe(false); - expect(operation.operationName).toBe("Foo"); - called = true; - }); + } + `; - const mockLink = new ApolloLink((operation) => { - return new Observable((obs) => { - const response = { status: 500, ok: false } as Response; - throwServerError(response, "ServerError", "app is crashing"); - }); + let called!: boolean; + const errorLink = onError(({ operation, networkError }) => { + expect(networkError!.message).toBe("app is crashing"); + expect(networkError!.name).toBe("ServerError"); + expect((networkError as ServerError).statusCode).toBe(500); + expect((networkError as ServerError).response.ok).toBe(false); + expect(operation.operationName).toBe("Foo"); + called = true; + }); + + const mockLink = new ApolloLink((operation) => { + return new Observable((obs) => { + const response = { status: 500, ok: false } as Response; + throwServerError(response, "ServerError", "app is crashing"); }); + }); - const link = errorLink.concat(mockLink); + const link = errorLink.concat(mockLink); + const stream = new ObservableStream(execute(link, { query })); - execute(link, { query }).subscribe({ - error: (e) => { - expect(e.message).toBe("app is crashing"); - expect(called).toBe(true); - resolve(); - }, - }); - } - ); - itAsync( - "sets graphQLErrors to undefined if networkError.result is an empty string", - (resolve, reject) => { - const query = gql` - query Foo { - foo { - bar - } + const error = await stream.takeError(); + + expect(error.message).toBe("app is crashing"); + expect(called).toBe(true); + }); + + it("sets graphQLErrors to undefined if networkError.result is an empty string", async () => { + const query = gql` + query Foo { + foo { + bar } - `; + } + `; - let called: boolean; - const errorLink = onError(({ graphQLErrors }) => { - expect(graphQLErrors).toBeUndefined(); - called = true; - }); + let called!: boolean; + const errorLink = onError(({ graphQLErrors }) => { + expect(graphQLErrors).toBeUndefined(); + called = true; + }); - const mockLink = new ApolloLink((operation) => { - return new Observable((obs) => { - const response = { status: 500, ok: false } as Response; - throwServerError(response, "", "app is crashing"); - }); + const mockLink = new ApolloLink((operation) => { + return new Observable((obs) => { + const response = { status: 500, ok: false } as Response; + throwServerError(response, "", "app is crashing"); }); + }); - const link = errorLink.concat(mockLink); + const link = errorLink.concat(mockLink); + const stream = new ObservableStream(execute(link, { query })); - execute(link, { query }).subscribe({ - error: (e) => { - expect(called).toBe(true); - resolve(); - }, - }); - } - ); - itAsync("completes if no errors", (resolve, reject) => { + await expect(stream).toEmitError(); + expect(called).toBe(true); + }); + + it("completes if no errors", async () => { const query = gql` { foo { @@ -197,12 +184,13 @@ describe("error handling", () => { }); const link = errorLink.concat(mockLink); + const stream = new ObservableStream(execute(link, { query })); - execute(link, { query }).subscribe({ - complete: resolve, - }); + await expect(stream).toEmitNext(); + await expect(stream).toComplete(); }); - itAsync("allows an error to be ignored", (resolve, reject) => { + + it("allows an error to be ignored", async () => { const query = gql` { foo { @@ -225,17 +213,16 @@ describe("error handling", () => { }); const link = errorLink.concat(mockLink); + const stream = new ObservableStream(execute(link, { query })); - execute(link, { query }).subscribe({ - next: ({ errors, data }) => { - expect(errors).toBe(null); - expect(data).toEqual({ foo: { id: 1 } }); - }, - complete: resolve, + await expect(stream).toEmitValue({ + errors: null, + data: { foo: { id: 1 } }, }); + await expect(stream).toComplete(); }); - itAsync("can be unsubcribed", (resolve, reject) => { + it("can be unsubcribed", async () => { const query = gql` { foo { @@ -258,62 +245,56 @@ describe("error handling", () => { }); const link = errorLink.concat(mockLink); + const stream = new ObservableStream(execute(link, { query })); - const sub = execute(link, { query }).subscribe({ - complete: () => { - reject("completed"); - }, - }); - - sub.unsubscribe(); + stream.unsubscribe(); - setTimeout(resolve, 10); + await expect(stream).not.toEmitAnything(); }); - itAsync( - "includes the operation and any data along with a graphql error", - (resolve, reject) => { - const query = gql` - query Foo { - foo { - bar - } + it("includes the operation and any data along with a graphql error", async () => { + const query = gql` + query Foo { + foo { + bar } - `; - - let called: boolean; - const errorLink = onError(({ graphQLErrors, response, operation }) => { - expect(graphQLErrors![0].message).toBe("resolver blew up"); - expect(response!.data!.foo).toBe(true); - expect(operation.operationName).toBe("Foo"); - expect(operation.getContext().bar).toBe(true); - called = true; - }); + } + `; - const mockLink = new ApolloLink((operation) => - Observable.of({ - data: { foo: true }, - errors: [ - { - message: "resolver blew up", - }, - ], - } as any) - ); - - const link = errorLink.concat(mockLink); - - execute(link, { query, context: { bar: true } }).subscribe((result) => { - expect(result.errors![0].message).toBe("resolver blew up"); - expect(called).toBe(true); - resolve(); - }); - } - ); + let called!: boolean; + const errorLink = onError(({ graphQLErrors, response, operation }) => { + expect(graphQLErrors![0].message).toBe("resolver blew up"); + expect(response!.data!.foo).toBe(true); + expect(operation.operationName).toBe("Foo"); + expect(operation.getContext().bar).toBe(true); + called = true; + }); + + const mockLink = new ApolloLink((operation) => + Observable.of({ + data: { foo: true }, + errors: [ + { + message: "resolver blew up", + }, + ], + } as any) + ); + + const link = errorLink.concat(mockLink); + const stream = new ObservableStream( + execute(link, { query, context: { bar: true } }) + ); + + const result = await stream.takeNext(); + + expect(result.errors![0].message).toBe("resolver blew up"); + expect(called).toBe(true); + }); }); describe("error handling with class", () => { - itAsync("has an easy way to handle GraphQL errors", (resolve, reject) => { + it("has an easy way to handle GraphQL errors", async () => { const query = gql` { foo { @@ -322,7 +303,7 @@ describe("error handling with class", () => { } `; - let called: boolean; + let called!: boolean; const errorLink = new ErrorLink(({ graphQLErrors, networkError }) => { expect(graphQLErrors![0].message).toBe("resolver blew up"); called = true; @@ -339,46 +320,43 @@ describe("error handling with class", () => { ); const link = errorLink.concat(mockLink); + const stream = new ObservableStream(execute(link, { query })); - execute(link, { query }).subscribe((result) => { - expect(result!.errors![0].message).toBe("resolver blew up"); - expect(called).toBe(true); - resolve(); - }); + const result = await stream.takeNext(); + + expect(result!.errors![0].message).toBe("resolver blew up"); + expect(called).toBe(true); }); - itAsync( - "has an easy way to log client side (network) errors", - (resolve, reject) => { - const query = gql` - { - foo { - bar - } + + it("has an easy way to log client side (network) errors", async () => { + const query = gql` + { + foo { + bar } - `; + } + `; - let called: boolean; - const errorLink = new ErrorLink(({ networkError }) => { - expect(networkError!.message).toBe("app is crashing"); - called = true; - }); + let called!: boolean; + const errorLink = new ErrorLink(({ networkError }) => { + expect(networkError!.message).toBe("app is crashing"); + called = true; + }); - const mockLink = new ApolloLink((operation) => { - throw new Error("app is crashing"); - }); + const mockLink = new ApolloLink((operation) => { + throw new Error("app is crashing"); + }); - const link = errorLink.concat(mockLink); + const link = errorLink.concat(mockLink); + const stream = new ObservableStream(execute(link, { query })); - execute(link, { query }).subscribe({ - error: (e) => { - expect(e.message).toBe("app is crashing"); - expect(called).toBe(true); - resolve(); - }, - }); - } - ); - itAsync("captures errors within links", (resolve, reject) => { + const error = await stream.takeError(); + + expect(error.message).toBe("app is crashing"); + expect(called).toBe(true); + }); + + it("captures errors within links", async () => { const query = gql` { foo { @@ -387,7 +365,7 @@ describe("error handling with class", () => { } `; - let called: boolean; + let called!: boolean; const errorLink = new ErrorLink(({ networkError }) => { expect(networkError!.message).toBe("app is crashing"); called = true; @@ -400,16 +378,15 @@ describe("error handling with class", () => { }); const link = errorLink.concat(mockLink); + const stream = new ObservableStream(execute(link, { query })); - execute(link, { query }).subscribe({ - error: (e) => { - expect(e.message).toBe("app is crashing"); - expect(called).toBe(true); - resolve(); - }, - }); + const error = await stream.takeError(); + + expect(error.message).toBe("app is crashing"); + expect(called).toBe(true); }); - itAsync("completes if no errors", (resolve, reject) => { + + it("completes if no errors", async () => { const query = gql` { foo { @@ -428,11 +405,13 @@ describe("error handling with class", () => { const link = errorLink.concat(mockLink); - execute(link, { query }).subscribe({ - complete: resolve, - }); + const stream = new ObservableStream(execute(link, { query })); + + await expect(stream).toEmitNext(); + await expect(stream).toComplete(); }); - itAsync("can be unsubcribed", (resolve, reject) => { + + it("can be unsubcribed", async () => { const query = gql` { foo { @@ -455,16 +434,11 @@ describe("error handling with class", () => { }); const link = errorLink.concat(mockLink); + const stream = new ObservableStream(execute(link, { query })); - const sub = execute(link, { query }).subscribe({ - complete: () => { - reject("completed"); - }, - }); - - sub.unsubscribe(); + stream.unsubscribe(); - setTimeout(resolve, 10); + await expect(stream).not.toEmitAnything(); }); }); @@ -491,118 +465,92 @@ describe("support for request retrying", () => { message: "some other error", }; - itAsync( - "returns the retried request when forward(operation) is called", - (resolve, reject) => { - let errorHandlerCalled = false; - - let timesCalled = 0; - const mockHttpLink = new ApolloLink((operation) => { - if (timesCalled === 0) { - timesCalled++; - // simulate the first request being an error - return new Observable((observer) => { - observer.next(ERROR_RESPONSE as any); - observer.complete(); - }); - } else { - return new Observable((observer) => { - observer.next(GOOD_RESPONSE); - observer.complete(); - }); - } - }); + it("returns the retried request when forward(operation) is called", async () => { + let errorHandlerCalled = false; - const errorLink = new ErrorLink( - ({ graphQLErrors, response, operation, forward }) => { - try { - if (graphQLErrors) { - errorHandlerCalled = true; - expect(graphQLErrors).toEqual(ERROR_RESPONSE.errors); - expect(response!.data).not.toBeDefined(); - expect(operation.operationName).toBe("Foo"); - expect(operation.getContext().bar).toBe(true); - // retry operation if it resulted in an error - return forward(operation); - } - } catch (error) { - reject(error); - } - } - ); - - const link = errorLink.concat(mockHttpLink); - - execute(link, { query: QUERY, context: { bar: true } }).subscribe({ - next(result) { - try { - expect(errorHandlerCalled).toBe(true); - expect(result).toEqual(GOOD_RESPONSE); - } catch (error) { - return reject(error); - } - }, - complete() { - resolve(); - }, - }); - } - ); - - itAsync( - "supports retrying when the initial request had networkError", - (resolve, reject) => { - let errorHandlerCalled = false; - - let timesCalled = 0; - const mockHttpLink = new ApolloLink((operation) => { - if (timesCalled === 0) { - timesCalled++; - // simulate the first request being an error - return new Observable((observer) => { - observer.error(NETWORK_ERROR); - }); - } else { - return new Observable((observer) => { - observer.next(GOOD_RESPONSE); - observer.complete(); - }); + let timesCalled = 0; + const mockHttpLink = new ApolloLink((operation) => { + if (timesCalled === 0) { + timesCalled++; + // simulate the first request being an error + return new Observable((observer) => { + observer.next(ERROR_RESPONSE as any); + observer.complete(); + }); + } else { + return new Observable((observer) => { + observer.next(GOOD_RESPONSE); + observer.complete(); + }); + } + }); + + const errorLink = new ErrorLink( + ({ graphQLErrors, response, operation, forward }) => { + if (graphQLErrors) { + errorHandlerCalled = true; + expect(graphQLErrors).toEqual(ERROR_RESPONSE.errors); + expect(response!.data).not.toBeDefined(); + expect(operation.operationName).toBe("Foo"); + expect(operation.getContext().bar).toBe(true); + // retry operation if it resulted in an error + return forward(operation); } - }); + } + ); + + const link = errorLink.concat(mockHttpLink); + + const stream = new ObservableStream( + execute(link, { query: QUERY, context: { bar: true } }) + ); + + await expect(stream).toEmitValue(GOOD_RESPONSE); + expect(errorHandlerCalled).toBe(true); + await expect(stream).toComplete(); + }); + + it("supports retrying when the initial request had networkError", async () => { + let errorHandlerCalled = false; - const errorLink = new ErrorLink( - ({ networkError, response, operation, forward }) => { - try { - if (networkError) { - errorHandlerCalled = true; - expect(networkError).toEqual(NETWORK_ERROR); - return forward(operation); - } - } catch (error) { - reject(error); - } + let timesCalled = 0; + const mockHttpLink = new ApolloLink((operation) => { + if (timesCalled === 0) { + timesCalled++; + // simulate the first request being an error + return new Observable((observer) => { + observer.error(NETWORK_ERROR); + }); + } else { + return new Observable((observer) => { + observer.next(GOOD_RESPONSE); + observer.complete(); + }); + } + }); + + const errorLink = new ErrorLink( + ({ networkError, response, operation, forward }) => { + if (networkError) { + errorHandlerCalled = true; + expect(networkError).toEqual(NETWORK_ERROR); + return forward(operation); } - ); - - const link = errorLink.concat(mockHttpLink); - - execute(link, { query: QUERY, context: { bar: true } }).subscribe({ - next(result) { - try { - expect(errorHandlerCalled).toBe(true); - expect(result).toEqual(GOOD_RESPONSE); - } catch (error) { - return reject(error); - } - }, - complete() { - resolve(); - }, - }); - } - ); + } + ); + + const link = errorLink.concat(mockHttpLink); + + const stream = new ObservableStream( + execute(link, { query: QUERY, context: { bar: true } }) + ); + + await expect(stream).toEmitValue(GOOD_RESPONSE); + expect(errorHandlerCalled).toBe(true); + await expect(stream).toComplete(); + }); - itAsync("returns errors from retried requests", (resolve, reject) => { + it("returns errors from retried requests", async () => { let errorHandlerCalled = false; let timesCalled = 0; @@ -623,38 +571,25 @@ describe("support for request retrying", () => { const errorLink = new ErrorLink( ({ graphQLErrors, networkError, response, operation, forward }) => { - try { - if (graphQLErrors) { - errorHandlerCalled = true; - expect(graphQLErrors).toEqual(ERROR_RESPONSE.errors); - expect(response!.data).not.toBeDefined(); - expect(operation.operationName).toBe("Foo"); - expect(operation.getContext().bar).toBe(true); - // retry operation if it resulted in an error - return forward(operation); - } - } catch (error) { - reject(error); + if (graphQLErrors) { + errorHandlerCalled = true; + expect(graphQLErrors).toEqual(ERROR_RESPONSE.errors); + expect(response!.data).not.toBeDefined(); + expect(operation.operationName).toBe("Foo"); + expect(operation.getContext().bar).toBe(true); + // retry operation if it resulted in an error + return forward(operation); } } ); const link = errorLink.concat(mockHttpLink); - let observerNextCalled = false; - execute(link, { query: QUERY, context: { bar: true } }).subscribe({ - next(result) { - // should not be called - observerNextCalled = true; - }, - error(error) { - // note that complete will not be after an error - // therefore we should end the test here with resolve() - expect(errorHandlerCalled).toBe(true); - expect(observerNextCalled).toBe(false); - expect(error).toEqual(NETWORK_ERROR); - resolve(); - }, - }); + const stream = new ObservableStream( + execute(link, { query: QUERY, context: { bar: true } }) + ); + + await expect(stream).toEmitError(NETWORK_ERROR); + expect(errorHandlerCalled).toBe(true); }); }); diff --git a/src/link/http/__tests__/HttpLink.ts b/src/link/http/__tests__/HttpLink.ts index ad58e4c40c9..5d8e9a155dd 100644 --- a/src/link/http/__tests__/HttpLink.ts +++ b/src/link/http/__tests__/HttpLink.ts @@ -19,7 +19,8 @@ import { ClientParseError } from "../serializeFetchParameter"; import { ServerParseError } from "../parseAndCheckHttpResponse"; import { FetchResult, ServerError } from "../../.."; import { voidFetchDuringEachTest } from "./helpers"; -import { itAsync } from "../../../testing"; +import { wait } from "../../../testing"; +import { ObservableStream } from "../../../testing/internal"; const sampleQuery = gql` query SampleQuery { @@ -85,22 +86,6 @@ const sampleSubscriptionWithDefer = gql` } `; -function makeCallback( - resolve: () => void, - reject: (error: Error) => void, - callback: (...args: TArgs) => any -) { - return function () { - try { - // @ts-expect-error - callback.apply(this, arguments); - resolve(); - } catch (error) { - reject(error as Error); - } - } as typeof callback; -} - function convertBatchedBody(body: BodyInit | null | undefined) { return JSON.parse(body as string); } @@ -153,26 +138,18 @@ describe("HttpLink", () => { expect(() => new HttpLink()).not.toThrow(); }); - itAsync( - "constructor creates link that can call next and then complete", - (resolve, reject) => { - const next = jest.fn(); - const link = new HttpLink({ uri: "/data" }); - const observable = execute(link, { - query: sampleQuery, - }); - observable.subscribe({ - next, - error: (error) => expect(false), - complete: () => { - expect(next).toHaveBeenCalledTimes(1); - resolve(); - }, - }); - } - ); + it("constructor creates link that can call next and then complete", async () => { + const link = new HttpLink({ uri: "/data" }); + const observable = execute(link, { + query: sampleQuery, + }); + const stream = new ObservableStream(observable); - itAsync("supports using a GET request", (resolve, reject) => { + await expect(stream).toEmitNext(); + await expect(stream).toComplete(); + }); + + it("supports using a GET request", async () => { const variables = { params: "stub" }; const extensions = { myExtension: "foo" }; @@ -183,298 +160,290 @@ describe("HttpLink", () => { includeUnusedVariables: true, }); - execute(link, { query: sampleQuery, variables, extensions }).subscribe({ - next: makeCallback(resolve, reject, () => { - const [uri, options] = fetchMock.lastCall()!; - const { method, body } = options!; - expect(body).toBeUndefined(); - expect(method).toBe("GET"); - expect(uri).toBe( - "/data?query=query%20SampleQuery%20%7B%0A%20%20stub%20%7B%0A%20%20%20%20id%0A%20%20%7D%0A%7D&operationName=SampleQuery&variables=%7B%22params%22%3A%22stub%22%7D&extensions=%7B%22myExtension%22%3A%22foo%22%7D" - ); - }), - error: (error) => reject(error), + const observable = execute(link, { + query: sampleQuery, + variables, + extensions, }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + await expect(stream).toComplete(); + + const [uri, options] = fetchMock.lastCall()!; + const { method, body } = options!; + + expect(body).toBeUndefined(); + expect(method).toBe("GET"); + expect(uri).toBe( + "/data?query=query%20SampleQuery%20%7B%0A%20%20stub%20%7B%0A%20%20%20%20id%0A%20%20%7D%0A%7D&operationName=SampleQuery&variables=%7B%22params%22%3A%22stub%22%7D&extensions=%7B%22myExtension%22%3A%22foo%22%7D" + ); }); - itAsync("supports using a GET request with search", (resolve, reject) => { + it("supports using a GET request with search", async () => { const variables = { params: "stub" }; const link = createHttpLink({ uri: "/data?foo=bar", fetchOptions: { method: "GET" }, }); + const observable = execute(link, { query: sampleQuery, variables }); + const stream = new ObservableStream(observable); - execute(link, { query: sampleQuery, variables }).subscribe({ - next: makeCallback(resolve, reject, () => { - const [uri, options] = fetchMock.lastCall()!; - const { method, body } = options!; - expect(body).toBeUndefined(); - expect(method).toBe("GET"); - expect(uri).toBe( - "/data?foo=bar&query=query%20SampleQuery%20%7B%0A%20%20stub%20%7B%0A%20%20%20%20id%0A%20%20%7D%0A%7D&operationName=SampleQuery&variables=%7B%7D" - ); - }), - error: (error) => reject(error), - }); + await expect(stream).toEmitNext(); + await expect(stream).toComplete(); + + const [uri, options] = fetchMock.lastCall()!; + const { method, body } = options!; + + expect(body).toBeUndefined(); + expect(method).toBe("GET"); + expect(uri).toBe( + "/data?foo=bar&query=query%20SampleQuery%20%7B%0A%20%20stub%20%7B%0A%20%20%20%20id%0A%20%20%7D%0A%7D&operationName=SampleQuery&variables=%7B%7D" + ); }); - itAsync( - "supports using a GET request on the context", - (resolve, reject) => { - const variables = { params: "stub" }; - const link = createHttpLink({ - uri: "/data", - }); + it("supports using a GET request on the context", async () => { + const variables = { params: "stub" }; + const link = createHttpLink({ + uri: "/data", + }); - execute(link, { - query: sampleQuery, - variables, - context: { - fetchOptions: { method: "GET" }, - }, - }).subscribe( - makeCallback(resolve, reject, () => { - const [uri, options] = fetchMock.lastCall()!; - const { method, body } = options!; - expect(body).toBeUndefined(); - expect(method).toBe("GET"); - expect(uri).toBe( - "/data?query=query%20SampleQuery%20%7B%0A%20%20stub%20%7B%0A%20%20%20%20id%0A%20%20%7D%0A%7D&operationName=SampleQuery&variables=%7B%7D" - ); - }) - ); - } - ); + const observable = execute(link, { + query: sampleQuery, + variables, + context: { + fetchOptions: { method: "GET" }, + }, + }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + await expect(stream).toComplete(); + + const [uri, options] = fetchMock.lastCall()!; + const { method, body } = options!; + + expect(body).toBeUndefined(); + expect(method).toBe("GET"); + expect(uri).toBe( + "/data?query=query%20SampleQuery%20%7B%0A%20%20stub%20%7B%0A%20%20%20%20id%0A%20%20%7D%0A%7D&operationName=SampleQuery&variables=%7B%7D" + ); + }); - itAsync("uses GET with useGETForQueries", (resolve, reject) => { + it("uses GET with useGETForQueries", async () => { const variables = { params: "stub" }; const link = createHttpLink({ uri: "/data", useGETForQueries: true, }); - execute(link, { + const observable = execute(link, { query: sampleQuery, variables, - }).subscribe( - makeCallback(resolve, reject, () => { - const [uri, options] = fetchMock.lastCall()!; - const { method, body } = options!; - expect(body).toBeUndefined(); - expect(method).toBe("GET"); - expect(uri).toBe( - "/data?query=query%20SampleQuery%20%7B%0A%20%20stub%20%7B%0A%20%20%20%20id%0A%20%20%7D%0A%7D&operationName=SampleQuery&variables=%7B%7D" - ); - }) + }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + await expect(stream).toComplete(); + + const [uri, options] = fetchMock.lastCall()!; + const { method, body } = options!; + expect(body).toBeUndefined(); + expect(method).toBe("GET"); + expect(uri).toBe( + "/data?query=query%20SampleQuery%20%7B%0A%20%20stub%20%7B%0A%20%20%20%20id%0A%20%20%7D%0A%7D&operationName=SampleQuery&variables=%7B%7D" ); }); - itAsync( - "uses POST for mutations with useGETForQueries", - (resolve, reject) => { - const variables = { params: "stub" }; - const link = createHttpLink({ - uri: "/data", - useGETForQueries: true, - }); + it("uses POST for mutations with useGETForQueries", async () => { + const variables = { params: "stub" }; + const link = createHttpLink({ + uri: "/data", + useGETForQueries: true, + }); - execute(link, { - query: sampleMutation, - variables, - }).subscribe( - makeCallback(resolve, reject, () => { - const [uri, options] = fetchMock.lastCall()!; - const { method, body } = options!; - expect(body).toBeDefined(); - expect(method).toBe("POST"); - expect(uri).toBe("/data"); - }) - ); - } - ); - - itAsync( - "strips unused variables, respecting nested fragments", - (resolve, reject) => { - const link = createHttpLink({ uri: "/data" }); - - const query = gql` - query PEOPLE($declaredAndUsed: String, $declaredButUnused: Int) { - people(surprise: $undeclared, noSurprise: $declaredAndUsed) { - ... on Doctor { - specialty(var: $usedByInlineFragment) - } - ...LawyerFragment + const observable = execute(link, { + query: sampleMutation, + variables, + }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + await expect(stream).toComplete(); + + const [uri, options] = fetchMock.lastCall()!; + const { method, body } = options!; + + expect(body).toBeDefined(); + expect(method).toBe("POST"); + expect(uri).toBe("/data"); + }); + + it("strips unused variables, respecting nested fragments", async () => { + const link = createHttpLink({ uri: "/data" }); + + const query = gql` + query PEOPLE($declaredAndUsed: String, $declaredButUnused: Int) { + people(surprise: $undeclared, noSurprise: $declaredAndUsed) { + ... on Doctor { + specialty(var: $usedByInlineFragment) } + ...LawyerFragment } - fragment LawyerFragment on Lawyer { - caseCount(var: $usedByNamedFragment) - } - `; + } + fragment LawyerFragment on Lawyer { + caseCount(var: $usedByNamedFragment) + } + `; + + const variables = { + unused: "strip", + declaredButUnused: "strip", + declaredAndUsed: "keep", + undeclared: "keep", + usedByInlineFragment: "keep", + usedByNamedFragment: "keep", + }; + + const observable = execute(link, { + query, + variables, + }); + const stream = new ObservableStream(observable); - const variables = { - unused: "strip", - declaredButUnused: "strip", + await expect(stream).toEmitNext(); + await expect(stream).toComplete(); + + const [uri, options] = fetchMock.lastCall()!; + const { method, body } = options!; + + expect(JSON.parse(body as string)).toEqual({ + operationName: "PEOPLE", + query: print(query), + variables: { declaredAndUsed: "keep", undeclared: "keep", usedByInlineFragment: "keep", usedByNamedFragment: "keep", - }; - - execute(link, { - query, - variables, - }).subscribe({ - next: makeCallback(resolve, reject, () => { - const [uri, options] = fetchMock.lastCall()!; - const { method, body } = options!; - expect(JSON.parse(body as string)).toEqual({ - operationName: "PEOPLE", - query: print(query), - variables: { - declaredAndUsed: "keep", - undeclared: "keep", - usedByInlineFragment: "keep", - usedByNamedFragment: "keep", - }, - }); - expect(method).toBe("POST"); - expect(uri).toBe("/data"); - }), - error: (error) => reject(error), - }); - } - ); + }, + }); + expect(method).toBe("POST"); + expect(uri).toBe("/data"); + }); - itAsync( - "should add client awareness settings to request headers", - (resolve, reject) => { - const variables = { params: "stub" }; - const link = createHttpLink({ - uri: "/data", - }); + it("should add client awareness settings to request headers", async () => { + const variables = { params: "stub" }; + const link = createHttpLink({ + uri: "/data", + }); - const clientAwareness = { - name: "Some Client Name", - version: "1.0.1", - }; + const clientAwareness = { + name: "Some Client Name", + version: "1.0.1", + }; - execute(link, { - query: sampleQuery, - variables, - context: { - clientAwareness, - }, - }).subscribe( - makeCallback(resolve, reject, () => { - const [, options] = fetchMock.lastCall()!; - const { headers } = options as any; - expect(headers["apollographql-client-name"]).toBeDefined(); - expect(headers["apollographql-client-name"]).toEqual( - clientAwareness.name - ); - expect(headers["apollographql-client-version"]).toBeDefined(); - expect(headers["apollographql-client-version"]).toEqual( - clientAwareness.version - ); - }) - ); - } - ); + const observable = execute(link, { + query: sampleQuery, + variables, + context: { + clientAwareness, + }, + }); + const stream = new ObservableStream(observable); - itAsync( - "should not add empty client awareness settings to request headers", - (resolve, reject) => { - const variables = { params: "stub" }; - const link = createHttpLink({ - uri: "/data", - }); + await expect(stream).toEmitNext(); + await expect(stream).toComplete(); - const hasOwn = Object.prototype.hasOwnProperty; - const clientAwareness = {}; - execute(link, { - query: sampleQuery, - variables, - context: { - clientAwareness, - }, - }).subscribe( - makeCallback(resolve, reject, () => { - const [, options] = fetchMock.lastCall()!; - const { headers } = options as any; - expect(hasOwn.call(headers, "apollographql-client-name")).toBe( - false - ); - expect(hasOwn.call(headers, "apollographql-client-version")).toBe( - false - ); - }) - ); - } - ); + const [, options] = fetchMock.lastCall()!; + const { headers } = options as any; + expect(headers["apollographql-client-name"]).toBeDefined(); + expect(headers["apollographql-client-name"]).toEqual( + clientAwareness.name + ); + expect(headers["apollographql-client-version"]).toBeDefined(); + expect(headers["apollographql-client-version"]).toEqual( + clientAwareness.version + ); + }); - itAsync( - "throws for GET if the variables can't be stringified", - (resolve, reject) => { - const link = createHttpLink({ - uri: "/data", - useGETForQueries: true, - includeUnusedVariables: true, - }); + it("should not add empty client awareness settings to request headers", async () => { + const variables = { params: "stub" }; + const link = createHttpLink({ + uri: "/data", + }); - let b; - const a: any = { b }; - b = { a }; - a.b = b; - const variables = { - a, - b, - }; - execute(link, { query: sampleQuery, variables }).subscribe( - (result) => { - reject("next should have been thrown from the link"); - }, - makeCallback(resolve, reject, (e: ClientParseError) => { - expect(e.message).toMatch(/Variables map is not serializable/); - expect(e.parseError.message).toMatch( - /Converting circular structure to JSON/ - ); - }) - ); - } - ); + const hasOwn = Object.prototype.hasOwnProperty; + const clientAwareness = {}; + const observable = execute(link, { + query: sampleQuery, + variables, + context: { + clientAwareness, + }, + }); + const stream = new ObservableStream(observable); - itAsync( - "throws for GET if the extensions can't be stringified", - (resolve, reject) => { - const link = createHttpLink({ - uri: "/data", - useGETForQueries: true, - includeExtensions: true, - }); + await expect(stream).toEmitNext(); + await expect(stream).toComplete(); - let b; - const a: any = { b }; - b = { a }; - a.b = b; - const extensions = { - a, - b, - }; - execute(link, { query: sampleQuery, extensions }).subscribe( - (result) => { - reject("next should have been thrown from the link"); - }, - makeCallback(resolve, reject, (e: ClientParseError) => { - expect(e.message).toMatch(/Extensions map is not serializable/); - expect(e.parseError.message).toMatch( - /Converting circular structure to JSON/ - ); - }) - ); - } - ); + const [, options] = fetchMock.lastCall()!; + const { headers } = options as any; + expect(hasOwn.call(headers, "apollographql-client-name")).toBe(false); + expect(hasOwn.call(headers, "apollographql-client-version")).toBe(false); + }); + + it("throws for GET if the variables can't be stringified", async () => { + const link = createHttpLink({ + uri: "/data", + useGETForQueries: true, + includeUnusedVariables: true, + }); + + let b; + const a: any = { b }; + b = { a }; + a.b = b; + const variables = { + a, + b, + }; + const observable = execute(link, { query: sampleQuery, variables }); + const stream = new ObservableStream(observable); + + const error = await stream.takeError(); + + expect(error.message).toMatch(/Variables map is not serializable/); + expect(error.parseError.message).toMatch( + /Converting circular structure to JSON/ + ); + }); + + it("throws for GET if the extensions can't be stringified", async () => { + const link = createHttpLink({ + uri: "/data", + useGETForQueries: true, + includeExtensions: true, + }); + + let b; + const a: any = { b }; + b = { a }; + a.b = b; + const extensions = { + a, + b, + }; + const observable = execute(link, { query: sampleQuery, extensions }); + const stream = new ObservableStream(observable); + + const error = await stream.takeError(); + + expect(error.message).toMatch(/Extensions map is not serializable/); + expect(error.parseError.message).toMatch( + /Converting circular structure to JSON/ + ); + }); it("raises warning if called with concat", () => { const link = createHttpLink(); @@ -494,71 +463,65 @@ describe("HttpLink", () => { expect(() => createHttpLink()).not.toThrow(); }); - itAsync("calls next and then complete", (resolve, reject) => { - const next = jest.fn(); + it("calls next and then complete", async () => { const link = createHttpLink({ uri: "data" }); const observable = execute(link, { query: sampleQuery, }); - observable.subscribe({ - next, - error: (error) => reject(error), - complete: makeCallback(resolve, reject, () => { - expect(next).toHaveBeenCalledTimes(1); - }), - }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + await expect(stream).toComplete(); }); - itAsync("calls error when fetch fails", (resolve, reject) => { + it("calls error when fetch fails", async () => { const link = createHttpLink({ uri: "error" }); const observable = execute(link, { query: sampleQuery, }); - observable.subscribe( - (result) => reject("next should not have been called"), - makeCallback(resolve, reject, (error: TypeError) => { - expect(error).toEqual(mockError.throws); - }), - () => reject("complete should not have been called") - ); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitError(mockError.throws); }); - itAsync("calls error when fetch fails", (resolve, reject) => { + it("calls error when fetch fails", async () => { const link = createHttpLink({ uri: "error" }); const observable = execute(link, { query: sampleMutation, }); - observable.subscribe( - (result) => reject("next should not have been called"), - makeCallback(resolve, reject, (error: TypeError) => { - expect(error).toEqual(mockError.throws); - }), - () => reject("complete should not have been called") - ); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitError(mockError.throws); }); - itAsync("unsubscribes without calling subscriber", (resolve, reject) => { + it("unsubscribes without calling subscriber", async () => { const link = createHttpLink({ uri: "data" }); const observable = execute(link, { query: sampleQuery, }); const subscription = observable.subscribe( - (result) => reject("next should not have been called"), - (error) => reject(error), - () => reject("complete should not have been called") + () => { + throw new Error("next should not have been called"); + }, + (error) => { + throw error; + }, + () => { + throw "complete should not have been called"; + } ); subscription.unsubscribe(); + expect(subscription.closed).toBe(true); - setTimeout(resolve, 50); + + // Ensure none of the callbacks throw after our assertion + await wait(10); }); - const verifyRequest = ( + const verifyRequest = async ( link: ApolloLink, - resolve: () => void, - includeExtensions: boolean, - reject: (error: any) => any + includeExtensions: boolean ) => { - const next = jest.fn(); const context = { info: "stub" }; const variables = { params: "stub" }; @@ -567,57 +530,37 @@ describe("HttpLink", () => { context, variables, }); - observable.subscribe({ - next, - error: (error) => reject(error), - complete: () => { - try { - let body = convertBatchedBody(fetchMock.lastCall()![1]!.body); - expect(body.query).toBe(print(sampleMutation)); - expect(body.variables).toEqual({}); - expect(body.context).not.toBeDefined(); - if (includeExtensions) { - expect(body.extensions).toBeDefined(); - } else { - expect(body.extensions).not.toBeDefined(); - } - expect(next).toHaveBeenCalledTimes(1); - - resolve(); - } catch (e) { - reject(e); - } - }, - }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + await expect(stream).toComplete(); + + let body = convertBatchedBody(fetchMock.lastCall()![1]!.body); + expect(body.query).toBe(print(sampleMutation)); + expect(body.variables).toEqual({}); + expect(body.context).not.toBeDefined(); + if (includeExtensions) { + expect(body.extensions).toBeDefined(); + } else { + expect(body.extensions).not.toBeDefined(); + } }; - itAsync( - "passes all arguments to multiple fetch body including extensions", - (resolve, reject) => { - const link = createHttpLink({ uri: "data", includeExtensions: true }); - verifyRequest( - link, - () => verifyRequest(link, resolve, true, reject), - true, - reject - ); - } - ); + it("passes all arguments to multiple fetch body including extensions", async () => { + const link = createHttpLink({ uri: "data", includeExtensions: true }); - itAsync( - "passes all arguments to multiple fetch body excluding extensions", - (resolve, reject) => { - const link = createHttpLink({ uri: "data" }); - verifyRequest( - link, - () => verifyRequest(link, resolve, false, reject), - false, - reject - ); - } - ); + await verifyRequest(link, true); + await verifyRequest(link, true); + }); + + it("passes all arguments to multiple fetch body excluding extensions", async () => { + const link = createHttpLink({ uri: "data" }); + + await verifyRequest(link, false); + await verifyRequest(link, false); + }); - itAsync("calls multiple subscribers", (resolve, reject) => { + it("calls multiple subscribers", async () => { const link = createHttpLink({ uri: "data" }); const context = { info: "stub" }; const variables = { params: "stub" }; @@ -630,159 +573,144 @@ describe("HttpLink", () => { observable.subscribe(subscriber); observable.subscribe(subscriber); - setTimeout(() => { - expect(subscriber.next).toHaveBeenCalledTimes(2); - expect(subscriber.complete).toHaveBeenCalledTimes(2); - expect(subscriber.error).not.toHaveBeenCalled(); - expect(fetchMock.calls().length).toBe(2); - resolve(); - }, 50); - }); - - itAsync( - "calls remaining subscribers after unsubscribe", - (resolve, reject) => { - const link = createHttpLink({ uri: "data" }); - const context = { info: "stub" }; - const variables = { params: "stub" }; - - const observable = execute(link, { - query: sampleMutation, - context, - variables, - }); + await wait(50); - observable.subscribe(subscriber); + expect(subscriber.next).toHaveBeenCalledTimes(2); + expect(subscriber.complete).toHaveBeenCalledTimes(2); + expect(subscriber.error).not.toHaveBeenCalled(); + expect(fetchMock.calls().length).toBe(2); + }); - setTimeout(() => { - const subscription = observable.subscribe(subscriber); - subscription.unsubscribe(); - }, 10); + it("calls remaining subscribers after unsubscribe", async () => { + const link = createHttpLink({ uri: "data" }); + const context = { info: "stub" }; + const variables = { params: "stub" }; - setTimeout( - makeCallback(resolve, reject, () => { - expect(subscriber.next).toHaveBeenCalledTimes(1); - expect(subscriber.complete).toHaveBeenCalledTimes(1); - expect(subscriber.error).not.toHaveBeenCalled(); - resolve(); - }), - 50 - ); - } - ); + const observable = execute(link, { + query: sampleMutation, + context, + variables, + }); + + observable.subscribe(subscriber); + + await wait(10); + + const subscription = observable.subscribe(subscriber); + subscription.unsubscribe(); + + await wait(50); - itAsync("allows for dynamic endpoint setting", (resolve, reject) => { + expect(subscriber.next).toHaveBeenCalledTimes(1); + expect(subscriber.complete).toHaveBeenCalledTimes(1); + expect(subscriber.error).not.toHaveBeenCalled(); + }); + + it("allows for dynamic endpoint setting", async () => { const variables = { params: "stub" }; const link = createHttpLink({ uri: "data" }); - execute(link, { + const observable = execute(link, { query: sampleQuery, variables, context: { uri: "data2" }, - }).subscribe((result) => { - expect(result).toEqual(data2); - resolve(); }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue(data2); }); - itAsync( - "adds headers to the request from the context", - (resolve, reject) => { - const variables = { params: "stub" }; - const middleware = new ApolloLink((operation, forward) => { - operation.setContext({ - headers: { authorization: "1234" }, - }); - return forward(operation).map((result) => { - const { headers } = operation.getContext(); - try { - expect(headers).toBeDefined(); - } catch (e) { - reject(e); - } - return result; - }); + it("adds headers to the request from the context", async () => { + const variables = { params: "stub" }; + const middleware = new ApolloLink((operation, forward) => { + operation.setContext({ + headers: { authorization: "1234" }, }); - const link = middleware.concat(createHttpLink({ uri: "data" })); - - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, () => { - const headers = fetchMock.lastCall()![1]!.headers as any; - expect(headers.authorization).toBe("1234"); - expect(headers["content-type"]).toBe("application/json"); - expect(headers.accept).toBe("*/*"); - }) - ); - } - ); + return forward(operation).map((result) => { + const { headers } = operation.getContext(); + expect(headers).toBeDefined(); - itAsync("adds headers to the request from the setup", (resolve, reject) => { + return result; + }); + }); + const link = middleware.concat(createHttpLink({ uri: "data" })); + + const observable = execute(link, { query: sampleQuery, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + + const headers = fetchMock.lastCall()![1]!.headers as any; + + expect(headers.authorization).toBe("1234"); + expect(headers["content-type"]).toBe("application/json"); + expect(headers.accept).toBe("*/*"); + }); + + it("adds headers to the request from the setup", async () => { const variables = { params: "stub" }; const link = createHttpLink({ uri: "data", headers: { authorization: "1234" }, }); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, () => { - const headers = fetchMock.lastCall()![1]!.headers as any; - expect(headers.authorization).toBe("1234"); - expect(headers["content-type"]).toBe("application/json"); - expect(headers.accept).toBe("*/*"); - }) - ); + const observable = execute(link, { query: sampleQuery, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + + const headers = fetchMock.lastCall()![1]!.headers as any; + expect(headers.authorization).toBe("1234"); + expect(headers["content-type"]).toBe("application/json"); + expect(headers.accept).toBe("*/*"); }); - itAsync( - "prioritizes context headers over setup headers", - (resolve, reject) => { - const variables = { params: "stub" }; - const middleware = new ApolloLink((operation, forward) => { - operation.setContext({ - headers: { authorization: "1234" }, - }); - return forward(operation); + it("prioritizes context headers over setup headers", async () => { + const variables = { params: "stub" }; + const middleware = new ApolloLink((operation, forward) => { + operation.setContext({ + headers: { authorization: "1234" }, }); - const link = middleware.concat( - createHttpLink({ uri: "data", headers: { authorization: "no user" } }) - ); + return forward(operation); + }); + const link = middleware.concat( + createHttpLink({ uri: "data", headers: { authorization: "no user" } }) + ); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, () => { - const headers = fetchMock.lastCall()![1]!.headers as any; - expect(headers.authorization).toBe("1234"); - expect(headers["content-type"]).toBe("application/json"); - expect(headers.accept).toBe("*/*"); - }) - ); - } - ); + const observable = execute(link, { query: sampleQuery, variables }); + const stream = new ObservableStream(observable); - itAsync( - "adds headers to the request from the context on an operation", - (resolve, reject) => { - const variables = { params: "stub" }; - const link = createHttpLink({ uri: "data" }); + await expect(stream).toEmitNext(); - const context = { - headers: { authorization: "1234" }, - }; - execute(link, { - query: sampleQuery, - variables, - context, - }).subscribe( - makeCallback(resolve, reject, () => { - const headers = fetchMock.lastCall()![1]!.headers as any; - expect(headers.authorization).toBe("1234"); - expect(headers["content-type"]).toBe("application/json"); - expect(headers.accept).toBe("*/*"); - }) - ); - } - ); + const headers = fetchMock.lastCall()![1]!.headers as any; + expect(headers.authorization).toBe("1234"); + expect(headers["content-type"]).toBe("application/json"); + expect(headers.accept).toBe("*/*"); + }); - itAsync("adds creds to the request from the context", (resolve, reject) => { + it("adds headers to the request from the context on an operation", async () => { + const variables = { params: "stub" }; + const link = createHttpLink({ uri: "data" }); + + const context = { + headers: { authorization: "1234" }, + }; + const observable = execute(link, { + query: sampleQuery, + variables, + context, + }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + + const headers = fetchMock.lastCall()![1]!.headers as any; + expect(headers.authorization).toBe("1234"); + expect(headers["content-type"]).toBe("application/json"); + expect(headers.accept).toBe("*/*"); + }); + + it("adds creds to the request from the context", async () => { const variables = { params: "stub" }; const middleware = new ApolloLink((operation, forward) => { operation.setContext({ @@ -792,50 +720,50 @@ describe("HttpLink", () => { }); const link = middleware.concat(createHttpLink({ uri: "data" })); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, () => { - const creds = fetchMock.lastCall()![1]!.credentials; - expect(creds).toBe("same-team-yo"); - }) - ); + const observable = execute(link, { query: sampleQuery, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + + const creds = fetchMock.lastCall()![1]!.credentials; + expect(creds).toBe("same-team-yo"); }); - itAsync("adds creds to the request from the setup", (resolve, reject) => { + it("adds creds to the request from the setup", async () => { const variables = { params: "stub" }; const link = createHttpLink({ uri: "data", credentials: "same-team-yo" }); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, () => { - const creds = fetchMock.lastCall()![1]!.credentials; - expect(creds).toBe("same-team-yo"); - }) - ); + const observable = execute(link, { query: sampleQuery, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + + const creds = fetchMock.lastCall()![1]!.credentials; + expect(creds).toBe("same-team-yo"); }); - itAsync( - "prioritizes creds from the context over the setup", - (resolve, reject) => { - const variables = { params: "stub" }; - const middleware = new ApolloLink((operation, forward) => { - operation.setContext({ - credentials: "same-team-yo", - }); - return forward(operation); + it("prioritizes creds from the context over the setup", async () => { + const variables = { params: "stub" }; + const middleware = new ApolloLink((operation, forward) => { + operation.setContext({ + credentials: "same-team-yo", }); - const link = middleware.concat( - createHttpLink({ uri: "data", credentials: "error" }) - ); + return forward(operation); + }); + const link = middleware.concat( + createHttpLink({ uri: "data", credentials: "error" }) + ); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, () => { - const creds = fetchMock.lastCall()![1]!.credentials; - expect(creds).toBe("same-team-yo"); - }) - ); - } - ); + const observable = execute(link, { query: sampleQuery, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); - itAsync("adds uri to the request from the context", (resolve, reject) => { + const creds = fetchMock.lastCall()![1]!.credentials; + expect(creds).toBe("same-team-yo"); + }); + + it("adds uri to the request from the context", async () => { const variables = { params: "stub" }; const middleware = new ApolloLink((operation, forward) => { operation.setContext({ @@ -845,27 +773,29 @@ describe("HttpLink", () => { }); const link = middleware.concat(createHttpLink()); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, () => { - const uri = fetchMock.lastUrl(); - expect(uri).toBe("/data"); - }) - ); + const observable = execute(link, { query: sampleQuery, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + + const uri = fetchMock.lastUrl(); + expect(uri).toBe("/data"); }); - itAsync("adds uri to the request from the setup", (resolve, reject) => { + it("adds uri to the request from the setup", async () => { const variables = { params: "stub" }; const link = createHttpLink({ uri: "data" }); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, () => { - const uri = fetchMock.lastUrl(); - expect(uri).toBe("/data"); - }) - ); + const observable = execute(link, { query: sampleQuery, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + + const uri = fetchMock.lastUrl(); + expect(uri).toBe("/data"); }); - itAsync("prioritizes context uri over setup uri", (resolve, reject) => { + it("prioritizes context uri over setup uri", async () => { const variables = { params: "stub" }; const middleware = new ApolloLink((operation, forward) => { operation.setContext({ @@ -877,168 +807,139 @@ describe("HttpLink", () => { createHttpLink({ uri: "data", credentials: "error" }) ); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, () => { - const uri = fetchMock.lastUrl(); + const observable = execute(link, { query: sampleQuery, variables }); + const stream = new ObservableStream(observable); - expect(uri).toBe("/apollo"); - }) - ); + await expect(stream).toEmitNext(); + + const uri = fetchMock.lastUrl(); + expect(uri).toBe("/apollo"); }); - itAsync("allows uri to be a function", (resolve, reject) => { + it("allows uri to be a function", async () => { const variables = { params: "stub" }; const customFetch: typeof fetch = (uri, options) => { const { operationName } = convertBatchedBody(options!.body); - try { - expect(operationName).toBe("SampleQuery"); - } catch (e) { - reject(e); - } + expect(operationName).toBe("SampleQuery"); + return fetch("dataFunc", options); }; const link = createHttpLink({ fetch: customFetch }); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, () => { - expect(fetchMock.lastUrl()).toBe("/dataFunc"); - }) - ); + const observable = execute(link, { query: sampleQuery, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + + expect(fetchMock.lastUrl()).toBe("/dataFunc"); }); - itAsync( - "adds fetchOptions to the request from the setup", - (resolve, reject) => { - const variables = { params: "stub" }; - const link = createHttpLink({ - uri: "data", - fetchOptions: { someOption: "foo", mode: "no-cors" }, - }); + it("adds fetchOptions to the request from the setup", async () => { + const variables = { params: "stub" }; + const link = createHttpLink({ + uri: "data", + fetchOptions: { someOption: "foo", mode: "no-cors" }, + }); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, () => { - const { someOption, mode, headers } = - fetchMock.lastCall()![1] as any; - expect(someOption).toBe("foo"); - expect(mode).toBe("no-cors"); - expect(headers["content-type"]).toBe("application/json"); - }) - ); - } - ); - - itAsync( - "adds fetchOptions to the request from the context", - (resolve, reject) => { - const variables = { params: "stub" }; - const middleware = new ApolloLink((operation, forward) => { - operation.setContext({ - fetchOptions: { - someOption: "foo", - }, - }); - return forward(operation); + const observable = execute(link, { query: sampleQuery, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + + const { someOption, mode, headers } = fetchMock.lastCall()![1] as any; + expect(someOption).toBe("foo"); + expect(mode).toBe("no-cors"); + expect(headers["content-type"]).toBe("application/json"); + }); + + it("adds fetchOptions to the request from the context", async () => { + const variables = { params: "stub" }; + const middleware = new ApolloLink((operation, forward) => { + operation.setContext({ + fetchOptions: { + someOption: "foo", + }, }); - const link = middleware.concat(createHttpLink({ uri: "data" })); + return forward(operation); + }); + const link = middleware.concat(createHttpLink({ uri: "data" })); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, () => { - const { someOption } = fetchMock.lastCall()![1] as any; - expect(someOption).toBe("foo"); - resolve(); - }) - ); - } - ); - - itAsync( - "uses the latest window.fetch function if options.fetch not configured", - (resolve, reject) => { - const httpLink = createHttpLink({ uri: "data" }); - - const fetch = window.fetch; - expect(typeof fetch).toBe("function"); - - const fetchSpy = jest.spyOn(window, "fetch"); - fetchSpy.mockImplementation(() => - Promise.resolve({ - text() { - return Promise.resolve( - JSON.stringify({ - data: { hello: "from spy" }, - }) - ); - }, - } as Response) - ); + const observable = execute(link, { query: sampleQuery, variables }); + const stream = new ObservableStream(observable); - const spyFn = window.fetch; - expect(spyFn).not.toBe(fetch); + await expect(stream).toEmitNext(); - subscriptions.add( - execute(httpLink, { - query: sampleQuery, - }).subscribe({ - error: reject, + const { someOption } = fetchMock.lastCall()![1] as any; + expect(someOption).toBe("foo"); + }); - next(result) { - expect(fetchSpy).toHaveBeenCalledTimes(1); - expect(result).toEqual({ - data: { hello: "from spy" }, - }); - - fetchSpy.mockRestore(); - expect(window.fetch).toBe(fetch); - - subscriptions.add( - execute(httpLink, { - query: sampleQuery, - }).subscribe({ - error: reject, - next(result) { - expect(result).toEqual({ - data: { hello: "world" }, - }); - resolve(); - }, - }) - ); - }, - }) - ); - } - ); - - itAsync( - "uses the print option function when defined", - (resolve, reject) => { - const customPrinter = jest.fn( - (ast: ASTNode, originalPrint: typeof print) => { - return stripIgnoredCharacters(originalPrint(ast)); - } - ); + it("uses the latest window.fetch function if options.fetch not configured", async () => { + const httpLink = createHttpLink({ uri: "data" }); - const httpLink = createHttpLink({ uri: "data", print: customPrinter }); + const fetch = window.fetch; + expect(typeof fetch).toBe("function"); - execute(httpLink, { - query: sampleQuery, - context: { - fetchOptions: { method: "GET" }, - }, - }).subscribe( - makeCallback(resolve, reject, () => { - expect(customPrinter).toHaveBeenCalledTimes(1); - const [uri] = fetchMock.lastCall()!; - expect(uri).toBe( - "/data?query=query%20SampleQuery%7Bstub%7Bid%7D%7D&operationName=SampleQuery&variables=%7B%7D" + const fetchSpy = jest.spyOn(window, "fetch"); + fetchSpy.mockImplementation(() => + Promise.resolve({ + text() { + return Promise.resolve( + JSON.stringify({ + data: { hello: "from spy" }, + }) ); - }) - ); - } - ); + }, + } as Response) + ); + + const spyFn = window.fetch; + expect(spyFn).not.toBe(fetch); + + const stream = new ObservableStream( + execute(httpLink, { query: sampleQuery }) + ); + + await expect(stream).toEmitValue({ data: { hello: "from spy" } }); + expect(fetchSpy).toHaveBeenCalledTimes(1); + + fetchSpy.mockRestore(); + expect(window.fetch).toBe(fetch); + + const stream2 = new ObservableStream( + execute(httpLink, { query: sampleQuery }) + ); + + await expect(stream2).toEmitValue({ data: { hello: "world" } }); + }); + + it("uses the print option function when defined", async () => { + const customPrinter = jest.fn( + (ast: ASTNode, originalPrint: typeof print) => { + return stripIgnoredCharacters(originalPrint(ast)); + } + ); + + const httpLink = createHttpLink({ uri: "data", print: customPrinter }); + + const observable = execute(httpLink, { + query: sampleQuery, + context: { + fetchOptions: { method: "GET" }, + }, + }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); - itAsync("prioritizes context over setup", (resolve, reject) => { + expect(customPrinter).toHaveBeenCalledTimes(1); + const [uri] = fetchMock.lastCall()!; + expect(uri).toBe( + "/data?query=query%20SampleQuery%7Bstub%7Bid%7D%7D&operationName=SampleQuery&variables=%7B%7D" + ); + }); + + it("prioritizes context over setup", async () => { const variables = { params: "stub" }; const middleware = new ApolloLink((operation, forward) => { operation.setContext({ @@ -1052,55 +953,53 @@ describe("HttpLink", () => { createHttpLink({ uri: "data", fetchOptions: { someOption: "bar" } }) ); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, () => { - const { someOption } = fetchMock.lastCall()![1] as any; - expect(someOption).toBe("foo"); - }) - ); + const observable = execute(link, { query: sampleQuery, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + + const { someOption } = fetchMock.lastCall()![1] as any; + expect(someOption).toBe("foo"); }); - itAsync( - "allows for not sending the query with the request", - (resolve, reject) => { - const variables = { params: "stub" }; - const middleware = new ApolloLink((operation, forward) => { - operation.setContext({ - http: { - includeQuery: false, - includeExtensions: true, - }, - }); - operation.extensions.persistedQuery = { hash: "1234" }; - return forward(operation); + it("allows for not sending the query with the request", async () => { + const variables = { params: "stub" }; + const middleware = new ApolloLink((operation, forward) => { + operation.setContext({ + http: { + includeQuery: false, + includeExtensions: true, + }, }); - const link = middleware.concat(createHttpLink({ uri: "data" })); + operation.extensions.persistedQuery = { hash: "1234" }; + return forward(operation); + }); + const link = middleware.concat(createHttpLink({ uri: "data" })); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, () => { - let body = convertBatchedBody(fetchMock.lastCall()![1]!.body); + const observable = execute(link, { query: sampleQuery, variables }); + const stream = new ObservableStream(observable); - expect(body.query).not.toBeDefined(); - expect(body.extensions).toEqual({ - persistedQuery: { hash: "1234" }, - }); - resolve(); - }) - ); - } - ); + await expect(stream).toEmitNext(); + + let body = convertBatchedBody(fetchMock.lastCall()![1]!.body); - itAsync("sets the raw response on context", (resolve, reject) => { + expect(body.query).not.toBeDefined(); + expect(body.extensions).toEqual({ + persistedQuery: { hash: "1234" }, + }); + }); + + it("sets the raw response on context", async () => { const middleware = new ApolloLink((operation, forward) => { return new Observable((ob) => { const op = forward(operation); const sub = op.subscribe({ next: ob.next.bind(ob), error: ob.error.bind(ob), - complete: makeCallback(resolve, reject, () => { + complete: () => { expect(operation.getContext().response.headers.toBeDefined); ob.complete(); - }), + }, }); return () => { @@ -1111,12 +1010,11 @@ describe("HttpLink", () => { const link = middleware.concat(createHttpLink({ uri: "data", fetch })); - execute(link, { query: sampleQuery }).subscribe( - (result) => { - resolve(); - }, - () => {} - ); + const observable = execute(link, { query: sampleQuery }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + await expect(stream).toComplete(); }); it("removes @client fields from the query before sending it to the server", async () => { @@ -1195,26 +1093,22 @@ describe("HttpLink", () => { describe("Dev warnings", () => { voidFetchDuringEachTest(); - itAsync("warns if fetch is undeclared", (resolve, reject) => { + it("warns if fetch is undeclared", async () => { try { createHttpLink({ uri: "data" }); - reject("warning wasn't called"); + throw new Error("warning wasn't called"); } catch (e) { - makeCallback(resolve, reject, () => - expect((e as Error).message).toMatch(/has not been found globally/) - )(); + expect((e as Error).message).toMatch(/has not been found globally/); } }); - itAsync("warns if fetch is undefined", (resolve, reject) => { + it("warns if fetch is undefined", async () => { window.fetch = undefined as any; try { createHttpLink({ uri: "data" }); - reject("warning wasn't called"); + throw new Error("warning wasn't called"); } catch (e) { - makeCallback(resolve, reject, () => - expect((e as Error).message).toMatch(/has not been found globally/) - )(); + expect((e as Error).message).toMatch(/has not been found globally/); } }); @@ -1259,18 +1153,18 @@ describe("HttpLink", () => { beforeEach(() => { fetch.mockReset(); }); - itAsync("makes it easy to do stuff on a 401", (resolve, reject) => { + it("makes it easy to do stuff on a 401", async () => { const middleware = new ApolloLink((operation, forward) => { return new Observable((ob) => { fetch.mockReturnValueOnce(Promise.resolve({ status: 401, text })); const op = forward(operation); const sub = op.subscribe({ next: ob.next.bind(ob), - error: makeCallback(resolve, reject, (e: ServerError) => { + error: (e: ServerError) => { expect(e.message).toMatch(/Received status code 401/); expect(e.statusCode).toEqual(401); ob.error(e); - }), + }, complete: ob.complete.bind(ob), }); @@ -1284,115 +1178,94 @@ describe("HttpLink", () => { createHttpLink({ uri: "data", fetch: fetch as any }) ); - execute(link, { query: sampleQuery }).subscribe( - (result) => { - reject("next should have been thrown from the network"); - }, - () => {} - ); + const observable = execute(link, { query: sampleQuery }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitError(); }); - itAsync("throws an error if response code is > 300", (resolve, reject) => { + it("throws an error if response code is > 300", async () => { fetch.mockReturnValueOnce(Promise.resolve({ status: 400, text })); const link = createHttpLink({ uri: "data", fetch: fetch as any }); - execute(link, { query: sampleQuery }).subscribe( - (result) => { - reject("next should have been thrown from the network"); - }, - makeCallback(resolve, reject, (e: ServerError) => { - expect(e.message).toMatch(/Received status code 400/); - expect(e.statusCode).toBe(400); - expect(e.result).toEqual(responseBody); - }) + const observable = execute(link, { query: sampleQuery }); + const stream = new ObservableStream(observable); + + const error: ServerError = await stream.takeError(); + + expect(error.message).toMatch(/Received status code 400/); + expect(error.statusCode).toBe(400); + expect(error.result).toEqual(responseBody); + }); + + it("throws an error if response code is > 300 and handles string response body", async () => { + fetch.mockReturnValueOnce( + Promise.resolve({ status: 302, text: textWithStringError }) ); + const link = createHttpLink({ uri: "data", fetch: fetch as any }); + const observable = execute(link, { query: sampleQuery }); + const stream = new ObservableStream(observable); + + const error: ServerError = await stream.takeError(); + + expect(error.message).toMatch(/Received status code 302/); + expect(error.statusCode).toBe(302); + expect(error.result).toEqual(responseBody); }); - itAsync( - "throws an error if response code is > 300 and handles string response body", - (resolve, reject) => { - fetch.mockReturnValueOnce( - Promise.resolve({ status: 302, text: textWithStringError }) - ); - const link = createHttpLink({ uri: "data", fetch: fetch as any }); - execute(link, { query: sampleQuery }).subscribe( - (result) => { - reject("next should have been thrown from the network"); - }, - makeCallback(resolve, reject, (e: ServerError) => { - expect(e.message).toMatch(/Received status code 302/); - expect(e.statusCode).toBe(302); - expect(e.result).toEqual(responseBody); - }) - ); - } - ); - itAsync( - "throws an error if response code is > 300 and returns data", - (resolve, reject) => { - fetch.mockReturnValueOnce( - Promise.resolve({ status: 400, text: textWithData }) - ); - const link = createHttpLink({ uri: "data", fetch: fetch as any }); + it("throws an error if response code is > 300 and returns data", async () => { + fetch.mockReturnValueOnce( + Promise.resolve({ status: 400, text: textWithData }) + ); - let called = false; + const link = createHttpLink({ uri: "data", fetch: fetch as any }); - execute(link, { query: sampleQuery }).subscribe( - (result) => { - called = true; - expect(result).toEqual(responseBody); - }, - (e) => { - expect(called).toBe(true); - expect(e.message).toMatch(/Received status code 400/); - expect(e.statusCode).toBe(400); - expect(e.result).toEqual(responseBody); - resolve(); - } - ); - } - ); - itAsync( - "throws an error if only errors are returned", - (resolve, reject) => { - fetch.mockReturnValueOnce( - Promise.resolve({ status: 400, text: textWithErrors }) - ); + const observable = execute(link, { query: sampleQuery }); + const stream = new ObservableStream(observable); - const link = createHttpLink({ uri: "data", fetch: fetch as any }); - execute(link, { query: sampleQuery }).subscribe( - (result) => { - reject("should not have called result because we have no data"); - }, - (e) => { - expect(e.message).toMatch(/Received status code 400/); - expect(e.statusCode).toBe(400); - expect(e.result).toEqual(responseBody); - resolve(); - } - ); - } - ); - itAsync( - "throws an error if empty response from the server ", - (resolve, reject) => { - fetch.mockReturnValueOnce(Promise.resolve({ text })); - text.mockReturnValueOnce(Promise.resolve('{ "body": "boo" }')); - const link = createHttpLink({ uri: "data", fetch: fetch as any }); + const result = await stream.takeNext(); - execute(link, { query: sampleQuery }).subscribe( - (result) => { - reject("next should have been thrown from the network"); - }, - makeCallback(resolve, reject, (e: Error) => { - expect(e.message).toMatch( - /Server response was missing for query 'SampleQuery'/ - ); - }) - ); - } - ); - itAsync("throws if the body can't be stringified", (resolve, reject) => { + expect(result).toEqual(responseBody); + + const error = await stream.takeError(); + + expect(error.message).toMatch(/Received status code 400/); + expect(error.statusCode).toBe(400); + expect(error.result).toEqual(responseBody); + }); + + it("throws an error if only errors are returned", async () => { + fetch.mockReturnValueOnce( + Promise.resolve({ status: 400, text: textWithErrors }) + ); + + const link = createHttpLink({ uri: "data", fetch: fetch as any }); + const observable = execute(link, { query: sampleQuery }); + const stream = new ObservableStream(observable); + + const error = await stream.takeError(); + + expect(error.message).toMatch(/Received status code 400/); + expect(error.statusCode).toBe(400); + expect(error.result).toEqual(responseBody); + }); + + it("throws an error if empty response from the server ", async () => { + fetch.mockReturnValueOnce(Promise.resolve({ text })); + text.mockReturnValueOnce(Promise.resolve('{ "body": "boo" }')); + const link = createHttpLink({ uri: "data", fetch: fetch as any }); + + const observable = execute(link, { query: sampleQuery }); + const stream = new ObservableStream(observable); + + const error = await stream.takeError(); + + expect(error.message).toMatch( + /Server response was missing for query 'SampleQuery'/ + ); + }); + + it("throws if the body can't be stringified", async () => { fetch.mockReturnValueOnce(Promise.resolve({ data: {}, text })); const link = createHttpLink({ uri: "data", @@ -1408,16 +1281,14 @@ describe("HttpLink", () => { a, b, }; - execute(link, { query: sampleQuery, variables }).subscribe( - (result) => { - reject("next should have been thrown from the link"); - }, - makeCallback(resolve, reject, (e: ClientParseError) => { - expect(e.message).toMatch(/Payload is not serializable/); - expect(e.parseError.message).toMatch( - /Converting circular structure to JSON/ - ); - }) + const observable = execute(link, { query: sampleQuery, variables }); + const stream = new ObservableStream(observable); + + const error: ClientParseError = await stream.takeError(); + + expect(error.message).toMatch(/Payload is not serializable/); + expect(error.parseError.message).toMatch( + /Converting circular structure to JSON/ ); }); @@ -1563,51 +1434,41 @@ describe("HttpLink", () => { const body = "{"; const unparsableJson = jest.fn(() => Promise.resolve(body)); - itAsync( - "throws a Server error if response is > 300 with unparsable json", - (resolve, reject) => { - fetch.mockReturnValueOnce( - Promise.resolve({ status: 400, text: unparsableJson }) - ); - const link = createHttpLink({ uri: "data", fetch: fetch as any }); + it("throws a Server error if response is > 300 with unparsable json", async () => { + fetch.mockReturnValueOnce( + Promise.resolve({ status: 400, text: unparsableJson }) + ); + const link = createHttpLink({ uri: "data", fetch: fetch as any }); - execute(link, { query: sampleQuery }).subscribe( - (result) => { - reject("next should have been thrown from the network"); - }, - makeCallback(resolve, reject, (e: ServerParseError) => { - expect(e.message).toMatch( - "Response not successful: Received status code 400" - ); - expect(e.statusCode).toBe(400); - expect(e.response).toBeDefined(); - expect(e.bodyText).toBe(undefined); - }) - ); - } - ); + const observable = execute(link, { query: sampleQuery }); + const stream = new ObservableStream(observable); - itAsync( - "throws a ServerParse error if response is 200 with unparsable json", - (resolve, reject) => { - fetch.mockReturnValueOnce( - Promise.resolve({ status: 200, text: unparsableJson }) - ); - const link = createHttpLink({ uri: "data", fetch: fetch as any }); + const error: ServerParseError = await stream.takeError(); - execute(link, { query: sampleQuery }).subscribe( - (result) => { - reject("next should have been thrown from the network"); - }, - makeCallback(resolve, reject, (e: ServerParseError) => { - expect(e.message).toMatch(/JSON/); - expect(e.statusCode).toBe(200); - expect(e.response).toBeDefined(); - expect(e.bodyText).toBe(body); - }) - ); - } - ); + expect(error.message).toMatch( + "Response not successful: Received status code 400" + ); + expect(error.statusCode).toBe(400); + expect(error.response).toBeDefined(); + expect(error.bodyText).toBe(undefined); + }); + + it("throws a ServerParse error if response is 200 with unparsable json", async () => { + fetch.mockReturnValueOnce( + Promise.resolve({ status: 200, text: unparsableJson }) + ); + const link = createHttpLink({ uri: "data", fetch: fetch as any }); + + const observable = execute(link, { query: sampleQuery }); + const stream = new ObservableStream(observable); + + const error: ServerParseError = await stream.takeError(); + + expect(error.message).toMatch(/JSON/); + expect(error.statusCode).toBe(200); + expect(error.response).toBeDefined(); + expect(error.bodyText).toBe(body); + }); }); describe("Multipart responses", () => { @@ -1854,72 +1715,63 @@ describe("HttpLink", () => { ); }); - itAsync( - "sets correct accept header on request with deferred query", - (resolve, reject) => { - const stream = Readable.from( - body.split("\r\n").map((line) => line + "\r\n") - ); - const fetch = jest.fn(async () => ({ - status: 200, - body: stream, - headers: new Headers({ "Content-Type": "multipart/mixed" }), - })); - const link = new HttpLink({ - fetch: fetch as any, - }); - execute(link, { - query: sampleDeferredQuery, - }).subscribe( - makeCallback(resolve, reject, () => { - expect(fetch).toHaveBeenCalledWith( - "/graphql", - expect.objectContaining({ - headers: { - "content-type": "application/json", - accept: - "multipart/mixed;deferSpec=20220824,application/json", - }, - }) - ); - }) - ); - } - ); + it("sets correct accept header on request with deferred query", async () => { + const stream = Readable.from( + body.split("\r\n").map((line) => line + "\r\n") + ); + const fetch = jest.fn(async () => ({ + status: 200, + body: stream, + headers: new Headers({ "Content-Type": "multipart/mixed" }), + })); + const link = new HttpLink({ + fetch: fetch as any, + }); + const observable = execute(link, { query: sampleDeferredQuery }); + const observableStream = new ObservableStream(observable); + + await expect(observableStream).toEmitNext(); + + expect(fetch).toHaveBeenCalledWith( + "/graphql", + expect.objectContaining({ + headers: { + "content-type": "application/json", + accept: "multipart/mixed;deferSpec=20220824,application/json", + }, + }) + ); + }); // ensure that custom directives beginning with '@defer..' do not trigger // custom accept header for multipart responses - itAsync( - "sets does not set accept header on query with custom directive begging with @defer", - (resolve, reject) => { - const stream = Readable.from( - body.split("\r\n").map((line) => line + "\r\n") - ); - const fetch = jest.fn(async () => ({ - status: 200, - body: stream, - headers: new Headers({ "Content-Type": "multipart/mixed" }), - })); - const link = new HttpLink({ - fetch: fetch as any, - }); - execute(link, { - query: sampleQueryCustomDirective, - }).subscribe( - makeCallback(resolve, reject, () => { - expect(fetch).toHaveBeenCalledWith( - "/graphql", - expect.objectContaining({ - headers: { - accept: "*/*", - "content-type": "application/json", - }, - }) - ); - }) - ); - } - ); + it("sets does not set accept header on query with custom directive begging with @defer", async () => { + const stream = Readable.from( + body.split("\r\n").map((line) => line + "\r\n") + ); + const fetch = jest.fn(async () => ({ + status: 200, + body: stream, + headers: new Headers({ "Content-Type": "multipart/mixed" }), + })); + const link = new HttpLink({ + fetch: fetch as any, + }); + const observable = execute(link, { query: sampleQueryCustomDirective }); + const observableStream = new ObservableStream(observable); + + await expect(observableStream).toEmitNext(); + + expect(fetch).toHaveBeenCalledWith( + "/graphql", + expect.objectContaining({ + headers: { + accept: "*/*", + "content-type": "application/json", + }, + }) + ); + }); }); describe("subscriptions", () => { @@ -2194,38 +2046,34 @@ describe("HttpLink", () => { ); }); - itAsync( - "sets correct accept header on request with subscription", - (resolve, reject) => { - const stream = Readable.from( - subscriptionsBody.split("\r\n").map((line) => line + "\r\n") - ); - const fetch = jest.fn(async () => ({ - status: 200, - body: stream, - headers: new Headers({ "Content-Type": "multipart/mixed" }), - })); - const link = new HttpLink({ - fetch: fetch as any, - }); - execute(link, { - query: sampleSubscription, - }).subscribe( - makeCallback(resolve, reject, () => { - expect(fetch).toHaveBeenCalledWith( - "/graphql", - expect.objectContaining({ - headers: { - "content-type": "application/json", - accept: - "multipart/mixed;boundary=graphql;subscriptionSpec=1.0,application/json", - }, - }) - ); - }) - ); - } - ); + it("sets correct accept header on request with subscription", async () => { + const stream = Readable.from( + subscriptionsBody.split("\r\n").map((line) => line + "\r\n") + ); + const fetch = jest.fn(async () => ({ + status: 200, + body: stream, + headers: new Headers({ "Content-Type": "multipart/mixed" }), + })); + const link = new HttpLink({ + fetch: fetch as any, + }); + const observable = execute(link, { query: sampleSubscription }); + const observableStream = new ObservableStream(observable); + + await expect(observableStream).toEmitNext(); + + expect(fetch).toHaveBeenCalledWith( + "/graphql", + expect.objectContaining({ + headers: { + "content-type": "application/json", + accept: + "multipart/mixed;boundary=graphql;subscriptionSpec=1.0,application/json", + }, + }) + ); + }); }); }); }); diff --git a/src/link/http/__tests__/parseAndCheckHttpResponse.ts b/src/link/http/__tests__/parseAndCheckHttpResponse.ts index 74e9b63018e..b667d95c35b 100644 --- a/src/link/http/__tests__/parseAndCheckHttpResponse.ts +++ b/src/link/http/__tests__/parseAndCheckHttpResponse.ts @@ -3,7 +3,6 @@ import fetchMock from "fetch-mock"; import { createOperation } from "../../utils/createOperation"; import { parseAndCheckHttpResponse } from "../parseAndCheckHttpResponse"; -import { itAsync } from "../../../testing"; const query = gql` query SampleQuery { @@ -20,98 +19,79 @@ describe("parseAndCheckResponse", () => { const operations = [createOperation({}, { query })]; - itAsync( - "throws a Server error when response is > 300 with unparsable json", - (resolve, reject) => { - const status = 400; - fetchMock.mock("begin:/error", status); - fetch("error") - .then(parseAndCheckHttpResponse(operations)) - .then(reject) - .catch((e) => { - expect(e.statusCode).toBe(status); - expect(e.name).toBe("ServerError"); - expect(e).toHaveProperty("response"); - expect(e.bodyText).toBe(undefined); - resolve(); - }) - .catch(reject); - } - ); - - itAsync( - "throws a ServerParse error when response is 200 with unparsable json", - (resolve, reject) => { - const status = 200; - fetchMock.mock("begin:/error", status); - fetch("error") - .then(parseAndCheckHttpResponse(operations)) - .then(reject) - .catch((e) => { - expect(e.statusCode).toBe(status); - expect(e.name).toBe("ServerParseError"); - expect(e).toHaveProperty("response"); - expect(e).toHaveProperty("bodyText"); - resolve(); - }) - .catch(reject); - } - ); - - itAsync( - "throws a network error with a status code and result", - (resolve, reject) => { - const status = 403; - const body = { data: "fail" }; //does not contain data or errors - fetchMock.mock("begin:/error", { - body, - status, - }); - fetch("error") - .then(parseAndCheckHttpResponse(operations)) - .then(reject) - .catch((e) => { - expect(e.statusCode).toBe(status); - expect(e.name).toBe("ServerError"); - expect(e).toHaveProperty("response"); - expect(e).toHaveProperty("result"); - resolve(); - }) - .catch(reject); - } - ); + it("throws a Server error when response is > 300 with unparsable json", async () => { + const status = 400; + fetchMock.mock("begin:/error", status); + + const error = await fetch("error") + .then(parseAndCheckHttpResponse(operations)) + .catch((error) => error); + + expect(error.statusCode).toBe(status); + expect(error.name).toBe("ServerError"); + expect(error).toHaveProperty("response"); + expect(error.bodyText).toBe(undefined); + }); + + it("throws a ServerParse error when response is 200 with unparsable json", async () => { + const status = 200; + fetchMock.mock("begin:/error", status); + const error = await fetch("error") + .then(parseAndCheckHttpResponse(operations)) + .catch((error) => error); + + expect(error.statusCode).toBe(status); + expect(error.name).toBe("ServerParseError"); + expect(error).toHaveProperty("response"); + expect(error).toHaveProperty("bodyText"); + }); - itAsync("throws a server error on incorrect data", (resolve, reject) => { + it("throws a network error with a status code and result", async () => { + const status = 403; + const body = { data: "fail" }; //does not contain data or errors + fetchMock.mock("begin:/error", { + body, + status, + }); + const error = await fetch("error") + .then(parseAndCheckHttpResponse(operations)) + .catch((error) => error); + + expect(error.statusCode).toBe(status); + expect(error.name).toBe("ServerError"); + expect(error).toHaveProperty("response"); + expect(error).toHaveProperty("result"); + }); + + it("throws a server error on incorrect data", async () => { const data = { hello: "world" }; //does not contain data or erros fetchMock.mock("begin:/incorrect", data); - fetch("incorrect") + const error = await fetch("incorrect") .then(parseAndCheckHttpResponse(operations)) - .then(reject) - .catch((e) => { - expect(e.statusCode).toBe(200); - expect(e.name).toBe("ServerError"); - expect(e).toHaveProperty("response"); - expect(e.result).toEqual(data); - resolve(); - }) - .catch(reject); + .catch((error) => error); + + expect(error.statusCode).toBe(200); + expect(error.name).toBe("ServerError"); + expect(error).toHaveProperty("response"); + expect(error.result).toEqual(data); }); - itAsync("is able to return a correct GraphQL result", (resolve, reject) => { + it("is able to return a correct GraphQL result", async () => { const errors = ["", "" + new Error("hi")]; const data = { data: { hello: "world" }, errors }; fetchMock.mock("begin:/data", { body: data, }); - fetch("data") - .then(parseAndCheckHttpResponse(operations)) - .then(({ data, errors: e }) => { - expect(data).toEqual({ hello: "world" }); - expect(e.length).toEqual(errors.length); - expect(e).toEqual(errors); - resolve(); - }) - .catch(reject); + + { + const { data, errors: e } = await fetch("data").then( + parseAndCheckHttpResponse(operations) + ); + + expect(data).toEqual({ hello: "world" }); + expect(e.length).toEqual(errors.length); + expect(e).toEqual(errors); + } }); }); diff --git a/src/link/persisted-queries/__tests__/persisted-queries.test.ts b/src/link/persisted-queries/__tests__/persisted-queries.test.ts index 7b4fecaf99f..84ce1840b81 100644 --- a/src/link/persisted-queries/__tests__/persisted-queries.test.ts +++ b/src/link/persisted-queries/__tests__/persisted-queries.test.ts @@ -9,8 +9,9 @@ import { Observable } from "../../../utilities"; import { createHttpLink } from "../../http/createHttpLink"; import { createPersistedQueryLink as createPersistedQuery, VERSION } from ".."; -import { itAsync } from "../../../testing"; +import { wait } from "../../../testing"; import { toPromise } from "../../utils"; +import { ObservableStream } from "../../../testing/internal"; // Necessary configuration in order to mock multiple requests // to a single (/graphql) endpoint @@ -79,52 +80,53 @@ describe("happy path", () => { fetchMock.restore(); }); - itAsync( - "sends a sha256 hash of the query under extensions", - (resolve, reject) => { - fetchMock.post( - "/graphql", - () => new Promise((resolve) => resolve({ body: response })) - ); - const link = createPersistedQuery({ sha256 }).concat(createHttpLink()); - execute(link, { query, variables }).subscribe((result) => { - expect(result.data).toEqual(data); - const [uri, request] = fetchMock.lastCall()!; - expect(uri).toEqual("/graphql"); - expect(request!.body!).toBe( - JSON.stringify({ - operationName: "Test", - variables, - extensions: { - persistedQuery: { - version: VERSION, - sha256Hash: hash, - }, - }, - }) - ); - resolve(); - }, reject); - } - ); + it("sends a sha256 hash of the query under extensions", async () => { + fetchMock.post( + "/graphql", + () => new Promise((resolve) => resolve({ body: response })) + ); + const link = createPersistedQuery({ sha256 }).concat(createHttpLink()); + const observable = execute(link, { query, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue({ data }); + + const [uri, request] = fetchMock.lastCall()!; - itAsync("sends a version along with the request", (resolve, reject) => { + expect(uri).toEqual("/graphql"); + expect(request!.body!).toBe( + JSON.stringify({ + operationName: "Test", + variables, + extensions: { + persistedQuery: { + version: VERSION, + sha256Hash: hash, + }, + }, + }) + ); + }); + + it("sends a version along with the request", async () => { fetchMock.post( "/graphql", () => new Promise((resolve) => resolve({ body: response })) ); const link = createPersistedQuery({ sha256 }).concat(createHttpLink()); - execute(link, { query, variables }).subscribe((result) => { - expect(result.data).toEqual(data); - const [uri, request] = fetchMock.lastCall()!; - expect(uri).toEqual("/graphql"); - const parsed = JSON.parse(request!.body!.toString()); - expect(parsed.extensions.persistedQuery.version).toBe(VERSION); - resolve(); - }, reject); + const observable = execute(link, { query, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue({ data }); + + const [uri, request] = fetchMock.lastCall()!; + expect(uri).toEqual("/graphql"); + + const parsed = JSON.parse(request!.body!.toString()); + expect(parsed.extensions.persistedQuery.version).toBe(VERSION); }); - itAsync("memoizes between requests", (resolve, reject) => { + it("memoizes between requests", async () => { fetchMock.post( "/graphql", () => new Promise((resolve) => resolve({ body: response })), @@ -140,15 +142,23 @@ describe("happy path", () => { createHttpLink() ); - execute(link, { query, variables }).subscribe((result) => { + { + const observable = execute(link, { query, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue({ data }); + await expect(stream).toComplete(); + expect(hashSpy).toHaveBeenCalledTimes(1); + } + + { + const observable = execute(link, { query, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue({ data }); + await expect(stream).toComplete(); expect(hashSpy).toHaveBeenCalledTimes(1); - expect(result.data).toEqual(data); - execute(link, { query, variables }).subscribe((result2) => { - expect(hashSpy).toHaveBeenCalledTimes(1); - expect(result2.data).toEqual(data); - resolve(); - }, reject); - }, reject); + } }); it("clears the cache when calling `resetHashCache`", async () => { @@ -177,7 +187,7 @@ describe("happy path", () => { await expect(hashRefs[0]).toBeGarbageCollected(); }); - itAsync("supports loading the hash from other method", (resolve, reject) => { + it("supports loading the hash from other method", async () => { fetchMock.post( "/graphql", () => new Promise((resolve) => resolve({ body: response })) @@ -187,33 +197,34 @@ describe("happy path", () => { createHttpLink() ); - execute(link, { query, variables }).subscribe((result) => { - expect(result.data).toEqual(data); - const [uri, request] = fetchMock.lastCall()!; - expect(uri).toEqual("/graphql"); - const parsed = JSON.parse(request!.body!.toString()); - expect(parsed.extensions.persistedQuery.sha256Hash).toBe("foo"); - resolve(); - }, reject); + const observable = execute(link, { query, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue({ data }); + + const [uri, request] = fetchMock.lastCall()!; + expect(uri).toEqual("/graphql"); + + const parsed = JSON.parse(request!.body!.toString()); + expect(parsed.extensions.persistedQuery.sha256Hash).toBe("foo"); }); - itAsync("errors if unable to convert to sha256", (resolve, reject) => { + it("errors if unable to convert to sha256", async () => { fetchMock.post( "/graphql", () => new Promise((resolve) => resolve({ body: response })) ); const link = createPersistedQuery({ sha256 }).concat(createHttpLink()); - execute(link, { query: "1234", variables } as any).subscribe( - reject as any, - (error) => { - expect(error.message).toMatch(/Invalid AST Node/); - resolve(); - } - ); + const observable = execute(link, { query: "1234", variables } as any); + const stream = new ObservableStream(observable); + + const error = await stream.takeError(); + + expect(error.message).toMatch(/Invalid AST Node/); }); - itAsync("unsubscribes correctly", (resolve, reject) => { + it("unsubscribes correctly", async () => { const delay = new ApolloLink(() => { return new Observable((ob) => { setTimeout(() => { @@ -224,92 +235,70 @@ describe("happy path", () => { }); const link = createPersistedQuery({ sha256 }).concat(delay); - const sub = execute(link, { query, variables }).subscribe( - reject, - reject, - reject - ); + const observable = execute(link, { query, variables }); + const stream = new ObservableStream(observable); - setTimeout(() => { - sub.unsubscribe(); - resolve(); - }, 10); + await wait(10); + + stream.unsubscribe(); + + await expect(stream).not.toEmitAnything({ timeout: 150 }); }); - itAsync( - "should error if `sha256` and `generateHash` options are both missing", - (resolve, reject) => { - const createPersistedQueryFn = createPersistedQuery as any; - try { - createPersistedQueryFn(); - reject("should have thrown an error"); - } catch (error) { - expect( - (error as Error).message.indexOf( - 'Missing/invalid "sha256" or "generateHash" function' - ) - ).toBe(0); - resolve(); - } - } - ); + it("should error if `sha256` and `generateHash` options are both missing", async () => { + const createPersistedQueryFn = createPersistedQuery as any; + + expect(() => createPersistedQueryFn()).toThrow( + 'Missing/invalid "sha256" or "generateHash" function' + ); + }); - itAsync( - "should error if `sha256` or `generateHash` options are not functions", - (resolve, reject) => { + it.each(["sha256", "generateHash"])( + "should error if `%s` option is not a function", + async (option) => { const createPersistedQueryFn = createPersistedQuery as any; - [{ sha256: "ooops" }, { generateHash: "ooops" }].forEach((options) => { - try { - createPersistedQueryFn(options); - reject("should have thrown an error"); - } catch (error) { - expect( - (error as Error).message.indexOf( - 'Missing/invalid "sha256" or "generateHash" function' - ) - ).toBe(0); - resolve(); - } - }); + + expect(() => createPersistedQueryFn({ [option]: "ooops" })).toThrow( + 'Missing/invalid "sha256" or "generateHash" function' + ); } ); - itAsync( - "should work with a synchronous SHA-256 function", - (resolve, reject) => { - const crypto = require("crypto"); - const sha256Hash = crypto.createHmac("sha256", queryString).digest("hex"); + it("should work with a synchronous SHA-256 function", async () => { + const crypto = require("crypto"); + const sha256Hash = crypto.createHmac("sha256", queryString).digest("hex"); - fetchMock.post( - "/graphql", - () => new Promise((resolve) => resolve({ body: response })) - ); - const link = createPersistedQuery({ - sha256(data) { - return crypto.createHmac("sha256", data).digest("hex"); + fetchMock.post( + "/graphql", + () => new Promise((resolve) => resolve({ body: response })) + ); + const link = createPersistedQuery({ + sha256(data) { + return crypto.createHmac("sha256", data).digest("hex"); + }, + }).concat(createHttpLink()); + + const observable = execute(link, { query, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue({ data }); + + const [uri, request] = fetchMock.lastCall()!; + + expect(uri).toEqual("/graphql"); + expect(request!.body!).toBe( + JSON.stringify({ + operationName: "Test", + variables, + extensions: { + persistedQuery: { + version: VERSION, + sha256Hash: sha256Hash, + }, }, - }).concat(createHttpLink()); - - execute(link, { query, variables }).subscribe((result) => { - expect(result.data).toEqual(data); - const [uri, request] = fetchMock.lastCall()!; - expect(uri).toEqual("/graphql"); - expect(request!.body!).toBe( - JSON.stringify({ - operationName: "Test", - variables, - extensions: { - persistedQuery: { - version: VERSION, - sha256Hash: sha256Hash, - }, - }, - }) - ); - resolve(); - }, reject); - } - ); + }) + ); + }); }); describe("failure path", () => { @@ -356,98 +345,99 @@ describe("failure path", () => { }) ); - itAsync( - "sends GET for the first response only with useGETForHashedQueries", - (resolve, reject) => { - const params = new URLSearchParams({ - operationName: "Test", - variables: JSON.stringify({ - id: 1, - }), - extensions: JSON.stringify({ - persistedQuery: { - version: 1, - sha256Hash: hash, - }, - }), - }).toString(); - fetchMock.get( - `/graphql?${params}`, - () => new Promise((resolve) => resolve({ body: errorResponse })) - ); - fetchMock.post( - "/graphql", - () => new Promise((resolve) => resolve({ body: response })) - ); - const link = createPersistedQuery({ - sha256, - useGETForHashedQueries: true, - }).concat(createHttpLink()); - execute(link, { query, variables }).subscribe((result) => { - expect(result.data).toEqual(data); - const [[, failure]] = fetchMock.calls(); - expect(failure!.method).toBe("GET"); - expect(failure!.body).not.toBeDefined(); - const [, [, success]] = fetchMock.calls(); - expect(success!.method).toBe("POST"); - expect(JSON.parse(success!.body!.toString()).query).toBe(queryString); - expect( - JSON.parse(success!.body!.toString()).extensions.persistedQuery - .sha256Hash - ).toBe(hash); - resolve(); - }, reject); - } - ); + it("sends GET for the first response only with useGETForHashedQueries", async () => { + const params = new URLSearchParams({ + operationName: "Test", + variables: JSON.stringify({ + id: 1, + }), + extensions: JSON.stringify({ + persistedQuery: { + version: 1, + sha256Hash: hash, + }, + }), + }).toString(); + fetchMock.get( + `/graphql?${params}`, + () => new Promise((resolve) => resolve({ body: errorResponse })) + ); + fetchMock.post( + "/graphql", + () => new Promise((resolve) => resolve({ body: response })) + ); + const link = createPersistedQuery({ + sha256, + useGETForHashedQueries: true, + }).concat(createHttpLink()); + const observable = execute(link, { query, variables }); + const stream = new ObservableStream(observable); - itAsync( - "sends POST for both requests without useGETForHashedQueries", - (resolve, reject) => { - fetchMock.post( - "/graphql", - () => new Promise((resolve) => resolve({ body: errorResponse })), - { repeat: 1 } - ); - fetchMock.post( - "/graphql", - () => new Promise((resolve) => resolve({ body: response })), - { repeat: 1 } - ); - const link = createPersistedQuery({ sha256 }).concat(createHttpLink()); - execute(link, { query, variables }).subscribe((result) => { - expect(result.data).toEqual(data); - const [[, failure]] = fetchMock.calls(); - expect(failure!.method).toBe("POST"); - expect(JSON.parse(failure!.body!.toString())).toEqual({ - operationName: "Test", - variables, - extensions: { - persistedQuery: { - version: VERSION, - sha256Hash: hash, - }, - }, - }); - const [, [, success]] = fetchMock.calls(); - expect(success!.method).toBe("POST"); - expect(JSON.parse(success!.body!.toString())).toEqual({ - operationName: "Test", - query: queryString, - variables, - extensions: { - persistedQuery: { - version: VERSION, - sha256Hash: hash, - }, - }, - }); - resolve(); - }, reject); - } - ); + await expect(stream).toEmitValue({ data }); + + const [[, failure]] = fetchMock.calls(); + + expect(failure!.method).toBe("GET"); + expect(failure!.body).not.toBeDefined(); + + const [, [, success]] = fetchMock.calls(); + + expect(success!.method).toBe("POST"); + expect(JSON.parse(success!.body!.toString()).query).toBe(queryString); + expect( + JSON.parse(success!.body!.toString()).extensions.persistedQuery.sha256Hash + ).toBe(hash); + }); + + it("sends POST for both requests without useGETForHashedQueries", async () => { + fetchMock.post( + "/graphql", + () => new Promise((resolve) => resolve({ body: errorResponse })), + { repeat: 1 } + ); + fetchMock.post( + "/graphql", + () => new Promise((resolve) => resolve({ body: response })), + { repeat: 1 } + ); + const link = createPersistedQuery({ sha256 }).concat(createHttpLink()); + const observable = execute(link, { query, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue({ data }); + + const [[, failure]] = fetchMock.calls(); + + expect(failure!.method).toBe("POST"); + expect(JSON.parse(failure!.body!.toString())).toEqual({ + operationName: "Test", + variables, + extensions: { + persistedQuery: { + version: VERSION, + sha256Hash: hash, + }, + }, + }); + + const [, [, success]] = fetchMock.calls(); + + expect(success!.method).toBe("POST"); + expect(JSON.parse(success!.body!.toString())).toEqual({ + operationName: "Test", + query: queryString, + variables, + extensions: { + persistedQuery: { + version: VERSION, + sha256Hash: hash, + }, + }, + }); + }); // https://github.com/apollographql/apollo-client/pull/7456 - itAsync("forces POST request when sending full query", (resolve, reject) => { + it("forces POST request when sending full query", async () => { fetchMock.post( "/graphql", () => new Promise((resolve) => resolve({ body: giveUpResponse })), @@ -469,29 +459,33 @@ describe("failure path", () => { return true; }, }).concat(createHttpLink()); - execute(link, { query, variables }).subscribe((result) => { - expect(result.data).toEqual(data); - const [[, failure]] = fetchMock.calls(); - expect(failure!.method).toBe("POST"); - expect(JSON.parse(failure!.body!.toString())).toEqual({ - operationName: "Test", - variables, - extensions: { - persistedQuery: { - version: VERSION, - sha256Hash: hash, - }, + const observable = execute(link, { query, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue({ data }); + + const [[, failure]] = fetchMock.calls(); + + expect(failure!.method).toBe("POST"); + expect(JSON.parse(failure!.body!.toString())).toEqual({ + operationName: "Test", + variables, + extensions: { + persistedQuery: { + version: VERSION, + sha256Hash: hash, }, - }); - const [, [, success]] = fetchMock.calls(); - expect(success!.method).toBe("POST"); - expect(JSON.parse(success!.body!.toString())).toEqual({ - operationName: "Test", - query: queryString, - variables, - }); - resolve(); - }, reject); + }, + }); + + const [, [, success]] = fetchMock.calls(); + + expect(success!.method).toBe("POST"); + expect(JSON.parse(success!.body!.toString())).toEqual({ + operationName: "Test", + query: queryString, + variables, + }); }); it.each([ @@ -583,7 +577,7 @@ describe("failure path", () => { } ); - itAsync("works with multiple errors", (resolve, reject) => { + it("works with multiple errors", async () => { fetchMock.post( "/graphql", () => new Promise((resolve) => resolve({ body: multiResponse })), @@ -595,76 +589,84 @@ describe("failure path", () => { { repeat: 1 } ); const link = createPersistedQuery({ sha256 }).concat(createHttpLink()); - execute(link, { query, variables }).subscribe((result) => { - expect(result.data).toEqual(data); - const [[, failure]] = fetchMock.calls(); - expect(JSON.parse(failure!.body!.toString()).query).not.toBeDefined(); - const [, [, success]] = fetchMock.calls(); - expect(JSON.parse(success!.body!.toString()).query).toBe(queryString); - expect( - JSON.parse(success!.body!.toString()).extensions.persistedQuery - .sha256Hash - ).toBe(hash); - resolve(); - }, reject); + const observable = execute(link, { query, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue({ data }); + + const [[, failure]] = fetchMock.calls(); + + expect(JSON.parse(failure!.body!.toString()).query).not.toBeDefined(); + + const [, [, success]] = fetchMock.calls(); + + expect(JSON.parse(success!.body!.toString()).query).toBe(queryString); + expect( + JSON.parse(success!.body!.toString()).extensions.persistedQuery.sha256Hash + ).toBe(hash); }); describe.each([[400], [500]])("status %s", (status) => { - itAsync( - `handles a ${status} network with a "PERSISTED_QUERY_NOT_FOUND" error and still retries`, - (resolve, reject) => { - let requestCount = 0; - fetchMock.post( - "/graphql", - () => new Promise((resolve) => resolve({ body: response })), - { repeat: 1 } - ); + it(`handles a ${status} network with a "PERSISTED_QUERY_NOT_FOUND" error and still retries`, async () => { + let requestCount = 0; + fetchMock.post( + "/graphql", + () => new Promise((resolve) => resolve({ body: response })), + { repeat: 1 } + ); - // mock it again so we can verify it doesn't try anymore - fetchMock.post( - "/graphql", - () => new Promise((resolve) => resolve({ body: response })), - { repeat: 5 } - ); + // mock it again so we can verify it doesn't try anymore + fetchMock.post( + "/graphql", + () => new Promise((resolve) => resolve({ body: response })), + { repeat: 5 } + ); - const fetcher = (...args: any[]) => { - if (++requestCount % 2) { - return Promise.resolve({ - json: () => Promise.resolve(errorResponseWithCode), - text: () => Promise.resolve(errorResponseWithCode), - status, - }); - } - // @ts-expect-error - return global.fetch.apply(null, args); - }; - const link = createPersistedQuery({ sha256 }).concat( - createHttpLink({ fetch: fetcher } as any) - ); + const fetcher = (...args: any[]) => { + if (++requestCount % 2) { + return Promise.resolve({ + json: () => Promise.resolve(errorResponseWithCode), + text: () => Promise.resolve(errorResponseWithCode), + status, + }); + } + // @ts-expect-error + return global.fetch.apply(null, args); + }; + const link = createPersistedQuery({ sha256 }).concat( + createHttpLink({ fetch: fetcher } as any) + ); - execute(link, { query, variables }).subscribe((result) => { - expect(result.data).toEqual(data); - const [[, success]] = fetchMock.calls(); - expect(JSON.parse(success!.body!.toString()).query).toBe(queryString); - expect( - JSON.parse(success!.body!.toString()).extensions.persistedQuery - .sha256Hash - ).toBe(hash); - execute(link, { query, variables }).subscribe((secondResult) => { - expect(secondResult.data).toEqual(data); - const [, [, success]] = fetchMock.calls(); - expect(JSON.parse(success!.body!.toString()).query).toBe( - queryString - ); - expect( - JSON.parse(success!.body!.toString()).extensions.persistedQuery - .sha256Hash - ).toBe(hash); - resolve(); - }, reject); - }, reject); + { + const observable = execute(link, { query, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue({ data }); + + const [[, success]] = fetchMock.calls(); + + expect(JSON.parse(success!.body!.toString()).query).toBe(queryString); + expect( + JSON.parse(success!.body!.toString()).extensions.persistedQuery + .sha256Hash + ).toBe(hash); } - ); + + { + const observable = execute(link, { query, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue({ data }); + + const [, [, success]] = fetchMock.calls(); + + expect(JSON.parse(success!.body!.toString()).query).toBe(queryString); + expect( + JSON.parse(success!.body!.toString()).extensions.persistedQuery + .sha256Hash + ).toBe(hash); + } + }); it(`will fail on an unrelated ${status} network error, but still send a hash the next request`, async () => { let failed = false; @@ -711,43 +713,42 @@ describe("failure path", () => { ).toBe(hash); }); - itAsync( - `handles ${status} response network error and graphql error without disabling persistedQuery support`, - (resolve, reject) => { - let failed = false; - fetchMock.post( - "/graphql", - () => new Promise((resolve) => resolve({ body: response })), - { repeat: 1 } - ); + it(`handles ${status} response network error and graphql error without disabling persistedQuery support`, async () => { + let failed = false; + fetchMock.post( + "/graphql", + () => new Promise((resolve) => resolve({ body: response })), + { repeat: 1 } + ); - const fetcher = (...args: any[]) => { - if (!failed) { - failed = true; - return Promise.resolve({ - json: () => Promise.resolve(errorResponse), - text: () => Promise.resolve(errorResponse), - status, - }); - } - // @ts-expect-error - return global.fetch.apply(null, args); - }; - - const link = createPersistedQuery({ sha256 }).concat( - createHttpLink({ fetch: fetcher } as any) - ); + const fetcher = (...args: any[]) => { + if (!failed) { + failed = true; + return Promise.resolve({ + json: () => Promise.resolve(errorResponse), + text: () => Promise.resolve(errorResponse), + status, + }); + } + // @ts-expect-error + return global.fetch.apply(null, args); + }; - execute(link, { query, variables }).subscribe((result) => { - expect(result.data).toEqual(data); - const [[, success]] = fetchMock.calls(); - expect(JSON.parse(success!.body!.toString()).query).toBe(queryString); - expect( - JSON.parse(success!.body!.toString()).extensions - ).not.toBeUndefined(); - resolve(); - }, reject); - } - ); + const link = createPersistedQuery({ sha256 }).concat( + createHttpLink({ fetch: fetcher } as any) + ); + + const observable = execute(link, { query, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue({ data }); + + const [[, success]] = fetchMock.calls(); + + expect(JSON.parse(success!.body!.toString()).query).toBe(queryString); + expect( + JSON.parse(success!.body!.toString()).extensions + ).not.toBeUndefined(); + }); }); }); diff --git a/src/link/schema/__tests__/schemaLink.ts b/src/link/schema/__tests__/schemaLink.ts index d4031b679ca..a536351002d 100644 --- a/src/link/schema/__tests__/schemaLink.ts +++ b/src/link/schema/__tests__/schemaLink.ts @@ -3,7 +3,7 @@ import gql from "graphql-tag"; import { execute } from "../../core/execute"; import { SchemaLink } from "../"; -import { itAsync } from "../../../testing"; +import { ObservableStream } from "../../../testing/internal"; const sampleQuery = gql` query SampleQuery { @@ -51,25 +51,18 @@ describe("SchemaLink", () => { expect(link.schema).toEqual(schema); }); - itAsync("calls next and then complete", (resolve, reject) => { - const next = jest.fn(); + it("calls next and then complete", async () => { const link = new SchemaLink({ schema }); const observable = execute(link, { query: sampleQuery, }); - observable.subscribe({ - next, - error: () => { - throw new Error("Received error"); - }, - complete: () => { - expect(next).toHaveBeenCalledTimes(1); - resolve(); - }, - }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + await expect(stream).toComplete(); }); - itAsync("calls error when fetch fails", (resolve, reject) => { + it("calls error when fetch fails", async () => { const link = new SchemaLink({ validate: true, schema: makeExecutableSchema({ @@ -86,98 +79,67 @@ describe("SchemaLink", () => { const observable = execute(link, { query: sampleQuery, }); - observable.subscribe((result) => { - expect(result.errors).toBeTruthy(); - expect(result.errors!.length).toBe(1); - expect(result.errors![0].message).toMatch(/Unauthorized/); - resolve(); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue({ + data: { sampleQuery: null }, + errors: [{ message: "Unauthorized", path: ["sampleQuery"] }], }); }); - itAsync( - "supports query which is executed synchronously", - (resolve, reject) => { - const next = jest.fn(); - const link = new SchemaLink({ schema }); - const introspectionQuery = gql` - query IntrospectionQuery { - __schema { - types { - name - } + it("supports query which is executed synchronously", async () => { + const link = new SchemaLink({ schema }); + const introspectionQuery = gql` + query IntrospectionQuery { + __schema { + types { + name } } - `; - const observable = execute(link, { - query: introspectionQuery, - }); - observable.subscribe( - next, - () => { - throw new Error("Received error"); - }, - () => { - expect(next).toHaveBeenCalledTimes(1); - resolve(); - } - ); - } - ); - - itAsync( - "passes operation context into execute with context function", - (resolve, reject) => { - const next = jest.fn(); - const contextValue = { some: "value" }; - const contextProvider = jest.fn((operation) => operation.getContext()); - const resolvers = { - Query: { - sampleQuery: (root: any, args: any, context: any) => { - try { - expect(context).toEqual(contextValue); - } catch (error) { - reject("Should pass context into resolver"); - } - }, + } + `; + const observable = execute(link, { + query: introspectionQuery, + }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + await expect(stream).toComplete(); + }); + + it("passes operation context into execute with context function", async () => { + const contextValue = { some: "value" }; + const contextProvider = jest.fn((operation) => operation.getContext()); + const resolvers = { + Query: { + sampleQuery: (root: any, args: any, context: any) => { + expect(context).toEqual(contextValue); }, - }; - const schemaWithResolvers = makeExecutableSchema({ - typeDefs, - resolvers, - }); - const link = new SchemaLink({ - schema: schemaWithResolvers, - context: contextProvider, - }); - const observable = execute(link, { - query: sampleQuery, - context: contextValue, - }); - observable.subscribe( - next, - (error) => reject("Shouldn't call onError"), - () => { - try { - expect(next).toHaveBeenCalledTimes(1); - expect(contextProvider).toHaveBeenCalledTimes(1); - resolve(); - } catch (e) { - reject(e); - } - } - ); - } - ); + }, + }; + const schemaWithResolvers = makeExecutableSchema({ + typeDefs, + resolvers, + }); + const link = new SchemaLink({ + schema: schemaWithResolvers, + context: contextProvider, + }); + const observable = execute(link, { + query: sampleQuery, + context: contextValue, + }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + await expect(stream).toComplete(); + expect(contextProvider).toHaveBeenCalledTimes(1); + }); - itAsync("passes static context into execute", (resolve, reject) => { - const next = jest.fn(); + it("passes static context into execute", async () => { const contextValue = { some: "value" }; const resolver = jest.fn((root, args, context) => { - try { - expect(context).toEqual(contextValue); - } catch (error) { - reject("Should pass context into resolver"); - } + expect(context).toEqual(contextValue); }); const resolvers = { @@ -196,22 +158,14 @@ describe("SchemaLink", () => { const observable = execute(link, { query: sampleQuery, }); - observable.subscribe( - next, - (error) => reject("Shouldn't call onError"), - () => { - try { - expect(next).toHaveBeenCalledTimes(1); - expect(resolver).toHaveBeenCalledTimes(1); - resolve(); - } catch (e) { - reject(e); - } - } - ); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + await expect(stream).toComplete(); + expect(resolver).toHaveBeenCalledTimes(1); }); - itAsync("reports errors for unknown queries", (resolve, reject) => { + it("reports errors for unknown queries", async () => { const link = new SchemaLink({ validate: true, schema: makeExecutableSchema({ @@ -225,11 +179,9 @@ describe("SchemaLink", () => { } `, }); - observable.subscribe((result) => { - expect(result.errors).toBeTruthy(); - expect(result.errors!.length).toBe(1); - expect(result.errors![0].message).toMatch(/Cannot query field "unknown"/); - resolve(); + const stream = new ObservableStream(observable); + await expect(stream).toEmitValue({ + errors: [{ message: 'Cannot query field "unknown" on type "Query".' }], }); }); }); diff --git a/src/link/utils/__tests__/toPromise.ts b/src/link/utils/__tests__/toPromise.ts index c81f3eb27b2..82bf7c04083 100644 --- a/src/link/utils/__tests__/toPromise.ts +++ b/src/link/utils/__tests__/toPromise.ts @@ -1,5 +1,4 @@ import { Observable } from "../../../utilities/observables/Observable"; -import { itAsync } from "../../../testing"; import { toPromise } from "../toPromise"; import { fromError } from "../fromError"; @@ -38,12 +37,11 @@ describe("toPromise", () => { console.warn = _warn; }); - itAsync("return error call as Promise rejection", (resolve, reject) => { - toPromise(Observable.of(data, data)).then((result) => { - expect(data).toEqual(result); - expect(spy).toHaveBeenCalled(); - resolve(); - }); + it("return error call as Promise rejection", async () => { + const result = await toPromise(Observable.of(data, data)); + + expect(data).toEqual(result); + expect(spy).toHaveBeenCalled(); }); }); }); diff --git a/src/link/ws/__tests__/webSocketLink.ts b/src/link/ws/__tests__/webSocketLink.ts index d24b118c44d..5859d6ed785 100644 --- a/src/link/ws/__tests__/webSocketLink.ts +++ b/src/link/ws/__tests__/webSocketLink.ts @@ -5,7 +5,7 @@ import gql from "graphql-tag"; import { Observable } from "../../../utilities"; import { execute } from "../../core"; import { WebSocketLink } from ".."; -import { itAsync } from "../../../testing"; +import { ObservableStream } from "../../../testing/internal"; const query = gql` query SampleQuery { @@ -43,95 +43,84 @@ describe("WebSocketLink", () => { // it('should pass the correct initialization parameters to the Subscription Client', () => { // }); - itAsync( - "should call request on the client for a query", - (resolve, reject) => { - const result = { data: { data: "result" } }; - const client: any = {}; - const observable = Observable.of(result); - client.__proto__ = SubscriptionClient.prototype; - client.request = jest.fn(); - client.request.mockReturnValueOnce(observable); - const link = new WebSocketLink(client); - - const obs = execute(link, { query }); - expect(obs).toEqual(observable); - obs.subscribe((data) => { - expect(data).toEqual(result); - expect(client.request).toHaveBeenCalledTimes(1); - resolve(); - }); - } - ); - - itAsync( - "should call query on the client for a mutation", - (resolve, reject) => { - const result = { data: { data: "result" } }; - const client: any = {}; - const observable = Observable.of(result); - client.__proto__ = SubscriptionClient.prototype; - client.request = jest.fn(); - client.request.mockReturnValueOnce(observable); - const link = new WebSocketLink(client); - - const obs = execute(link, { query: mutation }); - expect(obs).toEqual(observable); - obs.subscribe((data) => { - expect(data).toEqual(result); - expect(client.request).toHaveBeenCalledTimes(1); - resolve(); - }); - } - ); - - itAsync( - "should call request on the subscriptions client for subscription", - (resolve, reject) => { - const result = { data: { data: "result" } }; - const client: any = {}; - const observable = Observable.of(result); - client.__proto__ = SubscriptionClient.prototype; - client.request = jest.fn(); - client.request.mockReturnValueOnce(observable); - const link = new WebSocketLink(client); - - const obs = execute(link, { query: subscription }); - expect(obs).toEqual(observable); - obs.subscribe((data) => { - expect(data).toEqual(result); - expect(client.request).toHaveBeenCalledTimes(1); - resolve(); - }); - } - ); - - itAsync( - "should call next with multiple results for subscription", - (resolve, reject) => { - const results = [ - { data: { data: "result1" } }, - { data: { data: "result2" } }, - ]; - const client: any = {}; - client.__proto__ = SubscriptionClient.prototype; - client.request = jest.fn(() => { - const copy = [...results]; - return new Observable((observer) => { - observer.next(copy[0]); - observer.next(copy[1]); - }); - }); + it("should call request on the client for a query", async () => { + const result = { data: { data: "result" } }; + const client: any = {}; + const observable = Observable.of(result); + client.__proto__ = SubscriptionClient.prototype; + client.request = jest.fn(); + client.request.mockReturnValueOnce(observable); + const link = new WebSocketLink(client); + + const obs = execute(link, { query }); + expect(obs).toEqual(observable); + + const stream = new ObservableStream(obs); + + await expect(stream).toEmitValue(result); + expect(client.request).toHaveBeenCalledTimes(1); + }); + + it("should call query on the client for a mutation", async () => { + const result = { data: { data: "result" } }; + const client: any = {}; + const observable = Observable.of(result); + client.__proto__ = SubscriptionClient.prototype; + client.request = jest.fn(); + client.request.mockReturnValueOnce(observable); + const link = new WebSocketLink(client); + + const obs = execute(link, { query: mutation }); + expect(obs).toEqual(observable); - const link = new WebSocketLink(client); + const stream = new ObservableStream(obs); - execute(link, { query: subscription }).subscribe((data) => { - expect(client.request).toHaveBeenCalledTimes(1); - expect(data).toEqual(results.shift()); - if (results.length === 0) { - resolve(); - } + await expect(stream).toEmitValue(result); + expect(client.request).toHaveBeenCalledTimes(1); + }); + + it("should call request on the subscriptions client for subscription", async () => { + const result = { data: { data: "result" } }; + const client: any = {}; + const observable = Observable.of(result); + client.__proto__ = SubscriptionClient.prototype; + client.request = jest.fn(); + client.request.mockReturnValueOnce(observable); + const link = new WebSocketLink(client); + + const obs = execute(link, { query: subscription }); + expect(obs).toEqual(observable); + + const stream = new ObservableStream(obs); + + await expect(stream).toEmitValue(result); + expect(client.request).toHaveBeenCalledTimes(1); + }); + + it("should call next with multiple results for subscription", async () => { + const results = [ + { data: { data: "result1" } }, + { data: { data: "result2" } }, + ]; + const client: any = {}; + client.__proto__ = SubscriptionClient.prototype; + client.request = jest.fn(() => { + const copy = [...results]; + return new Observable((observer) => { + observer.next(copy[0]); + observer.next(copy[1]); }); - } - ); + }); + + const link = new WebSocketLink(client); + + const observable = execute(link, { query: subscription }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue(results.shift()); + await expect(stream).toEmitValue(results.shift()); + + expect(client.request).toHaveBeenCalledTimes(1); + expect(results).toHaveLength(0); + }); }); diff --git a/src/react/context/__tests__/ApolloConsumer.test.tsx b/src/react/context/__tests__/ApolloConsumer.test.tsx index aed5384c415..a27a02d782f 100644 --- a/src/react/context/__tests__/ApolloConsumer.test.tsx +++ b/src/react/context/__tests__/ApolloConsumer.test.tsx @@ -7,7 +7,6 @@ import { InMemoryCache as Cache } from "../../../cache"; import { ApolloProvider } from "../ApolloProvider"; import { ApolloConsumer } from "../ApolloConsumer"; import { getApolloContext } from "../ApolloContext"; -import { itAsync } from "../../../testing"; const client = new ApolloClient({ cache: new Cache(), @@ -15,17 +14,13 @@ const client = new ApolloClient({ }); describe(" component", () => { - itAsync("has a render prop", (resolve, reject) => { + it("has a render prop", (done) => { render( {(clientRender) => { - try { - expect(clientRender).toBe(client); - resolve(); - } catch (e) { - reject(e); - } + expect(clientRender).toBe(client); + done(); return null; }} diff --git a/src/react/hoc/__tests__/ssr/getDataFromTree.test.tsx b/src/react/hoc/__tests__/ssr/getDataFromTree.test.tsx index 70daf897951..dc250d8f56d 100644 --- a/src/react/hoc/__tests__/ssr/getDataFromTree.test.tsx +++ b/src/react/hoc/__tests__/ssr/getDataFromTree.test.tsx @@ -8,7 +8,7 @@ import { DocumentNode } from "graphql"; import { ApolloClient, TypedDocumentNode } from "../../../../core"; import { ApolloProvider } from "../../../context"; import { InMemoryCache as Cache } from "../../../../cache"; -import { itAsync, mockSingleLink } from "../../../../testing"; +import { mockSingleLink } from "../../../../testing"; import { Query } from "../../../components"; import { getDataFromTree, getMarkupFromTree } from "../../../ssr"; import { graphql } from "../../graphql"; @@ -543,86 +543,78 @@ describe("SSR", () => { }); }); - itAsync( - "should allow for setting state in a component", - (resolve, reject) => { - const query = gql` - query user($id: ID) { - currentUser(id: $id) { - firstName - } + it("should allow for setting state in a component", async () => { + const query = gql` + query user($id: ID) { + currentUser(id: $id) { + firstName } - `; - const resultData = { currentUser: { firstName: "James" } }; - const variables = { id: "1" }; - const link = mockSingleLink({ - request: { query, variables }, - result: { data: resultData }, - }); - - const cache = new Cache({ addTypename: false }); - const apolloClient = new ApolloClient({ - link, - cache, - }); - - interface Props { - id: string; } - interface Data { - currentUser: { - firstName: string; + `; + const resultData = { currentUser: { firstName: "James" } }; + const variables = { id: "1" }; + const link = mockSingleLink({ + request: { query, variables }, + result: { data: resultData }, + }); + + const cache = new Cache({ addTypename: false }); + const apolloClient = new ApolloClient({ + link, + cache, + }); + + interface Props { + id: string; + } + interface Data { + currentUser: { + firstName: string; + }; + } + interface Variables { + id: string; + } + + class Element extends React.Component< + ChildProps, + { thing: number } + > { + state = { thing: 1 }; + + static getDerivedStateFromProps() { + return { + thing: 2, }; } - interface Variables { - id: string; - } - class Element extends React.Component< - ChildProps, - { thing: number } - > { - state = { thing: 1 }; + render() { + const { data } = this.props; + expect(this.state.thing).toBe(2); + return ( +
+ {!data || data.loading || !data.currentUser ? + "loading" + : data.currentUser.firstName} +
+ ); + } + } - static getDerivedStateFromProps() { - return { - thing: 2, - }; - } + const ElementWithData = graphql(query)(Element); - render() { - const { data } = this.props; - expect(this.state.thing).toBe(2); - return ( -
- {!data || data.loading || !data.currentUser ? - "loading" - : data.currentUser.firstName} -
- ); - } - } + const app = ( + + + + ); - const ElementWithData = graphql(query)(Element); + await getDataFromTree(app); - const app = ( - - - - ); - - getDataFromTree(app) - .then(() => { - const initialState = cache.extract(); - expect(initialState).toBeTruthy(); - expect( - initialState.ROOT_QUERY!['currentUser({"id":"1"})'] - ).toBeTruthy(); - resolve(); - }) - .catch(console.error); - } - ); + const initialState = cache.extract(); + expect(initialState).toBeTruthy(); + expect(initialState.ROOT_QUERY!['currentUser({"id":"1"})']).toBeTruthy(); + }); it("should correctly initialize an empty state to null", () => { class Element extends React.Component { @@ -651,7 +643,7 @@ describe("SSR", () => { return getDataFromTree(); }); - itAsync("should allow prepping state from props", (resolve, reject) => { + it("should allow prepping state from props", async () => { const query = gql` query user($id: ID) { currentUser(id: $id) { @@ -730,16 +722,11 @@ describe("SSR", () => {
); - getDataFromTree(app) - .then(() => { - const initialState = apolloClient.cache.extract(); - expect(initialState).toBeTruthy(); - expect( - initialState.ROOT_QUERY!['currentUser({"id":"1"})'] - ).toBeTruthy(); - resolve(); - }) - .catch(console.error); + await getDataFromTree(app); + + const initialState = apolloClient.cache.extract(); + expect(initialState).toBeTruthy(); + expect(initialState.ROOT_QUERY!['currentUser({"id":"1"})']).toBeTruthy(); }); it("shouldn't run queries if ssr is turned to off", () => { diff --git a/src/react/hooks/__tests__/useMutation.test.tsx b/src/react/hooks/__tests__/useMutation.test.tsx index 8e81130201f..989c96294cc 100644 --- a/src/react/hooks/__tests__/useMutation.test.tsx +++ b/src/react/hooks/__tests__/useMutation.test.tsx @@ -19,7 +19,6 @@ import { } from "../../../core"; import { InMemoryCache } from "../../../cache"; import { - itAsync, MockedProvider, MockSubscriptionLink, mockSingleLink, @@ -1500,7 +1499,7 @@ describe("useMutation Hook", () => { await waitFor(() => expect(variablesMatched).toBe(true)); }); - itAsync("should be called with the provided context", (resolve, reject) => { + it("should be called with the provided context", async () => { const context = { id: 3 }; const variables = { @@ -1544,13 +1543,13 @@ describe("useMutation Hook", () => { ); - return waitFor(() => { + await waitFor(() => { expect(foundContext).toBe(true); - }).then(resolve, reject); + }); }); describe("If context is not provided", () => { - itAsync("should be undefined", (resolve, reject) => { + it("should be undefined", async () => { const variables = { description: "Get milk!", }; @@ -1587,92 +1586,89 @@ describe("useMutation Hook", () => { ); - return waitFor(() => { + await waitFor(() => { expect(checkedContext).toBe(true); - }).then(resolve, reject); + }); }); }); }); describe("Optimistic response", () => { - itAsync( - "should support optimistic response handling", - async (resolve, reject) => { - const optimisticResponse = { - __typename: "Mutation", - createTodo: { - id: 1, - description: "TEMPORARY", - priority: "High", - __typename: "Todo", - }, - }; + it("should support optimistic response handling", async () => { + const optimisticResponse = { + __typename: "Mutation", + createTodo: { + id: 1, + description: "TEMPORARY", + priority: "High", + __typename: "Todo", + }, + }; - const variables = { - description: "Get milk!", - }; + const variables = { + description: "Get milk!", + }; - const mocks = [ - { - request: { - query: CREATE_TODO_MUTATION, - variables, - }, - result: { data: CREATE_TODO_RESULT }, + const mocks = [ + { + request: { + query: CREATE_TODO_MUTATION, + variables, }, - ]; + result: { data: CREATE_TODO_RESULT }, + }, + ]; - const link = mockSingleLink(...mocks).setOnError(reject); - const cache = new InMemoryCache(); - const client = new ApolloClient({ - cache, - link, - }); + const link = mockSingleLink(...mocks); + const cache = new InMemoryCache(); + const client = new ApolloClient({ + cache, + link, + }); - let renderCount = 0; - const Component = () => { - const [createTodo, { loading, data }] = useMutation( - CREATE_TODO_MUTATION, - { optimisticResponse } - ); + let renderCount = 0; + const Component = () => { + const [createTodo, { loading, data }] = useMutation( + CREATE_TODO_MUTATION, + { optimisticResponse } + ); - switch (renderCount) { - case 0: - expect(loading).toBeFalsy(); - expect(data).toBeUndefined(); - void createTodo({ variables }); - - const dataInStore = client.cache.extract(true); - expect(dataInStore["Todo:1"]).toEqual( - optimisticResponse.createTodo - ); - - break; - case 1: - expect(loading).toBeTruthy(); - expect(data).toBeUndefined(); - break; - case 2: - expect(loading).toBeFalsy(); - expect(data).toEqual(CREATE_TODO_RESULT); - break; - default: - } - renderCount += 1; - return null; - }; + switch (renderCount) { + case 0: + expect(loading).toBeFalsy(); + expect(data).toBeUndefined(); + createTodo({ variables }); + + const dataInStore = client.cache.extract(true); + expect(dataInStore["Todo:1"]).toEqual( + optimisticResponse.createTodo + ); + + break; + case 1: + expect(loading).toBeTruthy(); + expect(data).toBeUndefined(); + break; + case 2: + expect(loading).toBeFalsy(); + expect(data).toEqual(CREATE_TODO_RESULT); + break; + default: + } + renderCount += 1; + return null; + }; - render( - - - - ); + render( + + + + ); - return waitFor(() => { - expect(renderCount).toBe(3); - }).then(resolve, reject); - } - ); + await waitFor(() => { + expect(renderCount).toBe(3); + }); + }); it("should be called with the provided context", async () => { const optimisticResponse = { diff --git a/src/react/hooks/__tests__/useReactiveVar.test.tsx b/src/react/hooks/__tests__/useReactiveVar.test.tsx index c78d7e6ec80..d8d5743cbe6 100644 --- a/src/react/hooks/__tests__/useReactiveVar.test.tsx +++ b/src/react/hooks/__tests__/useReactiveVar.test.tsx @@ -1,7 +1,6 @@ import React, { StrictMode, useEffect } from "react"; import { screen, render, waitFor, act } from "@testing-library/react"; -import { itAsync } from "../../../testing"; import { makeVar } from "../../../core"; import { useReactiveVar } from "../useReactiveVar"; @@ -47,92 +46,87 @@ describe("useReactiveVar Hook", () => { }); }); - itAsync( - "works when two components share a variable", - async (resolve, reject) => { - const counterVar = makeVar(0); - - let parentRenderCount = 0; - function Parent() { - const count = useReactiveVar(counterVar); + it("works when two components share a variable", async () => { + const counterVar = makeVar(0); - switch (++parentRenderCount) { - case 1: - expect(count).toBe(0); - break; - case 2: - expect(count).toBe(1); - break; - case 3: - expect(count).toBe(11); - break; - default: - reject(`too many (${parentRenderCount}) parent renders`); - } + let parentRenderCount = 0; + function Parent() { + const count = useReactiveVar(counterVar); - return ; + switch (++parentRenderCount) { + case 1: + expect(count).toBe(0); + break; + case 2: + expect(count).toBe(1); + break; + case 3: + expect(count).toBe(11); + break; + default: + throw new Error(`too many (${parentRenderCount}) parent renders`); } - let childRenderCount = 0; - function Child() { - const count = useReactiveVar(counterVar); + return ; + } - switch (++childRenderCount) { - case 1: - expect(count).toBe(0); - break; - case 2: - expect(count).toBe(1); - break; - case 3: - expect(count).toBe(11); - break; - default: - reject(`too many (${childRenderCount}) child renders`); - } + let childRenderCount = 0; + function Child() { + const count = useReactiveVar(counterVar); - return null; + switch (++childRenderCount) { + case 1: + expect(count).toBe(0); + break; + case 2: + expect(count).toBe(1); + break; + case 3: + expect(count).toBe(11); + break; + default: + throw new Error(`too many (${childRenderCount}) child renders`); } - render(); + return null; + } - await waitFor(() => { - expect(parentRenderCount).toBe(1); - }); + render(); - await waitFor(() => { - expect(childRenderCount).toBe(1); - }); + await waitFor(() => { + expect(parentRenderCount).toBe(1); + }); - expect(counterVar()).toBe(0); - act(() => { - counterVar(1); - }); + await waitFor(() => { + expect(childRenderCount).toBe(1); + }); - await waitFor(() => { - expect(parentRenderCount).toBe(2); - }); - await waitFor(() => { - expect(childRenderCount).toBe(2); - }); + expect(counterVar()).toBe(0); + act(() => { + counterVar(1); + }); - expect(counterVar()).toBe(1); - act(() => { - counterVar(counterVar() + 10); - }); + await waitFor(() => { + expect(parentRenderCount).toBe(2); + }); + await waitFor(() => { + expect(childRenderCount).toBe(2); + }); - await waitFor(() => { - expect(parentRenderCount).toBe(3); - }); - await waitFor(() => { - expect(childRenderCount).toBe(3); - }); + expect(counterVar()).toBe(1); + act(() => { + counterVar(counterVar() + 10); + }); - expect(counterVar()).toBe(11); + await waitFor(() => { + expect(parentRenderCount).toBe(3); + }); + await waitFor(() => { + expect(childRenderCount).toBe(3); + }); - resolve(); - } - ); + expect(counterVar()).toBe(11); + }); it("does not update if component has been unmounted", async () => { const counterVar = makeVar(0); @@ -252,7 +246,7 @@ describe("useReactiveVar Hook", () => { }); }); - itAsync("works with strict mode", async (resolve, reject) => { + it("works with strict mode", async () => { const counterVar = makeVar(0); const mock = jest.fn(); @@ -289,94 +283,84 @@ describe("useReactiveVar Hook", () => { expect(mock).toHaveBeenNthCalledWith(2, 1); } }); - - resolve(); }); - itAsync( - "works with multiple synchronous calls", - async (resolve, reject) => { - const counterVar = makeVar(0); - function Component() { - const count = useReactiveVar(counterVar); + it("works with multiple synchronous calls", async () => { + const counterVar = makeVar(0); + function Component() { + const count = useReactiveVar(counterVar); - return
{count}
; - } + return
{count}
; + } - render(); - void Promise.resolve().then(() => { - counterVar(1); - counterVar(2); - counterVar(3); - counterVar(4); - counterVar(5); - counterVar(6); - counterVar(7); - counterVar(8); - counterVar(9); - counterVar(10); - }); - - await waitFor(() => { - expect(screen.getAllByText("10")).toHaveLength(1); - }); - - resolve(); + render(); + Promise.resolve().then(() => { + counterVar(1); + counterVar(2); + counterVar(3); + counterVar(4); + counterVar(5); + counterVar(6); + counterVar(7); + counterVar(8); + counterVar(9); + counterVar(10); + }); + + await waitFor(() => { + expect(screen.getAllByText("10")).toHaveLength(1); + }); + }); + + it("should survive many rerenderings despite racing asynchronous updates", (done) => { + const rv = makeVar(0); + + function App() { + const value = useReactiveVar(rv); + return ( +
+

{value}

+
+ ); } - ); - - itAsync( - "should survive many rerenderings despite racing asynchronous updates", - (resolve, reject) => { - const rv = makeVar(0); - - function App() { - const value = useReactiveVar(rv); - return ( -
-

{value}

-
- ); - } - const goalCount = 1000; - let updateCount = 0; - let stopped = false; - - function spam() { - if (stopped) return; - try { - if (++updateCount <= goalCount) { - act(() => { - rv(updateCount); - setTimeout(spam, Math.random() * 10); - }); - } else { - stopped = true; - expect(rv()).toBe(goalCount); - screen - .findByText(String(goalCount)) - .then((element) => { - expect(element.nodeName.toLowerCase()).toBe("h1"); - }) - .then(resolve, reject); - } - } catch (e) { + const goalCount = 1000; + let updateCount = 0; + let stopped = false; + + function spam() { + if (stopped) return; + try { + if (++updateCount <= goalCount) { + act(() => { + rv(updateCount); + setTimeout(spam, Math.random() * 10); + }); + } else { stopped = true; - reject(e); + expect(rv()).toBe(goalCount); + screen + .findByText(String(goalCount)) + .then((element) => { + expect(element.nodeName.toLowerCase()).toBe("h1"); + }) + .then(done); } + } catch (e) { + stopped = true; + throw e; } - spam(); - spam(); - spam(); - spam(); - - render( - - - - ); } - ); + spam(); + spam(); + spam(); + spam(); + + render( + + + + ); + }); }); }); diff --git a/src/testing/core/observableToPromise.ts b/src/testing/core/observableToPromise.ts deleted file mode 100644 index 428517e1aff..00000000000 --- a/src/testing/core/observableToPromise.ts +++ /dev/null @@ -1,103 +0,0 @@ -import type { ObservableQuery, ApolloQueryResult } from "../../core/index.js"; -import type { ObservableSubscription } from "../../utilities/index.js"; - -export interface Options { - /** - * The ObservableQuery to subscribe to. - */ - observable: ObservableQuery; - /** - * Should we resolve after seeing all our callbacks? [default: true] - * (use this if you are racing the promise against another) - */ - shouldResolve?: boolean; - /** - * How long to wait after seeing desired callbacks before resolving? - * [default: -1 => don't wait] - */ - wait?: number; - /** - * An expected set of errors. - */ - errorCallbacks?: ((error: Error) => any)[]; -} - -export type ResultCallback = (result: ApolloQueryResult) => any; - -// Take an observable and N callbacks, and observe the observable, -// ensuring it is called exactly N times, resolving once it has done so. -// Optionally takes a timeout, which it will wait X ms after the Nth callback -// to ensure it is not called again. -export function observableToPromiseAndSubscription( - { observable, shouldResolve = true, wait = -1, errorCallbacks = [] }: Options, - ...cbs: ResultCallback[] -): { promise: Promise; subscription: ObservableSubscription } { - let subscription: ObservableSubscription = null as never; - const promise = new Promise((resolve, reject) => { - let errorIndex = 0; - let cbIndex = 0; - const results: any[] = []; - - const tryToResolve = () => { - if (!shouldResolve) { - return; - } - - const done = () => { - subscription.unsubscribe(); - // XXX: we could pass a few other things out here? - resolve(results); - }; - - if (cbIndex === cbs.length && errorIndex === errorCallbacks.length) { - if (wait === -1) { - done(); - } else { - setTimeout(done, wait); - } - } - }; - - let queue = Promise.resolve(); - - subscription = observable.subscribe({ - next(result: ApolloQueryResult) { - queue = queue - .then(() => { - const cb = cbs[cbIndex++]; - if (cb) return cb(result); - reject( - new Error( - `Observable 'next' method called more than ${cbs.length} times` - ) - ); - }) - .then((res) => { - results.push(res); - tryToResolve(); - }, reject); - }, - error(error: Error) { - queue = queue - .then(() => { - const errorCb = errorCallbacks[errorIndex++]; - if (errorCb) return errorCb(error); - reject(error); - }) - .then(tryToResolve, reject); - }, - }); - }); - - return { - promise, - subscription, - }; -} - -export default function ( - options: Options, - ...cbs: ResultCallback[] -): Promise { - return observableToPromiseAndSubscription(options, ...cbs).promise; -} diff --git a/src/testing/internal/ObservableStream.ts b/src/testing/internal/ObservableStream.ts index 63f550827c6..f6c53169b87 100644 --- a/src/testing/internal/ObservableStream.ts +++ b/src/testing/internal/ObservableStream.ts @@ -1,4 +1,7 @@ -import type { Observable } from "../../utilities/index.js"; +import type { + Observable, + ObservableSubscription, +} from "../../utilities/index.js"; import { ReadableStream } from "node:stream/web"; export interface TakeOptions { @@ -11,10 +14,12 @@ type ObservableEvent = export class ObservableStream { private reader: ReadableStreamDefaultReader>; + private subscription!: ObservableSubscription; + constructor(observable: Observable) { this.reader = new ReadableStream>({ - start(controller) { - observable.subscribe( + start: (controller) => { + this.subscription = observable.subscribe( (value) => controller.enqueue({ type: "next", value }), (error) => controller.enqueue({ type: "error", error }), () => controller.enqueue({ type: "complete" }) @@ -36,6 +41,10 @@ export class ObservableStream { ]); } + unsubscribe() { + this.subscription.unsubscribe(); + } + async takeNext(options?: TakeOptions): Promise { const event = await this.take(options); expect(event).toEqual({ type: "next", value: expect.anything() }); diff --git a/src/testing/matchers/toEmitError.ts b/src/testing/matchers/toEmitError.ts index 75e93aa56f2..f488e6f0de4 100644 --- a/src/testing/matchers/toEmitError.ts +++ b/src/testing/matchers/toEmitError.ts @@ -1,7 +1,15 @@ -import type { MatcherFunction } from "expect"; +import type { MatcherFunction, MatcherContext } from "expect"; import type { ObservableStream } from "../internal/index.js"; import type { TakeOptions } from "../internal/ObservableStream.js"; +function isErrorEqual(this: MatcherContext, expected: any, actual: any) { + if (typeof expected === "string" && actual instanceof Error) { + return actual.message === expected; + } + + return this.equals(expected, actual, this.customTesters); +} + export const toEmitError: MatcherFunction< [value?: any, options?: TakeOptions] > = async function (actual, expected, options) { @@ -15,9 +23,7 @@ export const toEmitError: MatcherFunction< try { const error = await stream.takeError(options); const pass = - expected === undefined ? true : ( - this.equals(expected, error, this.customTesters) - ); + expected === undefined ? true : isErrorEqual.call(this, expected, error); return { pass, @@ -37,7 +43,7 @@ export const toEmitError: MatcherFunction< "\n\n" + this.utils.printDiffOrStringify( expected, - error, + typeof expected === "string" ? error.message : error, "Expected", "Recieved", true diff --git a/src/testing/react/__tests__/MockedProvider.test.tsx b/src/testing/react/__tests__/MockedProvider.test.tsx index f4a5caed150..8751429cce9 100644 --- a/src/testing/react/__tests__/MockedProvider.test.tsx +++ b/src/testing/react/__tests__/MockedProvider.test.tsx @@ -3,7 +3,7 @@ import { DocumentNode } from "graphql"; import { act, render, screen, waitFor } from "@testing-library/react"; import gql from "graphql-tag"; -import { itAsync, MockedResponse, MockLink } from "../../core"; +import { MockedResponse, MockLink } from "../../core"; import { MockedProvider } from "../MockedProvider"; import { useQuery } from "../../../react/hooks"; import { InMemoryCache } from "../../../cache"; @@ -82,7 +82,7 @@ describe("General use", () => { errorThrown = false; }); - itAsync("should mock the data", (resolve, reject) => { + it("should mock the data", async () => { let finished = false; function Component({ username }: Variables) { const { loading, data } = useQuery(query, { variables }); @@ -99,106 +99,97 @@ describe("General use", () => { ); - waitFor(() => { + await waitFor(() => { expect(finished).toBe(true); - }).then(resolve, reject); + }); }); - itAsync( - "should pass the variables to the result function", - async (resolve, reject) => { - function Component({ ...variables }: Variables) { - useQuery(query, { variables }); - return null; - } + it("should pass the variables to the result function", async () => { + function Component({ ...variables }: Variables) { + useQuery(query, { variables }); + return null; + } - const mock2: MockedResponse = { - request: { - query, - variables, - }, - result: jest.fn().mockResolvedValue({ data: { user } }), - }; + const mock2: MockedResponse = { + request: { + query, + variables, + }, + result: jest.fn().mockResolvedValue({ data: { user } }), + }; - render( - - - - ); + render( + + + + ); - waitFor(() => { - expect(mock2.result as jest.Mock).toHaveBeenCalledWith(variables); - }).then(resolve, reject); + await waitFor(() => { + expect(mock2.result as jest.Mock).toHaveBeenCalledWith(variables); + }); + }); + + it("should pass the variables to the variableMatcher", async () => { + function Component({ ...variables }: Variables) { + useQuery(query, { variables }); + return null; } - ); - - itAsync( - "should pass the variables to the variableMatcher", - async (resolve, reject) => { - function Component({ ...variables }: Variables) { - useQuery(query, { variables }); - return null; - } - const mock2: MockedResponse = { - request: { - query, - }, - variableMatcher: jest.fn().mockReturnValue(true), - result: { data: { user } }, - }; + const mock2: MockedResponse = { + request: { + query, + }, + variableMatcher: jest.fn().mockReturnValue(true), + result: { data: { user } }, + }; - render( - - - - ); + render( + + + + ); - waitFor(() => { - expect(mock2.variableMatcher as jest.Mock).toHaveBeenCalledWith( - variables - ); - }).then(resolve, reject); - } - ); + await waitFor(() => { + expect(mock2.variableMatcher as jest.Mock).toHaveBeenCalledWith( + variables + ); + }); + }); - itAsync( - "should use a mock if the variableMatcher returns true", - async (resolve, reject) => { - let finished = false; + it("should use a mock if the variableMatcher returns true", async () => { + let finished = false; - function Component({ username }: Variables) { - const { loading, data } = useQuery(query, { - variables, - }); - if (!loading) { - expect(data!.user).toMatchSnapshot(); - finished = true; - } - return null; + function Component({ username }: Variables) { + const { loading, data } = useQuery(query, { + variables, + }); + if (!loading) { + expect(data!.user).toMatchSnapshot(); + finished = true; } + return null; + } - const mock2: MockedResponse = { - request: { - query, - }, - variableMatcher: (v) => v.username === variables.username, - result: { data: { user } }, - }; + const mock2: MockedResponse = { + request: { + query, + }, + variableMatcher: (v) => v.username === variables.username, + result: { data: { user } }, + }; - render( - - - - ); + render( + + + + ); - waitFor(() => { - expect(finished).toBe(true); - }).then(resolve, reject); - } - ); + await waitFor(() => { + expect(finished).toBe(true); + }); + }); - itAsync("should allow querying with the typename", (resolve, reject) => { + it("should allow querying with the typename", async () => { let finished = false; function Component({ username }: Variables) { const { loading, data } = useQuery(query, { variables }); @@ -225,12 +216,12 @@ describe("General use", () => { ); - waitFor(() => { + await waitFor(() => { expect(finished).toBe(true); - }).then(resolve, reject); + }); }); - itAsync("should allow using a custom cache", (resolve, reject) => { + it("should allow using a custom cache", async () => { let finished = false; const cache = new InMemoryCache(); cache.writeQuery({ @@ -254,169 +245,157 @@ describe("General use", () => { ); - waitFor(() => { + await waitFor(() => { expect(finished).toBe(true); - }).then(resolve, reject); + }); }); - itAsync( - "should error if the variables in the mock and component do not match", - (resolve, reject) => { - let finished = false; - function Component({ ...variables }: Variables) { - const { loading, error } = useQuery(query, { - variables, - }); - if (!loading) { - expect(error).toMatchSnapshot(); - finished = true; - } - return null; + it("should error if the variables in the mock and component do not match", async () => { + let finished = false; + function Component({ ...variables }: Variables) { + const { loading, error } = useQuery(query, { + variables, + }); + if (!loading) { + expect(error).toMatchSnapshot(); + finished = true; } + return null; + } - const variables2 = { - username: "other_user", - age: undefined, - }; + const variables2 = { + username: "other_user", + age: undefined, + }; - render( - - - - ); + render( + + + + ); - waitFor(() => { - expect(finished).toBe(true); - }).then(resolve, reject); - } - ); - - itAsync( - "should error if the variableMatcher returns false", - async (resolve, reject) => { - let finished = false; - function Component({ ...variables }: Variables) { - const { loading, error } = useQuery(query, { - variables, - }); - if (!loading) { - expect(error).toMatchSnapshot(); - finished = true; - } - return null; + await waitFor(() => { + expect(finished).toBe(true); + }); + }); + + it("should error if the variableMatcher returns false", async () => { + let finished = false; + function Component({ ...variables }: Variables) { + const { loading, error } = useQuery(query, { + variables, + }); + if (!loading) { + expect(error).toMatchSnapshot(); + finished = true; } + return null; + } - const mock2: MockedResponse = { - request: { - query, - }, - variableMatcher: () => false, - result: { data: { user } }, - }; + const mock2: MockedResponse = { + request: { + query, + }, + variableMatcher: () => false, + result: { data: { user } }, + }; - render( - - - - ); + render( + + + + ); - waitFor(() => { - expect(finished).toBe(true); - }).then(resolve, reject); - } - ); - - itAsync( - "should error if the variables do not deep equal", - (resolve, reject) => { - let finished = false; - function Component({ ...variables }: Variables) { - const { loading, error } = useQuery(query, { - variables, - }); - if (!loading) { - expect(error).toMatchSnapshot(); - finished = true; - } - return null; + await waitFor(() => { + expect(finished).toBe(true); + }); + }); + + it("should error if the variables do not deep equal", async () => { + let finished = false; + function Component({ ...variables }: Variables) { + const { loading, error } = useQuery(query, { + variables, + }); + if (!loading) { + expect(error).toMatchSnapshot(); + finished = true; } + return null; + } - const mocks2 = [ - { - request: { - query, - variables: { - age: 13, - username: "some_user", - }, + const mocks2 = [ + { + request: { + query, + variables: { + age: 13, + username: "some_user", }, - result: { data: { user } }, }, - ]; + result: { data: { user } }, + }, + ]; - const variables2 = { - username: "some_user", - age: 42, - }; + const variables2 = { + username: "some_user", + age: 42, + }; - render( - - - - ); + render( + + + + ); - waitFor(() => { - expect(finished).toBe(true); - }).then(resolve, reject); - } - ); - - itAsync( - "should not error if the variables match but have different order", - (resolve, reject) => { - let finished = false; - function Component({ ...variables }: Variables) { - const { loading, data } = useQuery(query, { - variables, - }); - if (!loading) { - expect(data).toMatchSnapshot(); - finished = true; - } - return null; + await waitFor(() => { + expect(finished).toBe(true); + }); + }); + + it("should not error if the variables match but have different order", async () => { + let finished = false; + function Component({ ...variables }: Variables) { + const { loading, data } = useQuery(query, { + variables, + }); + if (!loading) { + expect(data).toMatchSnapshot(); + finished = true; } + return null; + } - const mocks2 = [ - { - request: { - query, - variables: { - age: 13, - username: "some_user", - }, + const mocks2 = [ + { + request: { + query, + variables: { + age: 13, + username: "some_user", }, - result: { data: { user } }, }, - ]; + result: { data: { user } }, + }, + ]; - const variables2 = { - username: "some_user", - age: 13, - }; + const variables2 = { + username: "some_user", + age: 13, + }; - render( - - - - ); + render( + + + + ); - waitFor(() => { - expect(finished).toBe(true); - }).then(resolve, reject); - } - ); + await waitFor(() => { + expect(finished).toBe(true); + }); + }); - itAsync("should support mocking a network error", (resolve, reject) => { + it("should support mocking a network error", async () => { let finished = false; function Component({ ...variables }: Variables) { const { loading, error } = useQuery(query, { @@ -447,53 +426,50 @@ describe("General use", () => { ); - waitFor(() => { + await waitFor(() => { expect(finished).toBe(true); - }).then(resolve, reject); + }); }); - itAsync( - "should error if the query in the mock and component do not match", - (resolve, reject) => { - let finished = false; - function Component({ ...variables }: Variables) { - const { loading, error } = useQuery(query, { - variables, - }); - if (!loading) { - expect(error).toMatchSnapshot(); - finished = true; - } - return null; + it("should error if the query in the mock and component do not match", async () => { + let finished = false; + function Component({ ...variables }: Variables) { + const { loading, error } = useQuery(query, { + variables, + }); + if (!loading) { + expect(error).toMatchSnapshot(); + finished = true; } + return null; + } - const mocksDifferentQuery = [ - { - request: { - query: gql` - query OtherQuery { - otherQuery { - id - } + const mocksDifferentQuery = [ + { + request: { + query: gql` + query OtherQuery { + otherQuery { + id } - `, - variables, - }, - result: { data: { user } }, + } + `, + variables, }, - ]; + result: { data: { user } }, + }, + ]; - render( - - - - ); + render( + + + + ); - waitFor(() => { - expect(finished).toBe(true); - }).then(resolve, reject); - } - ); + await waitFor(() => { + expect(finished).toBe(true); + }); + }); it("should pass down props prop in mock as props for the component", () => { function Component({ ...variables }) { @@ -523,71 +499,68 @@ describe("General use", () => { unmount(); }); - itAsync( - "should support returning mocked results from a function", - (resolve, reject) => { - let finished = false; - let resultReturned = false; + it("should support returning mocked results from a function", async () => { + let finished = false; + let resultReturned = false; - const testUser = { - __typename: "User", - id: 12345, - }; + const testUser = { + __typename: "User", + id: 12345, + }; - function Component({ ...variables }: Variables) { - const { loading, data } = useQuery(query, { - variables, - }); - if (!loading) { - expect(data!.user).toEqual(testUser); - expect(resultReturned).toBe(true); - finished = true; - } - return null; + function Component({ ...variables }: Variables) { + const { loading, data } = useQuery(query, { + variables, + }); + if (!loading) { + expect(data!.user).toEqual(testUser); + expect(resultReturned).toBe(true); + finished = true; } + return null; + } - const testQuery: DocumentNode = gql` - query GetUser($username: String!) { - user(username: $username) { - id - } + const testQuery: DocumentNode = gql` + query GetUser($username: String!) { + user(username: $username) { + id } - `; + } + `; - const testVariables = { - username: "jsmith", - }; - const testMocks = [ - { - request: { - query: testQuery, - variables: testVariables, - }, - result() { - resultReturned = true; - return { - data: { - user: { - __typename: "User", - id: 12345, - }, + const testVariables = { + username: "jsmith", + }; + const testMocks = [ + { + request: { + query: testQuery, + variables: testVariables, + }, + result() { + resultReturned = true; + return { + data: { + user: { + __typename: "User", + id: 12345, }, - }; - }, + }, + }; }, - ]; + }, + ]; - render( - - - - ); + render( + + + + ); - waitFor(() => { - expect(finished).toBe(true); - }).then(resolve, reject); - } - ); + await waitFor(() => { + expect(finished).toBe(true); + }); + }); it('should return "No more mocked responses" errors in response', async () => { let finished = false; @@ -1028,66 +1001,60 @@ describe("General use", () => { consoleSpy.mockRestore(); }); - itAsync( - "should support custom error handling using setOnError", - (resolve, reject) => { - let finished = false; - function Component({ ...variables }: Variables) { - useQuery(query, { variables }); - return null; - } + it("should support custom error handling using setOnError", async () => { + let finished = false; + function Component({ ...variables }: Variables) { + useQuery(query, { variables }); + return null; + } - const mockLink = new MockLink([], true, { showWarnings: false }); - mockLink.setOnError((error) => { - expect(error).toMatchSnapshot(); - finished = true; - }); - const link = ApolloLink.from([errorLink, mockLink]); + const mockLink = new MockLink([], true, { showWarnings: false }); + mockLink.setOnError((error) => { + expect(error).toMatchSnapshot(); + finished = true; + }); + const link = ApolloLink.from([errorLink, mockLink]); - render( - - - - ); + render( + + + + ); - waitFor(() => { - expect(finished).toBe(true); - }).then(resolve, reject); - } - ); - - itAsync( - "should pipe exceptions thrown in custom onError functions through the link chain", - (resolve, reject) => { - let finished = false; - function Component({ ...variables }: Variables) { - const { loading, error } = useQuery(query, { - variables, - }); - if (!loading) { - expect(error).toMatchSnapshot(); - finished = true; - } - return null; - } + await waitFor(() => { + expect(finished).toBe(true); + }); + }); - const mockLink = new MockLink([], true, { showWarnings: false }); - mockLink.setOnError(() => { - throw new Error("oh no!"); + it("should pipe exceptions thrown in custom onError functions through the link chain", async () => { + let finished = false; + function Component({ ...variables }: Variables) { + const { loading, error } = useQuery(query, { + variables, }); - const link = ApolloLink.from([errorLink, mockLink]); + if (!loading) { + expect(error).toMatchSnapshot(); + finished = true; + } + return null; + } - render( - - - - ); + const mockLink = new MockLink([], true, { showWarnings: false }); + mockLink.setOnError(() => { + throw new Error("oh no!"); + }); + const link = ApolloLink.from([errorLink, mockLink]); - waitFor(() => { - expect(finished).toBe(true); - }).then(resolve, reject); - } - ); + render( + + + + ); + + await waitFor(() => { + expect(finished).toBe(true); + }); + }); it("should support loading state testing with delay", async () => { jest.useFakeTimers(); @@ -1224,100 +1191,94 @@ describe("General use", () => { }); describe("@client testing", () => { - itAsync( - "should support @client fields with a custom cache", - (resolve, reject) => { - let finished = false; - const cache = new InMemoryCache(); - - cache.writeQuery({ - query: gql` - { - networkStatus { - isOnline - } + it("should support @client fields with a custom cache", async () => { + let finished = false; + const cache = new InMemoryCache(); + + cache.writeQuery({ + query: gql` + { + networkStatus { + isOnline } - `, - data: { - networkStatus: { - __typename: "NetworkStatus", - isOnline: true, - }, + } + `, + data: { + networkStatus: { + __typename: "NetworkStatus", + isOnline: true, }, - }); + }, + }); - function Component() { - const { loading, data } = useQuery(gql` - { - networkStatus @client { - isOnline - } + function Component() { + const { loading, data } = useQuery(gql` + { + networkStatus @client { + isOnline } - `); - if (!loading) { - expect(data!.networkStatus.__typename).toEqual("NetworkStatus"); - expect(data!.networkStatus.isOnline).toEqual(true); - finished = true; } - return null; + `); + if (!loading) { + expect(data!.networkStatus.__typename).toEqual("NetworkStatus"); + expect(data!.networkStatus.isOnline).toEqual(true); + finished = true; } + return null; + } - render( - - - - ); + render( + + + + ); - waitFor(() => { - expect(finished).toBe(true); - }).then(resolve, reject); - } - ); - - itAsync( - "should support @client fields with field policies", - (resolve, reject) => { - let finished = false; - const cache = new InMemoryCache({ - typePolicies: { - Query: { - fields: { - networkStatus() { - return { - __typename: "NetworkStatus", - isOnline: true, - }; - }, + await waitFor(() => { + expect(finished).toBe(true); + }); + }); + + it("should support @client fields with field policies", async () => { + let finished = false; + const cache = new InMemoryCache({ + typePolicies: { + Query: { + fields: { + networkStatus() { + return { + __typename: "NetworkStatus", + isOnline: true, + }; }, }, }, - }); + }, + }); - function Component() { - const { loading, data } = useQuery(gql` - { - networkStatus @client { - isOnline - } + function Component() { + const { loading, data } = useQuery(gql` + { + networkStatus @client { + isOnline } - `); - if (!loading) { - expect(data!.networkStatus.__typename).toEqual("NetworkStatus"); - expect(data!.networkStatus.isOnline).toEqual(true); - finished = true; } - return null; + `); + if (!loading) { + expect(data!.networkStatus.__typename).toEqual("NetworkStatus"); + expect(data!.networkStatus.isOnline).toEqual(true); + finished = true; } + return null; + } - render( - - - - ); + render( + + + + ); - waitFor(() => { - expect(finished).toBe(true); - }).then(resolve, reject); - } - ); + await waitFor(() => { + expect(finished).toBe(true); + }); + }); }); diff --git a/src/utilities/observables/__tests__/Concast.ts b/src/utilities/observables/__tests__/Concast.ts index b590cde2fb8..256f6651929 100644 --- a/src/utilities/observables/__tests__/Concast.ts +++ b/src/utilities/observables/__tests__/Concast.ts @@ -1,9 +1,9 @@ -import { itAsync } from "../../../testing/core"; import { Observable, Observer } from "../Observable"; import { Concast, ConcastSourcesIterable } from "../Concast"; +import { ObservableStream } from "../../../testing/internal"; describe("Concast Observable (similar to Behavior Subject in RxJS)", () => { - itAsync("can concatenate other observables", (resolve, reject) => { + it("can concatenate other observables", async () => { const concast = new Concast([ Observable.of(1, 2, 3), Promise.resolve(Observable.of(4, 5)), @@ -12,114 +12,94 @@ describe("Concast Observable (similar to Behavior Subject in RxJS)", () => { Observable.of(11), ]); - const results: number[] = []; - concast.subscribe({ - next(num) { - results.push(num); - }, + const stream = new ObservableStream(concast); + + await expect(stream).toEmitValue(1); + await expect(stream).toEmitValue(2); + await expect(stream).toEmitValue(3); + await expect(stream).toEmitValue(4); + await expect(stream).toEmitValue(5); + await expect(stream).toEmitValue(6); + await expect(stream).toEmitValue(7); + await expect(stream).toEmitValue(8); + await expect(stream).toEmitValue(9); + await expect(stream).toEmitValue(10); + await expect(stream).toEmitValue(11); + await expect(stream).toComplete(); + + const finalResult = await concast.promise; - error: reject, + expect(finalResult).toBe(11); + }); + it("Can tolerate being completed before input Promise resolves", async () => { + let resolvePromise: (sources: ConcastSourcesIterable) => void; + const delayPromise = new Promise>( + (resolve) => { + resolvePromise = resolve; + } + ); + + const concast = new Concast(delayPromise); + const observer = { + next() { + throw new Error("should not have called observer.next"); + }, + error() { + throw new Error("Should not have called observer.error"); + }, complete() { - concast.promise - .then((finalResult) => { - expect(results).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]); - expect(finalResult).toBe(11); - resolve(); - }) - .catch(reject); + throw new Error("should not have called observer.complete"); }, - }); + }; + + concast.addObserver(observer); + concast.removeObserver(observer); + + const finalResult = await concast.promise; + expect(finalResult).toBeUndefined(); + + resolvePromise!([]); + const delayedPromiseResult = await delayPromise; + + expect(delayedPromiseResult).toEqual([]); }); - itAsync( - "Can tolerate being completed before input Promise resolves", - (resolve, reject) => { - let resolvePromise: (sources: ConcastSourcesIterable) => void; - const delayPromise = new Promise>( - (resolve) => { - resolvePromise = resolve; - } - ); - - const concast = new Concast(delayPromise); - const observer = { - next() { - reject(new Error("should not have called observer.next")); - }, - error: reject, - complete() { - reject(new Error("should not have called observer.complete")); - }, - }; - - concast.addObserver(observer); - concast.removeObserver(observer); - - return concast.promise - .then((finalResult) => { - expect(finalResult).toBeUndefined(); - resolvePromise([]); - return delayPromise; - }) - .then((delayedPromiseResult) => { - expect(delayedPromiseResult).toEqual([]); - resolve(); - }) - .catch(reject); - } - ); - - itAsync( - "behaves appropriately if unsubscribed before first result", - (resolve, reject) => { - const concast = new Concast([ - new Promise((resolve) => setTimeout(resolve, 100)).then(() => - Observable.of(1, 2, 3) - ), - ]); + it("behaves appropriately if unsubscribed before first result", async () => { + const concast = new Concast([ + new Promise((resolve) => setTimeout(resolve, 100)).then(() => + Observable.of(1, 2, 3) + ), + ]); - const cleanupCounts = { - first: 0, - second: 0, - }; + const cleanupCounts = { + first: 0, + second: 0, + }; - concast.beforeNext(() => { - ++cleanupCounts.first; - }); + concast.beforeNext(() => { + ++cleanupCounts.first; + }); + const stream = new ObservableStream(concast); - const unsubscribe = concast.subscribe({ - next() { - reject("should not have called observer.next"); - }, - error() { - reject("should not have called observer.error"); - }, - complete() { - reject("should not have called observer.complete"); - }, - }); + concast.beforeNext(() => { + ++cleanupCounts.second; + }); - concast.beforeNext(() => { - ++cleanupCounts.second; - }); + // Immediately unsubscribe the observer we just added, triggering + // completion. + stream.unsubscribe(); - // Immediately unsubscribe the observer we just added, triggering - // completion. - unsubscribe.unsubscribe(); - - return concast.promise - .then((finalResult) => { - expect(finalResult).toBeUndefined(); - expect(cleanupCounts).toEqual({ - first: 1, - second: 1, - }); - resolve(); - }) - .catch(reject); - } - ); + const finalResult = await concast.promise; + + expect(finalResult).toBeUndefined(); + expect(cleanupCounts).toEqual({ + first: 1, + second: 1, + }); + + await expect(stream).not.toEmitAnything(); + }); it("concast.beforeNext listeners run before next result/error", () => { const log: Array = []; diff --git a/src/utilities/observables/__tests__/asyncMap.ts b/src/utilities/observables/__tests__/asyncMap.ts index ce4227be45b..8f9d53071cd 100644 --- a/src/utilities/observables/__tests__/asyncMap.ts +++ b/src/utilities/observables/__tests__/asyncMap.ts @@ -1,6 +1,5 @@ import { Observable } from "../Observable"; import { asyncMap } from "../asyncMap"; -import { itAsync } from "../../../testing"; import { ObservableStream } from "../../../testing/internal"; const wait = (delayMs: number) => new Promise((resolve) => setTimeout(resolve, delayMs)); @@ -19,106 +18,66 @@ function make1234Observable() { }); } -function rejectExceptions( - reject: (reason: any) => any, - fn: (...args: Args) => Ret -) { - return function () { - try { - // @ts-expect-error - return fn.apply(this, arguments); - } catch (error) { - reject(error); - } - } as typeof fn; -} - describe("asyncMap", () => { - itAsync("keeps normal results in order", (resolve, reject) => { + it("keeps normal results in order", async () => { const values: number[] = []; - const mapped: number[] = []; - asyncMap(make1234Observable(), (value) => { + const observable = asyncMap(make1234Observable(), (value) => { values.push(value); // Make earlier results take longer than later results. const delay = 100 - value * 10; return wait(delay).then(() => value * 2); - }).subscribe({ - next(mappedValue) { - mapped.push(mappedValue); - }, - error: reject, - complete: rejectExceptions(reject, () => { - expect(values).toEqual([1, 2, 3, 4]); - expect(mapped).toEqual([2, 4, 6, 8]); - resolve(); - }), }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue(2); + await expect(stream).toEmitValue(4); + await expect(stream).toEmitValue(6); + await expect(stream).toEmitValue(8); + await expect(stream).toComplete(); + + expect(values).toEqual([1, 2, 3, 4]); }); - itAsync("handles exceptions from mapping functions", (resolve, reject) => { - const triples: number[] = []; - asyncMap(make1234Observable(), (num) => { + it("handles exceptions from mapping functions", async () => { + const observable = asyncMap(make1234Observable(), (num) => { if (num === 3) throw new Error("expected"); return num * 3; - }).subscribe({ - next: rejectExceptions(reject, (triple) => { - expect(triple).toBeLessThan(9); - triples.push(triple); - }), - error: rejectExceptions(reject, (error) => { - expect(error.message).toBe("expected"); - expect(triples).toEqual([3, 6]); - resolve(); - }), }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue(3); + await expect(stream).toEmitValue(6); + await expect(stream).toEmitError(new Error("expected")); + }); + + it("handles rejected promises from mapping functions", async () => { + const observable = asyncMap(make1234Observable(), (num) => { + if (num === 3) return Promise.reject(new Error("expected")); + return num * 3; + }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue(3); + await expect(stream).toEmitValue(6); + await expect(stream).toEmitError(new Error("expected")); }); - itAsync( - "handles rejected promises from mapping functions", - (resolve, reject) => { - const triples: number[] = []; - asyncMap(make1234Observable(), (num) => { - if (num === 3) return Promise.reject(new Error("expected")); + it("handles async exceptions from mapping functions", async () => { + const observable = asyncMap(make1234Observable(), (num) => + wait(10).then(() => { + if (num === 3) throw new Error("expected"); return num * 3; - }).subscribe({ - next: rejectExceptions(reject, (triple) => { - expect(triple).toBeLessThan(9); - triples.push(triple); - }), - error: rejectExceptions(reject, (error) => { - expect(error.message).toBe("expected"); - expect(triples).toEqual([3, 6]); - resolve(); - }), - }); - } - ); + }) + ); + const stream = new ObservableStream(observable); - itAsync( - "handles async exceptions from mapping functions", - (resolve, reject) => { - const triples: number[] = []; - asyncMap(make1234Observable(), (num) => - wait(10).then(() => { - if (num === 3) throw new Error("expected"); - return num * 3; - }) - ).subscribe({ - next: rejectExceptions(reject, (triple) => { - expect(triple).toBeLessThan(9); - triples.push(triple); - }), - error: rejectExceptions(reject, (error) => { - expect(error.message).toBe("expected"); - expect(triples).toEqual([3, 6]); - resolve(); - }), - }); - } - ); + await expect(stream).toEmitValue(3); + await expect(stream).toEmitValue(6); + await expect(stream).toEmitError(new Error("expected")); + }); - itAsync("handles exceptions from next functions", (resolve, reject) => { + it("handles exceptions from next functions", (done) => { const triples: number[] = []; asyncMap(make1234Observable(), (num) => { return num * 3; @@ -136,10 +95,10 @@ describe("asyncMap", () => { // expect(triples).toEqual([3, 6, 9]); // resolve(); // }), - complete: rejectExceptions(reject, () => { + complete: () => { expect(triples).toEqual([3, 6, 9, 12]); - resolve(); - }), + done(); + }, }); });